From b51a457e5a7231795708e1060c1557cc3809233a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 6 Mar 2019 14:36:30 +0900 Subject: [PATCH 0001/2376] Implement sorcerer's diffcalc changes --- .../Difficulty/CatchDifficultyCalculator.cs | 2 +- .../Preprocessing/CatchDifficultyHitObject.cs | 6 +-- .../Difficulty/Skills/Movement.cs | 46 ++++++++++++++----- 3 files changed, 38 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 8cfda5d532..2cb71428b9 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty { public class CatchDifficultyCalculator : DifficultyCalculator { - private const double star_scaling_factor = 0.145; + private const double star_scaling_factor = 0.15; protected override int SectionLength => 750; diff --git a/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs b/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs index 24e526ed19..f0c68e4392 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing public readonly float LastNormalizedPosition; /// - /// Milliseconds elapsed since the start time of the previous , with a minimum of 25ms. + /// Milliseconds elapsed since the start time of the previous , with a minimum of 40ms. /// public readonly double StrainTime; @@ -34,8 +34,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing NormalizedPosition = BaseObject.X * CatchPlayfield.BASE_WIDTH * scalingFactor; LastNormalizedPosition = LastObject.X * CatchPlayfield.BASE_WIDTH * scalingFactor; - // Every strain interval is hard capped at the equivalent of 600 BPM streaming speed as a safety measure - StrainTime = Math.Max(25, DeltaTime); + // Every strain interval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure + StrainTime = Math.Max(40, DeltaTime); } } } diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs index d146153294..b1b5ba0312 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs @@ -14,7 +14,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills { private const float absolute_player_positioning_error = 16f; private const float normalized_hitobject_radius = 41.0f; - private const double direction_change_bonus = 12.5; + private const double direction_change_bonus = 9.5; + private const double antiflow_bonus = 25.0; protected override double SkillMultiplier => 850; protected override double StrainDecayBase => 0.2; @@ -23,6 +24,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills private float? lastPlayerPosition; private float lastDistanceMoved; + private double lastStrainTime; protected override double StrainValueOf(DifficultyHitObject current) { @@ -39,8 +41,11 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills float distanceMoved = playerPosition - lastPlayerPosition.Value; - double distanceAddition = Math.Pow(Math.Abs(distanceMoved), 1.3) / 500; - double sqrtStrain = Math.Sqrt(catchCurrent.StrainTime); + // Reduce speed scaling + double weightedStrainTime = catchCurrent.StrainTime + 20; + + double distanceAddition = Math.Pow(Math.Abs(distanceMoved), 1.3) / 600; + double sqrtStrain = Math.Sqrt(weightedStrainTime); double bonus = 0; @@ -53,33 +58,50 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills distanceAddition += direction_change_bonus / sqrtStrain * bonusFactor; - // Bonus for tougher direction switches and "almost" hyperdashes at this point - if (catchCurrent.LastObject.DistanceToHyperDash <= 10 / CatchPlayfield.BASE_WIDTH) - bonus = 0.3 * bonusFactor; + // Direction changes after jumps (antiflow) are harder + double antiflowBonusFactor = Math.Min(Math.Sqrt(Math.Abs(distanceMoved)) / 10, 1); + + distanceAddition += (antiflow_bonus / sqrtStrain) * (Math.Sqrt(Math.Abs(lastDistanceMoved)) / (lastStrainTime / 40 + 10.0)) * antiflowBonusFactor; } // Base bonus for every movement, giving some weight to streams. - distanceAddition += 7.5 * Math.Min(Math.Abs(distanceMoved), normalized_hitobject_radius * 2) / (normalized_hitobject_radius * 6) / sqrtStrain; + distanceAddition += 10.0 * Math.Min(Math.Abs(distanceMoved), normalized_hitobject_radius * 2) / (normalized_hitobject_radius * 6) / sqrtStrain; } - // Bonus for "almost" hyperdashes at corner points - if (catchCurrent.LastObject.DistanceToHyperDash <= 10.0f / CatchPlayfield.BASE_WIDTH) + // Big bonus for edge hyperdashes + if (catchCurrent.LastObject.DistanceToHyperDash <= 14.0f / CatchPlayfield.BASE_WIDTH) { if (!catchCurrent.LastObject.HyperDash) - bonus += 1.0; + bonus += 5.0; else { // After a hyperdash we ARE in the correct position. Always! playerPosition = catchCurrent.NormalizedPosition; } - distanceAddition *= 1.0 + bonus * ((10 - catchCurrent.LastObject.DistanceToHyperDash * CatchPlayfield.BASE_WIDTH) / 10); + distanceAddition *= 1.0 + bonus * (14 - catchCurrent.LastObject.DistanceToHyperDash * CatchPlayfield.BASE_WIDTH) / 14 * (Math.Min(catchCurrent.StrainTime, 180) / 180); // Edge dashes are easier at lower ms values + } + + // Prevent wide, dense stacks of notes which fit on the catcher from greatly increasing SR + if (Math.Abs(distanceMoved) > 0.1) + { + if (Math.Abs(lastDistanceMoved) > 0.1 && Math.Sign(distanceMoved) != Math.Sign(lastDistanceMoved)) + { + if (Math.Abs(distanceMoved) <= (CatcherArea.CATCHER_SIZE) && Math.Abs(lastDistanceMoved) == Math.Abs(distanceMoved)) + { + if (catchCurrent.StrainTime <= 80 && lastStrainTime == catchCurrent.StrainTime) + { + distanceAddition *= Math.Max(((catchCurrent.StrainTime / 80) - 0.75) * 4, 0); + } + } + } } lastPlayerPosition = playerPosition; lastDistanceMoved = distanceMoved; + lastStrainTime = catchCurrent.StrainTime; - return distanceAddition / catchCurrent.StrainTime; + return distanceAddition / weightedStrainTime; } } } From 24fb25f1cdc770ecb7c016a8fba472c00507484d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 14 Mar 2019 23:39:45 +0900 Subject: [PATCH 0002/2376] Use fresh mods for each difficulty calculation --- .../Difficulty/CatchDifficultyCalculator.cs | 10 +++--- .../Difficulty/ManiaDifficultyCalculator.cs | 33 ++++++++++--------- .../Difficulty/OsuDifficultyCalculator.cs | 10 +++--- .../Difficulty/TaikoDifficultyCalculator.cs | 11 ++++--- ...DifficultyAdjustmentModCombinationsTest.cs | 14 ++++---- .../Difficulty/DifficultyCalculator.cs | 10 +++--- 6 files changed, 46 insertions(+), 42 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index f3b88bd928..d73ee19d41 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -90,12 +90,12 @@ namespace osu.Game.Rulesets.Catch.Difficulty new Movement(), }; - protected override Mod[] DifficultyAdjustmentMods => new Mod[] + protected override Type[] DifficultyAdjustmentMods => new[] { - new CatchModDoubleTime(), - new CatchModHalfTime(), - new CatchModHardRock(), - new CatchModEasy(), + typeof(CatchModDoubleTime), + typeof(CatchModHalfTime), + typeof(CatchModHardRock), + typeof(CatchModEasy), }; } } diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index 59fed1031f..bff3bfdb23 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; @@ -91,33 +92,33 @@ namespace osu.Game.Rulesets.Mania.Difficulty return skills.ToArray(); } - protected override Mod[] DifficultyAdjustmentMods + protected override Type[] DifficultyAdjustmentMods { get { - var mods = new Mod[] + var mods = new[] { - new ManiaModDoubleTime(), - new ManiaModHalfTime(), - new ManiaModEasy(), - new ManiaModHardRock(), + typeof(ManiaModDoubleTime), + typeof(ManiaModHalfTime), + typeof(ManiaModEasy), + typeof(ManiaModHardRock) }; if (isForCurrentRuleset) return mods; // if we are a convert, we can be played in any key mod. - return mods.Concat(new Mod[] + return mods.Concat(new[] { - new ManiaModKey1(), - new ManiaModKey2(), - new ManiaModKey3(), - new ManiaModKey4(), - new ManiaModKey5(), - new ManiaModKey6(), - new ManiaModKey7(), - new ManiaModKey8(), - new ManiaModKey9(), + typeof(ManiaModKey1), + typeof(ManiaModKey2), + typeof(ManiaModKey3), + typeof(ManiaModKey4), + typeof(ManiaModKey5), + typeof(ManiaModKey6), + typeof(ManiaModKey7), + typeof(ManiaModKey8), + typeof(ManiaModKey9), }).ToArray(); } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index e2a1542574..9c44eb6f97 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -74,12 +74,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty new Speed() }; - protected override Mod[] DifficultyAdjustmentMods => new Mod[] + protected override Type[] DifficultyAdjustmentMods => new[] { - new OsuModDoubleTime(), - new OsuModHalfTime(), - new OsuModEasy(), - new OsuModHardRock(), + typeof(OsuModDoubleTime), + typeof(OsuModHalfTime), + typeof(OsuModEasy), + typeof(OsuModHardRock), }; } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 685ad9949b..ad1fb4c2e5 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; @@ -47,12 +48,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[] { new Strain() }; - protected override Mod[] DifficultyAdjustmentMods => new Mod[] + protected override Type[] DifficultyAdjustmentMods => new[] { - new TaikoModDoubleTime(), - new TaikoModHalfTime(), - new TaikoModEasy(), - new TaikoModHardRock(), + typeof(TaikoModDoubleTime), + typeof(TaikoModHalfTime), + typeof(TaikoModEasy), + typeof(TaikoModHardRock), }; } } diff --git a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs index 760a033aff..3bce6fedbb 100644 --- a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs +++ b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs @@ -27,7 +27,7 @@ namespace osu.Game.Tests.NonVisual [Test] public void TestSingleMod() { - var combinations = new TestLegacyDifficultyCalculator(new ModA()).CreateDifficultyAdjustmentModCombinations(); + var combinations = new TestLegacyDifficultyCalculator(typeof(ModA)).CreateDifficultyAdjustmentModCombinations(); Assert.AreEqual(2, combinations.Length); Assert.IsTrue(combinations[0] is ModNoMod); @@ -37,7 +37,7 @@ namespace osu.Game.Tests.NonVisual [Test] public void TestDoubleMod() { - var combinations = new TestLegacyDifficultyCalculator(new ModA(), new ModB()).CreateDifficultyAdjustmentModCombinations(); + var combinations = new TestLegacyDifficultyCalculator(typeof(ModA), typeof(ModB)).CreateDifficultyAdjustmentModCombinations(); Assert.AreEqual(4, combinations.Length); Assert.IsTrue(combinations[0] is ModNoMod); @@ -52,7 +52,7 @@ namespace osu.Game.Tests.NonVisual [Test] public void TestIncompatibleMods() { - var combinations = new TestLegacyDifficultyCalculator(new ModA(), new ModIncompatibleWithA()).CreateDifficultyAdjustmentModCombinations(); + var combinations = new TestLegacyDifficultyCalculator(typeof(ModA), typeof(ModIncompatibleWithA)).CreateDifficultyAdjustmentModCombinations(); Assert.AreEqual(3, combinations.Length); Assert.IsTrue(combinations[0] is ModNoMod); @@ -63,7 +63,7 @@ namespace osu.Game.Tests.NonVisual [Test] public void TestDoubleIncompatibleMods() { - var combinations = new TestLegacyDifficultyCalculator(new ModA(), new ModB(), new ModIncompatibleWithA(), new ModIncompatibleWithAAndB()).CreateDifficultyAdjustmentModCombinations(); + var combinations = new TestLegacyDifficultyCalculator(typeof(ModA), typeof(ModB), typeof(ModIncompatibleWithA), typeof(ModIncompatibleWithAAndB)).CreateDifficultyAdjustmentModCombinations(); Assert.AreEqual(8, combinations.Length); Assert.IsTrue(combinations[0] is ModNoMod); @@ -86,7 +86,7 @@ namespace osu.Game.Tests.NonVisual [Test] public void TestIncompatibleThroughBaseType() { - var combinations = new TestLegacyDifficultyCalculator(new ModAofA(), new ModIncompatibleWithAofA()).CreateDifficultyAdjustmentModCombinations(); + var combinations = new TestLegacyDifficultyCalculator(typeof(ModAofA), typeof(ModIncompatibleWithAofA)).CreateDifficultyAdjustmentModCombinations(); Assert.AreEqual(3, combinations.Length); Assert.IsTrue(combinations[0] is ModNoMod); @@ -141,13 +141,13 @@ namespace osu.Game.Tests.NonVisual private class TestLegacyDifficultyCalculator : DifficultyCalculator { - public TestLegacyDifficultyCalculator(params Mod[] mods) + public TestLegacyDifficultyCalculator(params Type[] mods) : base(null, null) { DifficultyAdjustmentMods = mods; } - protected override Mod[] DifficultyAdjustmentMods { get; } + protected override Type[] DifficultyAdjustmentMods { get; } protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) { diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index db8bdde6bb..47ffa48b91 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -106,7 +106,7 @@ namespace osu.Game.Rulesets.Difficulty { return createDifficultyAdjustmentModCombinations(Enumerable.Empty(), DifficultyAdjustmentMods).ToArray(); - IEnumerable createDifficultyAdjustmentModCombinations(IEnumerable currentSet, Mod[] adjustmentSet, int currentSetCount = 0, int adjustmentSetStart = 0) + IEnumerable createDifficultyAdjustmentModCombinations(IEnumerable currentSet, Type[] adjustmentSet, int currentSetCount = 0, int adjustmentSetStart = 0) { switch (currentSetCount) { @@ -129,12 +129,14 @@ namespace osu.Game.Rulesets.Difficulty // combinations in further recursions, so a moving subset is used to eliminate this effect for (int i = adjustmentSetStart; i < adjustmentSet.Length; i++) { - var adjustmentMod = adjustmentSet[i]; + var adjustmentMod = createMod(); if (currentSet.Any(c => c.IncompatibleMods.Any(m => m.IsInstanceOfType(adjustmentMod)))) continue; - foreach (var combo in createDifficultyAdjustmentModCombinations(currentSet.Append(adjustmentMod), adjustmentSet, currentSetCount + 1, i + 1)) + foreach (var combo in createDifficultyAdjustmentModCombinations(currentSet.Append(createMod()), adjustmentSet, currentSetCount + 1, i + 1)) yield return combo; + + Mod createMod() => (Mod)Activator.CreateInstance(adjustmentSet[i]); } } } @@ -142,7 +144,7 @@ namespace osu.Game.Rulesets.Difficulty /// /// Retrieves all s which adjust the difficulty. /// - protected virtual Mod[] DifficultyAdjustmentMods => Array.Empty(); + protected virtual Type[] DifficultyAdjustmentMods => Array.Empty(); /// /// Creates to describe beatmap's calculated difficulty. From f959a2ee373f13a998bb8e752bc42d614ae12cd3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 16 Mar 2019 10:12:05 +0900 Subject: [PATCH 0003/2376] Update antiflow bonus --- osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs index b1b5ba0312..d06813f160 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs @@ -59,9 +59,9 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills distanceAddition += direction_change_bonus / sqrtStrain * bonusFactor; // Direction changes after jumps (antiflow) are harder - double antiflowBonusFactor = Math.Min(Math.Sqrt(Math.Abs(distanceMoved)) / 10, 1); + double antiflowBonusFactor = Math.Min(Math.Abs(distanceMoved) / 70, 1); - distanceAddition += (antiflow_bonus / sqrtStrain) * (Math.Sqrt(Math.Abs(lastDistanceMoved)) / (lastStrainTime / 40 + 10.0)) * antiflowBonusFactor; + distanceAddition += (antiflow_bonus / (catchCurrent.StrainTime / 40 + 10)) * (Math.Sqrt(Math.Abs(lastDistanceMoved)) / Math.Sqrt(lastStrainTime + 20)) * antiflowBonusFactor; } // Base bonus for every movement, giving some weight to streams. From 9ae6cde837470d8e5708788bd224631f82af8243 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 20 Mar 2019 12:14:26 +0900 Subject: [PATCH 0004/2376] Nerf back-and-forth hyperdash chains --- .../Difficulty/Skills/Movement.cs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs index d06813f160..d8e359bb48 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs @@ -15,9 +15,9 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills private const float absolute_player_positioning_error = 16f; private const float normalized_hitobject_radius = 41.0f; private const double direction_change_bonus = 9.5; - private const double antiflow_bonus = 25.0; + private const double antiflow_bonus = 26.0; - protected override double SkillMultiplier => 850; + protected override double SkillMultiplier => 860; protected override double StrainDecayBase => 0.2; protected override double DecayWeight => 0.94; @@ -25,6 +25,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills private float? lastPlayerPosition; private float lastDistanceMoved; private double lastStrainTime; + private bool lastHyperdash; protected override double StrainValueOf(DifficultyHitObject current) { @@ -44,7 +45,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills // Reduce speed scaling double weightedStrainTime = catchCurrent.StrainTime + 20; - double distanceAddition = Math.Pow(Math.Abs(distanceMoved), 1.3) / 600; + double distanceAddition = Math.Pow(Math.Abs(distanceMoved), 1.2) / 340; double sqrtStrain = Math.Sqrt(weightedStrainTime); double bonus = 0; @@ -61,7 +62,14 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills // Direction changes after jumps (antiflow) are harder double antiflowBonusFactor = Math.Min(Math.Abs(distanceMoved) / 70, 1); - distanceAddition += (antiflow_bonus / (catchCurrent.StrainTime / 40 + 10)) * (Math.Sqrt(Math.Abs(lastDistanceMoved)) / Math.Sqrt(lastStrainTime + 20)) * antiflowBonusFactor; + distanceAddition += (antiflow_bonus / (catchCurrent.StrainTime / 17.5 + 10)) * (Math.Sqrt(Math.Abs(lastDistanceMoved)) / Math.Sqrt(lastStrainTime + 25)) * antiflowBonusFactor; + + // Reduce strain slightly for Hyperdash chains + if (catchCurrent.LastObject.HyperDash && lastHyperdash) + distanceAddition *= 0.95; + + if (catchCurrent.LastObject.DistanceToHyperDash <= 14.0f / CatchPlayfield.BASE_WIDTH && !catchCurrent.LastObject.HyperDash) + bonus += 3.0; } // Base bonus for every movement, giving some weight to streams. @@ -100,6 +108,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills lastPlayerPosition = playerPosition; lastDistanceMoved = distanceMoved; lastStrainTime = catchCurrent.StrainTime; + lastHyperdash = catchCurrent.LastObject.HyperDash; return distanceAddition / weightedStrainTime; } From 9f12a36598f504539644674a89a33f370f9ef3c5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 20 Mar 2019 12:14:53 +0900 Subject: [PATCH 0005/2376] Buff slower edge dashes, nerf faster ones --- osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs index d8e359bb48..d06ab2f928 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills { private const float absolute_player_positioning_error = 16f; private const float normalized_hitobject_radius = 41.0f; - private const double direction_change_bonus = 9.5; + private const double direction_change_bonus = 9.8; private const double antiflow_bonus = 26.0; protected override double SkillMultiplier => 860; @@ -76,18 +76,18 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills distanceAddition += 10.0 * Math.Min(Math.Abs(distanceMoved), normalized_hitobject_radius * 2) / (normalized_hitobject_radius * 6) / sqrtStrain; } - // Big bonus for edge hyperdashes + // Big bonus for edge dashes if (catchCurrent.LastObject.DistanceToHyperDash <= 14.0f / CatchPlayfield.BASE_WIDTH) { if (!catchCurrent.LastObject.HyperDash) - bonus += 5.0; + bonus += 4.5; else { // After a hyperdash we ARE in the correct position. Always! playerPosition = catchCurrent.NormalizedPosition; } - distanceAddition *= 1.0 + bonus * (14 - catchCurrent.LastObject.DistanceToHyperDash * CatchPlayfield.BASE_WIDTH) / 14 * (Math.Min(catchCurrent.StrainTime, 180) / 180); // Edge dashes are easier at lower ms values + distanceAddition *= 1.0 + bonus * (14.0f - catchCurrent.LastObject.DistanceToHyperDash * CatchPlayfield.BASE_WIDTH) / 14.0f * (Math.Min(catchCurrent.StrainTime, 250) / 250); // Edge dashes are easier at lower ms values } // Prevent wide, dense stacks of notes which fit on the catcher from greatly increasing SR From 839dd7343f1cc6411077616ce0b2965e29130584 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 23 Mar 2019 15:57:22 +0900 Subject: [PATCH 0006/2376] Revert "Use fresh mods for each difficulty calculation" This reverts commit 24fb25f1cdc770ecb7c016a8fba472c00507484d. --- .../Difficulty/CatchDifficultyCalculator.cs | 10 +++--- .../Difficulty/ManiaDifficultyCalculator.cs | 33 +++++++++---------- .../Difficulty/OsuDifficultyCalculator.cs | 10 +++--- .../Difficulty/TaikoDifficultyCalculator.cs | 11 +++---- ...DifficultyAdjustmentModCombinationsTest.cs | 14 ++++---- .../Difficulty/DifficultyCalculator.cs | 10 +++--- 6 files changed, 42 insertions(+), 46 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 289deab8cd..47e2bc5259 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -91,12 +91,12 @@ namespace osu.Game.Rulesets.Catch.Difficulty new Movement(), }; - protected override Type[] DifficultyAdjustmentMods => new[] + protected override Mod[] DifficultyAdjustmentMods => new Mod[] { - typeof(CatchModDoubleTime), - typeof(CatchModHalfTime), - typeof(CatchModHardRock), - typeof(CatchModEasy), + new CatchModDoubleTime(), + new CatchModHalfTime(), + new CatchModHardRock(), + new CatchModEasy(), }; } } diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index bff3bfdb23..59fed1031f 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; @@ -92,33 +91,33 @@ namespace osu.Game.Rulesets.Mania.Difficulty return skills.ToArray(); } - protected override Type[] DifficultyAdjustmentMods + protected override Mod[] DifficultyAdjustmentMods { get { - var mods = new[] + var mods = new Mod[] { - typeof(ManiaModDoubleTime), - typeof(ManiaModHalfTime), - typeof(ManiaModEasy), - typeof(ManiaModHardRock) + new ManiaModDoubleTime(), + new ManiaModHalfTime(), + new ManiaModEasy(), + new ManiaModHardRock(), }; if (isForCurrentRuleset) return mods; // if we are a convert, we can be played in any key mod. - return mods.Concat(new[] + return mods.Concat(new Mod[] { - typeof(ManiaModKey1), - typeof(ManiaModKey2), - typeof(ManiaModKey3), - typeof(ManiaModKey4), - typeof(ManiaModKey5), - typeof(ManiaModKey6), - typeof(ManiaModKey7), - typeof(ManiaModKey8), - typeof(ManiaModKey9), + new ManiaModKey1(), + new ManiaModKey2(), + new ManiaModKey3(), + new ManiaModKey4(), + new ManiaModKey5(), + new ManiaModKey6(), + new ManiaModKey7(), + new ManiaModKey8(), + new ManiaModKey9(), }).ToArray(); } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 9c44eb6f97..e2a1542574 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -74,12 +74,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty new Speed() }; - protected override Type[] DifficultyAdjustmentMods => new[] + protected override Mod[] DifficultyAdjustmentMods => new Mod[] { - typeof(OsuModDoubleTime), - typeof(OsuModHalfTime), - typeof(OsuModEasy), - typeof(OsuModHardRock), + new OsuModDoubleTime(), + new OsuModHalfTime(), + new OsuModEasy(), + new OsuModHardRock(), }; } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index ad1fb4c2e5..685ad9949b 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; @@ -48,12 +47,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[] { new Strain() }; - protected override Type[] DifficultyAdjustmentMods => new[] + protected override Mod[] DifficultyAdjustmentMods => new Mod[] { - typeof(TaikoModDoubleTime), - typeof(TaikoModHalfTime), - typeof(TaikoModEasy), - typeof(TaikoModHardRock), + new TaikoModDoubleTime(), + new TaikoModHalfTime(), + new TaikoModEasy(), + new TaikoModHardRock(), }; } } diff --git a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs index 3bce6fedbb..760a033aff 100644 --- a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs +++ b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs @@ -27,7 +27,7 @@ namespace osu.Game.Tests.NonVisual [Test] public void TestSingleMod() { - var combinations = new TestLegacyDifficultyCalculator(typeof(ModA)).CreateDifficultyAdjustmentModCombinations(); + var combinations = new TestLegacyDifficultyCalculator(new ModA()).CreateDifficultyAdjustmentModCombinations(); Assert.AreEqual(2, combinations.Length); Assert.IsTrue(combinations[0] is ModNoMod); @@ -37,7 +37,7 @@ namespace osu.Game.Tests.NonVisual [Test] public void TestDoubleMod() { - var combinations = new TestLegacyDifficultyCalculator(typeof(ModA), typeof(ModB)).CreateDifficultyAdjustmentModCombinations(); + var combinations = new TestLegacyDifficultyCalculator(new ModA(), new ModB()).CreateDifficultyAdjustmentModCombinations(); Assert.AreEqual(4, combinations.Length); Assert.IsTrue(combinations[0] is ModNoMod); @@ -52,7 +52,7 @@ namespace osu.Game.Tests.NonVisual [Test] public void TestIncompatibleMods() { - var combinations = new TestLegacyDifficultyCalculator(typeof(ModA), typeof(ModIncompatibleWithA)).CreateDifficultyAdjustmentModCombinations(); + var combinations = new TestLegacyDifficultyCalculator(new ModA(), new ModIncompatibleWithA()).CreateDifficultyAdjustmentModCombinations(); Assert.AreEqual(3, combinations.Length); Assert.IsTrue(combinations[0] is ModNoMod); @@ -63,7 +63,7 @@ namespace osu.Game.Tests.NonVisual [Test] public void TestDoubleIncompatibleMods() { - var combinations = new TestLegacyDifficultyCalculator(typeof(ModA), typeof(ModB), typeof(ModIncompatibleWithA), typeof(ModIncompatibleWithAAndB)).CreateDifficultyAdjustmentModCombinations(); + var combinations = new TestLegacyDifficultyCalculator(new ModA(), new ModB(), new ModIncompatibleWithA(), new ModIncompatibleWithAAndB()).CreateDifficultyAdjustmentModCombinations(); Assert.AreEqual(8, combinations.Length); Assert.IsTrue(combinations[0] is ModNoMod); @@ -86,7 +86,7 @@ namespace osu.Game.Tests.NonVisual [Test] public void TestIncompatibleThroughBaseType() { - var combinations = new TestLegacyDifficultyCalculator(typeof(ModAofA), typeof(ModIncompatibleWithAofA)).CreateDifficultyAdjustmentModCombinations(); + var combinations = new TestLegacyDifficultyCalculator(new ModAofA(), new ModIncompatibleWithAofA()).CreateDifficultyAdjustmentModCombinations(); Assert.AreEqual(3, combinations.Length); Assert.IsTrue(combinations[0] is ModNoMod); @@ -141,13 +141,13 @@ namespace osu.Game.Tests.NonVisual private class TestLegacyDifficultyCalculator : DifficultyCalculator { - public TestLegacyDifficultyCalculator(params Type[] mods) + public TestLegacyDifficultyCalculator(params Mod[] mods) : base(null, null) { DifficultyAdjustmentMods = mods; } - protected override Type[] DifficultyAdjustmentMods { get; } + protected override Mod[] DifficultyAdjustmentMods { get; } protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) { diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 9b630865c2..aad55f8a38 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -108,7 +108,7 @@ namespace osu.Game.Rulesets.Difficulty { return createDifficultyAdjustmentModCombinations(Enumerable.Empty(), DifficultyAdjustmentMods).ToArray(); - IEnumerable createDifficultyAdjustmentModCombinations(IEnumerable currentSet, Type[] adjustmentSet, int currentSetCount = 0, int adjustmentSetStart = 0) + IEnumerable createDifficultyAdjustmentModCombinations(IEnumerable currentSet, Mod[] adjustmentSet, int currentSetCount = 0, int adjustmentSetStart = 0) { switch (currentSetCount) { @@ -131,14 +131,12 @@ namespace osu.Game.Rulesets.Difficulty // combinations in further recursions, so a moving subset is used to eliminate this effect for (int i = adjustmentSetStart; i < adjustmentSet.Length; i++) { - var adjustmentMod = createMod(); + var adjustmentMod = adjustmentSet[i]; if (currentSet.Any(c => c.IncompatibleMods.Any(m => m.IsInstanceOfType(adjustmentMod)))) continue; - foreach (var combo in createDifficultyAdjustmentModCombinations(currentSet.Append(createMod()), adjustmentSet, currentSetCount + 1, i + 1)) + foreach (var combo in createDifficultyAdjustmentModCombinations(currentSet.Append(adjustmentMod), adjustmentSet, currentSetCount + 1, i + 1)) yield return combo; - - Mod createMod() => (Mod)Activator.CreateInstance(adjustmentSet[i]); } } } @@ -146,7 +144,7 @@ namespace osu.Game.Rulesets.Difficulty /// /// Retrieves all s which adjust the difficulty. /// - protected virtual Type[] DifficultyAdjustmentMods => Array.Empty(); + protected virtual Mod[] DifficultyAdjustmentMods => Array.Empty(); /// /// Creates to describe beatmap's calculated difficulty. From be5ffdbf22cd5a8dc5fca3c87306042741e2dafa Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 23 Mar 2019 16:01:14 +0900 Subject: [PATCH 0007/2376] Adjust edge bonuses to consider clock rate --- .../Preprocessing/CatchDifficultyHitObject.cs | 2 ++ .../Difficulty/Skills/Movement.cs | 21 +++++++------------ 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs b/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs index f0c68e4392..b2b4129c8a 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs @@ -24,6 +24,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing /// Milliseconds elapsed since the start time of the previous , with a minimum of 40ms. /// public readonly double StrainTime; + public readonly double ClockRate; public CatchDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate, float halfCatcherWidth) : base(hitObject, lastObject, clockRate) @@ -36,6 +37,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing // Every strain interval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure StrainTime = Math.Max(40, DeltaTime); + ClockRate = clockRate; } } } diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs index d06ab2f928..227fb2820c 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills private const double direction_change_bonus = 9.8; private const double antiflow_bonus = 26.0; - protected override double SkillMultiplier => 860; + protected override double SkillMultiplier => 850; protected override double StrainDecayBase => 0.2; protected override double DecayWeight => 0.94; @@ -25,7 +25,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills private float? lastPlayerPosition; private float lastDistanceMoved; private double lastStrainTime; - private bool lastHyperdash; protected override double StrainValueOf(DifficultyHitObject current) { @@ -62,35 +61,32 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills // Direction changes after jumps (antiflow) are harder double antiflowBonusFactor = Math.Min(Math.Abs(distanceMoved) / 70, 1); - distanceAddition += (antiflow_bonus / (catchCurrent.StrainTime / 17.5 + 10)) * (Math.Sqrt(Math.Abs(lastDistanceMoved)) / Math.Sqrt(lastStrainTime + 25)) * antiflowBonusFactor; - - // Reduce strain slightly for Hyperdash chains - if (catchCurrent.LastObject.HyperDash && lastHyperdash) - distanceAddition *= 0.95; + distanceAddition += (antiflow_bonus / (catchCurrent.StrainTime / 17.5 + 10)) * (Math.Sqrt(Math.Abs(lastDistanceMoved)) / Math.Sqrt(lastStrainTime + 20)) * antiflowBonusFactor; + // Bonus for edge dashes on direction change if (catchCurrent.LastObject.DistanceToHyperDash <= 14.0f / CatchPlayfield.BASE_WIDTH && !catchCurrent.LastObject.HyperDash) - bonus += 3.0; + bonus += 1.0; } // Base bonus for every movement, giving some weight to streams. distanceAddition += 10.0 * Math.Min(Math.Abs(distanceMoved), normalized_hitobject_radius * 2) / (normalized_hitobject_radius * 6) / sqrtStrain; } - // Big bonus for edge dashes + // Bonus for edge dashes regardless of direction change if (catchCurrent.LastObject.DistanceToHyperDash <= 14.0f / CatchPlayfield.BASE_WIDTH) { if (!catchCurrent.LastObject.HyperDash) - bonus += 4.5; + bonus += 0.9; else { // After a hyperdash we ARE in the correct position. Always! playerPosition = catchCurrent.NormalizedPosition; } - distanceAddition *= 1.0 + bonus * (14.0f - catchCurrent.LastObject.DistanceToHyperDash * CatchPlayfield.BASE_WIDTH) / 14.0f * (Math.Min(catchCurrent.StrainTime, 250) / 250); // Edge dashes are easier at lower ms values + distanceAddition *= 1.0 + bonus * Math.Pow(14.0f - catchCurrent.LastObject.DistanceToHyperDash * CatchPlayfield.BASE_WIDTH, 1.6f) / 14.0f * (Math.Min(catchCurrent.StrainTime * catchCurrent.ClockRate, 250) / 250); // Edge dashes are easier at lower ms values } - // Prevent wide, dense stacks of notes which fit on the catcher from greatly increasing SR + // Prevent wide dense stacks of notes which fit on the catcher from greatly increasing SR if (Math.Abs(distanceMoved) > 0.1) { if (Math.Abs(lastDistanceMoved) > 0.1 && Math.Sign(distanceMoved) != Math.Sign(lastDistanceMoved)) @@ -108,7 +104,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills lastPlayerPosition = playerPosition; lastDistanceMoved = distanceMoved; lastStrainTime = catchCurrent.StrainTime; - lastHyperdash = catchCurrent.LastObject.HyperDash; return distanceAddition / weightedStrainTime; } From 2705263145b1dcae03bc5b58304eebe9aebfa7f3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 26 Mar 2019 13:25:52 +0900 Subject: [PATCH 0008/2376] Scale edge dash threshold with clock rate --- .../Difficulty/Skills/Movement.cs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs index 227fb2820c..27558838f4 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs @@ -62,28 +62,26 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills double antiflowBonusFactor = Math.Min(Math.Abs(distanceMoved) / 70, 1); distanceAddition += (antiflow_bonus / (catchCurrent.StrainTime / 17.5 + 10)) * (Math.Sqrt(Math.Abs(lastDistanceMoved)) / Math.Sqrt(lastStrainTime + 20)) * antiflowBonusFactor; - - // Bonus for edge dashes on direction change - if (catchCurrent.LastObject.DistanceToHyperDash <= 14.0f / CatchPlayfield.BASE_WIDTH && !catchCurrent.LastObject.HyperDash) - bonus += 1.0; } // Base bonus for every movement, giving some weight to streams. distanceAddition += 10.0 * Math.Min(Math.Abs(distanceMoved), normalized_hitobject_radius * 2) / (normalized_hitobject_radius * 6) / sqrtStrain; } - // Bonus for edge dashes regardless of direction change - if (catchCurrent.LastObject.DistanceToHyperDash <= 14.0f / CatchPlayfield.BASE_WIDTH) + // Bonus for edge dashes + double edgeDashThreshold = 15.5f * ((Math.Min(catchCurrent.StrainTime * catchCurrent.ClockRate, 250) * 0.9 + 25) / 250); + + if (catchCurrent.LastObject.DistanceToHyperDash <= edgeDashThreshold / CatchPlayfield.BASE_WIDTH) { if (!catchCurrent.LastObject.HyperDash) - bonus += 0.9; + bonus += 2.3; else { // After a hyperdash we ARE in the correct position. Always! playerPosition = catchCurrent.NormalizedPosition; } - distanceAddition *= 1.0 + bonus * Math.Pow(14.0f - catchCurrent.LastObject.DistanceToHyperDash * CatchPlayfield.BASE_WIDTH, 1.6f) / 14.0f * (Math.Min(catchCurrent.StrainTime * catchCurrent.ClockRate, 250) / 250); // Edge dashes are easier at lower ms values + distanceAddition *= 1.0 + bonus * Math.Pow(edgeDashThreshold - catchCurrent.LastObject.DistanceToHyperDash * CatchPlayfield.BASE_WIDTH, 1.6f) / edgeDashThreshold * (Math.Min(catchCurrent.StrainTime * catchCurrent.ClockRate, 265) / 265); // Edge dashes are easier at lower ms values } // Prevent wide dense stacks of notes which fit on the catcher from greatly increasing SR From 9d0d402336e6b7123ee715105d953979797cc44c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 27 Mar 2019 17:21:57 +0900 Subject: [PATCH 0009/2376] Apply pp calculator changes (Backported from https://github.com/ppy/osu-performance/compare/master...smoogipoo:sorcerer-catch-changes) --- .../Difficulty/CatchPerformanceCalculator.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs index 5a640f6d1a..28da047187 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs @@ -55,8 +55,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty // Longer maps are worth more float lengthBonus = - 0.95f + 0.4f * Math.Min(1.0f, numTotalHits / 3000.0f) + - (numTotalHits > 3000 ? (float)Math.Log10(numTotalHits / 3000.0f) * 0.5f : 0.0f); + 0.95f + 0.3f * Math.Min(1.0f, numTotalHits / 2500.0f) + + (numTotalHits > 2500 ? (float)Math.Log10(numTotalHits / 2500.0f) * 0.475f : 0.0f); // Longer maps are worth more value *= lengthBonus; @@ -73,14 +73,22 @@ namespace osu.Game.Rulesets.Catch.Difficulty float approachRateFactor = 1.0f; if (approachRate > 9.0f) approachRateFactor += 0.1f * (approachRate - 9.0f); // 10% for each AR above 9 + if (approachRate > 10.0f) + approachRateFactor += 0.2f * (float)Math.Pow(approachRate - 10.0f, 1.5f); // Additional 20% at AR 11, 40% total else if (approachRate < 8.0f) approachRateFactor += 0.025f * (8.0f - approachRate); // 2.5% for each AR below 8 value *= approachRateFactor; if (mods.Any(m => m is ModHidden)) - // Hiddens gives nothing on max approach rate, and more the lower it is + { value *= 1.05f + 0.075f * (10.0f - Math.Min(10.0f, approachRate)); // 7.5% for each AR below 10 + // Hiddens gives almost nothing on max approach rate, and more the lower it is + if (approachRate <= 10.0f) + value *= 1.05f + 0.075f * (10.0f - approachRate); // 7.5% for each AR below 10 + else if (approachRate > 10.0f) + value *= 1.01f + 0.04f * (11.0f - Math.Min(11.0f, approachRate)); // 5% at AR 10, 1% at AR 11 + } if (mods.Any(m => m is ModFlashlight)) // Apply length bonus again if flashlight is on simply because it becomes a lot harder on longer maps. From b402981fc644c85422d52449c90cd3120a3bd44d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 1 Apr 2019 10:57:01 +0900 Subject: [PATCH 0010/2376] Buff CS > 5 --- .../Difficulty/CatchDifficultyCalculator.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 47e2bc5259..a475aefd71 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -52,7 +52,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty using (var catcher = new CatcherArea.Catcher(beatmap.BeatmapInfo.BaseDifficulty)) { halfCatchWidth = catcher.CatchWidth * 0.5f; - halfCatchWidth *= 0.8f; // We're only using 80% of the catcher's width to simulate imperfect gameplay. + // We're only using 80% of the catcher's width to simulate imperfect gameplay. + halfCatchWidth *= Math.Min(1.05f - (0.05f * beatmap.BeatmapInfo.BaseDifficulty.CircleSize), 0.8f); // Reduce the catcher's width further at circle sizes above 5. } CatchHitObject lastObject = null; From b2396b82a5bf218ac61786f9e38ae8a6976d91c8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 1 Apr 2019 10:58:26 +0900 Subject: [PATCH 0011/2376] Change edge dashes to scale linearly once again --- .../Preprocessing/CatchDifficultyHitObject.cs | 3 +++ osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs | 10 ++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs b/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs index b2b4129c8a..9f6de35605 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs @@ -25,6 +25,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing /// public readonly double StrainTime; public readonly double ClockRate; + public readonly double HalfCatcherWidth; public CatchDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate, float halfCatcherWidth) : base(hitObject, lastObject, clockRate) @@ -38,6 +39,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing // Every strain interval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure StrainTime = Math.Max(40, DeltaTime); ClockRate = clockRate; + HalfCatcherWidth = halfCatcherWidth; } } } + diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs index 27558838f4..473b254d5b 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs @@ -68,20 +68,18 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills distanceAddition += 10.0 * Math.Min(Math.Abs(distanceMoved), normalized_hitobject_radius * 2) / (normalized_hitobject_radius * 6) / sqrtStrain; } - // Bonus for edge dashes - double edgeDashThreshold = 15.5f * ((Math.Min(catchCurrent.StrainTime * catchCurrent.ClockRate, 250) * 0.9 + 25) / 250); - - if (catchCurrent.LastObject.DistanceToHyperDash <= edgeDashThreshold / CatchPlayfield.BASE_WIDTH) + // Bonus for "almost" hyperdashes at corner points + if (catchCurrent.LastObject.DistanceToHyperDash <= 20.0f / CatchPlayfield.BASE_WIDTH) { if (!catchCurrent.LastObject.HyperDash) - bonus += 2.3; + bonus += 5.7; else { // After a hyperdash we ARE in the correct position. Always! playerPosition = catchCurrent.NormalizedPosition; } - distanceAddition *= 1.0 + bonus * Math.Pow(edgeDashThreshold - catchCurrent.LastObject.DistanceToHyperDash * CatchPlayfield.BASE_WIDTH, 1.6f) / edgeDashThreshold * (Math.Min(catchCurrent.StrainTime * catchCurrent.ClockRate, 265) / 265); // Edge dashes are easier at lower ms values + distanceAddition *= 1.0 + bonus * ((20 - catchCurrent.LastObject.DistanceToHyperDash * CatchPlayfield.BASE_WIDTH) / 20) * Math.Pow((Math.Min(catchCurrent.StrainTime, 265) / 265), 1.5); } // Prevent wide dense stacks of notes which fit on the catcher from greatly increasing SR From efee2fb283903f00b4039330117836c271016eb0 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 1 Apr 2019 11:00:26 +0900 Subject: [PATCH 0012/2376] Adjust antiflow calculations --- .../Difficulty/Skills/Movement.cs | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs index 473b254d5b..e78e5c0a58 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs @@ -14,10 +14,9 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills { private const float absolute_player_positioning_error = 16f; private const float normalized_hitobject_radius = 41.0f; - private const double direction_change_bonus = 9.8; - private const double antiflow_bonus = 26.0; + private const double direction_change_bonus = 21.0; - protected override double SkillMultiplier => 850; + protected override double SkillMultiplier => 900; protected override double StrainDecayBase => 0.2; protected override double DecayWeight => 0.94; @@ -29,6 +28,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills protected override double StrainValueOf(DifficultyHitObject current) { var catchCurrent = (CatchDifficultyHitObject)current; + double halfCatcherWidth = catchCurrent.HalfCatcherWidth; if (lastPlayerPosition == null) lastPlayerPosition = catchCurrent.LastNormalizedPosition; @@ -41,10 +41,9 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills float distanceMoved = playerPosition - lastPlayerPosition.Value; - // Reduce speed scaling - double weightedStrainTime = catchCurrent.StrainTime + 20; + double weightedStrainTime = catchCurrent.StrainTime + 18; - double distanceAddition = Math.Pow(Math.Abs(distanceMoved), 1.2) / 340; + double distanceAddition = (Math.Pow(Math.Abs(distanceMoved), 1.3) / 510); double sqrtStrain = Math.Sqrt(weightedStrainTime); double bonus = 0; @@ -54,18 +53,14 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills { if (Math.Abs(lastDistanceMoved) > 0.1 && Math.Sign(distanceMoved) != Math.Sign(lastDistanceMoved)) { - double bonusFactor = Math.Min(absolute_player_positioning_error, Math.Abs(distanceMoved)) / absolute_player_positioning_error; + double bonusFactor = Math.Min(halfCatcherWidth, Math.Abs(distanceMoved)) / halfCatcherWidth; + double antiflowFactor = Math.Max(Math.Min(halfCatcherWidth, Math.Abs(lastDistanceMoved)) / halfCatcherWidth, 0.3); - distanceAddition += direction_change_bonus / sqrtStrain * bonusFactor; - - // Direction changes after jumps (antiflow) are harder - double antiflowBonusFactor = Math.Min(Math.Abs(distanceMoved) / 70, 1); - - distanceAddition += (antiflow_bonus / (catchCurrent.StrainTime / 17.5 + 10)) * (Math.Sqrt(Math.Abs(lastDistanceMoved)) / Math.Sqrt(lastStrainTime + 20)) * antiflowBonusFactor; + distanceAddition += direction_change_bonus / Math.Sqrt(lastStrainTime + 18) * bonusFactor * antiflowFactor * Math.Max(1 - Math.Pow(weightedStrainTime / 1000, 2), 0); } // Base bonus for every movement, giving some weight to streams. - distanceAddition += 10.0 * Math.Min(Math.Abs(distanceMoved), normalized_hitobject_radius * 2) / (normalized_hitobject_radius * 6) / sqrtStrain; + distanceAddition += 12.5 * Math.Min(Math.Abs(distanceMoved), normalized_hitobject_radius * 2) / (normalized_hitobject_radius * 6) / sqrtStrain; } // Bonus for "almost" hyperdashes at corner points From 21e62c37d8d2fcacb49a1f475ae65d73f04b4abf Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Apr 2019 07:28:04 +0900 Subject: [PATCH 0013/2376] General fixes --- .../Difficulty/CatchDifficultyCalculator.cs | 2 +- .../Preprocessing/CatchDifficultyHitObject.cs | 3 --- .../Difficulty/Skills/Movement.cs | 13 ++++++------- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index a475aefd71..4cd297478a 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty { public class CatchDifficultyCalculator : DifficultyCalculator { - private const double star_scaling_factor = 0.15; + private const double star_scaling_factor = 0.153; protected override int SectionLength => 750; diff --git a/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs b/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs index 9f6de35605..b2b4129c8a 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs @@ -25,7 +25,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing /// public readonly double StrainTime; public readonly double ClockRate; - public readonly double HalfCatcherWidth; public CatchDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate, float halfCatcherWidth) : base(hitObject, lastObject, clockRate) @@ -39,8 +38,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing // Every strain interval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure StrainTime = Math.Max(40, DeltaTime); ClockRate = clockRate; - HalfCatcherWidth = halfCatcherWidth; } } } - diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs index e78e5c0a58..4ea5135c4f 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs @@ -28,7 +28,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills protected override double StrainValueOf(DifficultyHitObject current) { var catchCurrent = (CatchDifficultyHitObject)current; - double halfCatcherWidth = catchCurrent.HalfCatcherWidth; if (lastPlayerPosition == null) lastPlayerPosition = catchCurrent.LastNormalizedPosition; @@ -46,17 +45,17 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills double distanceAddition = (Math.Pow(Math.Abs(distanceMoved), 1.3) / 510); double sqrtStrain = Math.Sqrt(weightedStrainTime); - double bonus = 0; + double edgeDashBonus = 0; // Direction changes give an extra point! if (Math.Abs(distanceMoved) > 0.1) { if (Math.Abs(lastDistanceMoved) > 0.1 && Math.Sign(distanceMoved) != Math.Sign(lastDistanceMoved)) { - double bonusFactor = Math.Min(halfCatcherWidth, Math.Abs(distanceMoved)) / halfCatcherWidth; - double antiflowFactor = Math.Max(Math.Min(halfCatcherWidth, Math.Abs(lastDistanceMoved)) / halfCatcherWidth, 0.3); + double bonusFactor = Math.Min(50, Math.Abs(distanceMoved)) / 50; + double antiflowFactor = Math.Max(Math.Min(70, Math.Abs(lastDistanceMoved)) / 70, 0.3); - distanceAddition += direction_change_bonus / Math.Sqrt(lastStrainTime + 18) * bonusFactor * antiflowFactor * Math.Max(1 - Math.Pow(weightedStrainTime / 1000, 2), 0); + distanceAddition += direction_change_bonus / Math.Sqrt(lastStrainTime + 18) * bonusFactor * antiflowFactor * Math.Max(1 - Math.Pow(weightedStrainTime / 1000, 3), 0); } // Base bonus for every movement, giving some weight to streams. @@ -67,14 +66,14 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills if (catchCurrent.LastObject.DistanceToHyperDash <= 20.0f / CatchPlayfield.BASE_WIDTH) { if (!catchCurrent.LastObject.HyperDash) - bonus += 5.7; + edgeDashBonus += 5.7; else { // After a hyperdash we ARE in the correct position. Always! playerPosition = catchCurrent.NormalizedPosition; } - distanceAddition *= 1.0 + bonus * ((20 - catchCurrent.LastObject.DistanceToHyperDash * CatchPlayfield.BASE_WIDTH) / 20) * Math.Pow((Math.Min(catchCurrent.StrainTime, 265) / 265), 1.5); + distanceAddition *= 1.0 + edgeDashBonus * ((20 - catchCurrent.LastObject.DistanceToHyperDash * CatchPlayfield.BASE_WIDTH) / 20) * Math.Pow((Math.Min(catchCurrent.StrainTime * catchCurrent.ClockRate, 265) / 265), 1.5); // Edge Dashes are easier at lower ms values } // Prevent wide dense stacks of notes which fit on the catcher from greatly increasing SR From 5566c4881a25af18e2b34407260966d148f51ff0 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 3 Apr 2019 11:38:48 +0900 Subject: [PATCH 0014/2376] Buff DT --- osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs index 4ea5135c4f..7d5a7b4007 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills float distanceMoved = playerPosition - lastPlayerPosition.Value; - double weightedStrainTime = catchCurrent.StrainTime + 18; + double weightedStrainTime = catchCurrent.StrainTime + 10 + (8 / catchCurrent.ClockRate); double distanceAddition = (Math.Pow(Math.Abs(distanceMoved), 1.3) / 510); double sqrtStrain = Math.Sqrt(weightedStrainTime); From 2824a32db60b99222ac2f862e1f54c980af0aab7 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 3 Apr 2019 11:39:13 +0900 Subject: [PATCH 0015/2376] Adjust circle-size bonus point --- .../Difficulty/CatchDifficultyCalculator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 4cd297478a..c56881ba51 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -52,8 +52,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty using (var catcher = new CatcherArea.Catcher(beatmap.BeatmapInfo.BaseDifficulty)) { halfCatchWidth = catcher.CatchWidth * 0.5f; - // We're only using 80% of the catcher's width to simulate imperfect gameplay. - halfCatchWidth *= Math.Min(1.05f - (0.05f * beatmap.BeatmapInfo.BaseDifficulty.CircleSize), 0.8f); // Reduce the catcher's width further at circle sizes above 5. + // We're only using 80% of the catcher's width to simulate imperfect gameplay, reduced further at circle sizes above 5.5 + halfCatchWidth *= Math.Min(1.075f - (0.05f * beatmap.BeatmapInfo.BaseDifficulty.CircleSize), 0.8f); } CatchHitObject lastObject = null; From 9d116efdbd8731443e46195f666aa1c21752aec5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 10 Apr 2019 10:57:27 +0900 Subject: [PATCH 0016/2376] Limit to 10000 tiny ticks per slider --- osu.Game.Rulesets.Catch/Objects/JuiceStream.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index 2adc156efd..9baa6a8531 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -55,6 +55,8 @@ namespace osu.Game.Rulesets.Catch.Objects SliderEventDescriptor? lastEvent = null; + int ticksGenerated = 0; + foreach (var e in SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset)) { // generate tiny droplets since the last point @@ -70,6 +72,9 @@ namespace osu.Game.Rulesets.Catch.Objects for (double t = timeBetweenTiny; t < sinceLastTick; t += timeBetweenTiny) { + if (ticksGenerated++ >= 10000) + break; + AddNested(new TinyDroplet { Samples = tickSamples, From d773eb2c22c804e97977298fb57bc3cd265a7530 Mon Sep 17 00:00:00 2001 From: mcendu Date: Wed, 5 Feb 2020 14:05:12 +0800 Subject: [PATCH 0017/2376] refactor rotation logic to use explicit delta value --- .../Objects/Drawables/Pieces/SpinnerDisc.cs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs index e3dd2b1b4f..91e49e0264 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs @@ -98,6 +98,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces bool validAndTracking = tracking && spinner.StartTime <= Time.Current && spinner.EndTime > Time.Current; + var delta = thisAngle - lastAngle; + if (validAndTracking) { if (!rotationTransferred) @@ -106,13 +108,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces rotationTransferred = true; } - if (thisAngle - lastAngle > 180) + if (delta > 180) + { lastAngle += 360; - else if (lastAngle - thisAngle > 180) + delta -= 360; + } + else if (-delta > 180) + { lastAngle -= 360; + delta += 360; + } - currentRotation += thisAngle - lastAngle; - RotationAbsolute += Math.Abs(thisAngle - lastAngle) * Math.Sign(Clock.ElapsedFrameTime); + currentRotation += delta; + RotationAbsolute += Math.Abs(delta) * Math.Sign(Clock.ElapsedFrameTime); } lastAngle = thisAngle; From 9f79713fb3a7b14e4f502d96b9b5bf9e417342cc Mon Sep 17 00:00:00 2001 From: mcendu Date: Wed, 5 Feb 2020 14:23:59 +0800 Subject: [PATCH 0018/2376] move rotation logic to its own method --- .../Objects/Drawables/Pieces/SpinnerDisc.cs | 49 ++++++++++--------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs index 91e49e0264..58132635ca 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs @@ -96,32 +96,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces var thisAngle = -MathUtils.RadiansToDegrees(MathF.Atan2(mousePosition.X - DrawSize.X / 2, mousePosition.Y - DrawSize.Y / 2)); - bool validAndTracking = tracking && spinner.StartTime <= Time.Current && spinner.EndTime > Time.Current; - var delta = thisAngle - lastAngle; + bool validAndTracking = tracking && spinner.StartTime <= Time.Current && spinner.EndTime > Time.Current; + if (validAndTracking) - { - if (!rotationTransferred) - { - currentRotation = Rotation * 2; - rotationTransferred = true; - } - - if (delta > 180) - { - lastAngle += 360; - delta -= 360; - } - else if (-delta > 180) - { - lastAngle -= 360; - delta += 360; - } - - currentRotation += delta; - RotationAbsolute += Math.Abs(delta) * Math.Sign(Clock.ElapsedFrameTime); - } + Rotate(delta); lastAngle = thisAngle; @@ -136,5 +116,28 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces this.RotateTo(currentRotation / 2, validAndTracking ? 500 : 1500, Easing.OutExpo); } + + public void Rotate(float angle) + { + if (!rotationTransferred) + { + currentRotation = Rotation * 2; + rotationTransferred = true; + } + + if (angle > 180) + { + lastAngle += 360; + angle -= 360; + } + else if (-angle > 180) + { + lastAngle -= 360; + angle += 360; + } + + currentRotation += angle; + RotationAbsolute += Math.Abs(angle) * Math.Sign(Clock.ElapsedFrameTime); + } } } From 25a930c43877007279a2503e34b4fa7702860f21 Mon Sep 17 00:00:00 2001 From: mcendu Date: Sat, 8 Feb 2020 08:59:35 +0800 Subject: [PATCH 0019/2376] Implement OsuModSpunOut --- osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 17 ++++++++++++++- .../Objects/Drawables/DrawableSpinner.cs | 2 +- .../Objects/Drawables/Pieces/SpinnerDisc.cs | 21 ++++++++++++------- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index 1cdcddbd33..1ef53542a8 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -2,13 +2,17 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModSpunOut : Mod + public class OsuModSpunOut : Mod, IApplicableToDrawableHitObjects { public override string Name => "Spun Out"; public override string Acronym => "SO"; @@ -18,5 +22,16 @@ namespace osu.Game.Rulesets.Osu.Mods public override double ScoreMultiplier => 0.9; public override bool Ranked => true; public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(OsuModAutopilot) }; + + public void ApplyToDrawableHitObjects(IEnumerable drawables) + { + foreach (var hitObject in drawables) + { + if (hitObject is DrawableSpinner spinner) + { + spinner.Disc.AutoSpin = true; + } + } + } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index de11ab6419..b5265babd9 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -177,7 +177,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void Update() { Disc.Tracking = OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false; - if (!SpmCounter.IsPresent && Disc.Tracking) + if (!SpmCounter.IsPresent && (Disc.Tracking || Disc.AutoSpin)) SpmCounter.FadeIn(HitObject.TimeFadeIn); base.Update(); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs index 58132635ca..e042a3791d 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs @@ -73,6 +73,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces } } + public bool AutoSpin { get; set; } = false; + protected override bool OnMouseMove(MouseMoveEvent e) { mousePosition = Parent.ToLocalSpace(e.ScreenSpaceMousePosition); @@ -94,16 +96,21 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { base.Update(); - var thisAngle = -MathUtils.RadiansToDegrees(MathF.Atan2(mousePosition.X - DrawSize.X / 2, mousePosition.Y - DrawSize.Y / 2)); + bool valid = spinner.StartTime <= Time.Current && spinner.EndTime > Time.Current; - var delta = thisAngle - lastAngle; + if (valid && AutoSpin) + Rotate(6f); + else + { + var thisAngle = -MathUtils.RadiansToDegrees(MathF.Atan2(mousePosition.X - DrawSize.X / 2, mousePosition.Y - DrawSize.Y / 2)); - bool validAndTracking = tracking && spinner.StartTime <= Time.Current && spinner.EndTime > Time.Current; + var delta = thisAngle - lastAngle; - if (validAndTracking) - Rotate(delta); + if (valid && tracking) + Rotate(delta); - lastAngle = thisAngle; + lastAngle = thisAngle; + } if (Complete && updateCompleteTick()) { @@ -114,7 +121,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces .FadeTo(tracking_alpha, 250, Easing.OutQuint); } - this.RotateTo(currentRotation / 2, validAndTracking ? 500 : 1500, Easing.OutExpo); + this.RotateTo(currentRotation / 2, 500, Easing.OutExpo); } public void Rotate(float angle) From 0dee6ceab74a3826c2705d55e50921f58d38dc52 Mon Sep 17 00:00:00 2001 From: mcendu Date: Sat, 8 Feb 2020 09:06:29 +0800 Subject: [PATCH 0020/2376] Remove unnecessary using --- osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index 1ef53542a8..f1a1e47118 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables; From 4d9232a895ad4dd12e43cc0c9fc0b519a62091a2 Mon Sep 17 00:00:00 2001 From: mcendu Date: Sat, 8 Feb 2020 09:51:32 +0800 Subject: [PATCH 0021/2376] Move autospin logic to mods --- osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 24 +++++++++++++++++-- .../Objects/Drawables/DrawableSpinner.cs | 2 +- .../Objects/Drawables/Pieces/SpinnerDisc.cs | 21 ++++++---------- 3 files changed, 30 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index f1a1e47118..07c10966d3 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -3,15 +3,18 @@ using System; using System.Collections.Generic; +using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; +using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModSpunOut : Mod, IApplicableToDrawableHitObjects + public class OsuModSpunOut : Mod, IApplicableToDrawableHitObjects, IUpdatableByPlayfield { public override string Name => "Spun Out"; public override string Acronym => "SO"; @@ -22,15 +25,32 @@ namespace osu.Game.Rulesets.Osu.Mods public override bool Ranked => true; public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(OsuModAutopilot) }; + private double lastFrameTime; + private double frameDelay; + public void ApplyToDrawableHitObjects(IEnumerable drawables) { foreach (var hitObject in drawables) { if (hitObject is DrawableSpinner spinner) { - spinner.Disc.AutoSpin = true; + spinner.Disc.Trackable = false; + spinner.Disc.OnUpdate += d => + { + if (d is SpinnerDisc s) + { + if (s.Valid) + s.Rotate((float)frameDelay); + } + }; } } } + + public void Update(Playfield playfield) + { + frameDelay = playfield.Time.Current - lastFrameTime; + lastFrameTime = playfield.Time.Current; + } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index b5265babd9..2930134d4f 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -177,7 +177,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void Update() { Disc.Tracking = OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false; - if (!SpmCounter.IsPresent && (Disc.Tracking || Disc.AutoSpin)) + if (!SpmCounter.IsPresent && (Disc.Tracking || !Disc.Trackable)) SpmCounter.FadeIn(HitObject.TimeFadeIn); base.Update(); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs index e042a3791d..9a9d915cfe 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs @@ -73,7 +73,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces } } - public bool AutoSpin { get; set; } = false; + public bool Valid => spinner.StartTime <= Time.Current && spinner.EndTime > Time.Current; + public bool Trackable { get; set; } protected override bool OnMouseMove(MouseMoveEvent e) { @@ -95,22 +96,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces protected override void Update() { base.Update(); + var thisAngle = -MathUtils.RadiansToDegrees(MathF.Atan2(mousePosition.X - DrawSize.X / 2, mousePosition.Y - DrawSize.Y / 2)); - bool valid = spinner.StartTime <= Time.Current && spinner.EndTime > Time.Current; + var delta = thisAngle - lastAngle; - if (valid && AutoSpin) - Rotate(6f); - else - { - var thisAngle = -MathUtils.RadiansToDegrees(MathF.Atan2(mousePosition.X - DrawSize.X / 2, mousePosition.Y - DrawSize.Y / 2)); + if (Valid && tracking && Trackable) + Rotate(delta); - var delta = thisAngle - lastAngle; - - if (valid && tracking) - Rotate(delta); - - lastAngle = thisAngle; - } + lastAngle = thisAngle; if (Complete && updateCompleteTick()) { From ca09ae6849b94cdc2ef15c4b770212d1f8a92138 Mon Sep 17 00:00:00 2001 From: mcendu Date: Sat, 8 Feb 2020 09:53:20 +0800 Subject: [PATCH 0022/2376] fix formatting --- osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index 07c10966d3..c74e4e3e70 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Mods if (d is SpinnerDisc s) { if (s.Valid) - s.Rotate((float)frameDelay); + s.Rotate((float)frameDelay); } }; } From 204c2f0bde7cbd36563842d0f4831a65d2308fcc Mon Sep 17 00:00:00 2001 From: mcendu Date: Sat, 8 Feb 2020 10:16:04 +0800 Subject: [PATCH 0023/2376] add tests --- osu.Game.Rulesets.Osu.Tests/Class1.cs | 23 +++++++++++++++++++++ osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 1 - 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Rulesets.Osu.Tests/Class1.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Class1.cs b/osu.Game.Rulesets.Osu.Tests/Class1.cs new file mode 100644 index 0000000000..402c14fa64 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Class1.cs @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Game.Rulesets.Osu.Mods; + +namespace osu.Game.Rulesets.Osu.Tests +{ + [TestFixture] + public class TestSceneSpinnerSpunOut : TestSceneSpinner + { + public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] { typeof(OsuModSpunOut) }).ToList(); + + [SetUp] + public void SetUp() => Schedule(() => + { + SelectedMods.Value = new[] { new OsuModHidden() }; + }); + } +} diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index c74e4e3e70..eb49742db6 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Rulesets.Mods; From 715608c7985c2366905751be856b3984b6cf94cd Mon Sep 17 00:00:00 2001 From: mcendu Date: Sat, 8 Feb 2020 10:49:49 +0800 Subject: [PATCH 0024/2376] Fix test applying incorrect mod --- .../{Class1.cs => TestSceneSpinnerSpunOut.cs} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename osu.Game.Rulesets.Osu.Tests/{Class1.cs => TestSceneSpinnerSpunOut.cs} (90%) diff --git a/osu.Game.Rulesets.Osu.Tests/Class1.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerSpunOut.cs similarity index 90% rename from osu.Game.Rulesets.Osu.Tests/Class1.cs rename to osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerSpunOut.cs index 402c14fa64..a6c09691c7 100644 --- a/osu.Game.Rulesets.Osu.Tests/Class1.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerSpunOut.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Osu.Tests [SetUp] public void SetUp() => Schedule(() => { - SelectedMods.Value = new[] { new OsuModHidden() }; + SelectedMods.Value = new[] { new OsuModSpunOut() }; }); } } From efa95ecebb66fe587c6c2e9862c384586b29dbf9 Mon Sep 17 00:00:00 2001 From: mcendu Date: Sat, 8 Feb 2020 10:52:59 +0800 Subject: [PATCH 0025/2376] fix spinner unspinnable --- osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs index 9a9d915cfe..4e2758b3d5 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs @@ -74,7 +74,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces } public bool Valid => spinner.StartTime <= Time.Current && spinner.EndTime > Time.Current; - public bool Trackable { get; set; } + public bool Trackable { get; set; } = true; protected override bool OnMouseMove(MouseMoveEvent e) { From fbdf07dc201c87140c00be1121d227d003c3dd1e Mon Sep 17 00:00:00 2001 From: mcendu Date: Sat, 8 Feb 2020 11:06:37 +0800 Subject: [PATCH 0026/2376] Correct speed of spun out --- osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index eb49742db6..16fc7646c2 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Osu.Mods if (d is SpinnerDisc s) { if (s.Valid) - s.Rotate((float)frameDelay); + s.Rotate(180 / MathF.PI * ((float)frameDelay) / 40); } }; } From d821b6a15aee2f2d0d7559828a6a04752546ddba Mon Sep 17 00:00:00 2001 From: mcendu Date: Sat, 8 Feb 2020 11:16:48 +0800 Subject: [PATCH 0027/2376] make frameDelay a float --- osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index 16fc7646c2..1832910e71 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(OsuModAutopilot) }; private double lastFrameTime; - private double frameDelay; + private float frameDelay; public void ApplyToDrawableHitObjects(IEnumerable drawables) { @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Osu.Mods if (d is SpinnerDisc s) { if (s.Valid) - s.Rotate(180 / MathF.PI * ((float)frameDelay) / 40); + s.Rotate(180 / MathF.PI * frameDelay / 40); } }; } @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Mods public void Update(Playfield playfield) { - frameDelay = playfield.Time.Current - lastFrameTime; + frameDelay = (float)(playfield.Time.Current - lastFrameTime); lastFrameTime = playfield.Time.Current; } } From 2d672159317783629f3e5334621fc7341942531e Mon Sep 17 00:00:00 2001 From: mcendu Date: Sat, 8 Feb 2020 12:07:21 +0800 Subject: [PATCH 0028/2376] make target practice subject of unimplemented mod test --- .../Visual/UserInterface/TestSceneModSelectOverlay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 12ee4ceb2e..1e18e18631 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -97,7 +97,7 @@ namespace osu.Game.Tests.Visual.UserInterface var doubleTimeMod = harderMods.OfType().FirstOrDefault(m => m.Mods.Any(a => a is OsuModDoubleTime)); - var spunOutMod = easierMods.FirstOrDefault(m => m is OsuModSpunOut); + var targetMod = easierMods.FirstOrDefault(m => m is OsuModTarget); var easy = easierMods.FirstOrDefault(m => m is OsuModEasy); var hardRock = harderMods.FirstOrDefault(m => m is OsuModHardRock); @@ -109,7 +109,7 @@ namespace osu.Game.Tests.Visual.UserInterface testMultiplierTextColour(noFailMod, () => modSelect.LowMultiplierColour); testMultiplierTextColour(hiddenMod, () => modSelect.HighMultiplierColour); - testUnimplementedMod(spunOutMod); + testUnimplementedMod(targetMod); } [Test] From a4637a24a6c6d57a23f18db87ac954577ff9e1c3 Mon Sep 17 00:00:00 2001 From: mcendu Date: Sat, 8 Feb 2020 12:08:44 +0800 Subject: [PATCH 0029/2376] fix test scene crash --- .../Visual/UserInterface/TestSceneModSelectOverlay.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 1e18e18631..034324aadd 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -91,13 +91,14 @@ namespace osu.Game.Tests.Visual.UserInterface var easierMods = osu.GetModsFor(ModType.DifficultyReduction); var harderMods = osu.GetModsFor(ModType.DifficultyIncrease); + var conversionMods = osu.GetModsFor(ModType.Conversion); var noFailMod = osu.GetModsFor(ModType.DifficultyReduction).FirstOrDefault(m => m is OsuModNoFail); var hiddenMod = harderMods.FirstOrDefault(m => m is OsuModHidden); var doubleTimeMod = harderMods.OfType().FirstOrDefault(m => m.Mods.Any(a => a is OsuModDoubleTime)); - var targetMod = easierMods.FirstOrDefault(m => m is OsuModTarget); + var targetMod = conversionMods.FirstOrDefault(m => m is OsuModTarget); var easy = easierMods.FirstOrDefault(m => m is OsuModEasy); var hardRock = harderMods.FirstOrDefault(m => m is OsuModHardRock); From 8e20e641f440d4a2dff2c49b60816b0021a1cd55 Mon Sep 17 00:00:00 2001 From: mcendu Date: Sun, 9 Feb 2020 13:33:43 +0800 Subject: [PATCH 0030/2376] move spun out to automation --- osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index b794f5e22e..c4890afe36 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override string Name => "Spun Out"; public override string Acronym => "SO"; public override IconUsage? Icon => OsuIcon.ModSpunout; - public override ModType Type => ModType.DifficultyReduction; + public override ModType Type => ModType.Automation; public override string Description => @"Spinners will be automatically completed."; public override double ScoreMultiplier => 0.9; public override bool Ranked => true; From 83c67dc155518d1a9b2432432e0a4f250c370544 Mon Sep 17 00:00:00 2001 From: mcendu Date: Sun, 9 Feb 2020 13:34:35 +0800 Subject: [PATCH 0031/2376] move spun out to automation --- osu.Game.Rulesets.Osu/OsuRuleset.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 148869f5e8..ed73a54815 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -113,7 +113,6 @@ namespace osu.Game.Rulesets.Osu new OsuModEasy(), new OsuModNoFail(), new MultiMod(new OsuModHalfTime(), new OsuModDaycore()), - new OsuModSpunOut(), }; case ModType.DifficultyIncrease: @@ -139,6 +138,7 @@ namespace osu.Game.Rulesets.Osu new MultiMod(new OsuModAutoplay(), new OsuModCinema()), new OsuModRelax(), new OsuModAutopilot(), + new OsuModSpunOut(), }; case ModType.Fun: From c9520b299a40cd23d228dd62b38c87a1ee3b9632 Mon Sep 17 00:00:00 2001 From: mcendu Date: Sun, 9 Feb 2020 13:35:36 +0800 Subject: [PATCH 0032/2376] replace if check with variable decl --- osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index c4890afe36..6a2610ae05 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -36,11 +36,10 @@ namespace osu.Game.Rulesets.Osu.Mods spinner.Disc.Trackable = false; spinner.Disc.OnUpdate += d => { - if (d is SpinnerDisc s) - { - if (s.Valid) - s.Rotate(180 / MathF.PI * frameDelay / 40); - } + var s = d as SpinnerDisc; + + if (s.Valid) + s.Rotate(180 / MathF.PI * frameDelay / 40); }; } } From d314b38699d1211d034cc896a478b809d8aa15e5 Mon Sep 17 00:00:00 2001 From: mcendu Date: Sun, 9 Feb 2020 13:46:06 +0800 Subject: [PATCH 0033/2376] rename trackable to enabled and cleanup code --- osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 2 +- .../Objects/Drawables/DrawableSpinner.cs | 2 +- .../Objects/Drawables/Pieces/SpinnerDisc.cs | 9 +++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index 6a2610ae05..e02ded979f 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Mods { if (hitObject is DrawableSpinner spinner) { - spinner.Disc.Trackable = false; + spinner.Disc.Enabled = false; spinner.Disc.OnUpdate += d => { var s = d as SpinnerDisc; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 2930134d4f..de11ab6419 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -177,7 +177,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void Update() { Disc.Tracking = OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false; - if (!SpmCounter.IsPresent && (Disc.Tracking || !Disc.Trackable)) + if (!SpmCounter.IsPresent && Disc.Tracking) SpmCounter.FadeIn(HitObject.TimeFadeIn); base.Update(); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs index 4e2758b3d5..b062fc5afa 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs @@ -50,9 +50,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces get => tracking; set { - if (value == tracking) return; + if ((Enabled && value) == tracking) return; - tracking = value; + tracking = Enabled && value; background.FadeTo(tracking ? tracking_alpha : idle_alpha, 100); } @@ -74,7 +74,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces } public bool Valid => spinner.StartTime <= Time.Current && spinner.EndTime > Time.Current; - public bool Trackable { get; set; } = true; + + public bool Enabled { get; set; } = true; protected override bool OnMouseMove(MouseMoveEvent e) { @@ -100,7 +101,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces var delta = thisAngle - lastAngle; - if (Valid && tracking && Trackable) + if (Valid && tracking) Rotate(delta); lastAngle = thisAngle; From 68873830aadfde6495812766820c3b9b78dc94d3 Mon Sep 17 00:00:00 2001 From: mcendu Date: Sun, 9 Feb 2020 13:49:08 +0800 Subject: [PATCH 0034/2376] make spm counter show up automatically with spun out --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index de11ab6419..752bd7be85 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -177,7 +177,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void Update() { Disc.Tracking = OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false; - if (!SpmCounter.IsPresent && Disc.Tracking) + if (!SpmCounter.IsPresent && (Disc.Tracking || !Disc.Enabled)) SpmCounter.FadeIn(HitObject.TimeFadeIn); base.Update(); From 596f4f7d2e6c62c7a4c3fa43ce161c5b71c388d8 Mon Sep 17 00:00:00 2001 From: mcendu Date: Sun, 9 Feb 2020 14:54:46 +0800 Subject: [PATCH 0035/2376] use spinner's clock to drive spinner --- osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index e02ded979f..084be40672 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -13,7 +13,7 @@ using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModSpunOut : Mod, IApplicableToDrawableHitObjects, IUpdatableByPlayfield + public class OsuModSpunOut : Mod, IApplicableToDrawableHitObjects { public override string Name => "Spun Out"; public override string Acronym => "SO"; @@ -24,9 +24,6 @@ namespace osu.Game.Rulesets.Osu.Mods public override bool Ranked => true; public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(OsuModAutopilot) }; - private double lastFrameTime; - private float frameDelay; - public void ApplyToDrawableHitObjects(IEnumerable drawables) { foreach (var hitObject in drawables) @@ -39,16 +36,10 @@ namespace osu.Game.Rulesets.Osu.Mods var s = d as SpinnerDisc; if (s.Valid) - s.Rotate(180 / MathF.PI * frameDelay / 40); + s.Rotate(180 / MathF.PI * (float)s.Clock.ElapsedFrameTime / 40); }; } } } - - public void Update(Playfield playfield) - { - frameDelay = (float)(playfield.Time.Current - lastFrameTime); - lastFrameTime = playfield.Time.Current; - } } } From e78d94d4692d02ed5843e5f23882c660e23099d1 Mon Sep 17 00:00:00 2001 From: mcendu Date: Sun, 9 Feb 2020 14:56:47 +0800 Subject: [PATCH 0036/2376] return to use if for casting --- osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index 084be40672..2c1d1362a6 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -33,9 +33,7 @@ namespace osu.Game.Rulesets.Osu.Mods spinner.Disc.Enabled = false; spinner.Disc.OnUpdate += d => { - var s = d as SpinnerDisc; - - if (s.Valid) + if (d is SpinnerDisc s && s.Valid) s.Rotate(180 / MathF.PI * (float)s.Clock.ElapsedFrameTime / 40); }; } From 10a1948720b15e8de61149ff7abfcad89d5eca8d Mon Sep 17 00:00:00 2001 From: mcendu Date: Sun, 9 Feb 2020 16:19:21 +0800 Subject: [PATCH 0037/2376] remove using directive --- osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index 2c1d1362a6..d56e39b588 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -9,7 +9,6 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; -using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Osu.Mods { From 9aa5db88d4baed09dcda72005845cf2dad27e9f0 Mon Sep 17 00:00:00 2001 From: mcendu Date: Mon, 10 Feb 2020 14:14:04 +0800 Subject: [PATCH 0038/2376] move auto fade in to mod --- osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 13 +++++++++---- .../Objects/Drawables/DrawableSpinner.cs | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index d56e39b588..670f4a2cd8 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -3,12 +3,12 @@ using System; using System.Collections.Generic; +using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables; -using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; namespace osu.Game.Rulesets.Osu.Mods { @@ -30,10 +30,15 @@ namespace osu.Game.Rulesets.Osu.Mods if (hitObject is DrawableSpinner spinner) { spinner.Disc.Enabled = false; - spinner.Disc.OnUpdate += d => + spinner.OnUpdate += d => { - if (d is SpinnerDisc s && s.Valid) - s.Rotate(180 / MathF.PI * (float)s.Clock.ElapsedFrameTime / 40); + if (d is DrawableSpinner s) + { + if (s.Disc.Valid) + s.Disc.Rotate(180 / MathF.PI * (float)s.Clock.ElapsedFrameTime / 40); + if (!s.SpmCounter.IsPresent) + s.SpmCounter.FadeIn(s.HitObject.TimeFadeIn); + } }; } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 752bd7be85..de11ab6419 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -177,7 +177,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void Update() { Disc.Tracking = OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false; - if (!SpmCounter.IsPresent && (Disc.Tracking || !Disc.Enabled)) + if (!SpmCounter.IsPresent && Disc.Tracking) SpmCounter.FadeIn(HitObject.TimeFadeIn); base.Update(); From 403c03841d1d3497dfda7ffc3af8f2fb72d2daba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Feb 2020 21:39:45 +0100 Subject: [PATCH 0039/2376] Decouple test scene & add assertions --- .../TestSceneSpinnerSpunOut.cs | 51 ++++++++++++++++++- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerSpunOut.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerSpunOut.cs index a6c09691c7..e406f9ddff 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerSpunOut.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerSpunOut.cs @@ -5,19 +5,66 @@ using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; +using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Osu.Tests { [TestFixture] - public class TestSceneSpinnerSpunOut : TestSceneSpinner + public class TestSceneSpinnerSpunOut : OsuTestScene { - public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] { typeof(OsuModSpunOut) }).ToList(); + public override IReadOnlyList RequiredTypes => new[] + { + typeof(SpinnerDisc), + typeof(DrawableSpinner), + typeof(DrawableOsuHitObject), + typeof(OsuModSpunOut) + }; [SetUp] public void SetUp() => Schedule(() => { SelectedMods.Value = new[] { new OsuModSpunOut() }; }); + + [Test] + public void TestSpunOut() + { + DrawableSpinner spinner = null; + + AddStep("create spinner", () => spinner = createSpinner()); + + AddUntilStep("wait for end", () => Time.Current > spinner.LifetimeEnd); + + AddAssert("spinner is completed", () => spinner.Progress >= 1); + } + + private DrawableSpinner createSpinner() + { + var spinner = new Spinner + { + StartTime = Time.Current + 500, + EndTime = Time.Current + 2500 + }; + spinner.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + + var drawableSpinner = new DrawableSpinner(spinner) + { + Anchor = Anchor.Centre + }; + + foreach (var mod in SelectedMods.Value.OfType()) + mod.ApplyToDrawableHitObjects(new[] { drawableSpinner }); + + Add(drawableSpinner); + return drawableSpinner; + } } } From 686040d8ad7926ad515fcb1bfcf586ca8b1d5d04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Feb 2020 21:42:34 +0100 Subject: [PATCH 0040/2376] Extract auto-spin logic to method --- osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 22 +++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index 670f4a2cd8..37ef001223 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -30,18 +30,20 @@ namespace osu.Game.Rulesets.Osu.Mods if (hitObject is DrawableSpinner spinner) { spinner.Disc.Enabled = false; - spinner.OnUpdate += d => - { - if (d is DrawableSpinner s) - { - if (s.Disc.Valid) - s.Disc.Rotate(180 / MathF.PI * (float)s.Clock.ElapsedFrameTime / 40); - if (!s.SpmCounter.IsPresent) - s.SpmCounter.FadeIn(s.HitObject.TimeFadeIn); - } - }; + spinner.OnUpdate += autoSpin; } } } + + private void autoSpin(Drawable drawable) + { + if (drawable is DrawableSpinner spinner) + { + if (spinner.Disc.Valid) + spinner.Disc.Rotate(180 / MathF.PI * (float)spinner.Clock.ElapsedFrameTime / 40); + if (!spinner.SpmCounter.IsPresent) + spinner.SpmCounter.FadeIn(spinner.HitObject.TimeFadeIn); + } + } } } From b6378c7ae22ef88365a604a34289f574d6decae1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=9CNate?= Date: Sun, 16 Feb 2020 17:50:52 +0800 Subject: [PATCH 0041/2376] fix speed * Original /40 is due to documentation error. Co-Authored-By: clayton --- osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index 37ef001223..30b9c84538 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Mods if (drawable is DrawableSpinner spinner) { if (spinner.Disc.Valid) - spinner.Disc.Rotate(180 / MathF.PI * (float)spinner.Clock.ElapsedFrameTime / 40); + spinner.Disc.Rotate(180 / MathF.PI * (float)spinner.Clock.ElapsedFrameTime * 0.03f); if (!spinner.SpmCounter.IsPresent) spinner.SpmCounter.FadeIn(spinner.HitObject.TimeFadeIn); } From 5a0b93bdb2f2000cc7360319982cf652d0bdbbe1 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 20 Feb 2020 17:02:22 +0300 Subject: [PATCH 0042/2376] Add ShowTag method --- .../Online/TestSceneBeatmapListingOverlay.cs | 6 ++++++ osu.Game/Overlays/BeatmapListingOverlay.cs | 20 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs index 7c05d99c59..4aea29faa0 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs @@ -24,6 +24,12 @@ namespace osu.Game.Tests.Visual.Online Add(overlay = new BeatmapListingOverlay()); } + [Test] + public void TestShowTag() + { + AddStep("Show Rem tag", () => overlay.ShowTag("Rem")); + } + [Test] public void TestShow() { diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index 213e9a4244..e212d05442 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -158,6 +158,26 @@ namespace osu.Game.Overlays sortDirection.BindValueChanged(_ => queueUpdateSearch()); } + public void ShowTag(string tag) + { + var currentQuery = searchSection.Query.Value; + + if (currentQuery != tag) + { + setDefaultSearchValues(); + searchSection.Query.Value = tag; + } + + Show(); + } + + private void setDefaultSearchValues() + { + searchSection.Query.Value = string.Empty; + searchSection.Ruleset.Value = new RulesetInfo { Name = @"Any" }; + searchSection.Category.Value = BeatmapSearchCategory.Leaderboard; + } + private ScheduledDelegate queryChangedDebounce; private void queueUpdateSearch(bool queryTextChanged = false) From 6b2ae67eafda3bef8a2d720865f1ec2851e4fa86 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 20 Feb 2020 17:40:45 +0300 Subject: [PATCH 0043/2376] Implement Genre filter --- .../Online/TestSceneBeatmapListingOverlay.cs | 7 +++++ .../API/Requests/SearchBeatmapSetsRequest.cs | 26 ++++++++++++++++++- .../BeatmapListingSearchSection.cs | 4 +++ osu.Game/Overlays/BeatmapListingOverlay.cs | 18 ++++++++++++- 4 files changed, 53 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs index 4aea29faa0..9dec965818 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using osu.Game.Overlays; using NUnit.Framework; +using osu.Game.Online.API.Requests; namespace osu.Game.Tests.Visual.Online { @@ -30,6 +31,12 @@ namespace osu.Game.Tests.Visual.Online AddStep("Show Rem tag", () => overlay.ShowTag("Rem")); } + [Test] + public void TestShowGenre() + { + AddStep("Show Anime genre", () => overlay.ShowGenre(BeatmapSearchGenre.Anime)); + } + [Test] public void TestShow() { diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs index 930ca8fdf1..797a0a1015 100644 --- a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs +++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs @@ -16,15 +16,17 @@ namespace osu.Game.Online.API.Requests private readonly BeatmapSearchCategory searchCategory; private readonly DirectSortCriteria sortCriteria; private readonly SortDirection direction; + private readonly BeatmapSearchGenre genre; private string directionString => direction == SortDirection.Descending ? @"desc" : @"asc"; - public SearchBeatmapSetsRequest(string query, RulesetInfo ruleset, BeatmapSearchCategory searchCategory = BeatmapSearchCategory.Any, DirectSortCriteria sortCriteria = DirectSortCriteria.Ranked, SortDirection direction = SortDirection.Descending) + public SearchBeatmapSetsRequest(string query, RulesetInfo ruleset, BeatmapSearchCategory searchCategory = BeatmapSearchCategory.Any, DirectSortCriteria sortCriteria = DirectSortCriteria.Ranked, SortDirection direction = SortDirection.Descending, BeatmapSearchGenre genre = BeatmapSearchGenre.Any) { this.query = string.IsNullOrEmpty(query) ? string.Empty : System.Uri.EscapeDataString(query); this.ruleset = ruleset; this.searchCategory = searchCategory; this.sortCriteria = sortCriteria; this.direction = direction; + this.genre = genre; } protected override WebRequest CreateWebRequest() @@ -36,6 +38,10 @@ namespace osu.Game.Online.API.Requests req.AddParameter("m", ruleset.ID.Value.ToString()); req.AddParameter("s", searchCategory.ToString().ToLowerInvariant()); + + if (genre != BeatmapSearchGenre.Any) + req.AddParameter("g", ((int)genre).ToString()); + req.AddParameter("sort", $"{sortCriteria.ToString().ToLowerInvariant()}_{directionString}"); return req; @@ -62,4 +68,22 @@ namespace osu.Game.Online.API.Requests [Description("My Maps")] Mine, } + + public enum BeatmapSearchGenre + { + Any, + Unspecified, + + [Description("Video Game")] + Game, + Anime, + Rock, + Pop, + Other, + Novelty, + + [Description("Hip Hop")] + Hiphop = 9, + Electronic + } } diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchSection.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchSection.cs index f9799d8a6b..1e97720705 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchSection.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchSection.cs @@ -25,6 +25,8 @@ namespace osu.Game.Overlays.BeatmapListing public Bindable Category => categoryFilter.Current; + public Bindable Genre => genreFilter.Current; + public BeatmapSetInfo BeatmapSet { set @@ -43,6 +45,7 @@ namespace osu.Game.Overlays.BeatmapListing private readonly BeatmapSearchTextBox textBox; private readonly BeatmapSearchRulesetFilterRow modeFilter; private readonly BeatmapSearchFilterRow categoryFilter; + private readonly BeatmapSearchSmallFilterRow genreFilter; private readonly Box background; private readonly UpdateableBeatmapSetCover beatmapCover; @@ -98,6 +101,7 @@ namespace osu.Game.Overlays.BeatmapListing { modeFilter = new BeatmapSearchRulesetFilterRow(), categoryFilter = new BeatmapSearchFilterRow(@"Categories"), + genreFilter = new BeatmapSearchSmallFilterRow(@"Genre"), } } } diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index e212d05442..604af971f3 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -154,6 +154,7 @@ namespace osu.Game.Overlays searchSection.Ruleset.BindValueChanged(_ => queueUpdateSearch()); searchSection.Category.BindValueChanged(_ => queueUpdateSearch()); + searchSection.Genre.BindValueChanged(_ => queueUpdateSearch()); sortCriteria.BindValueChanged(_ => queueUpdateSearch()); sortDirection.BindValueChanged(_ => queueUpdateSearch()); } @@ -171,11 +172,25 @@ namespace osu.Game.Overlays Show(); } + public void ShowGenre(BeatmapSearchGenre genre) + { + var currentGenre = searchSection.Genre.Value; + + if (currentGenre != genre) + { + setDefaultSearchValues(); + searchSection.Genre.Value = genre; + } + + Show(); + } + private void setDefaultSearchValues() { searchSection.Query.Value = string.Empty; searchSection.Ruleset.Value = new RulesetInfo { Name = @"Any" }; searchSection.Category.Value = BeatmapSearchCategory.Leaderboard; + searchSection.Genre.Value = BeatmapSearchGenre.Any; } private ScheduledDelegate queryChangedDebounce; @@ -208,7 +223,8 @@ namespace osu.Game.Overlays searchSection.Ruleset.Value, searchSection.Category.Value, sortControl.Current.Value, - sortControl.SortDirection.Value); + sortControl.SortDirection.Value, + searchSection.Genre.Value); getSetsRequest.Success += response => Schedule(() => recreatePanels(response)); From 063a53017e82a4b883dd088bfb4ca36d72a9a6f5 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 20 Feb 2020 17:56:49 +0300 Subject: [PATCH 0044/2376] Implement Language filter --- .../Online/TestSceneBeatmapListingOverlay.cs | 6 +++++ .../API/Requests/SearchBeatmapSetsRequest.cs | 23 ++++++++++++++++++- .../BeatmapListingSearchSection.cs | 4 ++++ osu.Game/Overlays/BeatmapListingOverlay.cs | 18 ++++++++++++++- 4 files changed, 49 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs index 9dec965818..4dceb57129 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs @@ -37,6 +37,12 @@ namespace osu.Game.Tests.Visual.Online AddStep("Show Anime genre", () => overlay.ShowGenre(BeatmapSearchGenre.Anime)); } + [Test] + public void TestShowLanguage() + { + AddStep("Show Japanese language", () => overlay.ShowLanguage(BeatmapSearchLanguage.Japanese)); + } + [Test] public void TestShow() { diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs index 797a0a1015..c2679fcd5f 100644 --- a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs +++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs @@ -17,9 +17,10 @@ namespace osu.Game.Online.API.Requests private readonly DirectSortCriteria sortCriteria; private readonly SortDirection direction; private readonly BeatmapSearchGenre genre; + private readonly BeatmapSearchLanguage language; private string directionString => direction == SortDirection.Descending ? @"desc" : @"asc"; - public SearchBeatmapSetsRequest(string query, RulesetInfo ruleset, BeatmapSearchCategory searchCategory = BeatmapSearchCategory.Any, DirectSortCriteria sortCriteria = DirectSortCriteria.Ranked, SortDirection direction = SortDirection.Descending, BeatmapSearchGenre genre = BeatmapSearchGenre.Any) + public SearchBeatmapSetsRequest(string query, RulesetInfo ruleset, BeatmapSearchCategory searchCategory = BeatmapSearchCategory.Any, DirectSortCriteria sortCriteria = DirectSortCriteria.Ranked, SortDirection direction = SortDirection.Descending, BeatmapSearchGenre genre = BeatmapSearchGenre.Any, BeatmapSearchLanguage language = BeatmapSearchLanguage.Any) { this.query = string.IsNullOrEmpty(query) ? string.Empty : System.Uri.EscapeDataString(query); this.ruleset = ruleset; @@ -27,6 +28,7 @@ namespace osu.Game.Online.API.Requests this.sortCriteria = sortCriteria; this.direction = direction; this.genre = genre; + this.language = language; } protected override WebRequest CreateWebRequest() @@ -42,6 +44,9 @@ namespace osu.Game.Online.API.Requests if (genre != BeatmapSearchGenre.Any) req.AddParameter("g", ((int)genre).ToString()); + if (language != BeatmapSearchLanguage.Any) + req.AddParameter("l", ((int)language).ToString()); + req.AddParameter("sort", $"{sortCriteria.ToString().ToLowerInvariant()}_{directionString}"); return req; @@ -86,4 +91,20 @@ namespace osu.Game.Online.API.Requests Hiphop = 9, Electronic } + + public enum BeatmapSearchLanguage + { + Any, + English = 2, + Chilnese = 4, + French = 7, + German, + Italian = 11, + Japanese = 3, + Korean = 6, + Spanish = 10, + Swedish = 9, + Instrumantal = 5, + Other = 1 + } } diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchSection.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchSection.cs index 1e97720705..28619ea6fe 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchSection.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchSection.cs @@ -27,6 +27,8 @@ namespace osu.Game.Overlays.BeatmapListing public Bindable Genre => genreFilter.Current; + public Bindable Language => languageFilter.Current; + public BeatmapSetInfo BeatmapSet { set @@ -46,6 +48,7 @@ namespace osu.Game.Overlays.BeatmapListing private readonly BeatmapSearchRulesetFilterRow modeFilter; private readonly BeatmapSearchFilterRow categoryFilter; private readonly BeatmapSearchSmallFilterRow genreFilter; + private readonly BeatmapSearchSmallFilterRow languageFilter; private readonly Box background; private readonly UpdateableBeatmapSetCover beatmapCover; @@ -102,6 +105,7 @@ namespace osu.Game.Overlays.BeatmapListing modeFilter = new BeatmapSearchRulesetFilterRow(), categoryFilter = new BeatmapSearchFilterRow(@"Categories"), genreFilter = new BeatmapSearchSmallFilterRow(@"Genre"), + languageFilter = new BeatmapSearchSmallFilterRow(@"Language"), } } } diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index 604af971f3..414dabd7c1 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -155,6 +155,7 @@ namespace osu.Game.Overlays searchSection.Ruleset.BindValueChanged(_ => queueUpdateSearch()); searchSection.Category.BindValueChanged(_ => queueUpdateSearch()); searchSection.Genre.BindValueChanged(_ => queueUpdateSearch()); + searchSection.Language.BindValueChanged(_ => queueUpdateSearch()); sortCriteria.BindValueChanged(_ => queueUpdateSearch()); sortDirection.BindValueChanged(_ => queueUpdateSearch()); } @@ -185,12 +186,26 @@ namespace osu.Game.Overlays Show(); } + public void ShowLanguage(BeatmapSearchLanguage language) + { + var currentLanguage = searchSection.Language.Value; + + if (currentLanguage != language) + { + setDefaultSearchValues(); + searchSection.Language.Value = language; + } + + Show(); + } + private void setDefaultSearchValues() { searchSection.Query.Value = string.Empty; searchSection.Ruleset.Value = new RulesetInfo { Name = @"Any" }; searchSection.Category.Value = BeatmapSearchCategory.Leaderboard; searchSection.Genre.Value = BeatmapSearchGenre.Any; + searchSection.Language.Value = BeatmapSearchLanguage.Any; } private ScheduledDelegate queryChangedDebounce; @@ -224,7 +239,8 @@ namespace osu.Game.Overlays searchSection.Category.Value, sortControl.Current.Value, sortControl.SortDirection.Value, - searchSection.Genre.Value); + searchSection.Genre.Value, + searchSection.Language.Value); getSetsRequest.Success += response => Schedule(() => recreatePanels(response)); From eeae0a57746a4ed70199439499561b55f8311f2e Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Fri, 21 Feb 2020 00:56:33 +0300 Subject: [PATCH 0045/2376] Fix typos --- osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs index c2679fcd5f..1c1da33d8a 100644 --- a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs +++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs @@ -80,7 +80,7 @@ namespace osu.Game.Online.API.Requests Unspecified, [Description("Video Game")] - Game, + VideoGame, Anime, Rock, Pop, @@ -88,7 +88,7 @@ namespace osu.Game.Online.API.Requests Novelty, [Description("Hip Hop")] - Hiphop = 9, + HipHop = 9, Electronic } @@ -96,7 +96,7 @@ namespace osu.Game.Online.API.Requests { Any, English = 2, - Chilnese = 4, + Chinese = 4, French = 7, German, Italian = 11, @@ -104,7 +104,7 @@ namespace osu.Game.Online.API.Requests Korean = 6, Spanish = 10, Swedish = 9, - Instrumantal = 5, + Instrumental = 5, Other = 1 } } From d50cca626405266f86e7eae733b2af247cb2243d Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Fri, 21 Feb 2020 01:05:20 +0300 Subject: [PATCH 0046/2376] Minor enum adjustments for consistency --- .../API/Requests/SearchBeatmapSetsRequest.cs | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs index 1c1da33d8a..e329015f67 100644 --- a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs +++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs @@ -76,35 +76,35 @@ namespace osu.Game.Online.API.Requests public enum BeatmapSearchGenre { - Any, - Unspecified, + Any = 0, + Unspecified = 1, [Description("Video Game")] - VideoGame, - Anime, - Rock, - Pop, - Other, - Novelty, + VideoGame = 2, + Anime = 3, + Rock = 4, + Pop = 5, + Other = 6, + Novelty = 7, [Description("Hip Hop")] HipHop = 9, - Electronic + Electronic = 10 } public enum BeatmapSearchLanguage { Any, - English = 2, - Chinese = 4, - French = 7, + Other, + English, + Japanese, + Chinese, + Instrumental, + Korean, + French, German, - Italian = 11, - Japanese = 3, - Korean = 6, - Spanish = 10, - Swedish = 9, - Instrumental = 5, - Other = 1 + Swedish, + Spanish, + Italian } } From 58903759f13f307df17fd781780952a5d92f102e Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Fri, 21 Feb 2020 01:37:36 +0300 Subject: [PATCH 0047/2376] Implement enum attributes to set display order --- .../API/Requests/SearchBeatmapSetsRequest.cs | 41 +++++++++++++++++++ .../BeatmapListing/BeatmapSearchFilterRow.cs | 27 +++++++++++- 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs index e329015f67..58a41b6e08 100644 --- a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs +++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.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.ComponentModel; using osu.Framework.IO.Network; using osu.Game.Overlays; @@ -92,19 +93,59 @@ namespace osu.Game.Online.API.Requests Electronic = 10 } + [HasOrderedElements] public enum BeatmapSearchLanguage { + [Order(0)] Any, + + [Order(11)] Other, + + [Order(1)] English, + + [Order(6)] Japanese, + + [Order(2)] Chinese, + + [Order(10)] Instrumental, + + [Order(7)] Korean, + + [Order(3)] French, + + [Order(4)] German, + + [Order(9)] Swedish, + + [Order(8)] Spanish, + + [Order(5)] Italian } + + [AttributeUsage(AttributeTargets.Field)] + public class OrderAttribute : Attribute + { + public readonly int Order; + + public OrderAttribute(int order) + { + Order = order; + } + } + + [AttributeUsage(AttributeTargets.Enum)] + public class HasOrderedElementsAttribute : Attribute + { + } } diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs index 2c046a2bbf..467399dd20 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs @@ -2,6 +2,7 @@ // 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.Bindables; @@ -13,6 +14,7 @@ using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API.Requests; using osuTK; using osuTK.Graphics; @@ -79,9 +81,30 @@ namespace osu.Game.Overlays.BeatmapListing TabContainer.Spacing = new Vector2(10, 0); - if (typeof(T).IsEnum) + var type = typeof(T); + + if (type.IsEnum) { - foreach (var val in (T[])Enum.GetValues(typeof(T))) + if (Attribute.GetCustomAttribute(type, typeof(HasOrderedElementsAttribute)) != null) + { + var enumValues = Enum.GetValues(type).Cast().ToArray(); + var enumNames = Enum.GetNames(type); + + int[] enumPositions = Array.ConvertAll(enumNames, n => + { + var orderAttr = (OrderAttribute)type.GetField(n).GetCustomAttributes(typeof(OrderAttribute), false)[0]; + return orderAttr.Order; + }); + + Array.Sort(enumPositions, enumValues); + + foreach (var val in enumValues) + AddItem(val); + + return; + } + + foreach (var val in (T[])Enum.GetValues(type)) AddItem(val); } } From 20b49bea4b516180bfcddfd40cbc57635c38c84a Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Fri, 21 Feb 2020 01:49:03 +0300 Subject: [PATCH 0048/2376] Refactor SearchBeatmapSetsRequest --- .../API/Requests/SearchBeatmapSetsRequest.cs | 43 +++++++++++-------- osu.Game/Overlays/BeatmapListingOverlay.cs | 16 +++---- osu.Game/Overlays/DirectOverlay.cs | 10 ++--- 3 files changed, 38 insertions(+), 31 deletions(-) diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs index 58a41b6e08..aef0788b49 100644 --- a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs +++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs @@ -12,24 +12,31 @@ namespace osu.Game.Online.API.Requests { public class SearchBeatmapSetsRequest : APIRequest { + public BeatmapSearchCategory SearchCategory { get; set; } + + public DirectSortCriteria SortCriteria { get; set; } + + public SortDirection SortDirection { get; set; } + + public BeatmapSearchGenre Genre { get; set; } + + public BeatmapSearchLanguage Language { get; set; } + private readonly string query; private readonly RulesetInfo ruleset; - private readonly BeatmapSearchCategory searchCategory; - private readonly DirectSortCriteria sortCriteria; - private readonly SortDirection direction; - private readonly BeatmapSearchGenre genre; - private readonly BeatmapSearchLanguage language; - private string directionString => direction == SortDirection.Descending ? @"desc" : @"asc"; - public SearchBeatmapSetsRequest(string query, RulesetInfo ruleset, BeatmapSearchCategory searchCategory = BeatmapSearchCategory.Any, DirectSortCriteria sortCriteria = DirectSortCriteria.Ranked, SortDirection direction = SortDirection.Descending, BeatmapSearchGenre genre = BeatmapSearchGenre.Any, BeatmapSearchLanguage language = BeatmapSearchLanguage.Any) + private string directionString => SortDirection == SortDirection.Descending ? @"desc" : @"asc"; + + public SearchBeatmapSetsRequest(string query, RulesetInfo ruleset) { this.query = string.IsNullOrEmpty(query) ? string.Empty : System.Uri.EscapeDataString(query); this.ruleset = ruleset; - this.searchCategory = searchCategory; - this.sortCriteria = sortCriteria; - this.direction = direction; - this.genre = genre; - this.language = language; + + SearchCategory = BeatmapSearchCategory.Any; + SortCriteria = DirectSortCriteria.Ranked; + SortDirection = SortDirection.Descending; + Genre = BeatmapSearchGenre.Any; + Language = BeatmapSearchLanguage.Any; } protected override WebRequest CreateWebRequest() @@ -40,15 +47,15 @@ namespace osu.Game.Online.API.Requests if (ruleset.ID.HasValue) req.AddParameter("m", ruleset.ID.Value.ToString()); - req.AddParameter("s", searchCategory.ToString().ToLowerInvariant()); + req.AddParameter("s", SearchCategory.ToString().ToLowerInvariant()); - if (genre != BeatmapSearchGenre.Any) - req.AddParameter("g", ((int)genre).ToString()); + if (Genre != BeatmapSearchGenre.Any) + req.AddParameter("g", ((int)Genre).ToString()); - if (language != BeatmapSearchLanguage.Any) - req.AddParameter("l", ((int)language).ToString()); + if (Language != BeatmapSearchLanguage.Any) + req.AddParameter("l", ((int)Language).ToString()); - req.AddParameter("sort", $"{sortCriteria.ToString().ToLowerInvariant()}_{directionString}"); + req.AddParameter("sort", $"{SortCriteria.ToString().ToLowerInvariant()}_{directionString}"); return req; } diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index 414dabd7c1..5b7466df0d 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -233,14 +233,14 @@ namespace osu.Game.Overlays currentContent?.FadeColour(Color4.DimGray, 400, Easing.OutQuint); - getSetsRequest = new SearchBeatmapSetsRequest( - searchSection.Query.Value, - searchSection.Ruleset.Value, - searchSection.Category.Value, - sortControl.Current.Value, - sortControl.SortDirection.Value, - searchSection.Genre.Value, - searchSection.Language.Value); + getSetsRequest = new SearchBeatmapSetsRequest(searchSection.Query.Value, searchSection.Ruleset.Value) + { + SearchCategory = searchSection.Category.Value, + SortCriteria = sortControl.Current.Value, + SortDirection = sortControl.SortDirection.Value, + Genre = searchSection.Genre.Value, + Language = searchSection.Language.Value, + }; getSetsRequest.Success += response => Schedule(() => recreatePanels(response)); diff --git a/osu.Game/Overlays/DirectOverlay.cs b/osu.Game/Overlays/DirectOverlay.cs index a6f8b65a0d..0620e687e5 100644 --- a/osu.Game/Overlays/DirectOverlay.cs +++ b/osu.Game/Overlays/DirectOverlay.cs @@ -254,11 +254,11 @@ namespace osu.Game.Overlays previewTrackManager.StopAnyPlaying(this); - getSetsRequest = new SearchBeatmapSetsRequest( - currentQuery.Value, - ((FilterControl)Filter).Ruleset.Value, - Filter.DisplayStyleControl.Dropdown.Current.Value, - Filter.Tabs.Current.Value); //todo: sort direction (?) + getSetsRequest = new SearchBeatmapSetsRequest(currentQuery.Value, ((FilterControl)Filter).Ruleset.Value) + { + SearchCategory = Filter.DisplayStyleControl.Dropdown.Current.Value, + SortCriteria = Filter.Tabs.Current.Value + }; getSetsRequest.Success += response => { From 3c56118f45f184f8a0a274db99e346530f0893ad Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Fri, 21 Feb 2020 02:28:33 +0300 Subject: [PATCH 0049/2376] Implement BeatmapSearchParameters and refactor all the components --- .../TestSceneBeatmapListingSearchSection.cs | 27 +++++++- .../BeatmapListingSearchSection.cs | 35 +++++++---- .../BeatmapListing/BeatmapSearchParameters.cs | 30 +++++++++ osu.Game/Overlays/BeatmapListingOverlay.cs | 63 ++++++------------- 4 files changed, 96 insertions(+), 59 deletions(-) create mode 100644 osu.Game/Overlays/BeatmapListing/BeatmapSearchParameters.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchSection.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchSection.cs index 1d8db71527..f809c780f1 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchSection.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchSection.cs @@ -11,6 +11,7 @@ using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; using osu.Game.Overlays.BeatmapListing; +using osu.Game.Online.API.Requests; using osuTK; namespace osu.Game.Tests.Visual.UserInterface @@ -32,6 +33,8 @@ namespace osu.Game.Tests.Visual.UserInterface OsuSpriteText query; OsuSpriteText ruleset; OsuSpriteText category; + OsuSpriteText genre; + OsuSpriteText language; Add(section = new BeatmapListingSearchSection { @@ -49,12 +52,19 @@ namespace osu.Game.Tests.Visual.UserInterface query = new OsuSpriteText(), ruleset = new OsuSpriteText(), category = new OsuSpriteText(), + genre = new OsuSpriteText(), + language = new OsuSpriteText(), } }); - section.Query.BindValueChanged(q => query.Text = $"Query: {q.NewValue}", true); - section.Ruleset.BindValueChanged(r => ruleset.Text = $"Ruleset: {r.NewValue}", true); - section.Category.BindValueChanged(c => category.Text = $"Category: {c.NewValue}", true); + section.SearchParameters.BindValueChanged(parameters => + { + query.Text = $"Query: {parameters.NewValue.Query}"; + ruleset.Text = $"Ruleset: {parameters.NewValue.Ruleset}"; + category.Text = $"Category: {parameters.NewValue.Category}"; + genre.Text = $"Genre: {parameters.NewValue.Genre}"; + language.Text = $"Language: {parameters.NewValue.Language}"; + }, true); } [Test] @@ -65,6 +75,17 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("Set null beatmap", () => section.BeatmapSet = null); } + [Test] + public void TestParametersSet() + { + AddStep("Set big black tag", () => section.SetTag("big black")); + AddAssert("Check query is big black", () => section.SearchParameters.Value.Query == "big black"); + AddStep("Set anime genre", () => section.SetGenre(BeatmapSearchGenre.Anime)); + AddAssert("Check genre is anime", () => section.SearchParameters.Value.Genre == BeatmapSearchGenre.Anime); + AddStep("Set japanese language", () => section.SetLanguage(BeatmapSearchLanguage.Japanese)); + AddAssert("Check language is japanese", () => section.SearchParameters.Value.Language == BeatmapSearchLanguage.Japanese); + } + private static readonly BeatmapSetInfo beatmap_set = new BeatmapSetInfo { OnlineInfo = new BeatmapSetOnlineInfo diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchSection.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchSection.cs index 28619ea6fe..121b101861 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchSection.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchSection.cs @@ -6,7 +6,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Online.API.Requests; -using osu.Game.Rulesets; using osuTK; using osu.Framework.Bindables; using osu.Game.Beatmaps.Drawables; @@ -19,15 +18,7 @@ namespace osu.Game.Overlays.BeatmapListing { public class BeatmapListingSearchSection : CompositeDrawable { - public Bindable Query => textBox.Current; - - public Bindable Ruleset => modeFilter.Current; - - public Bindable Category => categoryFilter.Current; - - public Bindable Genre => genreFilter.Current; - - public Bindable Language => languageFilter.Current; + public Bindable SearchParameters = new Bindable(); public BeatmapSetInfo BeatmapSet { @@ -113,7 +104,13 @@ namespace osu.Game.Overlays.BeatmapListing } }); - Category.Value = BeatmapSearchCategory.Leaderboard; + categoryFilter.Current.Value = BeatmapSearchCategory.Leaderboard; + + textBox.Current.BindValueChanged(_ => changeSearchParameters()); + modeFilter.Current.BindValueChanged(_ => changeSearchParameters()); + categoryFilter.Current.BindValueChanged(_ => changeSearchParameters()); + genreFilter.Current.BindValueChanged(_ => changeSearchParameters()); + languageFilter.Current.BindValueChanged(_ => changeSearchParameters(), true); } [BackgroundDependencyLoader] @@ -122,6 +119,22 @@ namespace osu.Game.Overlays.BeatmapListing background.Colour = colourProvider.Dark6; } + public void SetTag(string tag) => textBox.Current.Value = tag; + + public void SetGenre(BeatmapSearchGenre genre) => genreFilter.Current.Value = genre; + + public void SetLanguage(BeatmapSearchLanguage language) => languageFilter.Current.Value = language; + + private void changeSearchParameters() + { + SearchParameters.Value = new BeatmapSearchParameters( + textBox.Current.Value, + modeFilter.Current.Value, + categoryFilter.Current.Value, + genreFilter.Current.Value, + languageFilter.Current.Value); + } + private class BeatmapSearchTextBox : SearchTextBox { protected override Color4 SelectionColour => Color4.Gray; diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchParameters.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchParameters.cs new file mode 100644 index 0000000000..6a681503f5 --- /dev/null +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchParameters.cs @@ -0,0 +1,30 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Online.API.Requests; +using osu.Game.Rulesets; + +namespace osu.Game.Overlays.BeatmapListing +{ + public class BeatmapSearchParameters + { + public readonly string Query; + + public readonly RulesetInfo Ruleset; + + public readonly BeatmapSearchCategory Category; + + public readonly BeatmapSearchGenre Genre; + + public readonly BeatmapSearchLanguage Language; + + public BeatmapSearchParameters(string query, RulesetInfo ruleset, BeatmapSearchCategory category, BeatmapSearchGenre genre, BeatmapSearchLanguage language) + { + Query = query; + Ruleset = ruleset; + Category = category; + Genre = genre; + Language = language; + } + } +} diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index 5b7466df0d..2449f561c1 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -144,70 +144,43 @@ namespace osu.Game.Overlays var sortCriteria = sortControl.Current; var sortDirection = sortControl.SortDirection; - searchSection.Query.BindValueChanged(query => + searchSection.SearchParameters.BindValueChanged(parameters => { - sortCriteria.Value = string.IsNullOrEmpty(query.NewValue) ? DirectSortCriteria.Ranked : DirectSortCriteria.Relevance; - sortDirection.Value = SortDirection.Descending; + if (parameters.OldValue.Query != parameters.NewValue.Query) + { + sortCriteria.Value = string.IsNullOrEmpty(parameters.NewValue.Query) ? DirectSortCriteria.Ranked : DirectSortCriteria.Relevance; + sortDirection.Value = SortDirection.Descending; - queueUpdateSearch(true); + queueUpdateSearch(true); + } + else + { + queueUpdateSearch(); + } }); - searchSection.Ruleset.BindValueChanged(_ => queueUpdateSearch()); - searchSection.Category.BindValueChanged(_ => queueUpdateSearch()); - searchSection.Genre.BindValueChanged(_ => queueUpdateSearch()); - searchSection.Language.BindValueChanged(_ => queueUpdateSearch()); sortCriteria.BindValueChanged(_ => queueUpdateSearch()); sortDirection.BindValueChanged(_ => queueUpdateSearch()); } public void ShowTag(string tag) { - var currentQuery = searchSection.Query.Value; - - if (currentQuery != tag) - { - setDefaultSearchValues(); - searchSection.Query.Value = tag; - } - + searchSection.SetTag(tag); Show(); } public void ShowGenre(BeatmapSearchGenre genre) { - var currentGenre = searchSection.Genre.Value; - - if (currentGenre != genre) - { - setDefaultSearchValues(); - searchSection.Genre.Value = genre; - } - + searchSection.SetGenre(genre); Show(); } public void ShowLanguage(BeatmapSearchLanguage language) { - var currentLanguage = searchSection.Language.Value; - - if (currentLanguage != language) - { - setDefaultSearchValues(); - searchSection.Language.Value = language; - } - + searchSection.SetLanguage(language); Show(); } - private void setDefaultSearchValues() - { - searchSection.Query.Value = string.Empty; - searchSection.Ruleset.Value = new RulesetInfo { Name = @"Any" }; - searchSection.Category.Value = BeatmapSearchCategory.Leaderboard; - searchSection.Genre.Value = BeatmapSearchGenre.Any; - searchSection.Language.Value = BeatmapSearchLanguage.Any; - } - private ScheduledDelegate queryChangedDebounce; private void queueUpdateSearch(bool queryTextChanged = false) @@ -233,13 +206,13 @@ namespace osu.Game.Overlays currentContent?.FadeColour(Color4.DimGray, 400, Easing.OutQuint); - getSetsRequest = new SearchBeatmapSetsRequest(searchSection.Query.Value, searchSection.Ruleset.Value) + getSetsRequest = new SearchBeatmapSetsRequest(searchSection.SearchParameters.Value.Query, searchSection.SearchParameters.Value.Ruleset) { - SearchCategory = searchSection.Category.Value, + SearchCategory = searchSection.SearchParameters.Value.Category, SortCriteria = sortControl.Current.Value, SortDirection = sortControl.SortDirection.Value, - Genre = searchSection.Genre.Value, - Language = searchSection.Language.Value, + Genre = searchSection.SearchParameters.Value.Genre, + Language = searchSection.SearchParameters.Value.Language }; getSetsRequest.Success += response => Schedule(() => recreatePanels(response)); From beb18006dacb2ae2ba2cddbb078bd8ec6a0c0c12 Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Wed, 19 Feb 2020 20:18:02 +0100 Subject: [PATCH 0050/2376] Show 0 pp if map is loved --- osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs | 2 +- .../Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs index f1250679c1..7baf6c7fcb 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs @@ -154,7 +154,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores { new OsuSpriteText { - Text = $@"{score.PP:N0}", + Text = $@"{score.PP ?? 0:N0}", Font = OsuFont.GetFont(size: text_size) }, new FillFlowContainer diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs index a15dc57d23..0616871897 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs @@ -96,7 +96,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores totalScoreColumn.Text = $@"{value.TotalScore:N0}"; accuracyColumn.Text = value.DisplayAccuracy; maxComboColumn.Text = $@"{value.MaxCombo:N0}x"; - ppColumn.Text = $@"{value.PP:N0}"; + ppColumn.Text = $@"{value.PP ?? 0:N0}"; statisticsColumns.ChildrenEnumerable = value.SortedStatistics.Select(kvp => createStatisticsColumn(kvp.Key, kvp.Value)); modsColumn.Mods = value.Mods; From 6c28fd21c7bb7b61676c21c9ec11c8a84ef9e629 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 24 Feb 2020 20:52:15 +0900 Subject: [PATCH 0051/2376] osu-side changes --- .../Mods/ManiaModFlashlight.cs | 14 +++----------- .../Objects/Drawables/Pieces/BodyPiece.cs | 14 ++++---------- osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs | 14 ++++---------- .../Mods/TaikoModFlashlight.cs | 16 ++++------------ osu.Game/Graphics/UserInterface/LineGraph.cs | 14 ++++---------- .../UI/Scrolling/ScrollingHitObjectContainer.cs | 13 ++++--------- .../Edit/Compose/Components/DistanceSnapGrid.cs | 14 ++++---------- osu.Game/Screens/Play/SquareGraph.cs | 16 +++++++--------- 8 files changed, 34 insertions(+), 81 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs index 6893e1e73b..86a00271e9 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs @@ -3,8 +3,8 @@ using System; using osu.Framework.Bindables; -using osu.Framework.Caching; using osu.Framework.Graphics; +using osu.Framework.Layout; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mods; using osuTK; @@ -22,21 +22,13 @@ namespace osu.Game.Rulesets.Mania.Mods private class ManiaFlashlight : Flashlight { - private readonly Cached flashlightProperties = new Cached(); + private readonly LayoutValue flashlightProperties = new LayoutValue(Invalidation.DrawSize); public ManiaFlashlight() { FlashlightSize = new Vector2(0, default_flashlight_size); - } - public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true) - { - if ((invalidation & Invalidation.DrawSize) > 0) - { - flashlightProperties.Invalidate(); - } - - return base.Invalidate(invalidation, source, shallPropagate); + AddLayout(flashlightProperties); } protected override void Update() diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/BodyPiece.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/BodyPiece.cs index 31a4857805..43f9ae2783 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/BodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/BodyPiece.cs @@ -2,13 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osu.Framework.Caching; using osuTK.Graphics; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; +using osu.Framework.Layout; using osu.Game.Graphics; namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces @@ -65,6 +65,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces } } }; + + AddLayout(subtractionCache); } protected override void LoadComplete() @@ -100,15 +102,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces } } - private readonly Cached subtractionCache = new Cached(); - - public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true) - { - if ((invalidation & Invalidation.DrawSize) > 0) - subtractionCache.Invalidate(); - - return base.Invalidate(invalidation, source, shallPropagate); - } + private readonly LayoutValue subtractionCache = new LayoutValue(Invalidation.DrawSize); protected override void Update() { diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs index 4e86662ec6..9a23a2be30 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs @@ -5,7 +5,6 @@ using System; using System.Diagnostics; using System.Runtime.InteropServices; using osu.Framework.Allocation; -using osu.Framework.Caching; using osu.Framework.Graphics; using osu.Framework.Graphics.Batches; using osu.Framework.Graphics.OpenGL.Vertices; @@ -14,6 +13,7 @@ using osu.Framework.Graphics.Shaders; using osu.Framework.Graphics.Textures; using osu.Framework.Input; using osu.Framework.Input.Events; +using osu.Framework.Layout; using osu.Framework.Timing; using osuTK; using osuTK.Graphics; @@ -43,6 +43,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor // -1 signals that the part is unusable, and should not be drawn parts[i].InvalidationID = -1; } + + AddLayout(partSizeCache); } [BackgroundDependencyLoader] @@ -72,20 +74,12 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor } } - private readonly Cached partSizeCache = new Cached(); + private readonly LayoutValue partSizeCache = new LayoutValue(Invalidation.DrawInfo | Invalidation.RequiredParentSizeToFit | Invalidation.Presence); private Vector2 partSize => partSizeCache.IsValid ? partSizeCache.Value : (partSizeCache.Value = new Vector2(Texture.DisplayWidth, Texture.DisplayHeight) * DrawInfo.Matrix.ExtractScale().Xy); - public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true) - { - if ((invalidation & (Invalidation.DrawInfo | Invalidation.RequiredParentSizeToFit | Invalidation.Presence)) > 0) - partSizeCache.Invalidate(); - - return base.Invalidate(invalidation, source, shallPropagate); - } - /// /// The amount of time to fade the cursor trail pieces. /// diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs index b7db3307ad..1253b7c8ae 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs @@ -2,8 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; -using osu.Framework.Caching; using osu.Framework.Graphics; +using osu.Framework.Layout; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.UI; @@ -30,13 +30,15 @@ namespace osu.Game.Rulesets.Taiko.Mods private class TaikoFlashlight : Flashlight { - private readonly Cached flashlightProperties = new Cached(); + private readonly LayoutValue flashlightProperties = new LayoutValue(Invalidation.DrawSize); private readonly TaikoPlayfield taikoPlayfield; public TaikoFlashlight(TaikoPlayfield taikoPlayfield) { this.taikoPlayfield = taikoPlayfield; FlashlightSize = new Vector2(0, getSizeFor(0)); + + AddLayout(flashlightProperties); } private float getSizeFor(int combo) @@ -56,16 +58,6 @@ namespace osu.Game.Rulesets.Taiko.Mods protected override string FragmentShader => "CircularFlashlight"; - public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true) - { - if ((invalidation & Invalidation.DrawSize) > 0) - { - flashlightProperties.Invalidate(); - } - - return base.Invalidate(invalidation, source, shallPropagate); - } - protected override void Update() { base.Update(); diff --git a/osu.Game/Graphics/UserInterface/LineGraph.cs b/osu.Game/Graphics/UserInterface/LineGraph.cs index 6d65b77cbf..42b523fc5c 100644 --- a/osu.Game/Graphics/UserInterface/LineGraph.cs +++ b/osu.Game/Graphics/UserInterface/LineGraph.cs @@ -4,11 +4,11 @@ using System; using System.Collections.Generic; using System.Linq; -using osu.Framework.Caching; using osuTK; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Lines; +using osu.Framework.Layout; using osuTK.Graphics; namespace osu.Game.Graphics.UserInterface @@ -83,17 +83,11 @@ namespace osu.Game.Graphics.UserInterface PathRadius = 1 } }); + + AddLayout(pathCached); } - public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true) - { - if ((invalidation & Invalidation.DrawSize) > 0) - pathCached.Invalidate(); - - return base.Invalidate(invalidation, source, shallPropagate); - } - - private readonly Cached pathCached = new Cached(); + private readonly LayoutValue pathCached = new LayoutValue(Invalidation.DrawSize); protected override void Update() { diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index 83a7f7289f..108f98d5fc 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -6,6 +6,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Caching; using osu.Framework.Graphics; +using osu.Framework.Layout; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; @@ -19,11 +20,13 @@ namespace osu.Game.Rulesets.UI.Scrolling [Resolved] private IScrollingInfo scrollingInfo { get; set; } - private readonly Cached initialStateCache = new Cached(); + private readonly LayoutValue initialStateCache = new LayoutValue(Invalidation.RequiredParentSizeToFit | Invalidation.DrawInfo); public ScrollingHitObjectContainer() { RelativeSizeAxes = Axes.Both; + + AddLayout(initialStateCache); } [BackgroundDependencyLoader] @@ -55,14 +58,6 @@ namespace osu.Game.Rulesets.UI.Scrolling return result; } - public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true) - { - if ((invalidation & (Invalidation.RequiredParentSizeToFit | Invalidation.DrawInfo)) > 0) - initialStateCache.Invalidate(); - - return base.Invalidate(invalidation, source, shallPropagate); - } - private float scrollLength; protected override void Update() diff --git a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs index 479de64eab..3a42938fc1 100644 --- a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs @@ -3,10 +3,10 @@ using System; using osu.Framework.Allocation; -using osu.Framework.Caching; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; +using osu.Framework.Layout; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; using osuTK; @@ -51,7 +51,7 @@ namespace osu.Game.Screens.Edit.Compose.Components [Resolved] private BindableBeatDivisor beatDivisor { get; set; } - private readonly Cached gridCache = new Cached(); + private readonly LayoutValue gridCache = new LayoutValue(Invalidation.RequiredParentSizeToFit); private readonly double? endTime; /// @@ -67,6 +67,8 @@ namespace osu.Game.Screens.Edit.Compose.Components StartTime = startTime; RelativeSizeAxes = Axes.Both; + + AddLayout(gridCache); } protected override void LoadComplete() @@ -92,14 +94,6 @@ namespace osu.Game.Screens.Edit.Compose.Components gridCache.Invalidate(); } - public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true) - { - if ((invalidation & Invalidation.RequiredParentSizeToFit) > 0) - gridCache.Invalidate(); - - return base.Invalidate(invalidation, source, shallPropagate); - } - protected override void Update() { base.Update(); diff --git a/osu.Game/Screens/Play/SquareGraph.cs b/osu.Game/Screens/Play/SquareGraph.cs index a667466965..36ce131411 100644 --- a/osu.Game/Screens/Play/SquareGraph.cs +++ b/osu.Game/Screens/Play/SquareGraph.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using osu.Framework; -using osu.Framework.Caching; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -14,6 +13,7 @@ using osuTK; using osuTK.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Allocation; +using osu.Framework.Layout; using osu.Framework.Threading; namespace osu.Game.Screens.Play @@ -22,6 +22,11 @@ namespace osu.Game.Screens.Play { private BufferedContainer columns; + public SquareGraph() + { + AddLayout(layout); + } + public int ColumnCount => columns?.Children.Count ?? 0; private int progress; @@ -68,14 +73,7 @@ namespace osu.Game.Screens.Play } } - public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true) - { - if ((invalidation & Invalidation.DrawSize) > 0) - layout.Invalidate(); - return base.Invalidate(invalidation, source, shallPropagate); - } - - private readonly Cached layout = new Cached(); + private readonly LayoutValue layout = new LayoutValue(Invalidation.DrawSize); private ScheduledDelegate scheduledCreate; protected override void Update() From f71c45cb1bb82b5f606e4465d2b514b6c02bda62 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 25 Feb 2020 11:30:33 +0900 Subject: [PATCH 0052/2376] Remove shallPropagate --- osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs | 2 +- osu.Game/Graphics/Backgrounds/Triangles.cs | 2 +- osu.Game/Screens/Menu/LogoVisualisation.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs index 9a23a2be30..37df5ec540 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs @@ -91,7 +91,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor { base.Update(); - Invalidate(Invalidation.DrawNode, shallPropagate: false); + Invalidate(Invalidation.DrawNode); const int fade_clock_reset_threshold = 1000000; diff --git a/osu.Game/Graphics/Backgrounds/Triangles.cs b/osu.Game/Graphics/Backgrounds/Triangles.cs index b9c7b26e3e..590e4b2a5c 100644 --- a/osu.Game/Graphics/Backgrounds/Triangles.cs +++ b/osu.Game/Graphics/Backgrounds/Triangles.cs @@ -129,7 +129,7 @@ namespace osu.Game.Graphics.Backgrounds { base.Update(); - Invalidate(Invalidation.DrawNode, shallPropagate: false); + Invalidate(Invalidation.DrawNode); if (CreateNewTriangles) addTriangles(false); diff --git a/osu.Game/Screens/Menu/LogoVisualisation.cs b/osu.Game/Screens/Menu/LogoVisualisation.cs index dcc68296f6..67537fa9df 100644 --- a/osu.Game/Screens/Menu/LogoVisualisation.cs +++ b/osu.Game/Screens/Menu/LogoVisualisation.cs @@ -150,7 +150,7 @@ namespace osu.Game.Screens.Menu frequencyAmplitudes[i] = 0; } - Invalidate(Invalidation.DrawNode, shallPropagate: false); + Invalidate(Invalidation.DrawNode); } protected override DrawNode CreateDrawNode() => new VisualisationDrawNode(this); From 334ec7bbd48ea58c524ba24f0ad4108db9d33dce Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 26 Feb 2020 15:06:30 +0900 Subject: [PATCH 0053/2376] Apply further framework changes --- osu.Game/Graphics/Containers/SectionsContainer.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/osu.Game/Graphics/Containers/SectionsContainer.cs b/osu.Game/Graphics/Containers/SectionsContainer.cs index 9d886c457f..07a50c39e1 100644 --- a/osu.Game/Graphics/Containers/SectionsContainer.cs +++ b/osu.Game/Graphics/Containers/SectionsContainer.cs @@ -6,6 +6,7 @@ using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Layout; namespace osu.Game.Graphics.Containers { @@ -142,15 +143,17 @@ namespace osu.Game.Graphics.Containers public void ScrollToTop() => scrollContainer.ScrollTo(0); - public override void InvalidateFromChild(Invalidation invalidation, Drawable source = null) + protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source) { - base.InvalidateFromChild(invalidation, source); + var result = base.OnInvalidate(invalidation, source); - if ((invalidation & Invalidation.DrawSize) != 0) + if (source == InvalidationSource.Child && (invalidation & Invalidation.DrawSize) != 0) { - if (source == ExpandableHeader) //We need to recalculate the positions if the ExpandableHeader changed its size - lastKnownScroll = -1; + lastKnownScroll = -1; + result = true; } + + return result; } private float lastKnownScroll; From 397e35d0a0128adf174a7539f13a2a70da153762 Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Wed, 26 Feb 2020 21:36:52 +0100 Subject: [PATCH 0054/2376] Hide pp column if map is loved or qualified --- .../Overlays/BeatmapSet/Scores/ScoreTable.cs | 36 +++++++++---------- .../Scores/TopScoreStatisticsSection.cs | 3 +- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs index 7baf6c7fcb..c9008adc85 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs @@ -88,11 +88,10 @@ namespace osu.Game.Overlays.BeatmapSet.Scores columns.Add(new TableColumn(score.SortedStatistics.LastOrDefault().Key.GetDescription(), Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 45, maxSize: 95))); - columns.AddRange(new[] - { - new TableColumn("pp", Anchor.CentreLeft, new Dimension(GridSizeMode.Absolute, 30)), - new TableColumn("mods", Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize)), - }); + if (score.PP.HasValue) + columns.Add(new TableColumn("pp", Anchor.CentreLeft, new Dimension(GridSizeMode.Absolute, 30))); + + columns.Add(new TableColumn("mods", Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize))); return columns.ToArray(); } @@ -150,24 +149,25 @@ namespace osu.Game.Overlays.BeatmapSet.Scores }); } - content.AddRange(new Drawable[] + if (score.PP.HasValue) { - new OsuSpriteText + content.Add(new OsuSpriteText { - Text = $@"{score.PP ?? 0:N0}", + Text = $@"{score.PP:N0}", Font = OsuFont.GetFont(size: text_size) - }, - new FillFlowContainer + }); + } + + content.Add(new FillFlowContainer + { + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(1), + ChildrenEnumerable = score.Mods.Select(m => new ModIcon(m) { - Direction = FillDirection.Horizontal, AutoSizeAxes = Axes.Both, - Spacing = new Vector2(1), - ChildrenEnumerable = score.Mods.Select(m => new ModIcon(m) - { - AutoSizeAxes = Axes.Both, - Scale = new Vector2(0.3f) - }) - }, + Scale = new Vector2(0.3f) + }) }); return content.ToArray(); diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs index 0616871897..12ad014f61 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs @@ -96,7 +96,8 @@ namespace osu.Game.Overlays.BeatmapSet.Scores totalScoreColumn.Text = $@"{value.TotalScore:N0}"; accuracyColumn.Text = value.DisplayAccuracy; maxComboColumn.Text = $@"{value.MaxCombo:N0}x"; - ppColumn.Text = $@"{value.PP ?? 0:N0}"; + ppColumn.Alpha = value.PP.HasValue ? 1 : 0; + ppColumn.Text = $@"{value.PP:N0}"; statisticsColumns.ChildrenEnumerable = value.SortedStatistics.Select(kvp => createStatisticsColumn(kvp.Key, kvp.Value)); modsColumn.Mods = value.Mods; From 7ad6ad0bb01fbc023a02080fd6b373571455a4e9 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 27 Feb 2020 13:32:23 +0900 Subject: [PATCH 0055/2376] Remove hacks that bypassed layout shortcomings --- osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs | 10 ++-------- .../Select/Carousel/DrawableCarouselBeatmapSet.cs | 11 ++--------- 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs index a7ed1f5846..6cd1aa912f 100644 --- a/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs @@ -246,10 +246,11 @@ namespace osu.Game.Screens.Multi FillMode = FillMode.Fill, Beatmap = { BindTarget = Beatmap } }, - new Container + new FillFlowContainer { Depth = -1, RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, // This makes the gradient not be perfectly horizontal, but diagonal at a ~40° angle Shear = new Vector2(0.8f, 0), Alpha = 0.5f, @@ -259,7 +260,6 @@ namespace osu.Game.Screens.Multi new Box { RelativeSizeAxes = Axes.Both, - RelativePositionAxes = Axes.Both, Colour = Color4.Black, Width = 0.4f, }, @@ -267,26 +267,20 @@ namespace osu.Game.Screens.Multi new Box { RelativeSizeAxes = Axes.Both, - RelativePositionAxes = Axes.Both, Colour = ColourInfo.GradientHorizontal(Color4.Black, new Color4(0f, 0f, 0f, 0.9f)), Width = 0.05f, - X = 0.4f, }, new Box { RelativeSizeAxes = Axes.Both, - RelativePositionAxes = Axes.Both, Colour = ColourInfo.GradientHorizontal(new Color4(0f, 0f, 0f, 0.9f), new Color4(0f, 0f, 0f, 0.1f)), Width = 0.2f, - X = 0.45f, }, new Box { RelativeSizeAxes = Axes.Both, - RelativePositionAxes = Axes.Both, Colour = ColourInfo.GradientHorizontal(new Color4(0f, 0f, 0f, 0.1f), new Color4(0, 0, 0, 0)), Width = 0.05f, - X = 0.65f, }, } } diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 1672131949..6cd145cfef 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -159,11 +159,11 @@ namespace osu.Game.Screens.Select.Carousel Origin = Anchor.Centre, FillMode = FillMode.Fill, }, - // Todo: This should be a fill flow, but has invalidation issues (see https://github.com/ppy/osu-framework/issues/223) - new Container + new FillFlowContainer { Depth = -1, RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, // This makes the gradient not be perfectly horizontal, but diagonal at a ~40° angle Shear = new Vector2(0.8f, 0), Alpha = 0.5f, @@ -173,7 +173,6 @@ namespace osu.Game.Screens.Select.Carousel new Box { RelativeSizeAxes = Axes.Both, - RelativePositionAxes = Axes.Both, Colour = Color4.Black, Width = 0.4f, }, @@ -181,26 +180,20 @@ namespace osu.Game.Screens.Select.Carousel new Box { RelativeSizeAxes = Axes.Both, - RelativePositionAxes = Axes.Both, Colour = ColourInfo.GradientHorizontal(Color4.Black, new Color4(0f, 0f, 0f, 0.9f)), Width = 0.05f, - X = 0.4f, }, new Box { RelativeSizeAxes = Axes.Both, - RelativePositionAxes = Axes.Both, Colour = ColourInfo.GradientHorizontal(new Color4(0f, 0f, 0f, 0.9f), new Color4(0f, 0f, 0f, 0.1f)), Width = 0.2f, - X = 0.45f, }, new Box { RelativeSizeAxes = Axes.Both, - RelativePositionAxes = Axes.Both, Colour = ColourInfo.GradientHorizontal(new Color4(0f, 0f, 0f, 0.1f), new Color4(0, 0, 0, 0)), Width = 0.05f, - X = 0.65f, }, } }, From 101a5876419b52fe97028b8f86620c8d04b78afe Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 27 Feb 2020 15:51:55 +0900 Subject: [PATCH 0056/2376] Disable triangles in triangles intro --- osu.Game/Screens/Menu/IntroTriangles.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Screens/Menu/IntroTriangles.cs b/osu.Game/Screens/Menu/IntroTriangles.cs index 29f32406e8..4e51ff939a 100644 --- a/osu.Game/Screens/Menu/IntroTriangles.cs +++ b/osu.Game/Screens/Menu/IntroTriangles.cs @@ -54,8 +54,6 @@ namespace osu.Game.Screens.Menu { base.LogoArriving(logo, resuming); - logo.Triangles = true; - if (!resuming) { PrepareMenuLoad(); From 3f5c4633bc235a6ed0add44b72295be71833c7a9 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 27 Feb 2020 16:40:56 +0900 Subject: [PATCH 0057/2376] Remove workarounds for CreateRoomRequest shortcomings --- osu.Game/Online/Multiplayer/PlaylistItem.cs | 4 +--- osu.Game/Online/Multiplayer/Room.cs | 9 --------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/osu.Game/Online/Multiplayer/PlaylistItem.cs b/osu.Game/Online/Multiplayer/PlaylistItem.cs index 11e4854174..9d6e8eb8e3 100644 --- a/osu.Game/Online/Multiplayer/PlaylistItem.cs +++ b/osu.Game/Online/Multiplayer/PlaylistItem.cs @@ -65,9 +65,7 @@ namespace osu.Game.Online.Multiplayer public void MapObjects(BeatmapManager beatmaps, RulesetStore rulesets) { - // If we don't have an api beatmap, the request occurred as a result of room creation, so we can query the local beatmap instead - // Todo: Is this a bug? Room creation only returns the beatmap ID - Beatmap.Value = apiBeatmap == null ? beatmaps.QueryBeatmap(b => b.OnlineBeatmapID == BeatmapID) : apiBeatmap.ToBeatmap(rulesets); + Beatmap.Value = apiBeatmap.ToBeatmap(rulesets); Ruleset.Value = rulesets.GetRuleset(RulesetID); Ruleset rulesetInstance = Ruleset.Value.CreateInstance(); diff --git a/osu.Game/Online/Multiplayer/Room.cs b/osu.Game/Online/Multiplayer/Room.cs index 2bfcc019fa..c55822c407 100644 --- a/osu.Game/Online/Multiplayer/Room.cs +++ b/osu.Game/Online/Multiplayer/Room.cs @@ -118,15 +118,6 @@ namespace osu.Game.Online.Multiplayer if (DateTimeOffset.Now >= EndDate.Value) Status.Value = new RoomStatusEnded(); - // transfer local beatmaps across to ensure we have Metadata available (CreateRoomRequest does not give us metadata as expected) - foreach (var item in other.Playlist) - { - var localItem = Playlist.FirstOrDefault(i => i.BeatmapID == item.BeatmapID); - - if (localItem != null) - item.Beatmap.Value.Metadata = localItem.Beatmap.Value.Metadata; - } - if (!Playlist.SequenceEqual(other.Playlist)) { Playlist.Clear(); From ffa8a50c6bcddfd643c6a5abd3c213b4fb750e2e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 27 Feb 2020 19:21:59 +0900 Subject: [PATCH 0058/2376] Make User IEquatable --- osu.Game/Users/User.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Users/User.cs b/osu.Game/Users/User.cs index c573fdd089..d25c552160 100644 --- a/osu.Game/Users/User.cs +++ b/osu.Game/Users/User.cs @@ -9,7 +9,7 @@ using osu.Framework.Bindables; namespace osu.Game.Users { - public class User + public class User : IEquatable { [JsonProperty(@"id")] public long Id = 1; @@ -244,5 +244,13 @@ namespace osu.Game.Users [Description("Touch Screen")] Touch, } + + public bool Equals(User other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + + return Id == other.Id; + } } } From 99442ec9c322f66c450b059698d41e696a64e102 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 27 Feb 2020 19:23:21 +0900 Subject: [PATCH 0059/2376] Implement single-room multiplayer room polling --- .../Online/API/Requests/GetRoomRequest.cs | 19 ++ .../Multi/Lounge/Components/RoomsContainer.cs | 4 +- .../Screens/Multi/Lounge/LoungeSubScreen.cs | 8 +- osu.Game/Screens/Multi/Multiplayer.cs | 37 ++- osu.Game/Screens/Multi/RoomManager.cs | 211 ++++++++++++++---- 5 files changed, 223 insertions(+), 56 deletions(-) create mode 100644 osu.Game/Online/API/Requests/GetRoomRequest.cs diff --git a/osu.Game/Online/API/Requests/GetRoomRequest.cs b/osu.Game/Online/API/Requests/GetRoomRequest.cs new file mode 100644 index 0000000000..531e1857de --- /dev/null +++ b/osu.Game/Online/API/Requests/GetRoomRequest.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.Game.Online.Multiplayer; + +namespace osu.Game.Online.API.Requests +{ + public class GetRoomRequest : APIRequest + { + private readonly int roomId; + + public GetRoomRequest(int roomId) + { + this.roomId = roomId; + } + + protected override string Target => $"rooms/{roomId}"; + } +} diff --git a/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs index 64618a1d85..f14aa5fd8c 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs @@ -28,7 +28,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components private Bindable filter { get; set; } [Resolved] - private Bindable currentRoom { get; set; } + private Bindable selectedRoom { get; set; } [Resolved] private IRoomManager roomManager { get; set; } @@ -122,7 +122,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components else roomFlow.Children.ForEach(r => r.State = r.Room == room ? SelectionState.Selected : SelectionState.NotSelected); - currentRoom.Value = room; + selectedRoom.Value = room; } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs index e2e5b1b549..7c10f0f975 100644 --- a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs @@ -26,7 +26,7 @@ namespace osu.Game.Screens.Multi.Lounge private readonly LoadingLayer loadingLayer; [Resolved] - private Bindable currentRoom { get; set; } + private Bindable selectedRoom { get; set; } public LoungeSubScreen() { @@ -101,8 +101,8 @@ namespace osu.Game.Screens.Multi.Lounge { base.OnResuming(last); - if (currentRoom.Value?.RoomID.Value == null) - currentRoom.Value = new Room(); + if (selectedRoom.Value?.RoomID.Value == null) + selectedRoom.Value = new Room(); onReturning(); } @@ -143,7 +143,7 @@ namespace osu.Game.Screens.Multi.Lounge if (!this.IsCurrentScreen()) return; - currentRoom.Value = room; + selectedRoom.Value = room; this.Push(new MatchSubScreen(room)); } diff --git a/osu.Game/Screens/Multi/Multiplayer.cs b/osu.Game/Screens/Multi/Multiplayer.cs index 1219919425..20b1f860a9 100644 --- a/osu.Game/Screens/Multi/Multiplayer.cs +++ b/osu.Game/Screens/Multi/Multiplayer.cs @@ -48,7 +48,7 @@ namespace osu.Game.Screens.Multi private readonly IBindable isIdle = new BindableBool(); [Cached] - private readonly Bindable currentRoom = new Bindable(); + private readonly Bindable selectedRoom = new Bindable(); [Cached] private readonly Bindable currentFilter = new Bindable(new FilterCriteria()); @@ -163,14 +163,39 @@ namespace osu.Game.Screens.Multi protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { var dependencies = new CachedModelDependencyContainer(base.CreateChildDependencies(parent)); - dependencies.Model.BindTo(currentRoom); + dependencies.Model.BindTo(selectedRoom); return dependencies; } private void updatePollingRate(bool idle) { - roomManager.TimeBetweenPolls = !this.IsCurrentScreen() || !(screenStack.CurrentScreen is LoungeSubScreen) ? 0 : (idle ? 120000 : 15000); - Logger.Log($"Polling adjusted to {roomManager.TimeBetweenPolls}"); + if (!this.IsCurrentScreen()) + { + roomManager.TimeBetweenListingPolls = 0; + roomManager.TimeBetweenSelectionPolls = 0; + } + else + { + switch (screenStack.CurrentScreen) + { + case LoungeSubScreen _: + roomManager.TimeBetweenListingPolls = idle ? 120000 : 15000; + roomManager.TimeBetweenSelectionPolls = idle ? 30000 : 5000; + break; + + case MatchSubScreen _: + roomManager.TimeBetweenListingPolls = 0; + roomManager.TimeBetweenSelectionPolls = idle ? 30000 : 5000; + break; + + default: + roomManager.TimeBetweenListingPolls = 0; + roomManager.TimeBetweenSelectionPolls = 0; + break; + } + } + + Logger.Log($"Polling adjusted (listing: {roomManager.TimeBetweenListingPolls}, selection: {roomManager.TimeBetweenSelectionPolls})"); } /// @@ -222,6 +247,8 @@ namespace osu.Game.Screens.Multi base.OnResuming(last); beginHandlingTrack(); + + updatePollingRate(isIdle.Value); } public override void OnSuspending(IScreen next) @@ -231,7 +258,7 @@ namespace osu.Game.Screens.Multi endHandlingTrack(); - roomManager.TimeBetweenPolls = 0; + updatePollingRate(isIdle.Value); } public override bool OnExiting(IScreen next) diff --git a/osu.Game/Screens/Multi/RoomManager.cs b/osu.Game/Screens/Multi/RoomManager.cs index cdaba85b9e..ef1e50d2ac 100644 --- a/osu.Game/Screens/Multi/RoomManager.cs +++ b/osu.Game/Screens/Multi/RoomManager.cs @@ -2,10 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Online; @@ -17,20 +20,24 @@ using osu.Game.Screens.Multi.Lounge.Components; namespace osu.Game.Screens.Multi { - public class RoomManager : PollingComponent, IRoomManager + public class RoomManager : CompositeDrawable, IRoomManager { public event Action RoomsUpdated; private readonly BindableList rooms = new BindableList(); public IBindableList Rooms => rooms; - private Room joinedRoom; + public double TimeBetweenListingPolls + { + get => listingPollingComponent.TimeBetweenPolls; + set => listingPollingComponent.TimeBetweenPolls = value; + } - [Resolved] - private Bindable currentFilter { get; set; } - - [Resolved] - private IAPIProvider api { get; set; } + public double TimeBetweenSelectionPolls + { + get => selectionPollingComponent.TimeBetweenPolls; + set => selectionPollingComponent.TimeBetweenPolls = value; + } [Resolved] private RulesetStore rulesets { get; set; } @@ -38,14 +45,26 @@ namespace osu.Game.Screens.Multi [Resolved] private BeatmapManager beatmaps { get; set; } - [BackgroundDependencyLoader] - private void load() + [Resolved] + private IAPIProvider api { get; set; } + + [Resolved] + private Bindable selectedRoom { get; set; } + + private readonly ListingPollingComponent listingPollingComponent; + private readonly SelectionPollingComponent selectionPollingComponent; + + private Room joinedRoom; + + public RoomManager() { - currentFilter.BindValueChanged(_ => + RelativeSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] { - if (IsLoaded) - PollImmediately(); - }); + listingPollingComponent = new ListingPollingComponent { RoomsReceived = onRoomsReceived }, + selectionPollingComponent = new SelectionPollingComponent { RoomReceived = onRoomReceived } + }; } protected override void Dispose(bool isDisposing) @@ -116,45 +135,52 @@ namespace osu.Game.Screens.Multi joinedRoom = null; } - private GetRoomsRequest pollReq; - - protected override Task Poll() + /// + /// Invoked when the listing of all s is received from the server. + /// + /// The listing. + private void onRoomsReceived(List listing) { - if (!api.IsLoggedIn) - return base.Poll(); - - var tcs = new TaskCompletionSource(); - - pollReq?.Cancel(); - pollReq = new GetRoomsRequest(currentFilter.Value.PrimaryFilter); - - pollReq.Success += result => + // Remove past matches + foreach (var r in rooms.ToList()) { - // Remove past matches - foreach (var r in rooms.ToList()) + if (listing.All(e => e.RoomID.Value != r.RoomID.Value)) + rooms.Remove(r); + } + + for (int i = 0; i < listing.Count; i++) + { + if (selectedRoom.Value?.RoomID?.Value == listing[i].RoomID.Value) { - if (result.All(e => e.RoomID.Value != r.RoomID.Value)) - rooms.Remove(r); + // The listing request contains less data than the selection request, so data from the selection request is always preferred while the room is selected. + continue; } - for (int i = 0; i < result.Count; i++) + var r = listing[i]; + r.Position.Value = i; + + update(r, r); + addRoom(r); + } + + RoomsUpdated?.Invoke(); + } + + /// + /// Invoked when a is received from the server. + /// + /// The received . + private void onRoomReceived(Room toUpdate) + { + foreach (var room in rooms) + { + if (room.RoomID.Value == toUpdate.RoomID.Value) { - var r = result[i]; - r.Position.Value = i; - - update(r, r); - addRoom(r); + toUpdate.Position.Value = room.Position.Value; + update(room, toUpdate); + break; } - - RoomsUpdated?.Invoke(); - tcs.SetResult(true); - }; - - pollReq.Failure += _ => tcs.SetResult(false); - - api.Queue(pollReq); - - return tcs.Task; + } } /// @@ -182,5 +208,100 @@ namespace osu.Game.Screens.Multi else existing.CopyFrom(room); } + + private class SelectionPollingComponent : PollingComponent + { + public Action RoomReceived; + + [Resolved] + private IAPIProvider api { get; set; } + + [Resolved] + private Bindable selectedRoom { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + selectedRoom.BindValueChanged(_ => + { + if (IsLoaded) + PollImmediately(); + }); + } + + private GetRoomRequest pollReq; + + protected override Task Poll() + { + if (!api.IsLoggedIn) + return base.Poll(); + + if (selectedRoom.Value?.RoomID.Value == null) + return base.Poll(); + + var tcs = new TaskCompletionSource(); + + pollReq?.Cancel(); + pollReq = new GetRoomRequest(selectedRoom.Value.RoomID.Value.Value); + + pollReq.Success += result => + { + RoomReceived?.Invoke(result); + tcs.SetResult(true); + }; + + pollReq.Failure += _ => tcs.SetResult(false); + + api.Queue(pollReq); + + return tcs.Task; + } + } + + private class ListingPollingComponent : PollingComponent + { + public Action> RoomsReceived; + + [Resolved] + private IAPIProvider api { get; set; } + + [Resolved] + private Bindable currentFilter { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + currentFilter.BindValueChanged(_ => + { + if (IsLoaded) + PollImmediately(); + }); + } + + private GetRoomsRequest pollReq; + + protected override Task Poll() + { + if (!api.IsLoggedIn) + return base.Poll(); + + var tcs = new TaskCompletionSource(); + + pollReq?.Cancel(); + pollReq = new GetRoomsRequest(currentFilter.Value.PrimaryFilter); + + pollReq.Success += result => + { + RoomsReceived?.Invoke(result); + tcs.SetResult(true); + }; + + pollReq.Failure += _ => tcs.SetResult(false); + + api.Queue(pollReq); + + return tcs.Task; + } + } } } From 97c07281d8a1f1b1ba647340425b7ed3bfda6b35 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 27 Feb 2020 19:24:13 +0900 Subject: [PATCH 0060/2376] Make ParticipantsList use the new participants property --- osu.Game/Online/Multiplayer/Room.cs | 2 +- .../Multi/Components/ParticipantsList.cs | 91 +++++++++---------- 2 files changed, 42 insertions(+), 51 deletions(-) diff --git a/osu.Game/Online/Multiplayer/Room.cs b/osu.Game/Online/Multiplayer/Room.cs index 2bfcc019fa..327f2c75da 100644 --- a/osu.Game/Online/Multiplayer/Room.cs +++ b/osu.Game/Online/Multiplayer/Room.cs @@ -59,7 +59,7 @@ namespace osu.Game.Online.Multiplayer public Bindable MaxParticipants { get; private set; } = new Bindable(); [Cached] - [JsonIgnore] + [JsonProperty("recent_participants")] public BindableList Participants { get; private set; } = new BindableList(); [Cached] diff --git a/osu.Game/Screens/Multi/Components/ParticipantsList.cs b/osu.Game/Screens/Multi/Components/ParticipantsList.cs index e383e0414b..16ab905322 100644 --- a/osu.Game/Screens/Multi/Components/ParticipantsList.cs +++ b/osu.Game/Screens/Multi/Components/ParticipantsList.cs @@ -7,9 +7,8 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; +using osu.Framework.Threading; using osu.Game.Graphics; -using osu.Game.Online.API; -using osu.Game.Online.API.Requests; using osu.Game.Users; using osu.Game.Users.Drawables; using osuTK; @@ -46,73 +45,69 @@ namespace osu.Game.Screens.Multi.Components set => fill.Direction = value; } - private readonly FillFlowContainer fill; + private readonly FillFlowContainer fill; public ParticipantsList() { - InternalChild = fill = new FillFlowContainer { Spacing = new Vector2(10) }; + InternalChild = fill = new FillFlowContainer { Spacing = new Vector2(10) }; } [BackgroundDependencyLoader] private void load() { - RoomID.BindValueChanged(_ => updateParticipants(), true); + Participants.CollectionChanged += (_, __) => updateParticipants(); + updateParticipants(); } - [Resolved] - private IAPIProvider api { get; set; } - - private GetRoomScoresRequest request; + private ScheduledDelegate scheduledUpdate; private void updateParticipants() { - var roomId = RoomID.Value ?? 0; - - request?.Cancel(); - - // nice little progressive fade - int time = 500; - - foreach (var c in fill.Children) + scheduledUpdate?.Cancel(); + scheduledUpdate = Schedule(() => { - c.Delay(500 - time).FadeOut(time, Easing.Out); - time = Math.Max(20, time - 20); - c.Expire(); - } + // Remove all extra tiles with a nice, progressive fade + int time = 500; - if (roomId == 0) return; + for (int i = Participants.Count; i < fill.Count; i++) + { + var tile = fill[i]; - request = new GetRoomScoresRequest(roomId); - request.Success += scores => Schedule(() => - { - if (roomId != RoomID.Value) - return; + tile.Delay(500 - time).FadeOut(time, Easing.Out); + time = Math.Max(20, time - 20); + tile.Expire(); + } - fill.Clear(); - foreach (var s in scores) - fill.Add(new UserTile(s.User)); + // Add new tiles for all new players + for (int i = fill.Count; i < Participants.Count; i++) + { + var tile = new UserTile(); + fill.Add(tile); - fill.FadeInFromZero(1000, Easing.OutQuint); + tile.ClearTransforms(); + tile.LifetimeEnd = double.MaxValue; + tile.FadeInFromZero(250, Easing.OutQuint); + } + + for (int i = 0; i < Participants.Count; i++) + fill[i].User = Participants[i]; }); - - api.Queue(request); - } - - protected override void Dispose(bool isDisposing) - { - request?.Cancel(); - base.Dispose(isDisposing); } private class UserTile : CompositeDrawable, IHasTooltip { - private readonly User user; - - public string TooltipText => user.Username; - - public UserTile(User user) + public User User + { + get => avatar.User; + set => avatar.User = value; + } + + public string TooltipText => User?.Username ?? string.Empty; + + private readonly UpdateableAvatar avatar; + + public UserTile() { - this.user = user; Size = new Vector2(TILE_SIZE); CornerRadius = 5f; Masking = true; @@ -124,11 +119,7 @@ namespace osu.Game.Screens.Multi.Components RelativeSizeAxes = Axes.Both, Colour = OsuColour.FromHex(@"27252d"), }, - new UpdateableAvatar - { - RelativeSizeAxes = Axes.Both, - User = user, - }, + avatar = new UpdateableAvatar { RelativeSizeAxes = Axes.Both }, }; } } From fe10a64137ec868a5e97fa087b79f2787d5553de Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 27 Feb 2020 19:37:04 +0900 Subject: [PATCH 0061/2376] Improve participants list transforms --- .../Multi/Components/ParticipantsList.cs | 61 +++++++++---------- 1 file changed, 29 insertions(+), 32 deletions(-) diff --git a/osu.Game/Screens/Multi/Components/ParticipantsList.cs b/osu.Game/Screens/Multi/Components/ParticipantsList.cs index 16ab905322..13de0202e1 100644 --- a/osu.Game/Screens/Multi/Components/ParticipantsList.cs +++ b/osu.Game/Screens/Multi/Components/ParticipantsList.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -25,7 +24,9 @@ namespace osu.Game.Screens.Multi.Components set { base.RelativeSizeAxes = value; - fill.RelativeSizeAxes = value; + + if (tiles != null) + tiles.RelativeSizeAxes = value; } } @@ -35,21 +36,24 @@ namespace osu.Game.Screens.Multi.Components set { base.AutoSizeAxes = value; - fill.AutoSizeAxes = value; + + if (tiles != null) + tiles.AutoSizeAxes = value; } } + private FillDirection direction = FillDirection.Full; + public FillDirection Direction { - get => fill.Direction; - set => fill.Direction = value; - } + get => direction; + set + { + direction = value; - private readonly FillFlowContainer fill; - - public ParticipantsList() - { - InternalChild = fill = new FillFlowContainer { Spacing = new Vector2(10) }; + if (tiles != null) + tiles.Direction = value; + } } [BackgroundDependencyLoader] @@ -60,37 +64,30 @@ namespace osu.Game.Screens.Multi.Components } private ScheduledDelegate scheduledUpdate; + private FillFlowContainer tiles; private void updateParticipants() { scheduledUpdate?.Cancel(); scheduledUpdate = Schedule(() => { - // Remove all extra tiles with a nice, progressive fade - int time = 500; + tiles?.FadeOut(250, Easing.Out).Expire(); - for (int i = Participants.Count; i < fill.Count; i++) + tiles = new FillFlowContainer { - var tile = fill[i]; - - tile.Delay(500 - time).FadeOut(time, Easing.Out); - time = Math.Max(20, time - 20); - tile.Expire(); - } - - // Add new tiles for all new players - for (int i = fill.Count; i < Participants.Count; i++) - { - var tile = new UserTile(); - fill.Add(tile); - - tile.ClearTransforms(); - tile.LifetimeEnd = double.MaxValue; - tile.FadeInFromZero(250, Easing.OutQuint); - } + Alpha = 0, + Direction = Direction, + AutoSizeAxes = AutoSizeAxes, + RelativeSizeAxes = RelativeSizeAxes, + Spacing = new Vector2(10) + }; for (int i = 0; i < Participants.Count; i++) - fill[i].User = Participants[i]; + tiles.Add(new UserTile { User = Participants[i] }); + + AddInternal(tiles); + + tiles.Delay(250).FadeIn(250, Easing.OutQuint); }); } From 22862256c1fc519bc5ae5150501b5edcdeca1446 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 27 Feb 2020 19:42:19 +0900 Subject: [PATCH 0062/2376] Increase time between polls --- osu.Game/Screens/Multi/Multiplayer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Multi/Multiplayer.cs b/osu.Game/Screens/Multi/Multiplayer.cs index 20b1f860a9..b0d773869a 100644 --- a/osu.Game/Screens/Multi/Multiplayer.cs +++ b/osu.Game/Screens/Multi/Multiplayer.cs @@ -180,7 +180,7 @@ namespace osu.Game.Screens.Multi { case LoungeSubScreen _: roomManager.TimeBetweenListingPolls = idle ? 120000 : 15000; - roomManager.TimeBetweenSelectionPolls = idle ? 30000 : 5000; + roomManager.TimeBetweenSelectionPolls = idle ? 120000 : 15000; break; case MatchSubScreen _: From 085968dd7fe8b96d71d0c55b6dbebc932d94e63b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 27 Feb 2020 19:42:28 +0900 Subject: [PATCH 0063/2376] Rename recent participants --- osu.Game/Online/Multiplayer/Room.cs | 8 ++++---- .../Screens/Multi/Components/OverlinedParticipants.cs | 2 +- osu.Game/Screens/Multi/Components/ParticipantsList.cs | 6 +++--- osu.Game/Screens/Multi/MultiplayerComposite.cs | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game/Online/Multiplayer/Room.cs b/osu.Game/Online/Multiplayer/Room.cs index 327f2c75da..fd700bff0b 100644 --- a/osu.Game/Online/Multiplayer/Room.cs +++ b/osu.Game/Online/Multiplayer/Room.cs @@ -60,7 +60,7 @@ namespace osu.Game.Online.Multiplayer [Cached] [JsonProperty("recent_participants")] - public BindableList Participants { get; private set; } = new BindableList(); + public BindableList RecentParticipants { get; private set; } = new BindableList(); [Cached] public Bindable ParticipantCount { get; private set; } = new Bindable(); @@ -133,10 +133,10 @@ namespace osu.Game.Online.Multiplayer Playlist.AddRange(other.Playlist); } - if (!Participants.SequenceEqual(other.Participants)) + if (!RecentParticipants.SequenceEqual(other.RecentParticipants)) { - Participants.Clear(); - Participants.AddRange(other.Participants); + RecentParticipants.Clear(); + RecentParticipants.AddRange(other.RecentParticipants); } Position = other.Position; diff --git a/osu.Game/Screens/Multi/Components/OverlinedParticipants.cs b/osu.Game/Screens/Multi/Components/OverlinedParticipants.cs index a709c6a57a..eb1782d147 100644 --- a/osu.Game/Screens/Multi/Components/OverlinedParticipants.cs +++ b/osu.Game/Screens/Multi/Components/OverlinedParticipants.cs @@ -16,7 +16,7 @@ namespace osu.Game.Screens.Multi.Components } public OverlinedParticipants(Direction direction) - : base("Participants") + : base("Recent participants") { OsuScrollContainer scroll; ParticipantsList list; diff --git a/osu.Game/Screens/Multi/Components/ParticipantsList.cs b/osu.Game/Screens/Multi/Components/ParticipantsList.cs index 13de0202e1..5a2dc19b66 100644 --- a/osu.Game/Screens/Multi/Components/ParticipantsList.cs +++ b/osu.Game/Screens/Multi/Components/ParticipantsList.cs @@ -59,7 +59,7 @@ namespace osu.Game.Screens.Multi.Components [BackgroundDependencyLoader] private void load() { - Participants.CollectionChanged += (_, __) => updateParticipants(); + RecentParticipants.CollectionChanged += (_, __) => updateParticipants(); updateParticipants(); } @@ -82,8 +82,8 @@ namespace osu.Game.Screens.Multi.Components Spacing = new Vector2(10) }; - for (int i = 0; i < Participants.Count; i++) - tiles.Add(new UserTile { User = Participants[i] }); + for (int i = 0; i < RecentParticipants.Count; i++) + tiles.Add(new UserTile { User = RecentParticipants[i] }); AddInternal(tiles); diff --git a/osu.Game/Screens/Multi/MultiplayerComposite.cs b/osu.Game/Screens/Multi/MultiplayerComposite.cs index 3f048eceab..e612e77748 100644 --- a/osu.Game/Screens/Multi/MultiplayerComposite.cs +++ b/osu.Game/Screens/Multi/MultiplayerComposite.cs @@ -31,7 +31,7 @@ namespace osu.Game.Screens.Multi protected BindableList Playlist { get; private set; } [Resolved(typeof(Room))] - protected BindableList Participants { get; private set; } + protected BindableList RecentParticipants { get; private set; } [Resolved(typeof(Room))] protected Bindable ParticipantCount { get; private set; } From dd2bd5c19dd5ab57c01953cfdc029ce713e28678 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 27 Feb 2020 20:01:23 +0900 Subject: [PATCH 0064/2376] Add delay for loading multiplayer beatmap covers --- .../Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs index 30346a8a96..eb05cbaf85 100644 --- a/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs +++ b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs @@ -16,6 +16,8 @@ namespace osu.Game.Beatmaps.Drawables { public readonly Bindable Beatmap = new Bindable(); + protected override double LoadDelay => 500; + [Resolved] private BeatmapManager beatmaps { get; set; } From b3220476d73db0798f1d583048c18481c7f1d8df Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 27 Feb 2020 20:05:12 +0900 Subject: [PATCH 0065/2376] Rename methods --- osu.Game/Screens/Multi/RoomManager.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Multi/RoomManager.cs b/osu.Game/Screens/Multi/RoomManager.cs index ef1e50d2ac..ad461af57f 100644 --- a/osu.Game/Screens/Multi/RoomManager.cs +++ b/osu.Game/Screens/Multi/RoomManager.cs @@ -62,8 +62,8 @@ namespace osu.Game.Screens.Multi InternalChildren = new Drawable[] { - listingPollingComponent = new ListingPollingComponent { RoomsReceived = onRoomsReceived }, - selectionPollingComponent = new SelectionPollingComponent { RoomReceived = onRoomReceived } + listingPollingComponent = new ListingPollingComponent { RoomsReceived = onListingReceived }, + selectionPollingComponent = new SelectionPollingComponent { RoomReceived = onSelectedRoomReceived } }; } @@ -139,7 +139,7 @@ namespace osu.Game.Screens.Multi /// Invoked when the listing of all s is received from the server. /// /// The listing. - private void onRoomsReceived(List listing) + private void onListingReceived(List listing) { // Remove past matches foreach (var r in rooms.ToList()) @@ -170,7 +170,7 @@ namespace osu.Game.Screens.Multi /// Invoked when a is received from the server. /// /// The received . - private void onRoomReceived(Room toUpdate) + private void onSelectedRoomReceived(Room toUpdate) { foreach (var room in rooms) { From d92e93ed3168502ef5e449343e40a4d4f5510054 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 27 Feb 2020 14:39:10 +0300 Subject: [PATCH 0066/2376] Move background creation out from UpdateStreamBadgeArea --- osu.Game/Overlays/Changelog/ChangelogHeader.cs | 14 ++++++++++++++ .../Overlays/Changelog/UpdateStreamBadgeArea.cs | 11 +---------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/osu.Game/Overlays/Changelog/ChangelogHeader.cs b/osu.Game/Overlays/Changelog/ChangelogHeader.cs index 8663ec586b..09bcd62021 100644 --- a/osu.Game/Overlays/Changelog/ChangelogHeader.cs +++ b/osu.Game/Overlays/Changelog/ChangelogHeader.cs @@ -4,9 +4,11 @@ using System; using System.Collections.Generic; using System.Linq; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API.Requests.Responses; @@ -22,6 +24,8 @@ namespace osu.Game.Overlays.Changelog private const string listing_string = "listing"; + private Box streamsBackground; + public ChangelogHeader() { TabControl.AddItem(listing_string); @@ -40,6 +44,12 @@ namespace osu.Game.Overlays.Changelog }; } + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + streamsBackground.Colour = colourProvider.Background5; + } + private ChangelogHeaderTitle title; private void showBuild(ValueChangedEvent e) @@ -72,6 +82,10 @@ namespace osu.Game.Overlays.Changelog AutoSizeAxes = Axes.Y, Children = new Drawable[] { + streamsBackground = new Box + { + RelativeSizeAxes = Axes.Both + }, Streams = new UpdateStreamBadgeArea(), } }; diff --git a/osu.Game/Overlays/Changelog/UpdateStreamBadgeArea.cs b/osu.Game/Overlays/Changelog/UpdateStreamBadgeArea.cs index 639c0d9780..faff7381c9 100644 --- a/osu.Game/Overlays/Changelog/UpdateStreamBadgeArea.cs +++ b/osu.Game/Overlays/Changelog/UpdateStreamBadgeArea.cs @@ -6,25 +6,16 @@ using osu.Framework.Input.Events; using osu.Game.Online.API.Requests.Responses; using System.Collections.Generic; using System.Linq; -using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; -using osu.Framework.Allocation; namespace osu.Game.Overlays.Changelog { public class UpdateStreamBadgeArea : TabControl { - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) + public UpdateStreamBadgeArea() { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - - AddInternal(new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background5, - }); } public void Populate(List streams) From a8c31c31add957afa4dd5f5c916115baeed094c2 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 27 Feb 2020 14:47:31 +0300 Subject: [PATCH 0067/2376] Move padding outside of the UpdateStreamBadgeArea --- .../Overlays/Changelog/ChangelogHeader.cs | 12 +++++++++++- .../Changelog/UpdateStreamBadgeArea.cs | 19 +++++-------------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/osu.Game/Overlays/Changelog/ChangelogHeader.cs b/osu.Game/Overlays/Changelog/ChangelogHeader.cs index 09bcd62021..f5b10ef0f2 100644 --- a/osu.Game/Overlays/Changelog/ChangelogHeader.cs +++ b/osu.Game/Overlays/Changelog/ChangelogHeader.cs @@ -86,7 +86,17 @@ namespace osu.Game.Overlays.Changelog { RelativeSizeAxes = Axes.Both }, - Streams = new UpdateStreamBadgeArea(), + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding + { + Horizontal = 85, + Vertical = 20 + }, + Child = Streams = new UpdateStreamBadgeArea() + } } }; diff --git a/osu.Game/Overlays/Changelog/UpdateStreamBadgeArea.cs b/osu.Game/Overlays/Changelog/UpdateStreamBadgeArea.cs index faff7381c9..d937bf775a 100644 --- a/osu.Game/Overlays/Changelog/UpdateStreamBadgeArea.cs +++ b/osu.Game/Overlays/Changelog/UpdateStreamBadgeArea.cs @@ -40,21 +40,12 @@ namespace osu.Game.Overlays.Changelog base.OnHoverLost(e); } - protected override TabFillFlowContainer CreateTabFlow() + protected override TabFillFlowContainer CreateTabFlow() => new TabFillFlowContainer { - var flow = base.CreateTabFlow(); - - flow.RelativeSizeAxes = Axes.X; - flow.AutoSizeAxes = Axes.Y; - flow.AllowMultiline = true; - flow.Padding = new MarginPadding - { - Vertical = 20, - Horizontal = 85, - }; - - return flow; - } + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + AllowMultiline = true, + }; protected override Dropdown CreateDropdown() => null; From f9aa6b9c07a99d5272302d8835b9a797c2277d0a Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 27 Feb 2020 15:33:01 +0300 Subject: [PATCH 0068/2376] Remove fadeContainer and adjust fade condition --- .../Overlays/Changelog/UpdateStreamBadge.cs | 69 +++++++++---------- 1 file changed, 31 insertions(+), 38 deletions(-) diff --git a/osu.Game/Overlays/Changelog/UpdateStreamBadge.cs b/osu.Game/Overlays/Changelog/UpdateStreamBadge.cs index 10aca31441..dc9d3ccaed 100644 --- a/osu.Game/Overlays/Changelog/UpdateStreamBadge.cs +++ b/osu.Game/Overlays/Changelog/UpdateStreamBadge.cs @@ -26,7 +26,6 @@ namespace osu.Game.Overlays.Changelog private readonly APIUpdateStream stream; - private Container fadeContainer; private FillFlowContainer text; private ExpandingBar expandingBar; @@ -44,47 +43,39 @@ namespace osu.Game.Overlays.Changelog AddRange(new Drawable[] { - fadeContainer = new Container + text = new FillFlowContainer { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Margin = new MarginPadding { Top = 6 }, + Children = new[] { - text = new FillFlowContainer + new OsuSpriteText { - AutoSizeAxes = Axes.X, - RelativeSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Margin = new MarginPadding { Top = 6 }, - Children = new[] - { - new OsuSpriteText - { - Text = stream.DisplayName, - Font = OsuFont.GetFont(size: 12, weight: FontWeight.Black), - }, - new OsuSpriteText - { - Text = stream.LatestBuild.DisplayVersion, - Font = OsuFont.GetFont(size: 16, weight: FontWeight.Regular), - }, - new OsuSpriteText - { - Text = stream.LatestBuild.Users > 0 ? $"{"user".ToQuantity(stream.LatestBuild.Users, "N0")} online" : null, - Font = OsuFont.GetFont(size: 10), - Colour = colourProvider.Foreground1 - }, - } + Text = stream.DisplayName, + Font = OsuFont.GetFont(size: 12, weight: FontWeight.Black), }, - expandingBar = new ExpandingBar + new OsuSpriteText { - Anchor = Anchor.TopCentre, - Colour = stream.Colour, - ExpandedSize = 4, - CollapsedSize = 2, - IsCollapsed = true + Text = stream.LatestBuild.DisplayVersion, + Font = OsuFont.GetFont(size: 16, weight: FontWeight.Regular), + }, + new OsuSpriteText + { + Text = stream.LatestBuild.Users > 0 ? $"{"user".ToQuantity(stream.LatestBuild.Users, "N0")} online" : null, + Font = OsuFont.GetFont(size: 10), + Colour = colourProvider.Foreground1 }, } }, + expandingBar = new ExpandingBar + { + Anchor = Anchor.TopCentre, + Colour = stream.Colour, + ExpandedSize = 4, + CollapsedSize = 2, + IsCollapsed = true + }, new HoverClickSounds() }); @@ -112,21 +103,23 @@ namespace osu.Game.Overlays.Changelog // Expand based on the local state bool shouldExpand = Active.Value || IsHovered; + bool allHighlighted = SelectedTab.Value == null && !externalDimRequested; + // Expand based on whether no build is selected and the badge area is hovered - shouldExpand |= SelectedTab.Value == null && !externalDimRequested; + shouldExpand |= allHighlighted; if (shouldExpand) { expandingBar.Expand(); - fadeContainer.FadeTo(1, transition_duration); + expandingBar.FadeTo(1, transition_duration, Easing.OutQuint); } else { expandingBar.Collapse(); - fadeContainer.FadeTo(0.5f, transition_duration); + expandingBar.FadeTo(0.5f, transition_duration, Easing.OutQuint); } - text.FadeTo(externalDimRequested && !IsHovered ? 0.5f : 1, transition_duration); + text.FadeTo(IsHovered || (Active.Value && !externalDimRequested) || allHighlighted ? 1 : 0.5f, transition_duration, Easing.OutQuint); } private bool externalDimRequested; From f486db48de22a4638c1eb1d00c1cb3d5ae27caaf Mon Sep 17 00:00:00 2001 From: voidedWarranties Date: Thu, 27 Feb 2020 17:30:06 -0800 Subject: [PATCH 0069/2376] Add labels and fix dropdown bindable values --- osu.Game/Configuration/SettingSourceAttribute.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs index 4bdbb5fc24..fe487cb1d0 100644 --- a/osu.Game/Configuration/SettingSourceAttribute.cs +++ b/osu.Game/Configuration/SettingSourceAttribute.cs @@ -8,7 +8,6 @@ using System.Reflection; using JetBrains.Annotations; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.UserInterface; using osu.Game.Overlays.Settings; namespace osu.Game.Configuration @@ -105,7 +104,8 @@ namespace osu.Game.Configuration var dropdownType = typeof(SettingsEnumDropdown<>).MakeGenericType(bindable.GetType().GetGenericArguments()[0]); var dropdown = (Drawable)Activator.CreateInstance(dropdownType); - dropdown.GetType().GetProperty(nameof(IHasCurrentValue.Current))?.SetValue(dropdown, obj); + dropdownType.GetProperty(nameof(SettingsDropdown.LabelText))?.SetValue(dropdown, attr.Label); + dropdownType.GetProperty(nameof(SettingsDropdown.Bindable))?.SetValue(dropdown, bindable); yield return dropdown; From 377ae3e685466fa1dd4c2519f102a29a8f7336fc Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Fri, 28 Feb 2020 12:48:06 +0300 Subject: [PATCH 0070/2376] Make a separate if section for all highlighted case --- .../Overlays/Changelog/UpdateStreamBadge.cs | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/osu.Game/Overlays/Changelog/UpdateStreamBadge.cs b/osu.Game/Overlays/Changelog/UpdateStreamBadge.cs index dc9d3ccaed..5e10410f37 100644 --- a/osu.Game/Overlays/Changelog/UpdateStreamBadge.cs +++ b/osu.Game/Overlays/Changelog/UpdateStreamBadge.cs @@ -100,26 +100,20 @@ namespace osu.Game.Overlays.Changelog private void updateState() { - // Expand based on the local state - bool shouldExpand = Active.Value || IsHovered; - - bool allHighlighted = SelectedTab.Value == null && !externalDimRequested; - - // Expand based on whether no build is selected and the badge area is hovered - shouldExpand |= allHighlighted; - - if (shouldExpand) + if (SelectedTab.Value == null && !externalDimRequested) { expandingBar.Expand(); expandingBar.FadeTo(1, transition_duration, Easing.OutQuint); - } - else - { - expandingBar.Collapse(); - expandingBar.FadeTo(0.5f, transition_duration, Easing.OutQuint); + text.FadeTo(1, transition_duration, Easing.OutQuint); + return; } - text.FadeTo(IsHovered || (Active.Value && !externalDimRequested) || allHighlighted ? 1 : 0.5f, transition_duration, Easing.OutQuint); + var shouldExpand = Active.Value || IsHovered; + + expandingBar.IsCollapsed = !shouldExpand; + expandingBar.FadeTo(shouldExpand ? 1 : 0.5f, transition_duration, Easing.OutQuint); + + text.FadeTo(IsHovered || (Active.Value && !externalDimRequested) ? 1 : 0.5f, transition_duration, Easing.OutQuint); } private bool externalDimRequested; From 7b9937b851bebad05ba6f9b06cee7759f24f48f3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Feb 2020 22:15:39 +0900 Subject: [PATCH 0071/2376] 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 28fbdb3367..2a6bfa0f88 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index c5ebf0f712..ca62a959d9 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -23,7 +23,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index abd562dc81..11e7991dfa 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + @@ -79,7 +79,7 @@ - + From 4ad2d0cfb606835a9fe82ddb3b22181588e04e33 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Feb 2020 16:07:08 +0900 Subject: [PATCH 0072/2376] Remove deprecated debug setting --- osu.Game/Overlays/Settings/Sections/Debug/GeneralSettings.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Debug/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Debug/GeneralSettings.cs index 457f064f89..9edb18e065 100644 --- a/osu.Game/Overlays/Settings/Sections/Debug/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Debug/GeneralSettings.cs @@ -22,11 +22,6 @@ namespace osu.Game.Overlays.Settings.Sections.Debug Bindable = frameworkConfig.GetBindable(FrameworkSetting.ShowLogOverlay) }, new SettingsCheckbox - { - LabelText = "Performance logging", - Bindable = config.GetBindable(DebugSetting.PerformanceLogging) - }, - new SettingsCheckbox { LabelText = "Bypass front-to-back render pass", Bindable = config.GetBindable(DebugSetting.BypassFrontToBackPass) From 394b88aa65d704cf602fb6f7bae4e2f6d0f95640 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 28 Feb 2020 19:11:18 +0900 Subject: [PATCH 0073/2376] Add thread mode dropdown --- .../Overlays/Settings/Sections/Graphics/RendererSettings.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs index 7317076c54..69ff9b43e5 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Framework.Graphics; +using osu.Framework.Platform; using osu.Game.Configuration; namespace osu.Game.Overlays.Settings.Sections.Graphics @@ -24,6 +25,11 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics LabelText = "Frame limiter", Bindable = config.GetBindable(FrameworkSetting.FrameSync) }, + new SettingsEnumDropdown + { + LabelText = "Threading mode", + Bindable = config.GetBindable(FrameworkSetting.ExecutionMode) + }, new SettingsCheckbox { LabelText = "Show FPS", From 036f155afe755d961c99fa891cc3b9095b733cf6 Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Fri, 28 Feb 2020 21:09:31 +0100 Subject: [PATCH 0074/2376] Adjust colours in DrawableMostPlayedBeatmap --- .../Historical/DrawableMostPlayedBeatmap.cs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs b/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs index e75ad2f161..dc8492562a 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs @@ -37,8 +37,10 @@ namespace osu.Game.Overlays.Profile.Sections.Historical } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OsuColour colours, OverlayColourProvider colourProvider) { + ProfileItemContainer container; + AddRangeInternal(new Drawable[] { new UpdateableBeatmapSetCover @@ -61,7 +63,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical CornerRadius = corner_radius, Children = new Drawable[] { - new ProfileItemContainer + container = new ProfileItemContainer { Child = new Container { @@ -78,11 +80,14 @@ namespace osu.Game.Overlays.Profile.Sections.Historical Children = new Drawable[] { new MostPlayedBeatmapMetadataContainer(beatmap), - new LinkFlowContainer(t => t.Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular)) + new LinkFlowContainer(t => + { + t.Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular); + t.Colour = colourProvider.Foreground1; + }) { AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, - Colour = colours.GreySeafoamLighter }.With(d => { d.AddText("mapped by "); @@ -103,6 +108,9 @@ namespace osu.Game.Overlays.Profile.Sections.Historical } } }); + + container.IdleColour = colourProvider.Background4; + container.HoverColour = colourProvider.Background3; } private class MostPlayedBeatmapMetadataContainer : BeatmapMetadataContainer From 5838af39c1a20cc8bc1dc5fc6aab93c4dc82403f Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Fri, 28 Feb 2020 21:09:49 +0100 Subject: [PATCH 0075/2376] Add background colour customization to ProfileItemContainer --- .../Profile/Sections/ProfileItemContainer.cs | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/ProfileItemContainer.cs b/osu.Game/Overlays/Profile/Sections/ProfileItemContainer.cs index f65c909155..a48f21f52b 100644 --- a/osu.Game/Overlays/Profile/Sections/ProfileItemContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/ProfileItemContainer.cs @@ -16,12 +16,19 @@ namespace osu.Game.Overlays.Profile.Sections protected override Container Content => content; - private Color4 idleColour; - private Color4 hoverColour; - private readonly Box background; private readonly Container content; + private Color4 idleColour; + + public Color4 IdleColour + { + get => idleColour; + set => idleColour = background.Colour = value; + } + + public Color4 HoverColour { get; set; } + public ProfileItemContainer() { RelativeSizeAxes = Axes.Both; @@ -44,20 +51,20 @@ namespace osu.Game.Overlays.Profile.Sections [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) { - background.Colour = idleColour = colourProvider.Background3; - hoverColour = colourProvider.Background2; + IdleColour = colourProvider.Background3; + HoverColour = colourProvider.Background2; } protected override bool OnHover(HoverEvent e) { - background.FadeColour(hoverColour, hover_duration, Easing.OutQuint); + background.FadeColour(HoverColour, hover_duration, Easing.OutQuint); return base.OnHover(e); } protected override void OnHoverLost(HoverLostEvent e) { base.OnHoverLost(e); - background.FadeColour(idleColour, hover_duration, Easing.OutQuint); + background.FadeColour(IdleColour, hover_duration, Easing.OutQuint); } } } From d71b51690235187b31fb73a28f70d1cedf583dd8 Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Fri, 28 Feb 2020 21:58:37 +0100 Subject: [PATCH 0076/2376] Check beatmap ranking status instead of the pp value --- osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs | 10 +++++++--- .../BeatmapSet/Scores/TopScoreStatisticsSection.cs | 3 ++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs index c9008adc85..25537537d9 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions; +using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -28,6 +29,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores private readonly FillFlowContainer backgroundFlow; private Color4 highAccuracyColour; + private bool isBeatmapRanked; public ScoreTable() { @@ -65,7 +67,9 @@ namespace osu.Game.Overlays.BeatmapSet.Scores for (int i = 0; i < value.Count; i++) backgroundFlow.Add(new ScoreTableRowBackground(i, value[i], row_height)); - Columns = createHeaders(value[0]); + isBeatmapRanked = value.First().Beatmap.Status == BeatmapSetOnlineStatus.Ranked; + + Columns = createHeaders(value.First()); Content = value.Select((s, i) => createContent(i, s)).ToArray().ToRectangular(); } } @@ -88,7 +92,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores columns.Add(new TableColumn(score.SortedStatistics.LastOrDefault().Key.GetDescription(), Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 45, maxSize: 95))); - if (score.PP.HasValue) + if (isBeatmapRanked) columns.Add(new TableColumn("pp", Anchor.CentreLeft, new Dimension(GridSizeMode.Absolute, 30))); columns.Add(new TableColumn("mods", Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize))); @@ -149,7 +153,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores }); } - if (score.PP.HasValue) + if (isBeatmapRanked) { content.Add(new OsuSpriteText { diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs index 12ad014f61..9ecc40eed2 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Mods; @@ -96,7 +97,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores totalScoreColumn.Text = $@"{value.TotalScore:N0}"; accuracyColumn.Text = value.DisplayAccuracy; maxComboColumn.Text = $@"{value.MaxCombo:N0}x"; - ppColumn.Alpha = value.PP.HasValue ? 1 : 0; + ppColumn.Alpha = value.Beatmap.Status == BeatmapSetOnlineStatus.Ranked ? 1 : 0; ppColumn.Text = $@"{value.PP:N0}"; statisticsColumns.ChildrenEnumerable = value.SortedStatistics.Select(kvp => createStatisticsColumn(kvp.Key, kvp.Value)); From 5dff7f0955a9108290277a971e0b5c7b2b73266a Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 29 Feb 2020 02:21:52 +0300 Subject: [PATCH 0077/2376] Adjust horizontal padding --- osu.Game/Overlays/Changelog/ChangelogHeader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Changelog/ChangelogHeader.cs b/osu.Game/Overlays/Changelog/ChangelogHeader.cs index f5b10ef0f2..dcadbf4cf5 100644 --- a/osu.Game/Overlays/Changelog/ChangelogHeader.cs +++ b/osu.Game/Overlays/Changelog/ChangelogHeader.cs @@ -92,7 +92,7 @@ namespace osu.Game.Overlays.Changelog AutoSizeAxes = Axes.Y, Padding = new MarginPadding { - Horizontal = 85, + Horizontal = 65, Vertical = 20 }, Child = Streams = new UpdateStreamBadgeArea() From f18a1cde53e840b35646a5e946a8218f0d9b5507 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 29 Feb 2020 08:39:27 +0900 Subject: [PATCH 0078/2376] Fix crash when reaching results screen on single threaded execution mode --- osu.Game/Screens/Play/Player.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index bd43b23a9f..11ca36e25f 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -609,9 +609,9 @@ namespace osu.Game.Screens.Play { var score = CreateScore(); if (DrawableRuleset.ReplayScore == null) - scoreManager.Import(score).Wait(); - - this.Push(CreateResults(score)); + scoreManager.Import(score).ContinueWith(_ => Schedule(() => this.Push(CreateResults(score)))); + else + this.Push(CreateResults(score)); }); } From a332b6980bc1dc250a6effcb4fa5e18b15f40ac0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 29 Feb 2020 14:28:26 +0900 Subject: [PATCH 0079/2376] 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 2a6bfa0f88..651d1beda1 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index ca62a959d9..168a358e47 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -23,7 +23,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 11e7991dfa..3c2a67e908 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + @@ -79,7 +79,7 @@ - + From f661806513767adaf69f5989bcf4b3b90397f994 Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Sat, 29 Feb 2020 15:29:00 +0100 Subject: [PATCH 0080/2376] Move checking logic out of ScoreTable --- osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs | 9 ++++----- osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs | 3 ++- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs index 25537537d9..3a58f481e1 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs @@ -29,7 +29,6 @@ namespace osu.Game.Overlays.BeatmapSet.Scores private readonly FillFlowContainer backgroundFlow; private Color4 highAccuracyColour; - private bool isBeatmapRanked; public ScoreTable() { @@ -67,13 +66,13 @@ namespace osu.Game.Overlays.BeatmapSet.Scores for (int i = 0; i < value.Count; i++) backgroundFlow.Add(new ScoreTableRowBackground(i, value[i], row_height)); - isBeatmapRanked = value.First().Beatmap.Status == BeatmapSetOnlineStatus.Ranked; - Columns = createHeaders(value.First()); Content = value.Select((s, i) => createContent(i, s)).ToArray().ToRectangular(); } } + public bool IsBeatmapRanked { get; set; } + private TableColumn[] createHeaders(ScoreInfo score) { var columns = new List @@ -92,7 +91,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores columns.Add(new TableColumn(score.SortedStatistics.LastOrDefault().Key.GetDescription(), Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 45, maxSize: 95))); - if (isBeatmapRanked) + if (IsBeatmapRanked) columns.Add(new TableColumn("pp", Anchor.CentreLeft, new Dimension(GridSizeMode.Absolute, 30))); columns.Add(new TableColumn("mods", Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize))); @@ -153,7 +152,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores }); } - if (isBeatmapRanked) + if (IsBeatmapRanked) { content.Add(new OsuSpriteText { diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs index e831c8ce42..5a931fffcb 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs @@ -59,11 +59,12 @@ namespace osu.Game.Overlays.BeatmapSet.Scores } var scoreInfos = value.Scores.Select(s => s.CreateScoreInfo(rulesets)).ToList(); + var topScore = scoreInfos.First(); scoreTable.Scores = scoreInfos; + scoreTable.IsBeatmapRanked = topScore.Beatmap.Status == BeatmapSetOnlineStatus.Ranked; scoreTable.Show(); - var topScore = scoreInfos.First(); var userScore = value.UserScore; var userScoreInfo = userScore?.Score.CreateScoreInfo(rulesets); From 4d19278df498a940b2356d84a00eeb77456cc6e7 Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Sat, 29 Feb 2020 15:43:48 +0100 Subject: [PATCH 0081/2376] Remove using directive --- osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs index 3a58f481e1..af6bf8299f 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs @@ -7,7 +7,6 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions; -using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; From bca58ddb42f73cc79116b1450c4db446bd015577 Mon Sep 17 00:00:00 2001 From: naoey Date: Sat, 29 Feb 2020 21:07:42 +0530 Subject: [PATCH 0082/2376] Make KeyCounter stop counting during breaks --- .../Visual/Gameplay/TestSceneAutoplay.cs | 16 ++++++++++++---- osu.Game/Screens/Play/Player.cs | 10 ++++++++-- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs index 4daab8d137..ebd89039a5 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs @@ -24,10 +24,17 @@ namespace osu.Game.Tests.Visual.Gameplay protected override void AddCheckSteps() { - AddUntilStep("score above zero", () => ((ScoreAccessiblePlayer)Player).ScoreProcessor.TotalScore.Value > 0); - AddUntilStep("key counter counted keys", () => ((ScoreAccessiblePlayer)Player).HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 2)); - AddStep("rewind", () => track.Seek(-10000)); - AddUntilStep("key counter reset", () => ((ScoreAccessiblePlayer)Player).HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0)); + ScoreAccessiblePlayer scoreAccessiblePlayer = null; + + AddUntilStep("player loaded", () => (scoreAccessiblePlayer = (ScoreAccessiblePlayer)Player) != null); + AddUntilStep("score above zero", () => scoreAccessiblePlayer.ScoreProcessor.TotalScore.Value > 0); + AddUntilStep("key counter counted keys", () => scoreAccessiblePlayer.HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 2)); + AddStep("seek to break time", () => scoreAccessiblePlayer.GameplayClockContainer.Seek(scoreAccessiblePlayer.BreakOverlay.Breaks.First().StartTime)); + AddUntilStep("wait for seek to complete", () => + scoreAccessiblePlayer.HUDOverlay.Progress.ReferenceClock.CurrentTime >= scoreAccessiblePlayer.BreakOverlay.Breaks.First().StartTime); + AddAssert("test keys not counting", () => !scoreAccessiblePlayer.HUDOverlay.KeyCounter.IsCounting); + AddStep("rewind", () => scoreAccessiblePlayer.GameplayClockContainer.Seek(-80000)); + AddUntilStep("key counter reset", () => scoreAccessiblePlayer.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0)); } protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) @@ -43,6 +50,7 @@ namespace osu.Game.Tests.Visual.Gameplay { public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; public new HUDOverlay HUDOverlay => base.HUDOverlay; + public new BreakOverlay BreakOverlay => base.BreakOverlay; public new GameplayClockContainer GameplayClockContainer => base.GameplayClockContainer; diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 11ca36e25f..237e364e69 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -184,7 +184,7 @@ namespace osu.Game.Screens.Play foreach (var mod in Mods.Value.OfType()) mod.ApplyToHealthProcessor(HealthProcessor); - BreakOverlay.IsBreakTime.ValueChanged += _ => updatePauseOnFocusLostState(); + BreakOverlay.IsBreakTime.BindValueChanged(_ => onBreakTimeChanged(), true); } private void addUnderlayComponents(Container target) @@ -229,7 +229,7 @@ namespace osu.Game.Screens.Play IsPaused = { BindTarget = GameplayClockContainer.IsPaused } }, PlayerSettingsOverlay = { PlaybackSettings = { UserPlaybackRate = { BindTarget = GameplayClockContainer.UserPlaybackRate } } }, - KeyCounter = { AlwaysVisible = { BindTarget = DrawableRuleset.HasReplayLoaded } }, + KeyCounter = { AlwaysVisible = { BindTarget = DrawableRuleset.HasReplayLoaded }, IsCounting = false }, RequestSeek = GameplayClockContainer.Seek, Anchor = Anchor.Centre, Origin = Anchor.Centre @@ -286,6 +286,12 @@ namespace osu.Game.Screens.Play HealthProcessor.IsBreakTime.BindTo(BreakOverlay.IsBreakTime); } + private void onBreakTimeChanged() + { + updatePauseOnFocusLostState(); + HUDOverlay.KeyCounter.IsCounting = !BreakOverlay.IsBreakTime.Value; + } + private void updatePauseOnFocusLostState() => HUDOverlay.HoldToQuit.PauseOnFocusLost = PauseOnFocusLost && !DrawableRuleset.HasReplayLoaded.Value From 1ce972dd5b4a06d7ca6774a7daaf79cf3902dc69 Mon Sep 17 00:00:00 2001 From: naoey Date: Sat, 29 Feb 2020 21:53:49 +0530 Subject: [PATCH 0083/2376] Remove unused variable --- osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs index ebd89039a5..8e16d74dca 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs @@ -14,8 +14,6 @@ namespace osu.Game.Tests.Visual.Gameplay [Description("Player instantiated with an autoplay mod.")] public class TestSceneAutoplay : TestSceneAllRulesetPlayers { - private ClockBackedTestWorkingBeatmap.TrackVirtualManual track; - protected override Player CreatePlayer(Ruleset ruleset) { SelectedMods.Value = SelectedMods.Value.Concat(new[] { ruleset.GetAutoplayMod() }).ToArray(); @@ -41,8 +39,6 @@ namespace osu.Game.Tests.Visual.Gameplay { var working = base.CreateWorkingBeatmap(beatmap, storyboard); - track = (ClockBackedTestWorkingBeatmap.TrackVirtualManual)working.Track; - return working; } From b9fef4f715717851998ec403582e46e6bf62207e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 1 Mar 2020 09:26:16 +0900 Subject: [PATCH 0084/2376] Update link location for appimage --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ae57b1d954..77c7eb9d2d 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ If you are looking to install or test osu! without setting up a development envi **Latest build:** -| [Windows (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | [macOS 10.12+](https://github.com/ppy/osu/releases/latest/download/osu.app.zip) | [Linux (x64)](https://github.com/ppy/osu/releases/latest/download/osu.x86_64.AppImage) | [iOS(iOS 10+)](https://osu.ppy.sh/home/testflight) | [Android (5+)](https://github.com/ppy/osu/releases/latest/download/sh.ppy.osulazer.apk) +| [Windows (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | [macOS 10.12+](https://github.com/ppy/osu/releases/latest/download/osu.app.zip) | [Linux (x64)](https://github.com/ppy/osu/releases/latest/download/osu.AppImage) | [iOS(iOS 10+)](https://osu.ppy.sh/home/testflight) | [Android (5+)](https://github.com/ppy/osu/releases/latest/download/sh.ppy.osulazer.apk) | ------------- | ------------- | ------------- | ------------- | ------------- | - When running on Windows 7 or 8.1, **[additional prerequisites](https://docs.microsoft.com/en-us/dotnet/core/install/dependencies?tabs=netcore31&pivots=os-windows)** may be required to correctly run .NET Core applications if your operating system is not up-to-date with the latest service packs. From 089ec4c7922a18fe4f4da718bbc4ede63ddf6289 Mon Sep 17 00:00:00 2001 From: voidedWarranties Date: Sat, 29 Feb 2020 21:16:28 -0800 Subject: [PATCH 0085/2376] Test scene for mod development --- .../TestSceneCatchModSandbox.cs | 28 +++++ .../TestSceneManiaModSandbox.cs | 28 +++++ .../Mods/TestSceneOsuModDifficultyAdjust.cs | 20 ++++ .../TestSceneOsuModSandbox.cs | 28 +++++ .../TestSceneTaikoModSandbox.cs | 28 +++++ osu.Game/Tests/Visual/PlayerTestScene.cs | 4 +- osu.Game/Tests/Visual/TestSceneModSandbox.cs | 108 ++++++++++++++++++ 7 files changed, 242 insertions(+), 2 deletions(-) create mode 100644 osu.Game.Rulesets.Catch.Tests/TestSceneCatchModSandbox.cs create mode 100644 osu.Game.Rulesets.Mania.Tests/TestSceneManiaModSandbox.cs create mode 100644 osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs create mode 100644 osu.Game.Rulesets.Osu.Tests/TestSceneOsuModSandbox.cs create mode 100644 osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoModSandbox.cs create mode 100644 osu.Game/Tests/Visual/TestSceneModSandbox.cs diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModSandbox.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModSandbox.cs new file mode 100644 index 0000000000..3abf8163bd --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModSandbox.cs @@ -0,0 +1,28 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Game.Rulesets.Mods; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Catch.Tests +{ + [TestFixture] + public class TestSceneCatchModSandbox : TestSceneModSandbox + { + public override IReadOnlyList RequiredTypes => base.RequiredTypes.Append(typeof(TestSceneCatchModSandbox)).ToList(); + + public TestSceneCatchModSandbox() + : this(null) + { + } + + public TestSceneCatchModSandbox(Mod mod = null) + : base(new CatchRuleset(), mod) + { + } + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaModSandbox.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaModSandbox.cs new file mode 100644 index 0000000000..2693cebb43 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaModSandbox.cs @@ -0,0 +1,28 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Game.Rulesets.Mods; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Mania.Tests +{ + [TestFixture] + public class TestSceneManiaModSandbox : TestSceneModSandbox + { + public override IReadOnlyList RequiredTypes => base.RequiredTypes.Append(typeof(TestSceneManiaModSandbox)).ToList(); + + public TestSceneManiaModSandbox() + : this(null) + { + } + + public TestSceneManiaModSandbox(Mod mod = null) + : base(new ManiaRuleset(), mod) + { + } + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs new file mode 100644 index 0000000000..7f09731a11 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.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; +using System.Collections.Generic; +using System.Linq; +using osu.Game.Rulesets.Osu.Mods; + +namespace osu.Game.Rulesets.Osu.Tests.Mods +{ + public class TestSceneOsuModDifficultyAdjust : TestSceneOsuModSandbox + { + public override IReadOnlyList RequiredTypes => base.RequiredTypes.Append(typeof(OsuModDifficultyAdjust)).ToList(); + + public TestSceneOsuModDifficultyAdjust() + : base(new OsuModDifficultyAdjust()) + { + } + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuModSandbox.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuModSandbox.cs new file mode 100644 index 0000000000..d2a9d1ea6e --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuModSandbox.cs @@ -0,0 +1,28 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Game.Rulesets.Mods; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Osu.Tests +{ + [TestFixture] + public class TestSceneOsuModSandbox : TestSceneModSandbox + { + public override IReadOnlyList RequiredTypes => base.RequiredTypes.Append(typeof(TestSceneOsuModSandbox)).ToList(); + + public TestSceneOsuModSandbox() + : this(null) + { + } + + public TestSceneOsuModSandbox(Mod mod = null) + : base(new OsuRuleset(), mod) + { + } + } +} diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoModSandbox.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoModSandbox.cs new file mode 100644 index 0000000000..f5481713f5 --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoModSandbox.cs @@ -0,0 +1,28 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Game.Rulesets.Mods; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Taiko.Tests +{ + [TestFixture] + public class TestSceneTaikoModSandbox : TestSceneModSandbox + { + public override IReadOnlyList RequiredTypes => base.RequiredTypes.Append(typeof(TestSceneTaikoModSandbox)).ToList(); + + public TestSceneTaikoModSandbox() + : this(null) + { + } + + public TestSceneTaikoModSandbox(Mod mod = null) + : base(new TaikoRuleset(), mod) + { + } + } +} diff --git a/osu.Game/Tests/Visual/PlayerTestScene.cs b/osu.Game/Tests/Visual/PlayerTestScene.cs index 7c5ba7d30f..1ca5256353 100644 --- a/osu.Game/Tests/Visual/PlayerTestScene.cs +++ b/osu.Game/Tests/Visual/PlayerTestScene.cs @@ -37,7 +37,7 @@ namespace osu.Game.Tests.Visual { base.SetUpSteps(); - AddStep(ruleset.RulesetInfo.Name, loadPlayer); + AddStep(ruleset.RulesetInfo.Name, LoadPlayer); AddUntilStep("player loaded", () => Player.IsLoaded && Player.Alpha == 1); } @@ -45,7 +45,7 @@ namespace osu.Game.Tests.Visual protected virtual bool Autoplay => false; - private void loadPlayer() + protected void LoadPlayer() { var beatmap = CreateBeatmap(ruleset.RulesetInfo); diff --git a/osu.Game/Tests/Visual/TestSceneModSandbox.cs b/osu.Game/Tests/Visual/TestSceneModSandbox.cs new file mode 100644 index 0000000000..5c32ebadce --- /dev/null +++ b/osu.Game/Tests/Visual/TestSceneModSandbox.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.Linq; +using System.Reflection; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Game.Configuration; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays.Mods; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Screens.Play; + +namespace osu.Game.Tests.Visual +{ + public abstract class TestSceneModSandbox : PlayerTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(TestSceneModSandbox) + }; + + protected Mod Mod; + private readonly TriangleButton button; + + protected TestSceneModSandbox(Ruleset ruleset, Mod mod = null) + : base(ruleset) + { + Mod = mod ?? new SandboxMod(); + + var props = Mod.GetType().GetProperties(BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.Instance); + var hasSettings = props.Any(prop => prop.GetCustomAttribute(true) != null); + + Child = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Padding = new MarginPadding(50), + Margin = new MarginPadding { Bottom = 20 }, + Width = 0.4f, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Children = new Drawable[] + { + new ModControlSection(Mod, Mod.CreateSettingsControls()), + button = new TriangleButton + { + RelativeSizeAxes = Axes.X, + Width = 0.5f, + Text = "Start", + Action = () => + { + button.Text = hasSettings ? "Apply Settings" : "Restart"; + LoadPlayer(); + } + } + } + }; + } + + [SetUpSteps] + public override void SetUpSteps() + { + } + + [BackgroundDependencyLoader] + private void load() + { + LocalConfig.GetBindable(OsuSetting.KeyOverlay).Value = true; + } + + protected override Player CreatePlayer(Ruleset ruleset) + { + SelectedMods.Value = SelectedMods.Value.Append(Mod).ToArray(); + + return base.CreatePlayer(ruleset); + } + + protected class SandboxMod : Mod + { + public override string Name => "Sandbox Test"; + public override string Acronym => "ST"; + public override double ScoreMultiplier => 1.0; + + [SettingSource("Test Setting")] + public Bindable TestSetting1 { get; } = new BindableBool + { + Default = true, + Value = true + }; + + [SettingSource("Test Setting 2")] + public Bindable TestSetting2 { get; } = new BindableFloat + { + Precision = 0.1f, + MinValue = 0, + MaxValue = 20 + }; + } + } +} From a02c5710ac07a2486be020a7b0e2d1d4feb035b1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 2 Mar 2020 10:06:49 +0900 Subject: [PATCH 0086/2376] Rename base class --- osu.Game.Rulesets.Catch.Tests/TestSceneCatchModSandbox.cs | 2 +- osu.Game.Rulesets.Mania.Tests/TestSceneManiaModSandbox.cs | 2 +- osu.Game.Rulesets.Osu.Tests/TestSceneOsuModSandbox.cs | 2 +- osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoModSandbox.cs | 2 +- .../{TestSceneModSandbox.cs => ModSandboxTestScene.cs} | 6 +++--- 5 files changed, 7 insertions(+), 7 deletions(-) rename osu.Game/Tests/Visual/{TestSceneModSandbox.cs => ModSandboxTestScene.cs} (95%) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModSandbox.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModSandbox.cs index 3abf8163bd..3e94121fd7 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModSandbox.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModSandbox.cs @@ -11,7 +11,7 @@ using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Catch.Tests { [TestFixture] - public class TestSceneCatchModSandbox : TestSceneModSandbox + public class TestSceneCatchModSandbox : ModSandboxTestScene { public override IReadOnlyList RequiredTypes => base.RequiredTypes.Append(typeof(TestSceneCatchModSandbox)).ToList(); diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaModSandbox.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaModSandbox.cs index 2693cebb43..897e7df1e9 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaModSandbox.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaModSandbox.cs @@ -11,7 +11,7 @@ using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Mania.Tests { [TestFixture] - public class TestSceneManiaModSandbox : TestSceneModSandbox + public class TestSceneManiaModSandbox : ModSandboxTestScene { public override IReadOnlyList RequiredTypes => base.RequiredTypes.Append(typeof(TestSceneManiaModSandbox)).ToList(); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuModSandbox.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuModSandbox.cs index d2a9d1ea6e..9f816ef2ed 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuModSandbox.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuModSandbox.cs @@ -11,7 +11,7 @@ using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Osu.Tests { [TestFixture] - public class TestSceneOsuModSandbox : TestSceneModSandbox + public class TestSceneOsuModSandbox : ModSandboxTestScene { public override IReadOnlyList RequiredTypes => base.RequiredTypes.Append(typeof(TestSceneOsuModSandbox)).ToList(); diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoModSandbox.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoModSandbox.cs index f5481713f5..ef62c7ed56 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoModSandbox.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoModSandbox.cs @@ -11,7 +11,7 @@ using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Taiko.Tests { [TestFixture] - public class TestSceneTaikoModSandbox : TestSceneModSandbox + public class TestSceneTaikoModSandbox : ModSandboxTestScene { public override IReadOnlyList RequiredTypes => base.RequiredTypes.Append(typeof(TestSceneTaikoModSandbox)).ToList(); diff --git a/osu.Game/Tests/Visual/TestSceneModSandbox.cs b/osu.Game/Tests/Visual/ModSandboxTestScene.cs similarity index 95% rename from osu.Game/Tests/Visual/TestSceneModSandbox.cs rename to osu.Game/Tests/Visual/ModSandboxTestScene.cs index 5c32ebadce..bb872123c5 100644 --- a/osu.Game/Tests/Visual/TestSceneModSandbox.cs +++ b/osu.Game/Tests/Visual/ModSandboxTestScene.cs @@ -19,17 +19,17 @@ using osu.Game.Screens.Play; namespace osu.Game.Tests.Visual { - public abstract class TestSceneModSandbox : PlayerTestScene + public abstract class ModSandboxTestScene : PlayerTestScene { public override IReadOnlyList RequiredTypes => new[] { - typeof(TestSceneModSandbox) + typeof(ModSandboxTestScene) }; protected Mod Mod; private readonly TriangleButton button; - protected TestSceneModSandbox(Ruleset ruleset, Mod mod = null) + protected ModSandboxTestScene(Ruleset ruleset, Mod mod = null) : base(ruleset) { Mod = mod ?? new SandboxMod(); From 5c15704c819ab25868c48f511a26b417a2bb96d0 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 2 Mar 2020 10:28:39 +0900 Subject: [PATCH 0087/2376] Improve abstract structure for testability --- osu.Game/Tests/Visual/ModSandboxTestScene.cs | 96 ++++++-------------- osu.Game/Tests/Visual/TestReplayPlayer.cs | 24 +++++ 2 files changed, 50 insertions(+), 70 deletions(-) create mode 100644 osu.Game/Tests/Visual/TestReplayPlayer.cs diff --git a/osu.Game/Tests/Visual/ModSandboxTestScene.cs b/osu.Game/Tests/Visual/ModSandboxTestScene.cs index bb872123c5..84bab6a9b9 100644 --- a/osu.Game/Tests/Visual/ModSandboxTestScene.cs +++ b/osu.Game/Tests/Visual/ModSandboxTestScene.cs @@ -4,15 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reflection; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Testing; -using osu.Game.Configuration; -using osu.Game.Graphics.UserInterface; -using osu.Game.Overlays.Mods; +using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Screens.Play; @@ -26,83 +18,47 @@ namespace osu.Game.Tests.Visual typeof(ModSandboxTestScene) }; - protected Mod Mod; - private readonly TriangleButton button; - - protected ModSandboxTestScene(Ruleset ruleset, Mod mod = null) + protected ModSandboxTestScene(Ruleset ruleset) : base(ruleset) { - Mod = mod ?? new SandboxMod(); - - var props = Mod.GetType().GetProperties(BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.Instance); - var hasSettings = props.Any(prop => prop.GetCustomAttribute(true) != null); - - Child = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Padding = new MarginPadding(50), - Margin = new MarginPadding { Bottom = 20 }, - Width = 0.4f, - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Children = new Drawable[] - { - new ModControlSection(Mod, Mod.CreateSettingsControls()), - button = new TriangleButton - { - RelativeSizeAxes = Axes.X, - Width = 0.5f, - Text = "Start", - Action = () => - { - button.Text = hasSettings ? "Apply Settings" : "Restart"; - LoadPlayer(); - } - } - } - }; } - [SetUpSteps] + private ModTestCaseData currentTest; + public override void SetUpSteps() { + foreach (var testCase in CreateTestCases()) + { + AddStep("set test case", () => currentTest = testCase); + base.SetUpSteps(); + } } - [BackgroundDependencyLoader] - private void load() - { - LocalConfig.GetBindable(OsuSetting.KeyOverlay).Value = true; - } + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => currentTest.Beatmap; protected override Player CreatePlayer(Ruleset ruleset) { - SelectedMods.Value = SelectedMods.Value.Append(Mod).ToArray(); + SelectedMods.Value = SelectedMods.Value.Append(currentTest.Mod).ToArray(); + + if (currentTest.Autoplay) + { + // We're simulating an auto-play via a replay so that the auto-play mod does not interfere + var beatmap = Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo, SelectedMods.Value); + var score = ruleset.GetAutoplayMod().CreateReplayScore(beatmap); + + return new TestReplayPlayer(score, false, false); + } return base.CreatePlayer(ruleset); } - protected class SandboxMod : Mod + protected abstract ModTestCaseData[] CreateTestCases(); + + protected class ModTestCaseData { - public override string Name => "Sandbox Test"; - public override string Acronym => "ST"; - public override double ScoreMultiplier => 1.0; - - [SettingSource("Test Setting")] - public Bindable TestSetting1 { get; } = new BindableBool - { - Default = true, - Value = true - }; - - [SettingSource("Test Setting 2")] - public Bindable TestSetting2 { get; } = new BindableFloat - { - Precision = 0.1f, - MinValue = 0, - MaxValue = 20 - }; + public Mod Mod; + public bool Autoplay; + public IBeatmap Beatmap; } } } diff --git a/osu.Game/Tests/Visual/TestReplayPlayer.cs b/osu.Game/Tests/Visual/TestReplayPlayer.cs new file mode 100644 index 0000000000..e99fcc1e37 --- /dev/null +++ b/osu.Game/Tests/Visual/TestReplayPlayer.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.Game.Rulesets.UI; +using osu.Game.Scoring; +using osu.Game.Screens.Play; + +namespace osu.Game.Tests.Visual +{ + public class TestReplayPlayer : ReplayPlayer + { + protected override bool PauseOnFocusLost { get; } + + public new DrawableRuleset DrawableRuleset => base.DrawableRuleset; + + public new GameplayClockContainer GameplayClockContainer => base.GameplayClockContainer; + + public TestReplayPlayer(Score score, bool allowPause = true, bool showResults = true, bool pauseOnFocusLost = false) + : base(score, allowPause, showResults) + { + PauseOnFocusLost = pauseOnFocusLost; + } + } +} From 239cfddcbb92503b662d89750953950b6a11a12d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 2 Mar 2020 10:50:41 +0900 Subject: [PATCH 0088/2376] Improve test scenes/cases --- .../TestSceneCatchModSandbox.cs | 28 ------- .../TestSceneManiaModSandbox.cs | 28 ------- .../Mods/TestSceneOsuModDifficultyAdjust.cs | 38 +++++++++- .../TestSceneOsuModSandbox.cs | 28 ------- .../TestSceneTaikoModSandbox.cs | 28 ------- osu.Game/Tests/Visual/ModSandboxTestScene.cs | 73 +++++++++++++++---- 6 files changed, 95 insertions(+), 128 deletions(-) delete mode 100644 osu.Game.Rulesets.Catch.Tests/TestSceneCatchModSandbox.cs delete mode 100644 osu.Game.Rulesets.Mania.Tests/TestSceneManiaModSandbox.cs delete mode 100644 osu.Game.Rulesets.Osu.Tests/TestSceneOsuModSandbox.cs delete mode 100644 osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoModSandbox.cs diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModSandbox.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModSandbox.cs deleted file mode 100644 index 3e94121fd7..0000000000 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModSandbox.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Collections.Generic; -using System.Linq; -using NUnit.Framework; -using osu.Game.Rulesets.Mods; -using osu.Game.Tests.Visual; - -namespace osu.Game.Rulesets.Catch.Tests -{ - [TestFixture] - public class TestSceneCatchModSandbox : ModSandboxTestScene - { - public override IReadOnlyList RequiredTypes => base.RequiredTypes.Append(typeof(TestSceneCatchModSandbox)).ToList(); - - public TestSceneCatchModSandbox() - : this(null) - { - } - - public TestSceneCatchModSandbox(Mod mod = null) - : base(new CatchRuleset(), mod) - { - } - } -} diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaModSandbox.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaModSandbox.cs deleted file mode 100644 index 897e7df1e9..0000000000 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaModSandbox.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Collections.Generic; -using System.Linq; -using NUnit.Framework; -using osu.Game.Rulesets.Mods; -using osu.Game.Tests.Visual; - -namespace osu.Game.Rulesets.Mania.Tests -{ - [TestFixture] - public class TestSceneManiaModSandbox : ModSandboxTestScene - { - public override IReadOnlyList RequiredTypes => base.RequiredTypes.Append(typeof(TestSceneManiaModSandbox)).ToList(); - - public TestSceneManiaModSandbox() - : this(null) - { - } - - public TestSceneManiaModSandbox(Mod mod = null) - : base(new ManiaRuleset(), mod) - { - } - } -} diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs index 7f09731a11..46a3c1dff3 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs @@ -5,16 +5,50 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Osu.Tests.Mods { - public class TestSceneOsuModDifficultyAdjust : TestSceneOsuModSandbox + public class TestSceneOsuModDifficultyAdjust : ModSandboxTestScene { public override IReadOnlyList RequiredTypes => base.RequiredTypes.Append(typeof(OsuModDifficultyAdjust)).ToList(); public TestSceneOsuModDifficultyAdjust() - : base(new OsuModDifficultyAdjust()) + : base(new OsuRuleset()) { } + + protected override ModTestCaseData[] CreateTestCases() => new[] + { + new ModTestCaseData("no adjustment", new OsuModDifficultyAdjust()) + { + Autoplay = true, + PassCondition = () => ((ScoreAccessibleTestPlayer)Player).ScoreProcessor.JudgedHits >= 2 + }, + new ModTestCaseData("cs = 10", new OsuModDifficultyAdjust { CircleSize = { Value = 10 } }) + { + Autoplay = true, + PassCondition = () => ((ScoreAccessibleTestPlayer)Player).ScoreProcessor.JudgedHits >= 2 + }, + new ModTestCaseData("ar = 10", new OsuModDifficultyAdjust { ApproachRate = { Value = 10 } }) + { + Autoplay = true, + PassCondition = () => ((ScoreAccessibleTestPlayer)Player).ScoreProcessor.JudgedHits >= 2 + }, + }; + + protected override TestPlayer CreateReplayPlayer(Score score) => new ScoreAccessibleTestPlayer(score); + + private class ScoreAccessibleTestPlayer : TestPlayer + { + public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; + + public ScoreAccessibleTestPlayer(Score score) + : base(score) + { + } + } } } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuModSandbox.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuModSandbox.cs deleted file mode 100644 index 9f816ef2ed..0000000000 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuModSandbox.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Collections.Generic; -using System.Linq; -using NUnit.Framework; -using osu.Game.Rulesets.Mods; -using osu.Game.Tests.Visual; - -namespace osu.Game.Rulesets.Osu.Tests -{ - [TestFixture] - public class TestSceneOsuModSandbox : ModSandboxTestScene - { - public override IReadOnlyList RequiredTypes => base.RequiredTypes.Append(typeof(TestSceneOsuModSandbox)).ToList(); - - public TestSceneOsuModSandbox() - : this(null) - { - } - - public TestSceneOsuModSandbox(Mod mod = null) - : base(new OsuRuleset(), mod) - { - } - } -} diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoModSandbox.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoModSandbox.cs deleted file mode 100644 index ef62c7ed56..0000000000 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoModSandbox.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Collections.Generic; -using System.Linq; -using NUnit.Framework; -using osu.Game.Rulesets.Mods; -using osu.Game.Tests.Visual; - -namespace osu.Game.Rulesets.Taiko.Tests -{ - [TestFixture] - public class TestSceneTaikoModSandbox : ModSandboxTestScene - { - public override IReadOnlyList RequiredTypes => base.RequiredTypes.Append(typeof(TestSceneTaikoModSandbox)).ToList(); - - public TestSceneTaikoModSandbox() - : this(null) - { - } - - public TestSceneTaikoModSandbox(Mod mod = null) - : base(new TaikoRuleset(), mod) - { - } - } -} diff --git a/osu.Game/Tests/Visual/ModSandboxTestScene.cs b/osu.Game/Tests/Visual/ModSandboxTestScene.cs index 84bab6a9b9..0610a145ae 100644 --- a/osu.Game/Tests/Visual/ModSandboxTestScene.cs +++ b/osu.Game/Tests/Visual/ModSandboxTestScene.cs @@ -4,9 +4,12 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using osu.Game.Beatmaps; +using osu.Game.Replays; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; +using osu.Game.Scoring; using osu.Game.Screens.Play; namespace osu.Game.Tests.Visual @@ -29,36 +32,78 @@ namespace osu.Game.Tests.Visual { foreach (var testCase in CreateTestCases()) { - AddStep("set test case", () => currentTest = testCase); + AddStep(testCase.Name, () => currentTest = testCase); base.SetUpSteps(); + AddUntilStep("test passed", () => testCase.PassCondition?.Invoke() ?? true); } } - protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => currentTest.Beatmap; + protected sealed override IBeatmap CreateBeatmap(RulesetInfo ruleset) => currentTest?.Beatmap ?? base.CreateBeatmap(ruleset); - protected override Player CreatePlayer(Ruleset ruleset) + protected sealed override Player CreatePlayer(Ruleset ruleset) { SelectedMods.Value = SelectedMods.Value.Append(currentTest.Mod).ToArray(); - if (currentTest.Autoplay) - { - // We're simulating an auto-play via a replay so that the auto-play mod does not interfere - var beatmap = Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo, SelectedMods.Value); - var score = ruleset.GetAutoplayMod().CreateReplayScore(beatmap); + var score = currentTest.Autoplay + ? ruleset.GetAutoplayMod().CreateReplayScore(Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo, SelectedMods.Value)) + : new Score { Replay = new Replay() }; - return new TestReplayPlayer(score, false, false); - } - - return base.CreatePlayer(ruleset); + return CreateReplayPlayer(score); } + /// + /// Creates the test cases for this test scene. + /// protected abstract ModTestCaseData[] CreateTestCases(); + /// + /// Creates the for a test case. + /// + /// The . + protected virtual TestPlayer CreateReplayPlayer(Score score) => new TestPlayer(score); + + protected class TestPlayer : TestReplayPlayer + { + public TestPlayer(Score score) + : base(score, false, false) + { + } + } + protected class ModTestCaseData { - public Mod Mod; - public bool Autoplay; + /// + /// Whether to use a replay to simulate an auto-play. True by default. + /// + public bool Autoplay = true; + + /// + /// The beatmap for this test case. + /// + [CanBeNull] public IBeatmap Beatmap; + + /// + /// The conditions that cause this test case to pass. + /// + [CanBeNull] + public Func PassCondition; + + /// + /// The name of this test case, displayed in the test browser. + /// + public readonly string Name; + + /// + /// The this test case tests. + /// + public readonly Mod Mod; + + public ModTestCaseData(string name, Mod mod) + { + Name = name; + Mod = mod; + } } } } From ce7cbf29ca1546f4fc8f1d81ca8cbb401537dfdf Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 2 Mar 2020 12:20:25 +0900 Subject: [PATCH 0089/2376] Move to using test methods for better separation --- .../Mods/TestSceneOsuModDifficultyAdjust.cs | 37 ++++++++++--------- osu.Game/Tests/Visual/ModSandboxTestScene.cs | 22 +++++------ osu.Game/Tests/Visual/PlayerTestScene.cs | 17 +++++++++ osu.Game/Tests/Visual/ScreenTestScene.cs | 2 +- 4 files changed, 48 insertions(+), 30 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs index 46a3c1dff3..1fc9ccccd1 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using NUnit.Framework; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; @@ -20,24 +21,26 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods { } - protected override ModTestCaseData[] CreateTestCases() => new[] + [Test] + public void TestNoAdjustment() => CreateModTest(new ModTestCaseData("no adjustment", new OsuModDifficultyAdjust()) { - new ModTestCaseData("no adjustment", new OsuModDifficultyAdjust()) - { - Autoplay = true, - PassCondition = () => ((ScoreAccessibleTestPlayer)Player).ScoreProcessor.JudgedHits >= 2 - }, - new ModTestCaseData("cs = 10", new OsuModDifficultyAdjust { CircleSize = { Value = 10 } }) - { - Autoplay = true, - PassCondition = () => ((ScoreAccessibleTestPlayer)Player).ScoreProcessor.JudgedHits >= 2 - }, - new ModTestCaseData("ar = 10", new OsuModDifficultyAdjust { ApproachRate = { Value = 10 } }) - { - Autoplay = true, - PassCondition = () => ((ScoreAccessibleTestPlayer)Player).ScoreProcessor.JudgedHits >= 2 - }, - }; + Autoplay = true, + PassCondition = () => ((ScoreAccessibleTestPlayer)Player).ScoreProcessor.JudgedHits >= 2 + }); + + [Test] + public void TestCircleSize10() => CreateModTest(new ModTestCaseData("cs = 10", new OsuModDifficultyAdjust { CircleSize = { Value = 10 } }) + { + Autoplay = true, + PassCondition = () => ((ScoreAccessibleTestPlayer)Player).ScoreProcessor.JudgedHits >= 2 + }); + + [Test] + public void TestApproachRate10() => CreateModTest(new ModTestCaseData("ar = 10", new OsuModDifficultyAdjust { ApproachRate = { Value = 10 } }) + { + Autoplay = true, + PassCondition = () => ((ScoreAccessibleTestPlayer)Player).ScoreProcessor.JudgedHits >= 2 + }); protected override TestPlayer CreateReplayPlayer(Score score) => new ScoreAccessibleTestPlayer(score); diff --git a/osu.Game/Tests/Visual/ModSandboxTestScene.cs b/osu.Game/Tests/Visual/ModSandboxTestScene.cs index 0610a145ae..a1fa757452 100644 --- a/osu.Game/Tests/Visual/ModSandboxTestScene.cs +++ b/osu.Game/Tests/Visual/ModSandboxTestScene.cs @@ -16,6 +16,8 @@ namespace osu.Game.Tests.Visual { public abstract class ModSandboxTestScene : PlayerTestScene { + protected sealed override bool HasCustomSteps => true; + public override IReadOnlyList RequiredTypes => new[] { typeof(ModSandboxTestScene) @@ -28,14 +30,15 @@ namespace osu.Game.Tests.Visual private ModTestCaseData currentTest; - public override void SetUpSteps() + protected void CreateModTest(ModTestCaseData testCaseData) => CreateTest(() => { - foreach (var testCase in CreateTestCases()) - { - AddStep(testCase.Name, () => currentTest = testCase); - base.SetUpSteps(); - AddUntilStep("test passed", () => testCase.PassCondition?.Invoke() ?? true); - } + AddStep("set test data", () => currentTest = testCaseData); + }); + + public override void TearDownSteps() + { + AddUntilStep("test passed", () => currentTest?.PassCondition?.Invoke() ?? true); + base.TearDownSteps(); } protected sealed override IBeatmap CreateBeatmap(RulesetInfo ruleset) => currentTest?.Beatmap ?? base.CreateBeatmap(ruleset); @@ -51,11 +54,6 @@ namespace osu.Game.Tests.Visual return CreateReplayPlayer(score); } - /// - /// Creates the test cases for this test scene. - /// - protected abstract ModTestCaseData[] CreateTestCases(); - /// /// Creates the for a test case. /// diff --git a/osu.Game/Tests/Visual/PlayerTestScene.cs b/osu.Game/Tests/Visual/PlayerTestScene.cs index 1ca5256353..0d5aac8cfd 100644 --- a/osu.Game/Tests/Visual/PlayerTestScene.cs +++ b/osu.Game/Tests/Visual/PlayerTestScene.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.IEnumerableExtensions; @@ -14,6 +15,11 @@ namespace osu.Game.Tests.Visual { public abstract class PlayerTestScene : RateAdjustedBeatmapTestScene { + /// + /// Whether custom test steps are provided. Custom tests should invoke to create the test steps. + /// + protected virtual bool HasCustomSteps { get; } = false; + private readonly Ruleset ruleset; protected Player Player; @@ -37,6 +43,17 @@ namespace osu.Game.Tests.Visual { base.SetUpSteps(); + if (!HasCustomSteps) + CreateTest(null); + } + + protected void CreateTest(Action action) + { + if (action != null && !HasCustomSteps) + throw new InvalidOperationException($"Cannot add custom test steps without {nameof(HasCustomSteps)} being set."); + + action?.Invoke(); + AddStep(ruleset.RulesetInfo.Name, LoadPlayer); AddUntilStep("player loaded", () => Player.IsLoaded && Player.Alpha == 1); } diff --git a/osu.Game/Tests/Visual/ScreenTestScene.cs b/osu.Game/Tests/Visual/ScreenTestScene.cs index feca592049..d26aacf2bc 100644 --- a/osu.Game/Tests/Visual/ScreenTestScene.cs +++ b/osu.Game/Tests/Visual/ScreenTestScene.cs @@ -34,7 +34,7 @@ namespace osu.Game.Tests.Visual public virtual void SetUpSteps() => addExitAllScreensStep(); [TearDownSteps] - public void TearDownSteps() => addExitAllScreensStep(); + public virtual void TearDownSteps() => addExitAllScreensStep(); private void addExitAllScreensStep() { From 6d939e9d415609f7d64dea9874456529d32b48f4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 2 Mar 2020 12:42:48 +0900 Subject: [PATCH 0090/2376] Add failing test scenes --- .../TestSceneCatchModPerfect.cs | 54 +++++++++++++++ .../TestSceneManiaModPerfect.cs | 26 ++++++++ .../TestSceneOsuModPerfect.cs | 52 +++++++++++++++ .../TestSceneTaikoModPerfect.cs | 30 +++++++++ osu.Game/Tests/Visual/ModPerfectTestScene.cs | 66 +++++++++++++++++++ 5 files changed, 228 insertions(+) create mode 100644 osu.Game.Rulesets.Catch.Tests/TestSceneCatchModPerfect.cs create mode 100644 osu.Game.Rulesets.Mania.Tests/TestSceneManiaModPerfect.cs create mode 100644 osu.Game.Rulesets.Osu.Tests/TestSceneOsuModPerfect.cs create mode 100644 osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoModPerfect.cs create mode 100644 osu.Game/Tests/Visual/ModPerfectTestScene.cs diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModPerfect.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModPerfect.cs new file mode 100644 index 0000000000..f5bd3b1133 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModPerfect.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 NUnit.Framework; +using osu.Game.Rulesets.Catch.Mods; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Tests.Visual; +using osuTK; + +namespace osu.Game.Rulesets.Catch.Tests +{ + public class TestSceneCatchModPerfect : ModPerfectTestScene + { + public TestSceneCatchModPerfect() + : base(new CatchRuleset(), new CatchModPerfect()) + { + } + + [TestCase(false)] + [TestCase(true)] + public void TestBananaShower(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestCase(new BananaShower { StartTime = 1000, EndTime = 3000 }, false), shouldMiss); + + [TestCase(false)] + [TestCase(true)] + public void TestFruit(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestCase(new Fruit { StartTime = 1000 }), shouldMiss); + + [TestCase(false)] + [TestCase(true)] + public void TestTestJuiceStream(bool shouldMiss) + { + var stream = new JuiceStream + { + StartTime = 1000, + Path = new SliderPath(PathType.Linear, new[] + { + Vector2.Zero, + new Vector2(100, 0), + }) + }; + + CreateHitObjectTest(new HitObjectTestCase(stream), shouldMiss); + } + + // We only care about testing misses, hits are tested via JuiceStream + [TestCase(true)] + public void TestDroplet(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestCase(new Droplet { StartTime = 1000 }), shouldMiss); + + // We only care about testing misses, hits are tested via JuiceStream + [TestCase(true)] + public void TestTinyDroplet(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestCase(new TinyDroplet { StartTime = 1000 }), shouldMiss); + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaModPerfect.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaModPerfect.cs new file mode 100644 index 0000000000..f49a19e218 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaModPerfect.cs @@ -0,0 +1,26 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Rulesets.Mania.Mods; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Mania.Tests +{ + public class TestSceneManiaModPerfect : ModPerfectTestScene + { + public TestSceneManiaModPerfect() + : base(new ManiaRuleset(), new ManiaModPerfect()) + { + } + + [TestCase(false)] + [TestCase(true)] + public void TestNote(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestCase(new Note { StartTime = 1000 }), shouldMiss); + + [TestCase(false)] + [TestCase(true)] + public void TestHoldNote(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestCase(new HoldNote { StartTime = 1000, EndTime = 3000 }), shouldMiss); + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuModPerfect.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuModPerfect.cs new file mode 100644 index 0000000000..02fd5b5a79 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuModPerfect.cs @@ -0,0 +1,52 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Tests.Visual; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests +{ + public class TestSceneOsuModPerfect : ModPerfectTestScene + { + public TestSceneOsuModPerfect() + : base(new OsuRuleset(), new OsuModPerfect()) + { + } + + [TestCase(false)] + [TestCase(true)] + public void TestHitCircle(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestCase(new HitCircle { StartTime = 1000 }), shouldMiss); + + [TestCase(false)] + [TestCase(true)] + public void TestSlider(bool shouldMiss) + { + var slider = new Slider + { + StartTime = 1000, + Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(100, 0), }) + }; + + CreateHitObjectTest(new HitObjectTestCase(slider), shouldMiss); + } + + [TestCase(false)] + [TestCase(true)] + public void TestSpinner(bool shouldMiss) + { + var spinner = new Spinner + { + StartTime = 1000, + EndTime = 3000, + Position = new Vector2(256, 192) + }; + + CreateHitObjectTest(new HitObjectTestCase(spinner), shouldMiss); + } + } +} diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoModPerfect.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoModPerfect.cs new file mode 100644 index 0000000000..4069ee7983 --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoModPerfect.cs @@ -0,0 +1,30 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Rulesets.Taiko.Mods; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Taiko.Tests +{ + public class TestSceneTaikoModPerfect : ModPerfectTestScene + { + public TestSceneTaikoModPerfect() + : base(new TaikoRuleset(), new TaikoModPerfect()) + { + } + + [TestCase(false)] + [TestCase(true)] + public void TestHit(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestCase(new CentreHit { StartTime = 1000 }), shouldMiss); + + [TestCase(false)] + [TestCase(true)] + public void TestDrumRoll(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestCase(new DrumRoll { StartTime = 1000, EndTime = 3000 }), shouldMiss); + + [TestCase(false)] + [TestCase(true)] + public void TestSwell(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestCase(new Swell { StartTime = 1000, EndTime = 3000 }), shouldMiss); + } +} diff --git a/osu.Game/Tests/Visual/ModPerfectTestScene.cs b/osu.Game/Tests/Visual/ModPerfectTestScene.cs new file mode 100644 index 0000000000..272b5366a9 --- /dev/null +++ b/osu.Game/Tests/Visual/ModPerfectTestScene.cs @@ -0,0 +1,66 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Extensions.TypeExtensions; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Scoring; + +namespace osu.Game.Tests.Visual +{ + public abstract class ModPerfectTestScene : ModSandboxTestScene + { + private readonly ModPerfect perfectMod; + + protected ModPerfectTestScene(Ruleset ruleset, ModPerfect perfectMod) + : base(ruleset) + { + this.perfectMod = perfectMod; + } + + protected void CreateHitObjectTest(HitObjectTestCase testCaseData, bool shouldMiss) => CreateModTest(new ModTestCaseData(testCaseData.HitObject.GetType().ReadableName(), perfectMod) + { + Beatmap = new Beatmap + { + BeatmapInfo = { Ruleset = Ruleset.Value }, + HitObjects = { testCaseData.HitObject } + }, + Autoplay = !shouldMiss, + PassCondition = () => ((PerfectModTestPlayer)Player).CheckFailed(shouldMiss && testCaseData.FailOnMiss) + }); + + protected sealed override TestPlayer CreateReplayPlayer(Score score) => new PerfectModTestPlayer(score); + + private class PerfectModTestPlayer : TestPlayer + { + public PerfectModTestPlayer(Score score) + : base(score) + { + } + + protected override bool AllowFail => true; + + public bool CheckFailed(bool failed) + { + if (!failed) + return ScoreProcessor.HasCompleted && !HealthProcessor.HasFailed; + + return ScoreProcessor.JudgedHits > 0 && HealthProcessor.HasFailed; + } + } + + protected class HitObjectTestCase + { + public readonly HitObject HitObject; + public readonly bool FailOnMiss; + + public HitObjectTestCase(HitObject hitObject, bool failOnMiss = true) + { + HitObject = hitObject; + FailOnMiss = failOnMiss; + } + } + } +} From e801ad514bacd08de9b09f4669278ac51a777223 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 2 Mar 2020 13:24:02 +0900 Subject: [PATCH 0091/2376] Fix ruleset nullref --- osu.Game/Tests/Visual/ModPerfectTestScene.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Tests/Visual/ModPerfectTestScene.cs b/osu.Game/Tests/Visual/ModPerfectTestScene.cs index 272b5366a9..ded1b3676e 100644 --- a/osu.Game/Tests/Visual/ModPerfectTestScene.cs +++ b/osu.Game/Tests/Visual/ModPerfectTestScene.cs @@ -12,11 +12,13 @@ namespace osu.Game.Tests.Visual { public abstract class ModPerfectTestScene : ModSandboxTestScene { + private readonly Ruleset ruleset; private readonly ModPerfect perfectMod; protected ModPerfectTestScene(Ruleset ruleset, ModPerfect perfectMod) : base(ruleset) { + this.ruleset = ruleset; this.perfectMod = perfectMod; } @@ -24,7 +26,7 @@ namespace osu.Game.Tests.Visual { Beatmap = new Beatmap { - BeatmapInfo = { Ruleset = Ruleset.Value }, + BeatmapInfo = { Ruleset = ruleset.RulesetInfo }, HitObjects = { testCaseData.HitObject } }, Autoplay = !shouldMiss, From cd43a0c9e8071c818fa289972076ff61fcc1b5c3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 2 Mar 2020 13:24:07 +0900 Subject: [PATCH 0092/2376] Better check for failing state --- osu.Game/Tests/Visual/ModPerfectTestScene.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Tests/Visual/ModPerfectTestScene.cs b/osu.Game/Tests/Visual/ModPerfectTestScene.cs index ded1b3676e..31d2ce9281 100644 --- a/osu.Game/Tests/Visual/ModPerfectTestScene.cs +++ b/osu.Game/Tests/Visual/ModPerfectTestScene.cs @@ -49,7 +49,7 @@ namespace osu.Game.Tests.Visual if (!failed) return ScoreProcessor.HasCompleted && !HealthProcessor.HasFailed; - return ScoreProcessor.JudgedHits > 0 && HealthProcessor.HasFailed; + return HealthProcessor.HasFailed; } } From bb4193d985cb47e450adafe03059b03fcff38705 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 2 Mar 2020 13:24:55 +0900 Subject: [PATCH 0093/2376] Fix taiko infinity health drain on some beatmaps --- osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs index edb089dbac..dd3c2289ea 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; @@ -39,7 +40,7 @@ namespace osu.Game.Rulesets.Taiko.Scoring { base.ApplyBeatmap(beatmap); - hpMultiplier = 1 / (object_count_factor * beatmap.HitObjects.OfType().Count() * BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.5, 0.75, 0.98)); + 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); } From e58fb3f52823a4feb1ead00097000e4e689cac48 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 2 Mar 2020 13:25:16 +0900 Subject: [PATCH 0094/2376] Make default taiko HP 1 for test scene --- .../TestSceneTaikoModPerfect.cs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoModPerfect.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoModPerfect.cs index 4069ee7983..b67810846e 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoModPerfect.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoModPerfect.cs @@ -2,8 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Scoring; using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Taiko.Tests @@ -11,7 +13,7 @@ namespace osu.Game.Rulesets.Taiko.Tests public class TestSceneTaikoModPerfect : ModPerfectTestScene { public TestSceneTaikoModPerfect() - : base(new TaikoRuleset(), new TaikoModPerfect()) + : base(new TestTaikoRuleset(), new TaikoModPerfect()) { } @@ -26,5 +28,20 @@ namespace osu.Game.Rulesets.Taiko.Tests [TestCase(false)] [TestCase(true)] public void TestSwell(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestCase(new Swell { StartTime = 1000, EndTime = 3000 }), shouldMiss); + + private class TestTaikoRuleset : TaikoRuleset + { + public override HealthProcessor CreateHealthProcessor(double drainStartTime) => new TestTaikoHealthProcessor(); + + private class TestTaikoHealthProcessor : TaikoHealthProcessor + { + protected override void Reset(bool storeResults) + { + base.Reset(storeResults); + + Health.Value = 1; // Don't care about the health condition (only the mod condition) + } + } + } } } From 6fb52e5370c67de6a5f67bff3d8f19b13dc5bc22 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 2 Mar 2020 13:25:36 +0900 Subject: [PATCH 0095/2376] Fix custom rulesets not being testable --- osu.Game/Tests/Visual/PlayerTestScene.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Tests/Visual/PlayerTestScene.cs b/osu.Game/Tests/Visual/PlayerTestScene.cs index 0d5aac8cfd..3d8eefaa9d 100644 --- a/osu.Game/Tests/Visual/PlayerTestScene.cs +++ b/osu.Game/Tests/Visual/PlayerTestScene.cs @@ -67,6 +67,7 @@ namespace osu.Game.Tests.Visual var beatmap = CreateBeatmap(ruleset.RulesetInfo); Beatmap.Value = CreateWorkingBeatmap(beatmap); + Ruleset.Value = ruleset.RulesetInfo; if (!AllowFail) { From 6d051d9e42599c694d9a989c3c6a897451d968b8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 2 Mar 2020 13:25:56 +0900 Subject: [PATCH 0096/2376] Fix perfect mod failure cases --- osu.Game.Rulesets.Catch/Mods/CatchModPerfect.cs | 6 ++++++ osu.Game/Rulesets/Mods/ModPerfect.cs | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModPerfect.cs b/osu.Game.Rulesets.Catch/Mods/CatchModPerfect.cs index fb92399102..e3391c47f1 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModPerfect.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModPerfect.cs @@ -1,11 +1,17 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Game.Rulesets.Catch.Judgements; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Catch.Mods { public class CatchModPerfect : ModPerfect { + protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) + => !(result.Judgement is CatchBananaJudgement) + && base.FailCondition(healthProcessor, result); } } diff --git a/osu.Game/Rulesets/Mods/ModPerfect.cs b/osu.Game/Rulesets/Mods/ModPerfect.cs index 882d3ebd6a..7fe606d584 100644 --- a/osu.Game/Rulesets/Mods/ModPerfect.cs +++ b/osu.Game/Rulesets/Mods/ModPerfect.cs @@ -15,6 +15,8 @@ namespace osu.Game.Rulesets.Mods public override IconUsage? Icon => OsuIcon.ModPerfect; public override string Description => "SS or quit."; - protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) => result.Type != result.Judgement.MaxResult; + protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) + => !(result.Judgement is IgnoreJudgement) + && result.Type != result.Judgement.MaxResult; } } From 89399b177c56e7e129a3f642c787ea384e9f0dca Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Mar 2020 16:41:00 +0900 Subject: [PATCH 0097/2376] 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 651d1beda1..983c622f77 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 168a358e47..c6c2708e64 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -23,7 +23,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 3c2a67e908..3552047cf8 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + @@ -79,7 +79,7 @@ - + From 5432371c74bf9c4787ae206ff154513abd8da60b Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2020 08:42:12 +0000 Subject: [PATCH 0098/2376] Bump Sentry from 2.0.3 to 2.1.0 Bumps [Sentry](https://github.com/getsentry/sentry-dotnet) from 2.0.3 to 2.1.0. - [Release notes](https://github.com/getsentry/sentry-dotnet/releases) - [Commits](https://github.com/getsentry/sentry-dotnet/compare/2.0.3...2.1.0) Signed-off-by: dependabot-preview[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 168a358e47..2b6ac585fa 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -24,7 +24,7 @@ - + From ee73f3e2b2c2fba744cd67fd499b6fd1ef5fb46c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Mar 2020 18:54:00 +0900 Subject: [PATCH 0099/2376] Change matching mode for global actions to better discern similar binds --- osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs | 5 +++-- osu.Game/Input/Bindings/GlobalActionContainer.cs | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs index ea274284ac..e83d899469 100644 --- a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs +++ b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs @@ -31,8 +31,9 @@ namespace osu.Game.Input.Bindings /// A reference to identify the current . Used to lookup mappings. Null for global mappings. /// An optional variant for the specified . Used when a ruleset has more than one possible keyboard layouts. /// Specify how to deal with multiple matches of s and s. - public DatabasedKeyBindingContainer(RulesetInfo ruleset = null, int? variant = null, SimultaneousBindingMode simultaneousMode = SimultaneousBindingMode.None) - : base(simultaneousMode) + /// Specify how to deal with exact matches. + public DatabasedKeyBindingContainer(RulesetInfo ruleset = null, int? variant = null, SimultaneousBindingMode simultaneousMode = SimultaneousBindingMode.None, KeyCombinationMatchingMode matchingMode = KeyCombinationMatchingMode.Any) + : base(simultaneousMode, matchingMode) { this.ruleset = ruleset; this.variant = variant; diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 7763577a14..bb0c586d73 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -15,6 +15,7 @@ namespace osu.Game.Input.Bindings private readonly Drawable handler; public GlobalActionContainer(OsuGameBase game) + : base(matchingMode: KeyCombinationMatchingMode.Modifiers) { if (game is IKeyBindingHandler) handler = game; From 489bf16beae7bbdb91e390db14b30f9c0ea33575 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Mar 2020 18:55:28 +0900 Subject: [PATCH 0100/2376] Add SelectNext and SelectPrevious global actions --- .../Input/Bindings/GlobalActionContainer.cs | 9 +++ osu.Game/Screens/Play/GameplayMenuOverlay.cs | 40 +++++--------- osu.Game/Screens/Select/BeatmapCarousel.cs | 55 +++++++++---------- 3 files changed, 50 insertions(+), 54 deletions(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index bb0c586d73..8f481d94c7 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -39,6 +39,9 @@ namespace osu.Game.Input.Bindings new KeyBinding(InputKey.Escape, GlobalAction.Back), new KeyBinding(InputKey.ExtraMouseButton1, GlobalAction.Back), + new KeyBinding(InputKey.Up, GlobalAction.SelectPrevious), + new KeyBinding(InputKey.Down, GlobalAction.SelectNext), + new KeyBinding(InputKey.Space, GlobalAction.Select), new KeyBinding(InputKey.Enter, GlobalAction.Select), new KeyBinding(InputKey.KeypadEnter, GlobalAction.Select), @@ -142,5 +145,11 @@ namespace osu.Game.Input.Bindings [Description("Toggle now playing overlay")] ToggleNowPlaying, + + [Description("Previous Selection")] + SelectPrevious, + + [Description("Next Selection")] + SelectNext, } } diff --git a/osu.Game/Screens/Play/GameplayMenuOverlay.cs b/osu.Game/Screens/Play/GameplayMenuOverlay.cs index 98e2bc5a03..6b37135c86 100644 --- a/osu.Game/Screens/Play/GameplayMenuOverlay.cs +++ b/osu.Game/Screens/Play/GameplayMenuOverlay.cs @@ -12,7 +12,6 @@ using osu.Game.Graphics; using osu.Framework.Allocation; using osu.Game.Graphics.UserInterface; using osu.Framework.Graphics.Shapes; -using osuTK.Input; using System.Collections.Generic; using System.Linq; using osu.Framework.Input.Bindings; @@ -204,35 +203,24 @@ namespace osu.Game.Screens.Play InternalButtons[selectionIndex].Selected.Value = true; } - protected override bool OnKeyDown(KeyDownEvent e) - { - if (!e.Repeat) - { - switch (e.Key) - { - case Key.Up: - if (selectionIndex == -1 || selectionIndex == 0) - setSelected(InternalButtons.Count - 1); - else - setSelected(selectionIndex - 1); - return true; - - case Key.Down: - if (selectionIndex == -1 || selectionIndex == InternalButtons.Count - 1) - setSelected(0); - else - setSelected(selectionIndex + 1); - return true; - } - } - - return base.OnKeyDown(e); - } - public bool OnPressed(GlobalAction action) { switch (action) { + case GlobalAction.SelectPrevious: + if (selectionIndex == -1 || selectionIndex == 0) + setSelected(InternalButtons.Count - 1); + else + setSelected(selectionIndex - 1); + return true; + + case GlobalAction.SelectNext: + if (selectionIndex == -1 || selectionIndex == InternalButtons.Count - 1) + setSelected(0); + else + setSelected(selectionIndex + 1); + return true; + case GlobalAction.Back: BackAction.Invoke(); return true; diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 7f36a23a86..1db97af2f0 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -16,15 +16,17 @@ using osu.Framework.Bindables; using osu.Framework.Caching; using osu.Framework.Threading; using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Beatmaps; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; +using osu.Game.Input.Bindings; using osu.Game.Screens.Select.Carousel; namespace osu.Game.Screens.Select { - public class BeatmapCarousel : CompositeDrawable + public class BeatmapCarousel : CompositeDrawable, IKeyBindingHandler { private const float bleed_top = FilterControl.HEIGHT; private const float bleed_bottom = Footer.HEIGHT; @@ -435,41 +437,38 @@ namespace osu.Game.Screens.Select protected override bool OnKeyDown(KeyDownEvent e) { - // allow for controlling volume when alt is held. - // this is required as the VolumeControlReceptor uses OnPressed, which is - // executed after all OnKeyDown events. - if (e.AltPressed) - return base.OnKeyDown(e); - - int direction = 0; - bool skipDifficulties = false; - switch (e.Key) { - case Key.Up: - direction = -1; - break; - - case Key.Down: - direction = 1; - break; - case Key.Left: - direction = -1; - skipDifficulties = true; - break; + SelectNext(-1, true); + return true; case Key.Right: - direction = 1; - skipDifficulties = true; - break; + SelectNext(1, true); + return true; } - if (direction == 0) - return base.OnKeyDown(e); + return false; + } - SelectNext(direction, skipDifficulties); - return true; + public bool OnPressed(GlobalAction action) + { + switch (action) + { + case GlobalAction.SelectNext: + SelectNext(1, false); + return true; + + case GlobalAction.SelectPrevious: + SelectNext(-1, false); + return true; + } + + return false; + } + + public void OnReleased(GlobalAction action) + { } protected override void Update() From 3404b09010608459508bf0f7829adde00bae011d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Mar 2020 18:55:52 +0900 Subject: [PATCH 0101/2376] Bring database snapshot in line with specs --- osu.Game/Migrations/OsuDbContextModelSnapshot.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Migrations/OsuDbContextModelSnapshot.cs b/osu.Game/Migrations/OsuDbContextModelSnapshot.cs index a6d9d1f3cb..bc4fc3342d 100644 --- a/osu.Game/Migrations/OsuDbContextModelSnapshot.cs +++ b/osu.Game/Migrations/OsuDbContextModelSnapshot.cs @@ -43,7 +43,7 @@ namespace osu.Game.Migrations b.Property("ID") .ValueGeneratedOnAdd(); - b.Property("AudioLeadIn"); + b.Property("AudioLeadIn"); b.Property("BPM"); From ed516f05c2fb6d054e20cb6ffd0e343f5603f74f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Mar 2020 18:56:09 +0900 Subject: [PATCH 0102/2376] Forward any unhandled scroll events to volume overlay --- osu.Game/OsuGame.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index a73d5b57c4..5781a7fbc4 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -25,6 +25,7 @@ using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics.Sprites; using osu.Framework.Input; using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; using osu.Framework.Platform; using osu.Framework.Threading; using osu.Game.Beatmaps; @@ -805,6 +806,13 @@ namespace osu.Game return d; } + protected override bool OnScroll(ScrollEvent e) + { + // forward any unhandled mouse scroll events to the volume control. + volume.Adjust(GlobalAction.IncreaseVolume, e.ScrollDelta.Y, e.IsPrecise); + return true; + } + public bool OnPressed(GlobalAction action) { if (introScreen == null) return false; From 88583b5c79c5c5becd55268ffc19ce6b1b76be5c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Mar 2020 18:56:41 +0900 Subject: [PATCH 0103/2376] Remove any existing bindings to increase/decrease volume --- ...02094919_RefreshVolumeBindings.Designer.cs | 506 ++++++++++++++++++ .../20200302094919_RefreshVolumeBindings.cs | 16 + 2 files changed, 522 insertions(+) create mode 100644 osu.Game/Migrations/20200302094919_RefreshVolumeBindings.Designer.cs create mode 100644 osu.Game/Migrations/20200302094919_RefreshVolumeBindings.cs diff --git a/osu.Game/Migrations/20200302094919_RefreshVolumeBindings.Designer.cs b/osu.Game/Migrations/20200302094919_RefreshVolumeBindings.Designer.cs new file mode 100644 index 0000000000..22316b0380 --- /dev/null +++ b/osu.Game/Migrations/20200302094919_RefreshVolumeBindings.Designer.cs @@ -0,0 +1,506 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using osu.Game.Database; + +namespace osu.Game.Migrations +{ + [DbContext(typeof(OsuDbContext))] + [Migration("20200302094919_RefreshVolumeBindings")] + partial class RefreshVolumeBindings + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.2.6-servicing-10079"); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("ApproachRate"); + + b.Property("CircleSize"); + + b.Property("DrainRate"); + + b.Property("OverallDifficulty"); + + b.Property("SliderMultiplier"); + + b.Property("SliderTickRate"); + + b.HasKey("ID"); + + b.ToTable("BeatmapDifficulty"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("AudioLeadIn"); + + b.Property("BPM"); + + b.Property("BaseDifficultyID"); + + b.Property("BeatDivisor"); + + b.Property("BeatmapSetInfoID"); + + b.Property("Countdown"); + + b.Property("DistanceSpacing"); + + b.Property("GridSize"); + + b.Property("Hash"); + + b.Property("Hidden"); + + b.Property("Length"); + + b.Property("LetterboxInBreaks"); + + b.Property("MD5Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapID"); + + b.Property("Path"); + + b.Property("RulesetID"); + + b.Property("SpecialStyle"); + + b.Property("StackLeniency"); + + b.Property("StarDifficulty"); + + b.Property("Status"); + + b.Property("StoredBookmarks"); + + b.Property("TimelineZoom"); + + b.Property("Version"); + + b.Property("WidescreenStoryboard"); + + b.HasKey("ID"); + + b.HasIndex("BaseDifficultyID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("Hash"); + + b.HasIndex("MD5Hash"); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("BeatmapInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Artist"); + + b.Property("ArtistUnicode"); + + b.Property("AudioFile"); + + b.Property("AuthorString") + .HasColumnName("Author"); + + b.Property("BackgroundFile"); + + b.Property("PreviewTime"); + + b.Property("Source"); + + b.Property("Tags"); + + b.Property("Title"); + + b.Property("TitleUnicode"); + + b.Property("VideoFile"); + + b.HasKey("ID"); + + b.ToTable("BeatmapMetadata"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("BeatmapSetInfoID"); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.HasKey("ID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("FileInfoID"); + + b.ToTable("BeatmapSetFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapSetID"); + + b.Property("Protected"); + + b.Property("Status"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapSetID") + .IsUnique(); + + b.ToTable("BeatmapSetInfo"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Key") + .HasColumnName("Key"); + + b.Property("RulesetID"); + + b.Property("SkinInfoID"); + + b.Property("StringValue") + .HasColumnName("Value"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("SkinInfoID"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("osu.Game.IO.FileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Hash"); + + b.Property("ReferenceCount"); + + b.HasKey("ID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("ReferenceCount"); + + b.ToTable("FileInfo"); + }); + + modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntAction") + .HasColumnName("Action"); + + b.Property("KeysString") + .HasColumnName("Keys"); + + b.Property("RulesetID"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("IntAction"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("KeyBinding"); + }); + + modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Available"); + + b.Property("InstantiationInfo"); + + b.Property("Name"); + + b.Property("ShortName"); + + b.HasKey("ID"); + + b.HasIndex("Available"); + + b.HasIndex("ShortName") + .IsUnique(); + + b.ToTable("RulesetInfo"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("ScoreInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("ScoreInfoID"); + + b.ToTable("ScoreFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Accuracy") + .HasColumnType("DECIMAL(1,4)"); + + b.Property("BeatmapInfoID"); + + b.Property("Combo"); + + b.Property("Date"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MaxCombo"); + + b.Property("ModsJson") + .HasColumnName("Mods"); + + b.Property("OnlineScoreID"); + + b.Property("PP"); + + b.Property("Rank"); + + b.Property("RulesetID"); + + b.Property("StatisticsJson") + .HasColumnName("Statistics"); + + b.Property("TotalScore"); + + b.Property("UserID") + .HasColumnName("UserID"); + + b.Property("UserString") + .HasColumnName("User"); + + b.HasKey("ID"); + + b.HasIndex("BeatmapInfoID"); + + b.HasIndex("OnlineScoreID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("ScoreInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("SkinInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("SkinInfoID"); + + b.ToTable("SkinFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Creator"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("Name"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.ToTable("SkinInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty") + .WithMany() + .HasForeignKey("BaseDifficultyID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet") + .WithMany("Beatmaps") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("Beatmaps") + .HasForeignKey("MetadataID"); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo") + .WithMany("Files") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("BeatmapSets") + .HasForeignKey("MetadataID"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Settings") + .HasForeignKey("SkinInfoID"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Scoring.ScoreInfo") + .WithMany("Files") + .HasForeignKey("ScoreInfoID"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapInfo", "Beatmap") + .WithMany("Scores") + .HasForeignKey("BeatmapInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Files") + .HasForeignKey("SkinInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/osu.Game/Migrations/20200302094919_RefreshVolumeBindings.cs b/osu.Game/Migrations/20200302094919_RefreshVolumeBindings.cs new file mode 100644 index 0000000000..ec4475971c --- /dev/null +++ b/osu.Game/Migrations/20200302094919_RefreshVolumeBindings.cs @@ -0,0 +1,16 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace osu.Game.Migrations +{ + public partial class RefreshVolumeBindings : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql("DELETE FROM KeyBinding WHERE action in (6,7)"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + } + } +} From 6989738710cc6e3da689b26087ab9f6f3dc09295 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Mar 2020 18:59:05 +0900 Subject: [PATCH 0104/2376] Change default global bindings for volume changing to include the alt key prefix --- osu.Game/Input/Bindings/GlobalActionContainer.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 8f481d94c7..71771abede 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -58,10 +58,11 @@ namespace osu.Game.Input.Bindings public IEnumerable AudioControlKeyBindings => new[] { - new KeyBinding(InputKey.Up, GlobalAction.IncreaseVolume), - new KeyBinding(InputKey.MouseWheelUp, GlobalAction.IncreaseVolume), - new KeyBinding(InputKey.Down, GlobalAction.DecreaseVolume), - new KeyBinding(InputKey.MouseWheelDown, GlobalAction.DecreaseVolume), + new KeyBinding(new[] { InputKey.Alt, InputKey.Up }, GlobalAction.IncreaseVolume), + new KeyBinding(new[] { InputKey.Alt, InputKey.MouseWheelUp }, GlobalAction.IncreaseVolume), + new KeyBinding(new[] { InputKey.Alt, InputKey.Down }, GlobalAction.DecreaseVolume), + new KeyBinding(new[] { InputKey.Alt, InputKey.MouseWheelDown }, GlobalAction.DecreaseVolume), + new KeyBinding(InputKey.F4, GlobalAction.ToggleMute), new KeyBinding(InputKey.TrackPrevious, GlobalAction.MusicPrev), From e608d807f4c8db1c90741cf80fff56dc769f292a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Mar 2020 18:59:38 +0900 Subject: [PATCH 0105/2376] Handle SelectPrevious / SelectNext as volume change operations if nothing else handled game-wide --- .../Overlays/Volume/VolumeControlReceptor.cs | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Volume/VolumeControlReceptor.cs b/osu.Game/Overlays/Volume/VolumeControlReceptor.cs index 76fad945cc..4bff8146b4 100644 --- a/osu.Game/Overlays/Volume/VolumeControlReceptor.cs +++ b/osu.Game/Overlays/Volume/VolumeControlReceptor.cs @@ -14,8 +14,25 @@ namespace osu.Game.Overlays.Volume public Func ActionRequested; public Func ScrollActionRequested; - public bool OnPressed(GlobalAction action) => ActionRequested?.Invoke(action) ?? false; - public bool OnScroll(GlobalAction action, float amount, bool isPrecise) => ScrollActionRequested?.Invoke(action, amount, isPrecise) ?? false; + public bool OnPressed(GlobalAction action) + { + // if nothing else handles selection actions in the game, it's safe to let volume be adjusted. + switch (action) + { + case GlobalAction.SelectPrevious: + action = GlobalAction.IncreaseVolume; + break; + + case GlobalAction.SelectNext: + action = GlobalAction.DecreaseVolume; + break; + } + + return ActionRequested?.Invoke(action) ?? false; + } + + public bool OnScroll(GlobalAction action, float amount, bool isPrecise) => + ScrollActionRequested?.Invoke(action, amount, isPrecise) ?? false; public void OnReleased(GlobalAction action) { From 81191a3d20d6f7150bc201bf40149d160dd3b259 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Mar 2020 18:59:55 +0900 Subject: [PATCH 0106/2376] Handle SelectPrevious/SelectNext locally to volume meter when hovered --- osu.Game/Overlays/Volume/VolumeMeter.cs | 27 ++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Volume/VolumeMeter.cs b/osu.Game/Overlays/Volume/VolumeMeter.cs index 7effd290e6..07accf8820 100644 --- a/osu.Game/Overlays/Volume/VolumeMeter.cs +++ b/osu.Game/Overlays/Volume/VolumeMeter.cs @@ -11,16 +11,18 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Utils; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Input.Bindings; using osuTK; using osuTK.Graphics; namespace osu.Game.Overlays.Volume { - public class VolumeMeter : Container + public class VolumeMeter : Container, IKeyBindingHandler { private CircularProgress volumeCircle; private CircularProgress volumeCircleGlow; @@ -260,5 +262,28 @@ namespace osu.Game.Overlays.Volume { this.ScaleTo(1f, transition_length, Easing.OutExpo); } + + public bool OnPressed(GlobalAction action) + { + if (!IsHovered) + return false; + + switch (action) + { + case GlobalAction.SelectPrevious: + adjust(1, false); + return true; + + case GlobalAction.SelectNext: + adjust(-1, false); + return true; + } + + return false; + } + + public void OnReleased(GlobalAction action) + { + } } } From e890e454200dbccb263df546d37ba209cc5fb7b6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Mar 2020 19:40:32 +0900 Subject: [PATCH 0107/2376] Fix Ctrl+Enter behaviour regression --- osu.Game/Screens/Select/PlaySongSelect.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index e744fd6a7b..af113781e5 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -4,6 +4,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; using osu.Framework.Screens; using osu.Game.Graphics; using osu.Game.Screens.Play; @@ -49,6 +50,20 @@ namespace osu.Game.Screens.Select } } + protected override bool OnKeyDown(KeyDownEvent e) + { + switch (e.Key) + { + case Key.Enter: + // this is a special hard-coded case; we can't rely on OnPressed (of SongSelect) as GlobalActionContainer is + // matching with exact modifier consideration (so Ctrl+Enter would be ignored). + FinaliseSelection(); + return true; + } + + return base.OnKeyDown(e); + } + protected override bool OnStart() { if (player != null) return false; From 23068034b103e6e5ac5c320cbfa37d43c19bb17b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Mar 2020 19:51:06 +0900 Subject: [PATCH 0108/2376] Rename bool and make property for legibility --- .../Overlays/Changelog/UpdateStreamBadge.cs | 23 ++++++++++--------- .../Changelog/UpdateStreamBadgeArea.cs | 4 ++-- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/osu.Game/Overlays/Changelog/UpdateStreamBadge.cs b/osu.Game/Overlays/Changelog/UpdateStreamBadge.cs index 5e10410f37..97c58614b8 100644 --- a/osu.Game/Overlays/Changelog/UpdateStreamBadge.cs +++ b/osu.Game/Overlays/Changelog/UpdateStreamBadge.cs @@ -100,7 +100,7 @@ namespace osu.Game.Overlays.Changelog private void updateState() { - if (SelectedTab.Value == null && !externalDimRequested) + if (SelectedTab.Value == null && !allStreamsDimmed) { expandingBar.Expand(); expandingBar.FadeTo(1, transition_duration, Easing.OutQuint); @@ -113,21 +113,22 @@ namespace osu.Game.Overlays.Changelog expandingBar.IsCollapsed = !shouldExpand; expandingBar.FadeTo(shouldExpand ? 1 : 0.5f, transition_duration, Easing.OutQuint); - text.FadeTo(IsHovered || (Active.Value && !externalDimRequested) ? 1 : 0.5f, transition_duration, Easing.OutQuint); + text.FadeTo(IsHovered || (Active.Value && !allStreamsDimmed) ? 1 : 0.5f, transition_duration, Easing.OutQuint); } - private bool externalDimRequested; + private bool allStreamsDimmed; - public void EnableDim() + public bool AllStreamsDimmed { - externalDimRequested = true; - updateState(); - } + get => allStreamsDimmed; + set + { + if (value == allStreamsDimmed) + return; - public void DisableDim() - { - externalDimRequested = false; - updateState(); + allStreamsDimmed = value; + updateState(); + } } } } diff --git a/osu.Game/Overlays/Changelog/UpdateStreamBadgeArea.cs b/osu.Game/Overlays/Changelog/UpdateStreamBadgeArea.cs index d937bf775a..314f94101a 100644 --- a/osu.Game/Overlays/Changelog/UpdateStreamBadgeArea.cs +++ b/osu.Game/Overlays/Changelog/UpdateStreamBadgeArea.cs @@ -27,7 +27,7 @@ namespace osu.Game.Overlays.Changelog protected override bool OnHover(HoverEvent e) { foreach (var streamBadge in TabContainer.Children.OfType()) - streamBadge.EnableDim(); + streamBadge.AllStreamsDimmed = true; return base.OnHover(e); } @@ -35,7 +35,7 @@ namespace osu.Game.Overlays.Changelog protected override void OnHoverLost(HoverLostEvent e) { foreach (var streamBadge in TabContainer.Children.OfType()) - streamBadge.DisableDim(); + streamBadge.AllStreamsDimmed = false; base.OnHoverLost(e); } From 9f73b2960d39d6039054df3590fd32a5da0d781b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 2 Mar 2020 20:08:04 +0900 Subject: [PATCH 0109/2376] Prevent unnecessary auto-size computations in mania --- osu.Game.Rulesets.Mania/UI/ManiaStage.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs b/osu.Game.Rulesets.Mania/UI/ManiaStage.cs index a28de7ea58..bfe9f1085b 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaStage.cs @@ -115,9 +115,8 @@ namespace osu.Game.Rulesets.Mania.UI { Anchor = Anchor.TopCentre, Origin = Anchor.Centre, - AutoSizeAxes = Axes.Both, + RelativeSizeAxes = Axes.Both, Y = HIT_TARGET_POSITION + 150, - BypassAutoSizeAxes = Axes.Both }, topLevelContainer = new Container { RelativeSizeAxes = Axes.Both } } From 69b47137311e121be5c1fdd9c7d865f570b548c4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Mar 2020 20:16:58 +0900 Subject: [PATCH 0110/2376] Refactor everything so I can read the code --- .../Graphics/UserInterface/ExpandingBar.cs | 20 +++++----- .../Overlays/Changelog/UpdateStreamBadge.cs | 40 +++++++++++-------- .../Changelog/UpdateStreamBadgeArea.cs | 4 +- 3 files changed, 36 insertions(+), 28 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/ExpandingBar.cs b/osu.Game/Graphics/UserInterface/ExpandingBar.cs index 439a6002d8..60cb35b4c4 100644 --- a/osu.Game/Graphics/UserInterface/ExpandingBar.cs +++ b/osu.Game/Graphics/UserInterface/ExpandingBar.cs @@ -13,17 +13,17 @@ namespace osu.Game.Graphics.UserInterface /// public class ExpandingBar : Circle { - private bool isCollapsed; + private bool expanded = true; - public bool IsCollapsed + public bool Expanded { - get => isCollapsed; + get => expanded; set { - if (value == isCollapsed) + if (value == expanded) return; - isCollapsed = value; + expanded = value; updateState(); } } @@ -83,19 +83,21 @@ namespace osu.Game.Graphics.UserInterface updateState(); } - public void Collapse() => IsCollapsed = true; + public void Collapse() => Expanded = false; - public void Expand() => IsCollapsed = false; + public void Expand() => Expanded = true; private void updateState() { - float newSize = IsCollapsed ? CollapsedSize : ExpandedSize; - Easing easingType = IsCollapsed ? Easing.Out : Easing.OutElastic; + float newSize = expanded ? ExpandedSize : CollapsedSize; + Easing easingType = expanded ? Easing.OutElastic : Easing.Out; if (RelativeSizeAxes == Axes.X) this.ResizeHeightTo(newSize, 400, easingType); else this.ResizeWidthTo(newSize, 400, easingType); + + this.FadeTo(expanded ? 1 : 0.5f, 100, Easing.OutQuint); } } } diff --git a/osu.Game/Overlays/Changelog/UpdateStreamBadge.cs b/osu.Game/Overlays/Changelog/UpdateStreamBadge.cs index 97c58614b8..6786bbc49f 100644 --- a/osu.Game/Overlays/Changelog/UpdateStreamBadge.cs +++ b/osu.Game/Overlays/Changelog/UpdateStreamBadge.cs @@ -74,7 +74,7 @@ namespace osu.Game.Overlays.Changelog Colour = stream.Colour, ExpandedSize = 4, CollapsedSize = 2, - IsCollapsed = true + Expanded = true }, new HoverClickSounds() }); @@ -100,33 +100,39 @@ namespace osu.Game.Overlays.Changelog private void updateState() { - if (SelectedTab.Value == null && !allStreamsDimmed) + // highlighted regardless if we are hovered + bool textHighlighted = IsHovered; + bool barExpanded = IsHovered; + + if (SelectedTab.Value == null) { - expandingBar.Expand(); - expandingBar.FadeTo(1, transition_duration, Easing.OutQuint); - text.FadeTo(1, transition_duration, Easing.OutQuint); - return; + // at listing, all badges are highlighted when user is not hovering any badge. + textHighlighted |= !userHoveringArea; + barExpanded |= !userHoveringArea; + } + else + { + // bar is always expanded when active + barExpanded |= Active.Value; + + // text is highlighted only when hovered or active (but not if in selection mode) + textHighlighted |= Active.Value && !userHoveringArea; } - var shouldExpand = Active.Value || IsHovered; - - expandingBar.IsCollapsed = !shouldExpand; - expandingBar.FadeTo(shouldExpand ? 1 : 0.5f, transition_duration, Easing.OutQuint); - - text.FadeTo(IsHovered || (Active.Value && !allStreamsDimmed) ? 1 : 0.5f, transition_duration, Easing.OutQuint); + expandingBar.Expanded = barExpanded; + text.FadeTo(textHighlighted ? 1 : 0.5f, transition_duration, Easing.OutQuint); } - private bool allStreamsDimmed; + private bool userHoveringArea; - public bool AllStreamsDimmed + public bool UserHoveringArea { - get => allStreamsDimmed; set { - if (value == allStreamsDimmed) + if (value == userHoveringArea) return; - allStreamsDimmed = value; + userHoveringArea = value; updateState(); } } diff --git a/osu.Game/Overlays/Changelog/UpdateStreamBadgeArea.cs b/osu.Game/Overlays/Changelog/UpdateStreamBadgeArea.cs index 314f94101a..ffb622dd37 100644 --- a/osu.Game/Overlays/Changelog/UpdateStreamBadgeArea.cs +++ b/osu.Game/Overlays/Changelog/UpdateStreamBadgeArea.cs @@ -27,7 +27,7 @@ namespace osu.Game.Overlays.Changelog protected override bool OnHover(HoverEvent e) { foreach (var streamBadge in TabContainer.Children.OfType()) - streamBadge.AllStreamsDimmed = true; + streamBadge.UserHoveringArea = true; return base.OnHover(e); } @@ -35,7 +35,7 @@ namespace osu.Game.Overlays.Changelog protected override void OnHoverLost(HoverLostEvent e) { foreach (var streamBadge in TabContainer.Children.OfType()) - streamBadge.AllStreamsDimmed = false; + streamBadge.UserHoveringArea = false; base.OnHoverLost(e); } From 3d344a076de0a934a5adfb92f2d1d01a3e1da31d Mon Sep 17 00:00:00 2001 From: naoey Date: Tue, 3 Mar 2020 06:17:25 +0530 Subject: [PATCH 0111/2376] Add test for disabled keycounter, don't discard change event values --- .../Visual/Gameplay/TestSceneKeyCounter.cs | 23 ++++++++++--------- osu.Game/Screens/Play/Player.cs | 19 ++++++++------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs index e7b3e007fc..10827bc0b9 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs @@ -47,21 +47,22 @@ namespace osu.Game.Tests.Visual.Gameplay Key testKey = ((KeyCounterKeyboard)kc.Children.First()).Key; - AddStep($"Press {testKey} key", () => + void addPressKeyStep() { - InputManager.PressKey(testKey); - InputManager.ReleaseKey(testKey); - }); + AddStep($"Press {testKey} key", () => + { + InputManager.PressKey(testKey); + InputManager.ReleaseKey(testKey); + }); + } + addPressKeyStep(); AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses == 1); - - AddStep($"Press {testKey} key", () => - { - InputManager.PressKey(testKey); - InputManager.ReleaseKey(testKey); - }); - + addPressKeyStep(); AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses == 2); + AddStep($"Disable counting", () => testCounter.IsCounting = false); + addPressKeyStep(); + AddAssert($"Check {testKey} count has not changed", () => testCounter.CountPresses == 2); Add(kc); } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 237e364e69..2d49c707ec 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -157,7 +157,10 @@ namespace osu.Game.Screens.Play addGameplayComponents(GameplayClockContainer, Beatmap.Value); addOverlayComponents(GameplayClockContainer, Beatmap.Value); - DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updatePauseOnFocusLostState(), true); + DrawableRuleset.HasReplayLoaded.BindValueChanged(e => + { + updatePauseOnFocusLostState(e.NewValue, BreakOverlay.IsBreakTime.Value); + }, true); // bind clock into components that require it DrawableRuleset.IsPaused.BindTo(GameplayClockContainer.IsPaused); @@ -184,7 +187,7 @@ namespace osu.Game.Screens.Play foreach (var mod in Mods.Value.OfType()) mod.ApplyToHealthProcessor(HealthProcessor); - BreakOverlay.IsBreakTime.BindValueChanged(_ => onBreakTimeChanged(), true); + BreakOverlay.IsBreakTime.BindValueChanged(onBreakTimeChanged, true); } private void addUnderlayComponents(Container target) @@ -286,16 +289,16 @@ namespace osu.Game.Screens.Play HealthProcessor.IsBreakTime.BindTo(BreakOverlay.IsBreakTime); } - private void onBreakTimeChanged() + private void onBreakTimeChanged(ValueChangedEvent changeEvent) { - updatePauseOnFocusLostState(); - HUDOverlay.KeyCounter.IsCounting = !BreakOverlay.IsBreakTime.Value; + updatePauseOnFocusLostState(DrawableRuleset.HasReplayLoaded.Value, changeEvent.NewValue); + HUDOverlay.KeyCounter.IsCounting = !changeEvent.NewValue; } - private void updatePauseOnFocusLostState() => + private void updatePauseOnFocusLostState(bool replayLoaded, bool isBreakTime) => HUDOverlay.HoldToQuit.PauseOnFocusLost = PauseOnFocusLost - && !DrawableRuleset.HasReplayLoaded.Value - && !BreakOverlay.IsBreakTime.Value; + && !replayLoaded + && !isBreakTime; private IBeatmap loadPlayableBeatmap() { From 90c2f7bd89620bc05cd0529f3a74780199e2fa0a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 3 Mar 2020 12:13:36 +0900 Subject: [PATCH 0112/2376] Fail tests by default --- osu.Game/Tests/Visual/ModSandboxTestScene.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Tests/Visual/ModSandboxTestScene.cs b/osu.Game/Tests/Visual/ModSandboxTestScene.cs index a1fa757452..8bfa373e46 100644 --- a/osu.Game/Tests/Visual/ModSandboxTestScene.cs +++ b/osu.Game/Tests/Visual/ModSandboxTestScene.cs @@ -37,7 +37,7 @@ namespace osu.Game.Tests.Visual public override void TearDownSteps() { - AddUntilStep("test passed", () => currentTest?.PassCondition?.Invoke() ?? true); + AddUntilStep("test passed", () => currentTest?.PassCondition?.Invoke() ?? false); base.TearDownSteps(); } From 1e26df64b6cc022123222e4e104fbb382670037d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 3 Mar 2020 12:58:07 +0900 Subject: [PATCH 0113/2376] Fix constructor test failures --- osu.Game/Tests/Visual/ModSandboxTestScene.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Tests/Visual/ModSandboxTestScene.cs b/osu.Game/Tests/Visual/ModSandboxTestScene.cs index 8bfa373e46..11612d0eca 100644 --- a/osu.Game/Tests/Visual/ModSandboxTestScene.cs +++ b/osu.Game/Tests/Visual/ModSandboxTestScene.cs @@ -37,7 +37,14 @@ namespace osu.Game.Tests.Visual public override void TearDownSteps() { - AddUntilStep("test passed", () => currentTest?.PassCondition?.Invoke() ?? false); + AddUntilStep("test passed", () => + { + if (currentTest == null) + return true; + + return currentTest.PassCondition?.Invoke() ?? false; + }); + base.TearDownSteps(); } From a1aecd4c3905bbd9cd9d9e1925d257ff40e6419a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 3 Mar 2020 12:59:41 +0900 Subject: [PATCH 0114/2376] Fix TrackVirtualManual not respecting rate adjustments --- osu.Game/Tests/Visual/OsuTestScene.cs | 57 ++++++--------------------- 1 file changed, 11 insertions(+), 46 deletions(-) diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index b203557fab..3faecc87d2 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -229,17 +229,10 @@ namespace osu.Game.Tests.Visual /// public class TrackVirtualManual : Track { + private readonly StopwatchClock stopwatchClock = new StopwatchClock(); + private readonly IFrameBasedClock referenceClock; - private readonly ManualClock clock = new ManualClock(); - - private bool running; - - /// - /// Local offset added to the reference clock to resolve correct time. - /// - private double offset; - public TrackVirtualManual(IFrameBasedClock referenceClock) { this.referenceClock = referenceClock; @@ -248,60 +241,32 @@ namespace osu.Game.Tests.Visual public override bool Seek(double seek) { - offset = Math.Clamp(seek, 0, Length); - lastReferenceTime = null; + var offset = Math.Clamp(seek, 0, Length); + + stopwatchClock.Seek(offset); return offset == seek; } - public override void Start() - { - running = true; - } + public override void Start() => stopwatchClock.Start(); public override void Reset() { - Seek(0); + stopwatchClock.Seek(0); base.Reset(); } - public override void Stop() - { - if (running) - { - running = false; - // on stopping, the current value should be transferred out of the clock, as we can no longer rely on - // the referenceClock (which will still be counting time). - offset = clock.CurrentTime; - lastReferenceTime = null; - } - } + public override void Stop() => stopwatchClock.Stop(); - public override bool IsRunning => running; + public override bool IsRunning => stopwatchClock.IsRunning; - private double? lastReferenceTime; - - public override double CurrentTime => clock.CurrentTime; + public override double CurrentTime => stopwatchClock.CurrentTime; protected override void UpdateState() { base.UpdateState(); - if (running) - { - double refTime = referenceClock.CurrentTime; - - if (!lastReferenceTime.HasValue) - { - // if the clock just started running, the current value should be transferred to the offset - // (to zero the progression of time). - offset -= refTime; - } - - lastReferenceTime = refTime; - } - - clock.CurrentTime = Math.Min((lastReferenceTime ?? 0) + offset, Length); + stopwatchClock.Rate = Rate * referenceClock.Rate; if (CurrentTime >= Length) { From cc5b44e46681eb6b46c6c0fc1109cd5b64dd108b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 3 Mar 2020 13:36:55 +0900 Subject: [PATCH 0115/2376] Add test scene --- .../Mods/TestSceneOsuModDoubleTime.cs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDoubleTime.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDoubleTime.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDoubleTime.cs new file mode 100644 index 0000000000..deb733c581 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDoubleTime.cs @@ -0,0 +1,41 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Osu.Tests.Mods +{ + public class TestSceneOsuModDoubleTime : ModSandboxTestScene + { + public TestSceneOsuModDoubleTime() + : base(new OsuRuleset()) + { + } + + [TestCase(0.5)] + [TestCase(1.01)] + [TestCase(1.5)] + [TestCase(2)] + [TestCase(5)] + public void TestDefaultRate(double rate) => CreateModTest(new ModTestCaseData("1.5x", new OsuModDoubleTime { SpeedChange = { Value = rate } }) + { + PassCondition = () => ((ScoreAccessibleTestPlayer)Player).ScoreProcessor.JudgedHits >= 2 + }); + + protected override TestPlayer CreateReplayPlayer(Score score) => new ScoreAccessibleTestPlayer(score); + + private class ScoreAccessibleTestPlayer : TestPlayer + { + public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; + + public ScoreAccessibleTestPlayer(Score score) + : base(score) + { + } + } + } +} From 2e4adc056f3e02d1e165945bc0b7f251cae50d65 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 3 Mar 2020 13:38:10 +0900 Subject: [PATCH 0116/2376] Fix potential deadlock during gameplay tests --- osu.Game/Screens/Play/GameplayClockContainer.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 1c061c215b..591e969ad8 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -123,6 +123,10 @@ namespace osu.Game.Screens.Play public void Restart() { + // The Reset() call below causes speed adjustments to be reset in an async context, leading to deadlocks. + // The deadlock can be prevented by resetting the track synchronously before entering the async context. + track.ResetSpeedAdjustments(); + Task.Run(() => { track.Reset(); From d11d29c1f7303988baad64f2efcb4979af6425db Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 3 Mar 2020 15:30:53 +0900 Subject: [PATCH 0117/2376] Adjust namespaces --- .../{ => Mods}/TestSceneCatchModPerfect.cs | 2 +- .../{ => Mods}/TestSceneManiaModPerfect.cs | 2 +- .../{ => Mods}/TestSceneOsuModPerfect.cs | 2 +- .../{ => Mods}/TestSceneTaikoModPerfect.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename osu.Game.Rulesets.Catch.Tests/{ => Mods}/TestSceneCatchModPerfect.cs (97%) rename osu.Game.Rulesets.Mania.Tests/{ => Mods}/TestSceneManiaModPerfect.cs (95%) rename osu.Game.Rulesets.Osu.Tests/{ => Mods}/TestSceneOsuModPerfect.cs (97%) rename osu.Game.Rulesets.Taiko.Tests/{ => Mods}/TestSceneTaikoModPerfect.cs (97%) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModPerfect.cs b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs similarity index 97% rename from osu.Game.Rulesets.Catch.Tests/TestSceneCatchModPerfect.cs rename to osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs index f5bd3b1133..3e28bac02f 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModPerfect.cs +++ b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs @@ -9,7 +9,7 @@ using osu.Game.Rulesets.Objects.Types; using osu.Game.Tests.Visual; using osuTK; -namespace osu.Game.Rulesets.Catch.Tests +namespace osu.Game.Rulesets.Catch.Tests.Mods { public class TestSceneCatchModPerfect : ModPerfectTestScene { diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaModPerfect.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModPerfect.cs similarity index 95% rename from osu.Game.Rulesets.Mania.Tests/TestSceneManiaModPerfect.cs rename to osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModPerfect.cs index f49a19e218..4e11a302c9 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaModPerfect.cs +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModPerfect.cs @@ -6,7 +6,7 @@ using osu.Game.Rulesets.Mania.Mods; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Tests.Visual; -namespace osu.Game.Rulesets.Mania.Tests +namespace osu.Game.Rulesets.Mania.Tests.Mods { public class TestSceneManiaModPerfect : ModPerfectTestScene { diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuModPerfect.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModPerfect.cs similarity index 97% rename from osu.Game.Rulesets.Osu.Tests/TestSceneOsuModPerfect.cs rename to osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModPerfect.cs index 02fd5b5a79..fc2dfa16ec 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuModPerfect.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModPerfect.cs @@ -9,7 +9,7 @@ using osu.Game.Rulesets.Osu.Objects; using osu.Game.Tests.Visual; using osuTK; -namespace osu.Game.Rulesets.Osu.Tests +namespace osu.Game.Rulesets.Osu.Tests.Mods { public class TestSceneOsuModPerfect : ModPerfectTestScene { diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoModPerfect.cs b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModPerfect.cs similarity index 97% rename from osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoModPerfect.cs rename to osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModPerfect.cs index b67810846e..fd9d01a3db 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoModPerfect.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModPerfect.cs @@ -8,7 +8,7 @@ using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Scoring; using osu.Game.Tests.Visual; -namespace osu.Game.Rulesets.Taiko.Tests +namespace osu.Game.Rulesets.Taiko.Tests.Mods { public class TestSceneTaikoModPerfect : ModPerfectTestScene { From a26ac31c64076d68335ba53018132eb860907101 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 3 Mar 2020 15:33:54 +0900 Subject: [PATCH 0118/2376] Fix test name --- osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs index 3e28bac02f..56d2fe1ee0 100644 --- a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs +++ b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods [TestCase(false)] [TestCase(true)] - public void TestTestJuiceStream(bool shouldMiss) + public void TestJuiceStream(bool shouldMiss) { var stream = new JuiceStream { From cb1129218135bd2b1e2d3a287d381c13e08725d6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 3 Mar 2020 16:06:31 +0900 Subject: [PATCH 0119/2376] Reduce social overlay/direct overlay paddings --- osu.Game/Overlays/SearchableList/SearchableListOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs b/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs index 0783c64c20..72796df6d5 100644 --- a/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs +++ b/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs @@ -15,7 +15,7 @@ namespace osu.Game.Overlays.SearchableList { public abstract class SearchableListOverlay : FullscreenOverlay { - public const float WIDTH_PADDING = 80; + public const float WIDTH_PADDING = 10; protected SearchableListOverlay(OverlayColourScheme colourScheme) : base(colourScheme) From f1f4f1ffbd29bfe1126159c217084e552058f3a8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Mar 2020 18:04:12 +0900 Subject: [PATCH 0120/2376] Add torus font --- .../Resources/Fonts/Aquatico-Light.bin | Bin 4998 -> 0 bytes .../Resources/Fonts/Aquatico-Light_0.png | Bin 68407 -> 0 bytes .../Resources/Fonts/Aquatico-Regular.bin | Bin 4994 -> 0 bytes .../Resources/Fonts/Aquatico-Regular_0.png | Bin 69000 -> 0 bytes .../Gameplay/Components/MatchHeader.cs | 4 +- .../Gameplay/Components/MatchScoreDisplay.cs | 4 +- .../Screens/TeamIntro/TeamIntroScreen.cs | 4 +- .../Screens/TeamWin/TeamWinScreen.cs | 8 +- osu.Game.Tournament/TournamentFont.cs | 75 ------------------ osu.Game.Tournament/TournamentGameBase.cs | 3 - osu.Game/Graphics/OsuFont.cs | 17 ++-- osu.Game/OsuGameBase.cs | 5 ++ 12 files changed, 22 insertions(+), 98 deletions(-) delete mode 100644 osu.Game.Tournament/Resources/Fonts/Aquatico-Light.bin delete mode 100644 osu.Game.Tournament/Resources/Fonts/Aquatico-Light_0.png delete mode 100644 osu.Game.Tournament/Resources/Fonts/Aquatico-Regular.bin delete mode 100644 osu.Game.Tournament/Resources/Fonts/Aquatico-Regular_0.png delete mode 100644 osu.Game.Tournament/TournamentFont.cs diff --git a/osu.Game.Tournament/Resources/Fonts/Aquatico-Light.bin b/osu.Game.Tournament/Resources/Fonts/Aquatico-Light.bin deleted file mode 100644 index 42cfdf08de4977e66acd9063ef31741f88715f0f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4998 zcmZ9O$&XZ36vof1YC$6UOoK#>Od^8_O?Lw-Gzv7>Ey|#X4HzR~kimgL8AMGq{sB0| zg_`Kb7$YVoy3mDjr7K z#f*s9A3y8r5Jg8vtUG>k&xynPj?Uh6_~4-vQ7Q9^$8;|`ioB$wI$eMNzivrh*R}WX zk=@;0$BrC~j*Al_W^2WY7>YvwN9>C^5v8uERR>~H^hY(Ogmx}(llEmXznB^~WlcMm zbETb+598j(x_)W5OZ!pR(%7bTH)!uD=jr$2%Es6j>+`c)rOlUipx72GV}rELxKG*w zY3CJVVoa4bHLjPoP};*vZANU#&njklhqUjrwmGjuTO{orJ$p;sEbXS4E$vQebBaYp zPb`r(GwzbMSlSQG7#nkpCDLxJ%qwn<30k)-RBPEKty-C1Op1xpwqy^MO5?wt*ePvt z-mhD~zo|s5j{Bu`#yUOQBki|p#Po(QGo|%PJ0lNzvIm`Ux3p!_K9{z%;R{|Zm-cq$ zP5EN)*GQ8d9~F;i9bQezo~o8Orx?o`aaU@c_KfeU=cBvvEc@LftxxNgx2)6Jc%kjt zRnms!)udRU7TTX{khY_Ef80#oFYTYoh3IWOi}wT4&dRHv7Hv@4CwdpwG-gtJm5Lp&SKk;HccZjp zs^i+mvo~nngVHXjM+@{UIcv_!hoqI2U*(>2mdE8LX}x;3tD&jip7%7g16sFJ z+6Bd3(a`W8fcn7dVbawao1Hst{k!(Wa(&-qd|@aIU3VIz4Zi9 zuW6`BthKhINyM?{taz3fqj?wNn)obS$r>;GmBhr6-&iJcHc^L(+Due#!VMESo2X+f zF}JQ4vImfTf$SB;e)aqW`wsJvWRJZzLtr8)Do+SR?ox=(@>Lm z-kN*P6Ye?Won$lY7ox_nh;Ab;Ai_-EhtYx|E5#Cc8mKM|F+lyLJuDPC|AU zBF;$S_jX1hLlSZlSq@ z5ViU4@g_NY?mByjtVR<5OyO?I#9b4aj%AHZ#}aGEY9td`nd}gE$;z6YM6rpLSZON_R^srgHccA%;GON{0(G2|~X@~J+?1$`T}Co-x1R75t)4a3nVd6ExDLp*L9Q3F2atV%>=pg- zevqR<)LTyw^_r;HM7<_w#X2e)%}-FJ$!8dMeTFduxG|O>GBA;qiA+soZ=xa-Rhp=L zEHO#!SI^{%&|D?t$|1V}v7et{KNI_zsAD+qM;*h7)o3XbT1reNaYr%{*F;e(XUNeYX8<`X$eHrSvTis*tQ*dmItE!eF63Mw=l369{|p2G diff --git a/osu.Game.Tournament/Resources/Fonts/Aquatico-Light_0.png b/osu.Game.Tournament/Resources/Fonts/Aquatico-Light_0.png deleted file mode 100644 index 332d9ca0564224402f3b28d295b9ec05d3736822..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 68407 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 Rd(f$N8tR(DYOlLK{x5_sc-8;_ diff --git a/osu.Game.Tournament/Resources/Fonts/Aquatico-Regular.bin b/osu.Game.Tournament/Resources/Fonts/Aquatico-Regular.bin deleted file mode 100644 index 3047c2eb3ef7354d250cf4c88b919269a696a06c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4994 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 DisplayedCountSpriteText.Font = value - ? TournamentFont.GetFont(typeface: TournamentTypeface.Aquatico, weight: FontWeight.Regular, size: 60) - : TournamentFont.GetFont(typeface: TournamentTypeface.Aquatico, weight: FontWeight.Light, size: 40); + ? OsuFont.Torus.With(weight: FontWeight.Regular, size: 60) + : OsuFont.Torus.With(weight: FontWeight.Light, size: 40); } } } diff --git a/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs b/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs index 47c923ff30..75d63cde13 100644 --- a/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs +++ b/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs @@ -201,7 +201,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro new OsuSpriteText { Text = team?.FullName.Value.ToUpper() ?? "???", - Font = TournamentFont.GetFont(TournamentTypeface.Aquatico, 40, FontWeight.Light), + Font = OsuFont.Torus.With(size: 40, weight: FontWeight.Light), Colour = Color4.Black, Origin = Anchor.TopCentre, Anchor = Anchor.TopCentre, @@ -209,7 +209,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro new OsuSpriteText { Text = teamName.ToUpper(), - Font = TournamentFont.GetFont(TournamentTypeface.Aquatico, 20, FontWeight.Regular), + Font = OsuFont.Torus.With(size: 20, weight: FontWeight.Regular), Colour = colour, Origin = Anchor.TopCentre, Anchor = Anchor.TopCentre, diff --git a/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs b/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs index a0216c5db3..b72cacc66f 100644 --- a/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs +++ b/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs @@ -126,7 +126,7 @@ namespace osu.Game.Tournament.Screens.TeamWin Origin = Anchor.TopCentre, Colour = col, Text = "WINNER", - Font = TournamentFont.GetFont(TournamentTypeface.Aquatico, 15, FontWeight.Regular), + Font = OsuFont.Torus.With(size: 15, weight: FontWeight.Regular), }, new OsuSpriteText { @@ -134,7 +134,7 @@ namespace osu.Game.Tournament.Screens.TeamWin Origin = Anchor.TopCentre, Colour = col, Text = match.Round.Value?.Name.Value ?? "Unknown Round", - Font = TournamentFont.GetFont(TournamentTypeface.Aquatico, 50, FontWeight.Light), + Font = OsuFont.Torus.With(size: 50, weight: FontWeight.Light), Spacing = new Vector2(10, 0), }, new OsuSpriteText @@ -143,7 +143,7 @@ namespace osu.Game.Tournament.Screens.TeamWin Origin = Anchor.TopCentre, Colour = col, Text = match.Date.Value.ToUniversalTime().ToString("dd MMMM HH:mm UTC"), - Font = TournamentFont.GetFont(TournamentTypeface.Aquatico, 20, FontWeight.Light), + Font = OsuFont.Torus.With(size: 20, weight: FontWeight.Light), }, } } @@ -203,7 +203,7 @@ namespace osu.Game.Tournament.Screens.TeamWin new OsuSpriteText { Text = team?.FullName.Value.ToUpper() ?? "???", - Font = TournamentFont.GetFont(TournamentTypeface.Aquatico, 40, FontWeight.Light), + Font = OsuFont.Torus.With(size: 40, weight: FontWeight.Light), Colour = Color4.Black, Origin = Anchor.TopCentre, Anchor = Anchor.TopCentre, diff --git a/osu.Game.Tournament/TournamentFont.cs b/osu.Game.Tournament/TournamentFont.cs deleted file mode 100644 index 32f0264562..0000000000 --- a/osu.Game.Tournament/TournamentFont.cs +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Graphics.Sprites; -using osu.Game.Graphics; - -namespace osu.Game.Tournament -{ - public static class TournamentFont - { - /// - /// The default font size. - /// - public const float DEFAULT_FONT_SIZE = 16; - - /// - /// Retrieves a . - /// - /// The font typeface. - /// The size of the text in local space. For a value of 16, a single line will have a height of 16px. - /// The font weight. - /// Whether the font is italic. - /// Whether all characters should be spaced the same distance apart. - /// The . - public static FontUsage GetFont(TournamentTypeface typeface = TournamentTypeface.Aquatico, float size = DEFAULT_FONT_SIZE, FontWeight weight = FontWeight.Medium, bool italics = false, bool fixedWidth = false) - => new FontUsage(GetFamilyString(typeface), size, GetWeightString(typeface, weight), italics, fixedWidth); - - /// - /// Retrieves the string representation of a . - /// - /// The . - /// The string representation. - public static string GetFamilyString(TournamentTypeface typeface) - { - switch (typeface) - { - case TournamentTypeface.Aquatico: - return "Aquatico"; - } - - return null; - } - - /// - /// Retrieves the string representation of a . - /// - /// The . - /// The . - /// The string representation of in the specified . - public static string GetWeightString(TournamentTypeface typeface, FontWeight weight) - => GetWeightString(GetFamilyString(typeface), weight); - - /// - /// Retrieves the string representation of a . - /// - /// The family string. - /// The . - /// The string representation of in the specified . - public static string GetWeightString(string family, FontWeight weight) - { - string weightString = weight.ToString(); - - // Only exo has an explicit "regular" weight, other fonts do not - if (weight == FontWeight.Regular && family != GetFamilyString(TournamentTypeface.Aquatico)) - weightString = string.Empty; - - return weightString; - } - } - - public enum TournamentTypeface - { - Aquatico - } -} diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 1c94856a4e..9916b0e042 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -54,9 +54,6 @@ namespace osu.Game.Tournament { Resources.AddStore(new DllResourceStore(typeof(TournamentGameBase).Assembly)); - AddFont(Resources, @"Resources/Fonts/Aquatico-Regular"); - AddFont(Resources, @"Resources/Fonts/Aquatico-Light"); - Textures.AddStore(new TextureLoaderStore(new ResourceStore(new StorageBackedResourceStore(storage)))); this.storage = storage; diff --git a/osu.Game/Graphics/OsuFont.cs b/osu.Game/Graphics/OsuFont.cs index 22250d4a56..54a25875bf 100644 --- a/osu.Game/Graphics/OsuFont.cs +++ b/osu.Game/Graphics/OsuFont.cs @@ -19,6 +19,8 @@ namespace osu.Game.Graphics public static FontUsage Numeric => GetFont(Typeface.Venera, weight: FontWeight.Regular); + public static FontUsage Torus => GetFont(Typeface.Torus, weight: FontWeight.Regular); + /// /// Retrieves a . /// @@ -45,6 +47,9 @@ namespace osu.Game.Graphics case Typeface.Venera: return "Venera"; + + case Typeface.Torus: + return "Torus"; } return null; @@ -65,16 +70,7 @@ namespace osu.Game.Graphics /// The family string. /// The . /// The string representation of in the specified . - public static string GetWeightString(string family, FontWeight weight) - { - string weightString = weight.ToString(); - - // Only exo has an explicit "regular" weight, other fonts do not - if (family != GetFamilyString(Typeface.Exo) && weight == FontWeight.Regular) - weightString = string.Empty; - - return weightString; - } + public static string GetWeightString(string family, FontWeight weight) => weight.ToString(); } public static class OsuFontExtensions @@ -102,6 +98,7 @@ namespace osu.Game.Graphics { Exo, Venera, + Torus } public enum FontWeight diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 07c9d37a86..676b4b6c7a 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -157,6 +157,11 @@ namespace osu.Game AddFont(Resources, @"Fonts/Exo2.0-Black"); AddFont(Resources, @"Fonts/Exo2.0-BlackItalic"); + AddFont(Resources, @"Fonts/Torus-SemiBold"); + AddFont(Resources, @"Fonts/Torus-Bold"); + AddFont(Resources, @"Fonts/Torus-Regular"); + AddFont(Resources, @"Fonts/Torus-Light"); + AddFont(Resources, @"Fonts/Venera"); AddFont(Resources, @"Fonts/Venera-Light"); AddFont(Resources, @"Fonts/Venera-Medium"); From 08756186e983d533d739979cdcfaeca61c5a89f7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Mar 2020 18:35:28 +0900 Subject: [PATCH 0121/2376] 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 983c622f77..d62011bf1d 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 3b00b807a2..e407fca8b7 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -22,7 +22,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 3552047cf8..cc0b8074bb 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + From a2273234cb9009745fc7c6b2eb080d720a584717 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Mar 2020 18:09:25 +0900 Subject: [PATCH 0122/2376] Better handle startup when ladder cannot be read correctly --- osu.Game.Tournament/TournamentGameBase.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 9916b0e042..69f2bf57be 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -121,10 +121,9 @@ namespace osu.Game.Tournament using (var sr = new StreamReader(stream)) ladder = JsonConvert.DeserializeObject(sr.ReadToEnd()); } - else - { + + if (ladder == null) ladder = new LadderInfo(); - } if (ladder.Ruleset.Value == null) ladder.Ruleset.Value = RulesetStore.AvailableRulesets.First(); From 372060bc2b37dd5547defdfe1455e38f85e90310 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Mar 2020 18:11:21 +0900 Subject: [PATCH 0123/2376] Only trigger changes if new user information is actually populated --- osu.Game.Tournament/TournamentGameBase.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 69f2bf57be..dd93215fc1 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -201,9 +201,11 @@ namespace osu.Game.Tournament { foreach (var p in t.Players) { - if (p.Username == null || p.Statistics == null) + if (string.IsNullOrEmpty(p.Username) || p.Statistics == null) + { PopulateUser(p); - addedInfo = true; + addedInfo = true; + } } } From 093f2affdffcc95fb91dec285954e823bbc229d0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Mar 2020 18:12:42 +0900 Subject: [PATCH 0124/2376] Add seeding screen --- .../Screens/TestSceneSeedingScreen.cs | 125 +++++++ osu.Game.Tournament/Models/SeedingBeatmap.cs | 23 ++ osu.Game.Tournament/Models/SeedingResult.cs | 21 ++ osu.Game.Tournament/Models/TournamentTeam.cs | 27 ++ .../Screens/Editors/SeedingEditor.cs | 288 ++++++++++++++++ .../Screens/Editors/TeamEditorScreen.cs | 27 +- .../Ladder/Components/LadderEditorSettings.cs | 43 --- .../Components/LadderSettingsDropdown.cs | 26 ++ .../Ladder/Components/SettingsTeamDropdown.cs | 55 +++ .../Screens/TeamIntro/SeedingScreen.cs | 316 ++++++++++++++++++ osu.Game.Tournament/TournamentGameBase.cs | 18 + osu.Game.Tournament/TournamentSceneManager.cs | 14 + osu.Game.Tournament/TournamentSpriteText.cs | 16 + 13 files changed, 955 insertions(+), 44 deletions(-) create mode 100644 osu.Game.Tournament.Tests/Screens/TestSceneSeedingScreen.cs create mode 100644 osu.Game.Tournament/Models/SeedingBeatmap.cs create mode 100644 osu.Game.Tournament/Models/SeedingResult.cs create mode 100644 osu.Game.Tournament/Screens/Editors/SeedingEditor.cs create mode 100644 osu.Game.Tournament/Screens/Ladder/Components/LadderSettingsDropdown.cs create mode 100644 osu.Game.Tournament/Screens/Ladder/Components/SettingsTeamDropdown.cs create mode 100644 osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs create mode 100644 osu.Game.Tournament/TournamentSpriteText.cs diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneSeedingScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneSeedingScreen.cs new file mode 100644 index 0000000000..eb46d75705 --- /dev/null +++ b/osu.Game.Tournament.Tests/Screens/TestSceneSeedingScreen.cs @@ -0,0 +1,125 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Beatmaps; +using osu.Game.Tournament.Models; +using osu.Game.Tournament.Screens.TeamIntro; +using osu.Game.Users; + +namespace osu.Game.Tournament.Tests.Screens +{ + public class TestSceneSeedingScreen : LadderTestScene + { + [Cached] + private readonly LadderInfo ladder = new LadderInfo(); + + [BackgroundDependencyLoader] + private void load() + { + ladder.CurrentMatch.Value = new TournamentMatch + { + Team1 = + { + Value = new TournamentTeam + { + FlagName = { Value = "JP" }, + FullName = { Value = "Japan" }, + LastYearPlacing = { Value = 10 }, + Seed = { Value = "Low" }, + SeedingResults = + { + new SeedingResult + { + Mod = { Value = "NM" }, + Seed = { Value = 10 }, + Beatmaps = + { + new SeedingBeatmap + { + BeatmapInfo = new BeatmapInfo { Metadata = new BeatmapMetadata { Title = "Test Title", Artist = "Test Artist" } }, + Score = 12345672, + Seed = { Value = 24 }, + }, + new SeedingBeatmap + { + BeatmapInfo = new BeatmapInfo { Metadata = new BeatmapMetadata { Title = "Test Title", Artist = "Test Artist" } }, + Score = 1234567, + Seed = { Value = 12 }, + }, + new SeedingBeatmap + { + BeatmapInfo = new BeatmapInfo { Metadata = new BeatmapMetadata { Title = "Test Title", Artist = "Test Artist" } }, + Score = 1234567, + Seed = { Value = 16 }, + } + } + }, + new SeedingResult + { + Mod = { Value = "DT" }, + Seed = { Value = 5 }, + Beatmaps = + { + new SeedingBeatmap + { + BeatmapInfo = new BeatmapInfo { Metadata = new BeatmapMetadata { Title = "Test Title", Artist = "Test Artist" } }, + Score = 234567, + Seed = { Value = 3 }, + }, + new SeedingBeatmap + { + BeatmapInfo = new BeatmapInfo { Metadata = new BeatmapMetadata { Title = "Test Title", Artist = "Test Artist" } }, + Score = 234567, + Seed = { Value = 6 }, + }, + new SeedingBeatmap + { + BeatmapInfo = new BeatmapInfo { Metadata = new BeatmapMetadata { Title = "Test Title", Artist = "Test Artist" } }, + Score = 234567, + Seed = { Value = 12 }, + } + } + } + }, + Players = + { + new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 12 } } }, + new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 16 } } }, + new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 20 } } }, + new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 24 } } }, + new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 30 } } }, + } + } + }, + Team2 = + { + Value = new TournamentTeam + { + FlagName = { Value = "US" }, + FullName = { Value = "United States" }, + Players = + { + new User { Username = "Hello" }, + new User { Username = "Hello" }, + new User { Username = "Hello" }, + new User { Username = "Hello" }, + new User { Username = "Hello" }, + } + } + }, + Round = + { + Value = new TournamentRound { Name = { Value = "Quarterfinals" } } + } + }; + + Add(new SeedingScreen + { + FillMode = FillMode.Fit, + FillAspectRatio = 16 / 9f + }); + } + } +} diff --git a/osu.Game.Tournament/Models/SeedingBeatmap.cs b/osu.Game.Tournament/Models/SeedingBeatmap.cs new file mode 100644 index 0000000000..2cd6fa7188 --- /dev/null +++ b/osu.Game.Tournament/Models/SeedingBeatmap.cs @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osu.Game.Beatmaps; + +namespace osu.Game.Tournament.Models +{ + public class SeedingBeatmap + { + public int ID; + + public BeatmapInfo BeatmapInfo; + + public long Score; + + public Bindable Seed = new BindableInt + { + MinValue = 1, + MaxValue = 64 + }; + } +} diff --git a/osu.Game.Tournament/Models/SeedingResult.cs b/osu.Game.Tournament/Models/SeedingResult.cs new file mode 100644 index 0000000000..87aaf8bf36 --- /dev/null +++ b/osu.Game.Tournament/Models/SeedingResult.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 System.Collections.Generic; +using osu.Framework.Bindables; + +namespace osu.Game.Tournament.Models +{ + public class SeedingResult + { + public List Beatmaps = new List(); + + public Bindable Mod = new Bindable(); + + public Bindable Seed = new BindableInt + { + MinValue = 1, + MaxValue = 64 + }; + } +} diff --git a/osu.Game.Tournament/Models/TournamentTeam.cs b/osu.Game.Tournament/Models/TournamentTeam.cs index 54b8a35180..7fca75cea4 100644 --- a/osu.Game.Tournament/Models/TournamentTeam.cs +++ b/osu.Game.Tournament/Models/TournamentTeam.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using Newtonsoft.Json; using osu.Framework.Bindables; using osu.Game.Users; @@ -29,6 +30,32 @@ namespace osu.Game.Tournament.Models /// public Bindable Acronym = new Bindable(string.Empty); + public BindableList SeedingResults = new BindableList(); + + public double AverageRank + { + get + { + var ranks = Players.Select(p => p.Statistics?.Ranks.Global) + .Where(i => i.HasValue) + .Select(i => i.Value) + .ToArray(); + + if (ranks.Length == 0) + return 0; + + return ranks.Average(); + } + } + + public Bindable Seed = new Bindable(string.Empty); + + public Bindable LastYearPlacing = new BindableInt + { + MinValue = 1, + MaxValue = 64 + }; + [JsonProperty] public BindableList Players { get; set; } = new BindableList(); diff --git a/osu.Game.Tournament/Screens/Editors/SeedingEditor.cs b/osu.Game.Tournament/Screens/Editors/SeedingEditor.cs new file mode 100644 index 0000000000..68003a1c43 --- /dev/null +++ b/osu.Game.Tournament/Screens/Editors/SeedingEditor.cs @@ -0,0 +1,288 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Beatmaps; +using osu.Game.Graphics; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Overlays.Settings; +using osu.Game.Rulesets; +using osu.Game.Tournament.Components; +using osu.Game.Tournament.Models; +using osuTK; + +namespace osu.Game.Tournament.Screens.Editors +{ + public class SeedingEditorScreen : TournamentEditorScreen + { + private readonly TournamentTeam team; + + protected override BindableList Storage => team.SeedingResults; + + public SeedingEditorScreen(TournamentTeam team) + { + this.team = team; + } + + public class SeeingResultRow : CompositeDrawable, IModelBacked + { + public SeedingResult Model { get; } + + [Resolved] + private LadderInfo ladderInfo { get; set; } + + public SeeingResultRow(TournamentTeam team, SeedingResult round) + { + Model = round; + + Masking = true; + CornerRadius = 10; + + SeedingBeatmapEditor beatmapEditor = new SeedingBeatmapEditor(round) + { + Width = 0.95f + }; + + InternalChildren = new Drawable[] + { + new Box + { + Colour = OsuColour.Gray(0.1f), + RelativeSizeAxes = Axes.Both, + }, + new FillFlowContainer + { + Margin = new MarginPadding(5), + Padding = new MarginPadding { Right = 160 }, + Spacing = new Vector2(5), + Direction = FillDirection.Full, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + new SettingsTextBox + { + LabelText = "Mod", + Width = 0.33f, + Bindable = Model.Mod + }, + new SettingsSlider + { + LabelText = "Seed", + Width = 0.33f, + Bindable = Model.Seed + }, + new SettingsButton + { + Width = 0.2f, + Margin = new MarginPadding(10), + Text = "Add beatmap", + Action = () => beatmapEditor.CreateNew() + }, + beatmapEditor + } + }, + new DangerousSettingsButton + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.None, + Width = 150, + Text = "Delete SeeingResult", + Action = () => + { + Expire(); + team.SeedingResults.Remove(Model); + }, + } + }; + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + } + + public class SeedingBeatmapEditor : CompositeDrawable + { + private readonly SeedingResult round; + private readonly FillFlowContainer flow; + + public SeedingBeatmapEditor(SeedingResult round) + { + this.round = round; + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + InternalChild = flow = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + LayoutDuration = 200, + LayoutEasing = Easing.OutQuint, + ChildrenEnumerable = round.Beatmaps.Select(p => new SeedingBeatmapRow(round, p)) + }; + } + + public void CreateNew() + { + var user = new SeedingBeatmap(); + round.Beatmaps.Add(user); + flow.Add(new SeedingBeatmapRow(round, user)); + } + + public class SeedingBeatmapRow : CompositeDrawable + { + private readonly SeedingResult result; + public SeedingBeatmap Model { get; } + + [Resolved] + protected IAPIProvider API { get; private set; } + + private readonly Bindable beatmapId = new Bindable(); + + private readonly Bindable score = new Bindable(); + + private readonly Container drawableContainer; + + public SeedingBeatmapRow(SeedingResult result, SeedingBeatmap beatmap) + { + this.result = result; + Model = beatmap; + + Margin = new MarginPadding(10); + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + Masking = true; + CornerRadius = 5; + + InternalChildren = new Drawable[] + { + new Box + { + Colour = OsuColour.Gray(0.2f), + RelativeSizeAxes = Axes.Both, + }, + new FillFlowContainer + { + Margin = new MarginPadding(5), + Padding = new MarginPadding { Right = 160 }, + Spacing = new Vector2(5), + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new SettingsNumberBox + { + LabelText = "Beatmap ID", + RelativeSizeAxes = Axes.None, + Width = 200, + Bindable = beatmapId, + }, + new SettingsSlider + { + LabelText = "Seed", + RelativeSizeAxes = Axes.None, + Width = 200, + Bindable = beatmap.Seed + }, + new SettingsTextBox + { + LabelText = "Score", + RelativeSizeAxes = Axes.None, + Width = 200, + Bindable = score, + }, + drawableContainer = new Container + { + Size = new Vector2(100, 70), + }, + } + }, + new DangerousSettingsButton + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.None, + Width = 150, + Text = "Delete Beatmap", + Action = () => + { + Expire(); + result.Beatmaps.Remove(beatmap); + }, + } + }; + } + + [BackgroundDependencyLoader] + private void load(RulesetStore rulesets) + { + beatmapId.Value = Model.ID.ToString(); + beatmapId.BindValueChanged(idString => + { + int parsed; + + int.TryParse(idString.NewValue, out parsed); + + Model.ID = parsed; + + if (idString.NewValue != idString.OldValue) + Model.BeatmapInfo = null; + + if (Model.BeatmapInfo != null) + { + updatePanel(); + return; + } + + var req = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = Model.ID }); + + req.Success += res => + { + Model.BeatmapInfo = res.ToBeatmap(rulesets); + updatePanel(); + }; + + req.Failure += _ => + { + Model.BeatmapInfo = null; + updatePanel(); + }; + + API.Queue(req); + }, true); + + score.Value = Model.Score.ToString(); + score.BindValueChanged(str => long.TryParse(str.NewValue, out Model.Score)); + } + + private void updatePanel() + { + drawableContainer.Clear(); + + if (Model.BeatmapInfo != null) + { + drawableContainer.Child = new TournamentBeatmapPanel(Model.BeatmapInfo, result.Mod.Value) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Width = 300 + }; + } + } + } + } + } + + protected override SeeingResultRow CreateDrawable(SeedingResult model) => new SeeingResultRow(team, model); + } +} diff --git a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs index 494dd73edd..d6fa1a29a8 100644 --- a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs @@ -57,6 +57,9 @@ namespace osu.Game.Tournament.Screens.Editors private readonly Container drawableContainer; + [Resolved(canBeNull: true)] + private TournamentSceneManager sceneManager { get; set; } + [Resolved] private LadderInfo ladderInfo { get; set; } @@ -113,6 +116,18 @@ namespace osu.Game.Tournament.Screens.Editors Width = 0.2f, Bindable = Model.FlagName }, + new SettingsTextBox + { + LabelText = "Seed", + Width = 0.2f, + Bindable = Model.Seed + }, + new SettingsSlider + { + LabelText = "Last Year Placement", + Width = 0.33f, + Bindable = Model.LastYearPlacing + }, new SettingsButton { Width = 0.11f, @@ -131,7 +146,17 @@ namespace osu.Game.Tournament.Screens.Editors ladderInfo.Teams.Remove(Model); }, }, - playerEditor + playerEditor, + new SettingsButton + { + Width = 0.2f, + Margin = new MarginPadding(10), + Text = "Edit seeding results", + Action = () => + { + sceneManager.SetScreen(new SeedingEditorScreen(team)); + } + }, } }, }; diff --git a/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs b/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs index 8ab083ddaf..672b7ecc02 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs @@ -127,49 +127,6 @@ namespace osu.Game.Tournament.Screens.Ladder.Components } } - private class SettingsTeamDropdown : LadderSettingsDropdown - { - public SettingsTeamDropdown(BindableList teams) - { - foreach (var t in teams.Prepend(new TournamentTeam())) - add(t); - - teams.CollectionChanged += (_, args) => - { - switch (args.Action) - { - case NotifyCollectionChangedAction.Add: - args.NewItems.Cast().ForEach(add); - break; - - case NotifyCollectionChangedAction.Remove: - args.OldItems.Cast().ForEach(i => Control.RemoveDropdownItem(i)); - break; - } - }; - } - - private readonly List refBindables = new List(); - - private T boundReference(T obj) - where T : IBindable - { - obj = (T)obj.GetBoundCopy(); - refBindables.Add(obj); - return obj; - } - - private void add(TournamentTeam team) - { - Control.AddDropdownItem(team); - boundReference(team.FullName).BindValueChanged(_ => - { - Control.RemoveDropdownItem(team); - Control.AddDropdownItem(team); - }); - } - } - private class LadderSettingsDropdown : SettingsDropdown { protected override OsuDropdown CreateDropdown() => new DropdownControl(); diff --git a/osu.Game.Tournament/Screens/Ladder/Components/LadderSettingsDropdown.cs b/osu.Game.Tournament/Screens/Ladder/Components/LadderSettingsDropdown.cs new file mode 100644 index 0000000000..347e4d91e0 --- /dev/null +++ b/osu.Game.Tournament/Screens/Ladder/Components/LadderSettingsDropdown.cs @@ -0,0 +1,26 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays.Settings; + +namespace osu.Game.Tournament.Screens.Ladder.Components +{ + public class LadderSettingsDropdown : SettingsDropdown + { + protected override OsuDropdown CreateDropdown() => new DropdownControl(); + + private new class DropdownControl : SettingsDropdown.DropdownControl + { + protected override DropdownMenu CreateMenu() => new Menu(); + + private new class Menu : OsuDropdownMenu + { + public Menu() + { + MaxHeight = 200; + } + } + } + } +} diff --git a/osu.Game.Tournament/Screens/Ladder/Components/SettingsTeamDropdown.cs b/osu.Game.Tournament/Screens/Ladder/Components/SettingsTeamDropdown.cs new file mode 100644 index 0000000000..a630e51e44 --- /dev/null +++ b/osu.Game.Tournament/Screens/Ladder/Components/SettingsTeamDropdown.cs @@ -0,0 +1,55 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using osu.Framework.Bindables; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Game.Tournament.Models; + +namespace osu.Game.Tournament.Screens.Ladder.Components +{ + public class SettingsTeamDropdown : LadderSettingsDropdown + { + public SettingsTeamDropdown(BindableList teams) + { + foreach (var t in teams.Prepend(new TournamentTeam())) + add(t); + + teams.CollectionChanged += (_, args) => + { + switch (args.Action) + { + case NotifyCollectionChangedAction.Add: + args.NewItems.Cast().ForEach(add); + break; + + case NotifyCollectionChangedAction.Remove: + args.OldItems.Cast().ForEach(i => Control.RemoveDropdownItem(i)); + break; + } + }; + } + + private readonly List refBindables = new List(); + + private T boundReference(T obj) + where T : IBindable + { + obj = (T)obj.GetBoundCopy(); + refBindables.Add(obj); + return obj; + } + + private void add(TournamentTeam team) + { + Control.AddDropdownItem(team); + boundReference(team.FullName).BindValueChanged(_ => + { + Control.RemoveDropdownItem(team); + Control.AddDropdownItem(team); + }); + } + } +} diff --git a/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs b/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs new file mode 100644 index 0000000000..db5363c155 --- /dev/null +++ b/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs @@ -0,0 +1,316 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Framework.Platform; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Tournament.Components; +using osu.Game.Tournament.Models; +using osu.Game.Tournament.Screens.Ladder.Components; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Tournament.Screens.TeamIntro +{ + public class SeedingScreen : TournamentScreen, IProvideVideo + { + private Container mainContainer; + + private readonly Bindable currentMatch = new Bindable(); + + private readonly Bindable currentTeam = new Bindable(); + + [BackgroundDependencyLoader] + private void load(Storage storage) + { + RelativeSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + new TourneyVideo(storage.GetStream(@"videos/seeding.m4v")) + { + RelativeSizeAxes = Axes.Both, + Loop = true, + }, + mainContainer = new Container + { + RelativeSizeAxes = Axes.Both, + }, + new ControlPanel + { + Children = new Drawable[] + { + new TourneyButton + { + RelativeSizeAxes = Axes.X, + Text = "Show first team", + Action = () => currentTeam.Value = currentMatch.Value.Team1.Value, + }, + new TourneyButton + { + RelativeSizeAxes = Axes.X, + Text = "Show second team", + Action = () => currentTeam.Value = currentMatch.Value.Team2.Value, + }, + new SettingsTeamDropdown(LadderInfo.Teams) + { + LabelText = "Show specific team", + Bindable = currentTeam, + } + } + } + }; + + currentMatch.BindValueChanged(matchChanged); + currentMatch.BindTo(LadderInfo.CurrentMatch); + + currentTeam.BindValueChanged(teamChanged, true); + } + + private void teamChanged(ValueChangedEvent team) + { + if (team.NewValue == null) + { + mainContainer.Clear(); + return; + } + + showTeam(team.NewValue); + } + + private void matchChanged(ValueChangedEvent match) => + currentTeam.Value = currentMatch.Value.Team1.Value; + + private void showTeam(TournamentTeam team) + { + mainContainer.Children = new Drawable[] + { + new LeftInfo(team) { Position = new Vector2(55, 150), }, + new RightInfo(team) { Position = new Vector2(500, 150), }, + }; + } + + private class RightInfo : CompositeDrawable + { + public RightInfo(TournamentTeam team) + { + FillFlowContainer fill; + + Width = 400; + + InternalChildren = new Drawable[] + { + fill = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + }, + }; + + foreach (var seeding in team.SeedingResults) + { + fill.Add(new ModRow(seeding.Mod.Value, seeding.Seed.Value)); + foreach (var beatmap in seeding.Beatmaps) + fill.Add(new BeatmapScoreRow(beatmap)); + } + } + + private class BeatmapScoreRow : CompositeDrawable + { + public BeatmapScoreRow(SeedingBeatmap beatmap) + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + InternalChildren = new Drawable[] + { + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5), + Children = new Drawable[] + { + new TournamentSpriteText { Text = beatmap.BeatmapInfo.Metadata.Title, Colour = Color4.Black, }, + new TournamentSpriteText { Text = "by", Colour = Color4.Black, Font = OsuFont.Torus.With(weight: FontWeight.Regular) }, + new TournamentSpriteText { Text = beatmap.BeatmapInfo.Metadata.Artist, Colour = Color4.Black, Font = OsuFont.Torus.With(weight: FontWeight.Regular) }, + } + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Y, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(40), + Children = new Drawable[] + { + new TournamentSpriteText { Text = beatmap.Score.ToString("#,0"), Colour = Color4.Black, Width = 80 }, + new TournamentSpriteText { Text = "#" + beatmap.Seed.Value.ToString("#,0"), Colour = Color4.Black, Font = OsuFont.Torus.With(weight: FontWeight.Regular) }, + } + }, + }; + } + } + + private class ModRow : CompositeDrawable + { + private readonly string mods; + private readonly int seeding; + + public ModRow(string mods, int seeding) + { + this.mods = mods; + this.seeding = seeding; + + Padding = new MarginPadding { Vertical = 10 }; + + AutoSizeAxes = Axes.Y; + } + + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + InternalChildren = new Drawable[] + { + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5), + Children = new Drawable[] + { + new Sprite + { + Texture = textures.Get($"mods/{mods.ToLower()}"), + Scale = new Vector2(0.5f) + }, + new Container + { + Size = new Vector2(50, 16), + CornerRadius = 10, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + }, + new TournamentSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = seeding.ToString("#,0"), + }, + } + }, + } + }, + }; + } + } + } + + private class LeftInfo : CompositeDrawable + { + public LeftInfo(TournamentTeam team) + { + FillFlowContainer fill; + + Width = 200; + + if (team == null) return; + + InternalChildren = new Drawable[] + { + fill = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new TeamDisplay(team) { Margin = new MarginPadding { Bottom = 30 } }, + new RowDisplay("Average Rank:", $"#{team.AverageRank:#,0}"), + new RowDisplay("Seed:", team.Seed.Value), + new RowDisplay("Last year's placing:", team.LastYearPlacing.Value > 0 ? $"#{team.LastYearPlacing:#,0}" : "0"), + new Container { Margin = new MarginPadding { Bottom = 30 } }, + } + }, + }; + + foreach (var p in team.Players) + fill.Add(new RowDisplay(p.Username, p.Statistics?.Ranks.Global?.ToString("\\##,0") ?? "-")); + } + + internal class RowDisplay : CompositeDrawable + { + public RowDisplay(string left, string right) + { + AutoSizeAxes = Axes.Y; + RelativeSizeAxes = Axes.X; + + var colour = OsuColour.Gray(0.3f); + + InternalChildren = new Drawable[] + { + new TournamentSpriteText + { + Text = left, + Colour = colour, + Font = OsuFont.Torus.With(size: 22), + }, + new TournamentSpriteText + { + Text = right, + Colour = colour, + Anchor = Anchor.TopRight, + Origin = Anchor.TopLeft, + Font = OsuFont.Torus.With(size: 22, weight: FontWeight.Regular), + }, + }; + } + } + + private class TeamDisplay : DrawableTournamentTeam + { + public TeamDisplay(TournamentTeam team) + : base(team) + { + AutoSizeAxes = Axes.Both; + + Flag.RelativeSizeAxes = Axes.None; + Flag.Size = new Vector2(300, 200); + Flag.Scale = new Vector2(0.3f); + + InternalChild = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 5), + Children = new Drawable[] + { + Flag, + new OsuSpriteText + { + Text = team?.FullName.Value ?? "???", + Font = OsuFont.Torus.With(size: 32, weight: FontWeight.SemiBold), + Colour = Color4.Black, + }, + } + }; + } + } + } + } +} diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index dd93215fc1..64a5618d73 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -241,6 +241,24 @@ namespace osu.Game.Tournament } } + foreach (var t in ladder.Teams) + { + foreach (var s in t.SeedingResults) + { + foreach (var b in s.Beatmaps) + { + if (b.BeatmapInfo == null && b.ID > 0) + { + var req = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = b.ID }); + req.Perform(API); + b.BeatmapInfo = req.Result?.ToBeatmap(RulesetStore); + + addedInfo = true; + } + } + } + } + return addedInfo; } diff --git a/osu.Game.Tournament/TournamentSceneManager.cs b/osu.Game.Tournament/TournamentSceneManager.cs index de3d685c31..38df40772f 100644 --- a/osu.Game.Tournament/TournamentSceneManager.cs +++ b/osu.Game.Tournament/TournamentSceneManager.cs @@ -80,6 +80,7 @@ namespace osu.Game.Tournament new ShowcaseScreen(), new MapPoolScreen(), new TeamIntroScreen(), + new SeedingScreen(), new DrawingsScreen(), new GameplayScreen(), new TeamWinScreen() @@ -121,6 +122,7 @@ namespace osu.Game.Tournament new ScreenButton(typeof(LadderScreen)) { Text = "Bracket", RequestSelection = SetScreen }, new Separator(), new ScreenButton(typeof(TeamIntroScreen)) { Text = "TeamIntro", RequestSelection = SetScreen }, + new ScreenButton(typeof(SeedingScreen)) { Text = "Seeding", RequestSelection = SetScreen }, new Separator(), new ScreenButton(typeof(MapPoolScreen)) { Text = "MapPool", RequestSelection = SetScreen }, new ScreenButton(typeof(GameplayScreen)) { Text = "Gameplay", RequestSelection = SetScreen }, @@ -146,8 +148,20 @@ namespace osu.Game.Tournament private Drawable currentScreen; private ScheduledDelegate scheduledHide; + private Drawable temporaryScreen; + + public void SetScreen(Drawable screen) + { + currentScreen?.Hide(); + currentScreen = null; + + screens.Add(temporaryScreen = screen); + } + public void SetScreen(Type screenType) { + temporaryScreen?.Expire(); + var target = screens.FirstOrDefault(s => s.GetType() == screenType); if (target == null || currentScreen == target) return; diff --git a/osu.Game.Tournament/TournamentSpriteText.cs b/osu.Game.Tournament/TournamentSpriteText.cs new file mode 100644 index 0000000000..e550dfbfae --- /dev/null +++ b/osu.Game.Tournament/TournamentSpriteText.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 osu.Game.Graphics; +using osu.Game.Graphics.Sprites; + +namespace osu.Game.Tournament +{ + public class TournamentSpriteText : OsuSpriteText + { + public TournamentSpriteText() + { + Font = OsuFont.Torus; + } + } +} From b6edd17242a2f94da3fe03e9c2bf2833b0d6bdec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Mar 2020 18:59:23 +0900 Subject: [PATCH 0125/2376] Fix TeamWin test scene --- osu.Game.Tournament.Tests/Screens/TestSceneTeamWinScreen.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneTeamWinScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneTeamWinScreen.cs index 5cb35a506f..1a2faa76c1 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneTeamWinScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneTeamWinScreen.cs @@ -21,6 +21,7 @@ namespace osu.Game.Tournament.Tests.Screens match.Team1.Value = Ladder.Teams.FirstOrDefault(t => t.Acronym.Value == "USA"); match.Team2.Value = Ladder.Teams.FirstOrDefault(t => t.Acronym.Value == "JPN"); match.Round.Value = Ladder.Rounds.FirstOrDefault(g => g.Name.Value == "Finals"); + match.Completed.Value = true; ladder.CurrentMatch.Value = match; Add(new TeamWinScreen From 1b355d02d6910b596a547670a1c6ffaaffc0fbd0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Mar 2020 19:10:29 +0900 Subject: [PATCH 0126/2376] Update video resource paths --- osu.Game.Tournament/Screens/Ladder/LadderScreen.cs | 2 +- osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs | 2 +- osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs | 2 +- osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs | 4 ++-- osu.Game.Tournament/TournamentSceneManager.cs | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs b/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs index 8ea366e1b4..5528c9e9f5 100644 --- a/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs +++ b/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs @@ -42,7 +42,7 @@ namespace osu.Game.Tournament.Screens.Ladder RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - new TourneyVideo(storage.GetStream(@"BG Side Logo - OWC.m4v")) + new TourneyVideo(storage.GetStream(@"videos/ladder.m4v")) { RelativeSizeAxes = Axes.Both, Loop = true, diff --git a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs index 4b46264055..140ae54bfb 100644 --- a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs +++ b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs @@ -34,7 +34,7 @@ namespace osu.Game.Tournament.Screens.Schedule InternalChildren = new Drawable[] { - new TourneyVideo(storage.GetStream(@"BG Side Logo - OWC.m4v")) + new TourneyVideo(storage.GetStream(@"videos/schedule.m4v")) { RelativeSizeAxes = Axes.Both, Loop = true, diff --git a/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs b/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs index 75d63cde13..bff5c6aac3 100644 --- a/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs +++ b/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs @@ -29,7 +29,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro InternalChildren = new Drawable[] { - new TourneyVideo(storage.GetStream(@"BG Team - Both OWC.m4v")) + new TourneyVideo(storage.GetStream(@"videos/teamintro.m4v")) { RelativeSizeAxes = Axes.Both, Loop = true, diff --git a/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs b/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs index b72cacc66f..cc1f2a73ae 100644 --- a/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs +++ b/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs @@ -33,13 +33,13 @@ namespace osu.Game.Tournament.Screens.TeamWin InternalChildren = new Drawable[] { - blueWinVideo = new TourneyVideo(storage.GetStream(@"BG Team - Win Blue.m4v")) + blueWinVideo = new TourneyVideo(storage.GetStream(@"videos/teamwin-blue.m4v")) { Alpha = 1, RelativeSizeAxes = Axes.Both, Loop = true, }, - redWinVideo = new TourneyVideo(storage.GetStream(@"BG Team - Win Red.m4v")) + redWinVideo = new TourneyVideo(storage.GetStream(@"videos/teamwin-red.m4v")) { Alpha = 0, RelativeSizeAxes = Axes.Both, diff --git a/osu.Game.Tournament/TournamentSceneManager.cs b/osu.Game.Tournament/TournamentSceneManager.cs index 38df40772f..9f5f2b6827 100644 --- a/osu.Game.Tournament/TournamentSceneManager.cs +++ b/osu.Game.Tournament/TournamentSceneManager.cs @@ -61,7 +61,7 @@ namespace osu.Game.Tournament //Masking = true, Children = new Drawable[] { - video = new TourneyVideo(storage.GetStream("BG Logoless - OWC.m4v")) + video = new TourneyVideo(storage.GetStream("videos/main.m4v")) { Loop = true, RelativeSizeAxes = Axes.Both, From e678a068b6ecca9df29c408c7e4858c75f39e477 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Mar 2020 19:14:44 +0900 Subject: [PATCH 0127/2376] Bring design up-to-date with 2019 standards --- .../Components/ControlPanel.cs | 3 +- .../Components/DrawableTournamentTeam.cs | 7 +- osu.Game.Tournament/Components/SongBar.cs | 7 +- .../Components/TournamentBeatmapPanel.cs | 21 +-- .../Screens/Drawings/Components/Group.cs | 7 +- .../Screens/Drawings/DrawingsScreen.cs | 9 +- .../Gameplay/Components/MatchHeader.cs | 48 ++--- .../Ladder/Components/DrawableMatchTeam.cs | 11 +- .../Components/DrawableTournamentRound.cs | 11 +- .../Screens/MapPool/MapPoolScreen.cs | 3 +- .../Screens/Schedule/ScheduleScreen.cs | 15 +- osu.Game.Tournament/Screens/SetupScreen.cs | 5 +- .../Screens/Showcase/ShowcaseScreen.cs | 2 +- .../Screens/Showcase/TournamentLogo.cs | 16 +- .../Screens/TeamIntro/TeamIntroScreen.cs | 76 +++----- .../Screens/TeamWin/TeamWinScreen.cs | 176 +++++++----------- osu.Game.Tournament/TournamentGame.cs | 4 + osu.Game.Tournament/TournamentGameBase.cs | 5 +- 18 files changed, 168 insertions(+), 258 deletions(-) diff --git a/osu.Game.Tournament/Components/ControlPanel.cs b/osu.Game.Tournament/Components/ControlPanel.cs index a9bb1bf42f..fa5c941f1a 100644 --- a/osu.Game.Tournament/Components/ControlPanel.cs +++ b/osu.Game.Tournament/Components/ControlPanel.cs @@ -5,7 +5,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; using osuTK; using osuTK.Graphics; @@ -35,7 +34,7 @@ namespace osu.Game.Tournament.Components RelativeSizeAxes = Axes.Both, Colour = new Color4(54, 54, 54, 255) }, - new OsuSpriteText + new TournamentSpriteText { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, diff --git a/osu.Game.Tournament/Components/DrawableTournamentTeam.cs b/osu.Game.Tournament/Components/DrawableTournamentTeam.cs index 361bd92770..99116d4a17 100644 --- a/osu.Game.Tournament/Components/DrawableTournamentTeam.cs +++ b/osu.Game.Tournament/Components/DrawableTournamentTeam.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; using osu.Game.Tournament.Models; namespace osu.Game.Tournament.Components @@ -19,7 +18,7 @@ namespace osu.Game.Tournament.Components public readonly TournamentTeam Team; protected readonly Sprite Flag; - protected readonly OsuSpriteText AcronymText; + protected readonly TournamentSpriteText AcronymText; [UsedImplicitly] private Bindable acronym; @@ -37,9 +36,9 @@ namespace osu.Game.Tournament.Components FillMode = FillMode.Fit }; - AcronymText = new OsuSpriteText + AcronymText = new TournamentSpriteText { - Font = OsuFont.GetFont(weight: FontWeight.Regular), + Font = OsuFont.Torus.With(weight: FontWeight.Regular), }; } diff --git a/osu.Game.Tournament/Components/SongBar.cs b/osu.Game.Tournament/Components/SongBar.cs index 8a46da9565..48ea36a8f3 100644 --- a/osu.Game.Tournament/Components/SongBar.cs +++ b/osu.Game.Tournament/Components/SongBar.cs @@ -13,7 +13,6 @@ using osu.Framework.Graphics.Sprites; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Legacy; using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; using osu.Game.Rulesets; using osu.Game.Screens.Menu; using osuTK; @@ -262,7 +261,7 @@ namespace osu.Game.Tournament.Components static void cp(SpriteText s, Color4 colour) { s.Colour = colour; - s.Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 15); + s.Font = OsuFont.Torus.With(weight: FontWeight.Bold, size: 15); } for (var i = 0; i < tuples.Length; i++) @@ -278,9 +277,9 @@ namespace osu.Game.Tournament.Components }); } - AddText(new OsuSpriteText { Text = heading }, s => cp(s, OsuColour.Gray(0.33f))); + AddText(new TournamentSpriteText { Text = heading }, s => cp(s, OsuColour.Gray(0.33f))); AddText(" ", s => cp(s, OsuColour.Gray(0.33f))); - AddText(new OsuSpriteText { Text = content }, s => cp(s, OsuColour.Gray(0.5f))); + AddText(new TournamentSpriteText { Text = content }, s => cp(s, OsuColour.Gray(0.5f))); } } } diff --git a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs index 51483a0964..394ffe304e 100644 --- a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs +++ b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs @@ -15,7 +15,6 @@ using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; using osu.Game.Tournament.Models; using osuTK; using osuTK.Graphics; @@ -77,14 +76,14 @@ namespace osu.Game.Tournament.Components Direction = FillDirection.Vertical, Children = new Drawable[] { - new OsuSpriteText + new TournamentSpriteText { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Text = new LocalisedString(( $"{Beatmap.Metadata.ArtistUnicode ?? Beatmap.Metadata.Artist} - {Beatmap.Metadata.TitleUnicode ?? Beatmap.Metadata.Title}", $"{Beatmap.Metadata.Artist} - {Beatmap.Metadata.Title}")), - Font = OsuFont.GetFont(weight: FontWeight.Bold, italics: true), + Font = OsuFont.Torus.With(weight: FontWeight.Bold), }, new FillFlowContainer { @@ -95,28 +94,28 @@ namespace osu.Game.Tournament.Components Direction = FillDirection.Horizontal, Children = new Drawable[] { - new OsuSpriteText + new TournamentSpriteText { Text = "mapper", Padding = new MarginPadding { Right = 5 }, - Font = OsuFont.GetFont(italics: true, weight: FontWeight.Regular, size: 14) + Font = OsuFont.Torus.With(weight: FontWeight.Regular, size: 14) }, - new OsuSpriteText + new TournamentSpriteText { Text = Beatmap.Metadata.AuthorString, Padding = new MarginPadding { Right = 20 }, - Font = OsuFont.GetFont(italics: true, weight: FontWeight.Bold, size: 14) + Font = OsuFont.Torus.With(weight: FontWeight.Bold, size: 14) }, - new OsuSpriteText + new TournamentSpriteText { Text = "difficulty", Padding = new MarginPadding { Right = 5 }, - Font = OsuFont.GetFont(italics: true, weight: FontWeight.Regular, size: 14) + Font = OsuFont.Torus.With(weight: FontWeight.Regular, size: 14) }, - new OsuSpriteText + new TournamentSpriteText { Text = Beatmap.Version, - Font = OsuFont.GetFont(italics: true, weight: FontWeight.Bold, size: 14) + Font = OsuFont.Torus.With(weight: FontWeight.Bold, size: 14) }, } } diff --git a/osu.Game.Tournament/Screens/Drawings/Components/Group.cs b/osu.Game.Tournament/Screens/Drawings/Components/Group.cs index 549ff26018..4126f2db65 100644 --- a/osu.Game.Tournament/Screens/Drawings/Components/Group.cs +++ b/osu.Game.Tournament/Screens/Drawings/Components/Group.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; using osu.Game.Tournament.Components; using osu.Game.Tournament.Models; using osuTK; @@ -43,7 +42,7 @@ namespace osu.Game.Tournament.Screens.Drawings.Components Colour = new Color4(54, 54, 54, 255) }, // Group name - new OsuSpriteText + new TournamentSpriteText { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, @@ -51,7 +50,7 @@ namespace osu.Game.Tournament.Screens.Drawings.Components Position = new Vector2(0, 7f), Text = $"GROUP {name.ToUpperInvariant()}", - Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 8), + Font = OsuFont.Torus.With(weight: FontWeight.Bold, size: 8), Colour = new Color4(255, 204, 34, 255), }, teams = new FillFlowContainer @@ -134,7 +133,7 @@ namespace osu.Game.Tournament.Screens.Drawings.Components AcronymText.Anchor = Anchor.TopCentre; AcronymText.Origin = Anchor.TopCentre; AcronymText.Text = team.Acronym.Value.ToUpperInvariant(); - AcronymText.Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 10); + AcronymText.Font = OsuFont.Torus.With(weight: FontWeight.Bold, size: 10); InternalChildren = new Drawable[] { diff --git a/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs b/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs index 5efa0a1e69..8be66ff98c 100644 --- a/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs +++ b/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs @@ -14,7 +14,6 @@ using osu.Framework.Graphics.Textures; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; using osu.Game.Tournament.Components; using osu.Game.Tournament.Models; using osu.Game.Tournament.Screens.Drawings.Components; @@ -29,7 +28,7 @@ namespace osu.Game.Tournament.Screens.Drawings private ScrollingTeamContainer teamsContainer; private GroupContainer groupsContainer; - private OsuSpriteText fullTeamNameText; + private TournamentSpriteText fullTeamNameText; private readonly List allTeams = new List(); @@ -109,18 +108,18 @@ namespace osu.Game.Tournament.Screens.Drawings RelativeSizeAxes = Axes.X, }, // Scrolling team name - fullTeamNameText = new OsuSpriteText + fullTeamNameText = new TournamentSpriteText { Anchor = Anchor.Centre, Origin = Anchor.TopCentre, Position = new Vector2(0, 45f), - Colour = OsuColour.Gray(0.33f), + Colour = OsuColour.Gray(0.95f), Alpha = 0, - Font = OsuFont.GetFont(weight: FontWeight.Light, size: 42), + Font = OsuFont.Torus.With(weight: FontWeight.Light, size: 42), } } }, diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs b/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs index d8f2df2e93..ce17c392d0 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs @@ -5,9 +5,9 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Tournament.Components; using osu.Game.Tournament.Models; @@ -30,7 +30,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components new TournamentLogo(), new RoundDisplay { - Y = 10, + Y = 5, Anchor = Anchor.BottomCentre, Origin = Anchor.TopCentre, }, @@ -51,9 +51,6 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components { private readonly TeamColour teamColour; - private readonly Color4 red = new Color4(129, 68, 65, 255); - private readonly Color4 blue = new Color4(41, 91, 97, 255); - private readonly Bindable currentMatch = new Bindable(); private readonly Bindable currentTeam = new Bindable(); private readonly Bindable currentTeamScore = new Bindable(); @@ -106,7 +103,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components private void teamChanged(TournamentTeam team) { - var colour = teamColour == TeamColour.Red ? red : blue; + var colour = teamColour == TeamColour.Red ? TournamentGame.COLOUR_RED : TournamentGame.COLOUR_BLUE; var flip = teamColour != TeamColour.Red; InternalChildren = new Drawable[] @@ -169,7 +166,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components Children = new Drawable[] { Flag, - new OsuSpriteText + new TournamentSpriteText { Text = team?.FullName.Value.ToUpper() ?? "???", X = (flip ? -1 : 1) * 90, @@ -188,10 +185,31 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components { private readonly Bindable currentMatch = new Bindable(); + private readonly TournamentSpriteText text; + public RoundDisplay() { Width = 200; Height = 20; + + Masking = true; + CornerRadius = 10; + + InternalChildren = new Drawable[] + { + new Box + { + Colour = OsuColour.Gray(0.18f), + RelativeSizeAxes = Axes.Both, + }, + text = new TournamentSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = Color4.White, + Font = OsuFont.Torus.With(weight: FontWeight.Regular, size: 16), + }, + }; } [BackgroundDependencyLoader] @@ -201,20 +219,8 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components currentMatch.BindTo(ladder.CurrentMatch); } - private void matchChanged(ValueChangedEvent match) - { - InternalChildren = new Drawable[] - { - new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Colour = Color4.White, - Text = match.NewValue.Round.Value?.Name.Value ?? "Unknown Round", - Font = OsuFont.Torus.With(weight: FontWeight.Regular, size: 18), - }, - }; - } + private void matchChanged(ValueChangedEvent match) => + text.Text = match.NewValue.Round.Value?.Name.Value ?? "Unknown Round"; } } } diff --git a/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs b/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs index 031d6bf3d2..88d7b95b0c 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs @@ -11,7 +11,6 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Tournament.Components; using osu.Game.Tournament.Models; @@ -26,7 +25,7 @@ namespace osu.Game.Tournament.Screens.Ladder.Components { private readonly TournamentMatch match; private readonly bool losers; - private OsuSpriteText scoreText; + private TournamentSpriteText scoreText; private Box background; private readonly Bindable score = new Bindable(); @@ -69,7 +68,7 @@ namespace osu.Game.Tournament.Screens.Ladder.Components AcronymText.Anchor = AcronymText.Origin = Anchor.CentreLeft; AcronymText.Padding = new MarginPadding { Left = 50 }; - AcronymText.Font = OsuFont.GetFont(size: 24); + AcronymText.Font = OsuFont.Torus.With(size: 24); if (match != null) { @@ -119,11 +118,11 @@ namespace osu.Game.Tournament.Screens.Ladder.Components Alpha = 0.8f, RelativeSizeAxes = Axes.Both, }, - scoreText = new OsuSpriteText + scoreText = new TournamentSpriteText { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Font = OsuFont.GetFont(size: 20), + Font = OsuFont.Torus.With(size: 20), } } } @@ -184,7 +183,7 @@ namespace osu.Game.Tournament.Screens.Ladder.Components background.FadeColour(winner ? colourWinner : colourNormal, winner ? 500 : 0, Easing.OutQuint); - scoreText.Font = AcronymText.Font = OsuFont.GetFont(weight: winner ? FontWeight.Bold : FontWeight.Regular); + scoreText.Font = AcronymText.Font = OsuFont.Torus.With(weight: winner ? FontWeight.Bold : FontWeight.Regular); } public MenuItem[] ContextMenuItems diff --git a/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentRound.cs b/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentRound.cs index dacd98d3b8..d14ebb4d03 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentRound.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentRound.cs @@ -6,7 +6,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; using osu.Game.Tournament.Models; using osuTK.Graphics; @@ -22,8 +21,8 @@ namespace osu.Game.Tournament.Screens.Ladder.Components public DrawableTournamentRound(TournamentRound round, bool losers = false) { - OsuSpriteText textName; - OsuSpriteText textDescription; + TournamentSpriteText textName; + TournamentSpriteText textDescription; AutoSizeAxes = Axes.Both; InternalChild = new FillFlowContainer @@ -32,15 +31,15 @@ namespace osu.Game.Tournament.Screens.Ladder.Components AutoSizeAxes = Axes.Both, Children = new Drawable[] { - textDescription = new OsuSpriteText + textDescription = new TournamentSpriteText { Colour = Color4.Black, Origin = Anchor.TopCentre, Anchor = Anchor.TopCentre }, - textName = new OsuSpriteText + textName = new TournamentSpriteText { - Font = OsuFont.GetFont(weight: FontWeight.Bold), + Font = OsuFont.Torus.With(weight: FontWeight.Bold), Colour = Color4.Black, Origin = Anchor.TopCentre, Anchor = Anchor.TopCentre diff --git a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs index c3875716b8..c42d0a6da3 100644 --- a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs +++ b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Framework.Threading; using osu.Game.Beatmaps; -using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Tournament.Components; using osu.Game.Tournament.IPC; @@ -56,7 +55,7 @@ namespace osu.Game.Tournament.Screens.MapPool { Children = new Drawable[] { - new OsuSpriteText + new TournamentSpriteText { Text = "Current Mode" }, diff --git a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs index 140ae54bfb..080570eac4 100644 --- a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs +++ b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs @@ -10,7 +10,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Platform; using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; using osu.Game.Tournament.Components; using osu.Game.Tournament.Models; using osu.Game.Tournament.Screens.Ladder.Components; @@ -107,20 +106,20 @@ namespace osu.Game.Tournament.Screens.Schedule Height = 0.25f, Children = new Drawable[] { - new OsuSpriteText + new TournamentSpriteText { Margin = new MarginPadding { Left = -10, Bottom = 10, Top = -5 }, Spacing = new Vector2(10, 0), Text = match.NewValue.Round.Value?.Name.Value, Colour = Color4.Black, - Font = OsuFont.GetFont(size: 20) + Font = OsuFont.Torus.With(size: 20) }, new ScheduleMatch(match.NewValue, false), - new OsuSpriteText + new TournamentSpriteText { Text = "Start Time " + match.NewValue.Date.Value.ToUniversalTime().ToString("HH:mm UTC"), Colour = Color4.Black, - Font = OsuFont.GetFont(size: 20) + Font = OsuFont.Torus.With(size: 20) }, } } @@ -150,7 +149,7 @@ namespace osu.Game.Tournament.Screens.Schedule Alpha = conditional ? 0.6f : 1, Margin = new MarginPadding { Horizontal = 10, Vertical = 5 }, }); - AddInternal(new OsuSpriteText + AddInternal(new TournamentSpriteText { Anchor = Anchor.BottomRight, Origin = Anchor.BottomLeft, @@ -174,13 +173,13 @@ namespace osu.Game.Tournament.Screens.Schedule Padding = new MarginPadding { Left = 30, Top = 30 }; InternalChildren = new Drawable[] { - new OsuSpriteText + new TournamentSpriteText { X = 30, Text = title, Colour = Color4.Black, Spacing = new Vector2(10, 0), - Font = OsuFont.GetFont(size: 30) + Font = OsuFont.Torus.With(size: 30) }, content = new FillFlowContainer { diff --git a/osu.Game.Tournament/Screens/SetupScreen.cs b/osu.Game.Tournament/Screens/SetupScreen.cs index 8e1481d87c..023582166c 100644 --- a/osu.Game.Tournament/Screens/SetupScreen.cs +++ b/osu.Game.Tournament/Screens/SetupScreen.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Online.API; @@ -147,7 +146,7 @@ namespace osu.Game.Tournament.Screens public Action Action; - private OsuSpriteText valueText; + private TournamentSpriteText valueText; protected override Drawable CreateComponent() => new Container { @@ -155,7 +154,7 @@ namespace osu.Game.Tournament.Screens RelativeSizeAxes = Axes.X, Children = new Drawable[] { - valueText = new OsuSpriteText + valueText = new TournamentSpriteText { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, diff --git a/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs b/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs index 20928499bf..d809dfc994 100644 --- a/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs +++ b/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs @@ -10,7 +10,7 @@ namespace osu.Game.Tournament.Screens.Showcase [BackgroundDependencyLoader] private void load() { - AddInternal(new TournamentLogo(false)); + AddInternal(new TournamentLogo()); } } } diff --git a/osu.Game.Tournament/Screens/Showcase/TournamentLogo.cs b/osu.Game.Tournament/Screens/Showcase/TournamentLogo.cs index 1fee2b29e8..6ad5ccaf0c 100644 --- a/osu.Game.Tournament/Screens/Showcase/TournamentLogo.cs +++ b/osu.Game.Tournament/Screens/Showcase/TournamentLogo.cs @@ -11,20 +11,12 @@ namespace osu.Game.Tournament.Screens.Showcase { public class TournamentLogo : CompositeDrawable { - public TournamentLogo(bool includeRoundBackground = true) + public TournamentLogo() { RelativeSizeAxes = Axes.X; Margin = new MarginPadding { Vertical = 5 }; - if (includeRoundBackground) - { - AutoSizeAxes = Axes.Y; - } - else - { - Masking = true; - Height = 100; - } + Height = 100; } [BackgroundDependencyLoader] @@ -32,9 +24,11 @@ namespace osu.Game.Tournament.Screens.Showcase { InternalChild = new Sprite { - Texture = textures.Get("game-screen-logo"), Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, + FillMode = FillMode.Fit, + RelativeSizeAxes = Axes.Both, + Texture = textures.Get("game-screen-logo"), }; } } diff --git a/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs b/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs index bff5c6aac3..6559113f55 100644 --- a/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs +++ b/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs @@ -7,12 +7,9 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Platform; using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; using osu.Game.Tournament.Components; using osu.Game.Tournament.Models; -using osu.Game.Tournament.Screens.Showcase; using osuTK; -using osuTK.Graphics; namespace osu.Game.Tournament.Screens.TeamIntro { @@ -34,7 +31,6 @@ namespace osu.Game.Tournament.Screens.TeamIntro RelativeSizeAxes = Axes.Both, Loop = true, }, - new TournamentLogo(false), mainContainer = new Container { RelativeSizeAxes = Axes.Both, @@ -75,8 +71,9 @@ namespace osu.Game.Tournament.Screens.TeamIntro { RelativeSizeAxes = Axes.Both, Height = 0.25f, - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Y = 180, } }; } @@ -85,8 +82,6 @@ namespace osu.Game.Tournament.Screens.TeamIntro { public RoundDisplay(TournamentMatch match) { - var col = OsuColour.Gray(0.33f); - InternalChildren = new Drawable[] { new FillFlowContainer @@ -98,31 +93,13 @@ namespace osu.Game.Tournament.Screens.TeamIntro Spacing = new Vector2(0, 10), Children = new Drawable[] { - new OsuSpriteText + new TournamentSpriteText { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Colour = col, - Text = "COMING UP NEXT", - Spacing = new Vector2(2, 0), - Font = OsuFont.GetFont(size: 15, weight: FontWeight.Black) - }, - new OsuSpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Colour = col, + Colour = OsuColour.Gray(0.33f), Text = match.Round.Value?.Name.Value ?? "Unknown Round", - Spacing = new Vector2(10, 0), - Font = OsuFont.GetFont(size: 50, weight: FontWeight.Light) - }, - new OsuSpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Colour = col, - Text = match.Date.Value.ToUniversalTime().ToString("dd MMMM HH:mm UTC"), - Font = OsuFont.GetFont(size: 20) + Font = OsuFont.Torus.With(size: 26, weight: FontWeight.Light) }, } } @@ -132,21 +109,19 @@ namespace osu.Game.Tournament.Screens.TeamIntro private class TeamWithPlayers : CompositeDrawable { - private readonly Color4 red = new Color4(129, 68, 65, 255); - private readonly Color4 blue = new Color4(41, 91, 97, 255); - public TeamWithPlayers(TournamentTeam team, bool left = false) { FillFlowContainer players; - var colour = left ? red : blue; + var colour = left ? TournamentGame.COLOUR_RED : TournamentGame.COLOUR_BLUE; InternalChildren = new Drawable[] { - new TeamDisplay(team, left ? "Team Red" : "Team Blue", colour) + new TeamDisplay(team) { Anchor = left ? Anchor.CentreRight : Anchor.CentreLeft, - Origin = Anchor.Centre, + Origin = Anchor.TopCentre, RelativePositionAxes = Axes.Both, - X = (left ? -1 : 1) * 0.36f, + X = (left ? -1 : 1) * 0.3145f, + Y = -0.077f, }, players = new FillFlowContainer { @@ -157,7 +132,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro Anchor = left ? Anchor.CentreRight : Anchor.CentreLeft, Origin = left ? Anchor.CentreRight : Anchor.CentreLeft, RelativePositionAxes = Axes.Both, - X = (left ? -1 : 1) * 0.66f, + X = (left ? -1 : 1) * 0.58f, }, }; @@ -165,10 +140,10 @@ namespace osu.Game.Tournament.Screens.TeamIntro { foreach (var p in team.Players) { - players.Add(new OsuSpriteText + players.Add(new TournamentSpriteText { Text = p.Username, - Font = OsuFont.GetFont(size: 24), + Font = OsuFont.Torus.With(size: 24), Colour = colour, Anchor = left ? Anchor.CentreRight : Anchor.CentreLeft, Origin = left ? Anchor.CentreRight : Anchor.CentreLeft, @@ -179,7 +154,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro private class TeamDisplay : DrawableTournamentTeam { - public TeamDisplay(TournamentTeam team, string teamName, Color4 colour) + public TeamDisplay(TournamentTeam team) : base(team) { AutoSizeAxes = Axes.Both; @@ -187,33 +162,24 @@ namespace osu.Game.Tournament.Screens.TeamIntro Flag.Anchor = Flag.Origin = Anchor.TopCentre; Flag.RelativeSizeAxes = Axes.None; Flag.Size = new Vector2(300, 200); - Flag.Scale = new Vector2(0.4f); - Flag.Margin = new MarginPadding { Bottom = 20 }; + Flag.Scale = new Vector2(0.32f); InternalChild = new FillFlowContainer { AutoSizeAxes = Axes.Both, Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 5), + Spacing = new Vector2(160), Children = new Drawable[] { Flag, - new OsuSpriteText + new TournamentSpriteText { - Text = team?.FullName.Value.ToUpper() ?? "???", - Font = OsuFont.Torus.With(size: 40, weight: FontWeight.Light), - Colour = Color4.Black, + Text = team?.FullName.Value ?? "???", + Font = OsuFont.Torus.With(size: 20, weight: FontWeight.Regular), + Colour = OsuColour.Gray(0.2f), Origin = Anchor.TopCentre, Anchor = Anchor.TopCentre, }, - new OsuSpriteText - { - Text = teamName.ToUpper(), - Font = OsuFont.Torus.With(size: 20, weight: FontWeight.Regular), - Colour = colour, - Origin = Anchor.TopCentre, - Anchor = Anchor.TopCentre, - } } }; } diff --git a/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs b/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs index cc1f2a73ae..30b86f8421 100644 --- a/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs +++ b/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs @@ -7,10 +7,8 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Platform; using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; using osu.Game.Tournament.Components; using osu.Game.Tournament.Models; -using osu.Game.Tournament.Screens.Showcase; using osuTK; using osuTK.Graphics; @@ -45,10 +43,6 @@ namespace osu.Game.Tournament.Screens.TeamWin RelativeSizeAxes = Axes.Both, Loop = true, }, - new TournamentLogo(false) - { - Y = 40, - }, mainContainer = new Container { RelativeSizeAxes = Axes.Both, @@ -85,141 +79,99 @@ namespace osu.Game.Tournament.Screens.TeamWin mainContainer.Children = new Drawable[] { + new TeamFlagDisplay(match.Winner) + { + Size = new Vector2(300, 200), + Scale = new Vector2(0.5f), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + X = -387, + }, + new TournamentSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.TopLeft, + Position = new Vector2(78, -70), + Colour = OsuColour.Gray(0.33f), + Text = match.Round.Value?.Name.Value ?? "Unknown Round", + Font = OsuFont.Torus.With(size: 30, weight: FontWeight.Regular) + }, new TeamWithPlayers(match.Winner, redWin) { RelativeSizeAxes = Axes.Both, Width = 0.5f, Height = 0.6f, Anchor = Anchor.Centre, - Origin = Anchor.Centre + Origin = Anchor.TopLeft, + Position = new Vector2(78, 0), }, - new RoundDisplay(match) - { - RelativeSizeAxes = Axes.Both, - Height = 0.25f, - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - } }; } - private class RoundDisplay : CompositeDrawable - { - public RoundDisplay(TournamentMatch match) - { - var col = OsuColour.Gray(0.33f); - - InternalChildren = new Drawable[] - { - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 10), - Children = new Drawable[] - { - new OsuSpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Colour = col, - Text = "WINNER", - Font = OsuFont.Torus.With(size: 15, weight: FontWeight.Regular), - }, - new OsuSpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Colour = col, - Text = match.Round.Value?.Name.Value ?? "Unknown Round", - Font = OsuFont.Torus.With(size: 50, weight: FontWeight.Light), - Spacing = new Vector2(10, 0), - }, - new OsuSpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Colour = col, - Text = match.Date.Value.ToUniversalTime().ToString("dd MMMM HH:mm UTC"), - Font = OsuFont.Torus.With(size: 20, weight: FontWeight.Light), - }, - } - } - }; - } - } - private class TeamWithPlayers : CompositeDrawable { - private readonly Color4 red = new Color4(129, 68, 65, 255); - private readonly Color4 blue = new Color4(41, 91, 97, 255); - public TeamWithPlayers(TournamentTeam team, bool left = false) { - var colour = left ? red : blue; + FillFlowContainer players; + + var colour = left ? TournamentGame.COLOUR_RED : TournamentGame.COLOUR_BLUE; InternalChildren = new Drawable[] { - new TeamDisplay(team, left ? "Team Red" : "Team Blue", colour) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, new FillFlowContainer { Direction = FillDirection.Vertical, AutoSizeAxes = Axes.Both, - Spacing = new Vector2(0, 5), - Padding = new MarginPadding(20), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativePositionAxes = Axes.Both, - }, - }; - } - - private class TeamDisplay : DrawableTournamentTeam - { - public TeamDisplay(TournamentTeam team, string teamName, Color4 colour) - : base(team) - { - AutoSizeAxes = Axes.Both; - - Flag.Anchor = Flag.Origin = Anchor.TopCentre; - Flag.RelativeSizeAxes = Axes.None; - Flag.Size = new Vector2(300, 200); - Flag.Scale = new Vector2(0.4f); - Flag.Margin = new MarginPadding { Bottom = 20 }; - - InternalChild = new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 5), Children = new Drawable[] { - Flag, - new OsuSpriteText + new TournamentSpriteText { - Text = team?.FullName.Value.ToUpper() ?? "???", - Font = OsuFont.Torus.With(size: 40, weight: FontWeight.Light), + Text = "WINNER", + Font = OsuFont.Torus.With(size: 24, weight: FontWeight.SemiBold), Colour = Color4.Black, - Origin = Anchor.TopCentre, - Anchor = Anchor.TopCentre, }, - new OsuSpriteText + new TournamentSpriteText { - Text = teamName.ToUpper(), - Font = OsuFont.GetFont(size: 20), - Colour = colour, - Origin = Anchor.TopCentre, - Anchor = Anchor.TopCentre, - } + Text = team?.FullName.Value ?? "???", + Font = OsuFont.Torus.With(size: 30, weight: FontWeight.SemiBold), + Colour = Color4.Black, + }, + players = new FillFlowContainer + { + Direction = FillDirection.Vertical, + AutoSizeAxes = Axes.Both, + Padding = new MarginPadding { Top = 10 }, + }, } - }; + }, + }; + + if (team != null) + { + foreach (var p in team.Players) + { + players.Add(new TournamentSpriteText + { + Text = p.Username, + Font = OsuFont.Torus.With(size: 24), + Colour = colour, + Anchor = left ? Anchor.CentreRight : Anchor.CentreLeft, + Origin = left ? Anchor.CentreRight : Anchor.CentreLeft, + }); + } } } } + + private class TeamFlagDisplay : DrawableTournamentTeam + { + public TeamFlagDisplay(TournamentTeam team) + : base(team) + { + InternalChildren = new Drawable[] + { + Flag + }; + } + } } } diff --git a/osu.Game.Tournament/TournamentGame.cs b/osu.Game.Tournament/TournamentGame.cs index 7dbcf37af6..608fc5f04a 100644 --- a/osu.Game.Tournament/TournamentGame.cs +++ b/osu.Game.Tournament/TournamentGame.cs @@ -3,11 +3,15 @@ using osu.Framework.Graphics; using osu.Game.Graphics.Cursor; +using osuTK.Graphics; namespace osu.Game.Tournament { public class TournamentGame : TournamentGameBase { + public static readonly Color4 COLOUR_RED = new Color4(144, 0, 0, 255); + public static readonly Color4 COLOUR_BLUE = new Color4(0, 84, 144, 255); + protected override void LoadComplete() { base.LoadComplete(); diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 64a5618d73..435f315c8d 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -18,7 +18,6 @@ using osu.Framework.IO.Stores; using osu.Framework.Platform; using osu.Game.Beatmaps; using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; using osu.Game.Online.API.Requests; using osu.Game.Tournament.IPC; using osu.Game.Tournament.Models; @@ -101,10 +100,10 @@ namespace osu.Game.Tournament Colour = Color4.Red, RelativeSizeAxes = Axes.Both, }, - new OsuSpriteText + new TournamentSpriteText { Text = "Please make the window wider", - Font = OsuFont.Default.With(weight: "bold"), + Font = OsuFont.Torus.With(weight: FontWeight.Bold), Colour = Color4.White, Padding = new MarginPadding(20) } From 8dcdd6db6fc39b1e33a65675a5eb4c8cdccde8e5 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 3 Mar 2020 16:20:36 +0300 Subject: [PATCH 0128/2376] Rename UpdateStream components to ChangelogUpdateStream --- .../Visual/Online/TestSceneChangelogOverlay.cs | 4 ++-- osu.Game/Overlays/Changelog/ChangelogHeader.cs | 4 ++-- ...ateStreamBadge.cs => ChangelogUpdateStreamBadge.cs} | 4 ++-- ...mBadgeArea.cs => ChangelogUpdateStreamBadgeArea.cs} | 10 +++++----- 4 files changed, 11 insertions(+), 11 deletions(-) rename osu.Game/Overlays/Changelog/{UpdateStreamBadge.cs => ChangelogUpdateStreamBadge.cs} (97%) rename osu.Game/Overlays/Changelog/{UpdateStreamBadgeArea.cs => ChangelogUpdateStreamBadgeArea.cs} (84%) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs index 7a8570c09b..530a486de8 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs @@ -17,8 +17,8 @@ namespace osu.Game.Tests.Visual.Online public override IReadOnlyList RequiredTypes => new[] { - typeof(UpdateStreamBadgeArea), - typeof(UpdateStreamBadge), + typeof(ChangelogUpdateStreamBadgeArea), + typeof(ChangelogUpdateStreamBadge), typeof(ChangelogHeader), typeof(ChangelogContent), typeof(ChangelogListing), diff --git a/osu.Game/Overlays/Changelog/ChangelogHeader.cs b/osu.Game/Overlays/Changelog/ChangelogHeader.cs index dcadbf4cf5..0667bedfc6 100644 --- a/osu.Game/Overlays/Changelog/ChangelogHeader.cs +++ b/osu.Game/Overlays/Changelog/ChangelogHeader.cs @@ -20,7 +20,7 @@ namespace osu.Game.Overlays.Changelog public Action ListingSelected; - public UpdateStreamBadgeArea Streams; + public ChangelogUpdateStreamBadgeArea Streams; private const string listing_string = "listing"; @@ -95,7 +95,7 @@ namespace osu.Game.Overlays.Changelog Horizontal = 65, Vertical = 20 }, - Child = Streams = new UpdateStreamBadgeArea() + Child = Streams = new ChangelogUpdateStreamBadgeArea() } } }; diff --git a/osu.Game/Overlays/Changelog/UpdateStreamBadge.cs b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamBadge.cs similarity index 97% rename from osu.Game/Overlays/Changelog/UpdateStreamBadge.cs rename to osu.Game/Overlays/Changelog/ChangelogUpdateStreamBadge.cs index 6786bbc49f..20cc564013 100644 --- a/osu.Game/Overlays/Changelog/UpdateStreamBadge.cs +++ b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamBadge.cs @@ -17,7 +17,7 @@ using osuTK; namespace osu.Game.Overlays.Changelog { - public class UpdateStreamBadge : TabItem + public class ChangelogUpdateStreamBadge : TabItem { private const float badge_width = 100; private const float transition_duration = 100; @@ -29,7 +29,7 @@ namespace osu.Game.Overlays.Changelog private FillFlowContainer text; private ExpandingBar expandingBar; - public UpdateStreamBadge(APIUpdateStream stream) + public ChangelogUpdateStreamBadge(APIUpdateStream stream) : base(stream) { this.stream = stream; diff --git a/osu.Game/Overlays/Changelog/UpdateStreamBadgeArea.cs b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamBadgeArea.cs similarity index 84% rename from osu.Game/Overlays/Changelog/UpdateStreamBadgeArea.cs rename to osu.Game/Overlays/Changelog/ChangelogUpdateStreamBadgeArea.cs index ffb622dd37..5ab86a72f3 100644 --- a/osu.Game/Overlays/Changelog/UpdateStreamBadgeArea.cs +++ b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamBadgeArea.cs @@ -10,9 +10,9 @@ using osu.Framework.Graphics.UserInterface; namespace osu.Game.Overlays.Changelog { - public class UpdateStreamBadgeArea : TabControl + public class ChangelogUpdateStreamBadgeArea : TabControl { - public UpdateStreamBadgeArea() + public ChangelogUpdateStreamBadgeArea() { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; @@ -26,7 +26,7 @@ namespace osu.Game.Overlays.Changelog protected override bool OnHover(HoverEvent e) { - foreach (var streamBadge in TabContainer.Children.OfType()) + foreach (var streamBadge in TabContainer.Children.OfType()) streamBadge.UserHoveringArea = true; return base.OnHover(e); @@ -34,7 +34,7 @@ namespace osu.Game.Overlays.Changelog protected override void OnHoverLost(HoverLostEvent e) { - foreach (var streamBadge in TabContainer.Children.OfType()) + foreach (var streamBadge in TabContainer.Children.OfType()) streamBadge.UserHoveringArea = false; base.OnHoverLost(e); @@ -50,6 +50,6 @@ namespace osu.Game.Overlays.Changelog protected override Dropdown CreateDropdown() => null; protected override TabItem CreateTabItem(APIUpdateStream value) => - new UpdateStreamBadge(value) { SelectedTab = { BindTarget = Current } }; + new ChangelogUpdateStreamBadge(value) { SelectedTab = { BindTarget = Current } }; } } From db56fb57594ad14d1713c68778d207c21a3892e0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Mar 2020 22:28:47 +0900 Subject: [PATCH 0129/2376] Fix venera font usage --- osu.Game/Graphics/OsuFont.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/OsuFont.cs b/osu.Game/Graphics/OsuFont.cs index 54a25875bf..113816a14a 100644 --- a/osu.Game/Graphics/OsuFont.cs +++ b/osu.Game/Graphics/OsuFont.cs @@ -17,7 +17,7 @@ namespace osu.Game.Graphics /// public static FontUsage Default => GetFont(); - public static FontUsage Numeric => GetFont(Typeface.Venera, weight: FontWeight.Regular); + public static FontUsage Numeric => GetFont(Typeface.Venera); public static FontUsage Torus => GetFont(Typeface.Torus, weight: FontWeight.Regular); From 937d9da43b183104f4cca9a4de427922a960bf37 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 3 Mar 2020 17:01:58 +0300 Subject: [PATCH 0130/2376] Implement OverlayUpdateStreamControl component --- .../Online/TestSceneChangelogOverlay.cs | 4 +- .../Overlays/Changelog/ChangelogHeader.cs | 4 +- .../Changelog/ChangelogUpdateStreamControl.cs | 12 +++ .../Changelog/ChangelogUpdateStreamItem.cs | 25 ++++++ ...eArea.cs => OverlayUpdateStreamControl.cs} | 53 ++++++------ ...eamBadge.cs => OverlayUpdateStreamItem.cs} | 80 ++++++++++--------- 6 files changed, 111 insertions(+), 67 deletions(-) create mode 100644 osu.Game/Overlays/Changelog/ChangelogUpdateStreamControl.cs create mode 100644 osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs rename osu.Game/Overlays/{Changelog/ChangelogUpdateStreamBadgeArea.cs => OverlayUpdateStreamControl.cs} (61%) rename osu.Game/Overlays/{Changelog/ChangelogUpdateStreamBadge.cs => OverlayUpdateStreamItem.cs} (78%) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs index 530a486de8..864fd31a0f 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs @@ -17,8 +17,8 @@ namespace osu.Game.Tests.Visual.Online public override IReadOnlyList RequiredTypes => new[] { - typeof(ChangelogUpdateStreamBadgeArea), - typeof(ChangelogUpdateStreamBadge), + typeof(ChangelogUpdateStreamControl), + typeof(ChangelogUpdateStreamItem), typeof(ChangelogHeader), typeof(ChangelogContent), typeof(ChangelogListing), diff --git a/osu.Game/Overlays/Changelog/ChangelogHeader.cs b/osu.Game/Overlays/Changelog/ChangelogHeader.cs index 0667bedfc6..532efeb4bd 100644 --- a/osu.Game/Overlays/Changelog/ChangelogHeader.cs +++ b/osu.Game/Overlays/Changelog/ChangelogHeader.cs @@ -20,7 +20,7 @@ namespace osu.Game.Overlays.Changelog public Action ListingSelected; - public ChangelogUpdateStreamBadgeArea Streams; + public ChangelogUpdateStreamControl Streams; private const string listing_string = "listing"; @@ -95,7 +95,7 @@ namespace osu.Game.Overlays.Changelog Horizontal = 65, Vertical = 20 }, - Child = Streams = new ChangelogUpdateStreamBadgeArea() + Child = Streams = new ChangelogUpdateStreamControl() } } }; diff --git a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamControl.cs b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamControl.cs new file mode 100644 index 0000000000..555f0904d6 --- /dev/null +++ b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamControl.cs @@ -0,0 +1,12 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Overlays.Changelog +{ + public class ChangelogUpdateStreamControl : OverlayUpdateStreamControl + { + protected override OverlayUpdateStreamItem CreateStreamItem(APIUpdateStream value) => new ChangelogUpdateStreamItem(value); + } +} diff --git a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs new file mode 100644 index 0000000000..6a4801bc4b --- /dev/null +++ b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.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 Humanizer; +using osu.Game.Online.API.Requests.Responses; +using osuTK.Graphics; + +namespace osu.Game.Overlays.Changelog +{ + public class ChangelogUpdateStreamItem : OverlayUpdateStreamItem + { + public ChangelogUpdateStreamItem(APIUpdateStream stream) + : base(stream) + { + } + + protected override string GetMainText() => Value.DisplayName; + + protected override string GetAdditionalText() => Value.LatestBuild.DisplayVersion; + + protected override string GetInfoText() => Value.LatestBuild.Users > 0 ? $"{"user".ToQuantity(Value.LatestBuild.Users, "N0")} online" : null; + + protected override Color4 GetBarColour() => Value.Colour; + } +} diff --git a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamBadgeArea.cs b/osu.Game/Overlays/OverlayUpdateStreamControl.cs similarity index 61% rename from osu.Game/Overlays/Changelog/ChangelogUpdateStreamBadgeArea.cs rename to osu.Game/Overlays/OverlayUpdateStreamControl.cs index 5ab86a72f3..0fdf6c0111 100644 --- a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamBadgeArea.cs +++ b/osu.Game/Overlays/OverlayUpdateStreamControl.cs @@ -3,42 +3,32 @@ using osu.Framework.Graphics; using osu.Framework.Input.Events; -using osu.Game.Online.API.Requests.Responses; using System.Collections.Generic; using System.Linq; using osu.Framework.Graphics.UserInterface; +using JetBrains.Annotations; -namespace osu.Game.Overlays.Changelog +namespace osu.Game.Overlays { - public class ChangelogUpdateStreamBadgeArea : TabControl + public abstract class OverlayUpdateStreamControl : TabControl { - public ChangelogUpdateStreamBadgeArea() + protected OverlayUpdateStreamControl() { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; } - public void Populate(List streams) + public void Populate(List streams) => streams.ForEach(AddItem); + + protected override Dropdown CreateDropdown() => null; + + protected override TabItem CreateTabItem(T value) => CreateStreamItem(value).With(item => { - foreach (var updateStream in streams) - AddItem(updateStream); - } + item.SelectedItem.BindTo(Current); + }); - protected override bool OnHover(HoverEvent e) - { - foreach (var streamBadge in TabContainer.Children.OfType()) - streamBadge.UserHoveringArea = true; - - return base.OnHover(e); - } - - protected override void OnHoverLost(HoverLostEvent e) - { - foreach (var streamBadge in TabContainer.Children.OfType()) - streamBadge.UserHoveringArea = false; - - base.OnHoverLost(e); - } + [NotNull] + protected abstract OverlayUpdateStreamItem CreateStreamItem(T value); protected override TabFillFlowContainer CreateTabFlow() => new TabFillFlowContainer { @@ -47,9 +37,20 @@ namespace osu.Game.Overlays.Changelog AllowMultiline = true, }; - protected override Dropdown CreateDropdown() => null; + protected override bool OnHover(HoverEvent e) + { + foreach (var streamBadge in TabContainer.Children.OfType>()) + streamBadge.UserHoveringArea = true; - protected override TabItem CreateTabItem(APIUpdateStream value) => - new ChangelogUpdateStreamBadge(value) { SelectedTab = { BindTarget = Current } }; + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + foreach (var streamBadge in TabContainer.Children.OfType>()) + streamBadge.UserHoveringArea = false; + + base.OnHoverLost(e); + } } } diff --git a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamBadge.cs b/osu.Game/Overlays/OverlayUpdateStreamItem.cs similarity index 78% rename from osu.Game/Overlays/Changelog/ChangelogUpdateStreamBadge.cs rename to osu.Game/Overlays/OverlayUpdateStreamItem.cs index 20cc564013..5014aac5b0 100644 --- a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamBadge.cs +++ b/osu.Game/Overlays/OverlayUpdateStreamItem.cs @@ -1,44 +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 Humanizer; -using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; -using osu.Game.Graphics; -using osu.Game.Online.API.Requests.Responses; using osu.Framework.Graphics.UserInterface; -using osu.Game.Graphics.Sprites; +using osu.Framework.Bindables; +using osu.Framework.Graphics.Containers; using osu.Game.Graphics.UserInterface; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Allocation; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics; using osuTK; +using osuTK.Graphics; -namespace osu.Game.Overlays.Changelog +namespace osu.Game.Overlays { - public class ChangelogUpdateStreamBadge : TabItem + public abstract class OverlayUpdateStreamItem : TabItem { - private const float badge_width = 100; private const float transition_duration = 100; + private const float tab_width = 100; - public readonly Bindable SelectedTab = new Bindable(); + public readonly Bindable SelectedItem = new Bindable(); - private readonly APIUpdateStream stream; + private bool userHoveringArea; + + public bool UserHoveringArea + { + set + { + if (value == userHoveringArea) + return; + + userHoveringArea = value; + updateState(); + } + } private FillFlowContainer text; private ExpandingBar expandingBar; - public ChangelogUpdateStreamBadge(APIUpdateStream stream) - : base(stream) + public OverlayUpdateStreamItem(T value) + : base(value) { - this.stream = stream; } [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) { - Size = new Vector2(stream.IsFeatured ? badge_width * 2 : badge_width, 60); + Size = new Vector2(GetWidth(), 60); Padding = new MarginPadding(5); AddRange(new Drawable[] @@ -52,17 +62,17 @@ namespace osu.Game.Overlays.Changelog { new OsuSpriteText { - Text = stream.DisplayName, + Text = GetMainText(), Font = OsuFont.GetFont(size: 12, weight: FontWeight.Black), }, new OsuSpriteText { - Text = stream.LatestBuild.DisplayVersion, + Text = GetAdditionalText(), Font = OsuFont.GetFont(size: 16, weight: FontWeight.Regular), }, new OsuSpriteText { - Text = stream.LatestBuild.Users > 0 ? $"{"user".ToQuantity(stream.LatestBuild.Users, "N0")} online" : null, + Text = GetInfoText(), Font = OsuFont.GetFont(size: 10), Colour = colourProvider.Foreground1 }, @@ -71,7 +81,7 @@ namespace osu.Game.Overlays.Changelog expandingBar = new ExpandingBar { Anchor = Anchor.TopCentre, - Colour = stream.Colour, + Colour = GetBarColour(), ExpandedSize = 4, CollapsedSize = 2, Expanded = true @@ -79,9 +89,19 @@ namespace osu.Game.Overlays.Changelog new HoverClickSounds() }); - SelectedTab.BindValueChanged(_ => updateState(), true); + SelectedItem.BindValueChanged(_ => updateState(), true); } + protected abstract string GetMainText(); + + protected abstract string GetAdditionalText(); + + protected virtual string GetInfoText() => string.Empty; + + protected abstract Color4 GetBarColour(); + + protected virtual float GetWidth() => tab_width; + protected override void OnActivated() => updateState(); protected override void OnDeactivated() => updateState(); @@ -104,7 +124,7 @@ namespace osu.Game.Overlays.Changelog bool textHighlighted = IsHovered; bool barExpanded = IsHovered; - if (SelectedTab.Value == null) + if (SelectedItem.Value == null) { // at listing, all badges are highlighted when user is not hovering any badge. textHighlighted |= !userHoveringArea; @@ -122,19 +142,5 @@ namespace osu.Game.Overlays.Changelog expandingBar.Expanded = barExpanded; text.FadeTo(textHighlighted ? 1 : 0.5f, transition_duration, Easing.OutQuint); } - - private bool userHoveringArea; - - public bool UserHoveringArea - { - set - { - if (value == userHoveringArea) - return; - - userHoveringArea = value; - updateState(); - } - } } } From c0f7a83f6f3d4aaab9ce0d4d9ee2ae9674d238de Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 3 Mar 2020 17:10:25 +0300 Subject: [PATCH 0131/2376] Fix featured stream item width --- osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs index 6a4801bc4b..b796348242 100644 --- a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs +++ b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs @@ -14,6 +14,14 @@ namespace osu.Game.Overlays.Changelog { } + protected override float GetWidth() + { + if (Value.IsFeatured) + return base.GetWidth() * 2; + + return base.GetWidth(); + } + protected override string GetMainText() => Value.DisplayName; protected override string GetAdditionalText() => Value.LatestBuild.DisplayVersion; From 160d64eecf1cff32ed662fbfcbc129fcfe2b928f Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 3 Mar 2020 17:37:01 +0300 Subject: [PATCH 0132/2376] FriendsOnlineStatusControl basic implementation --- .../TestSceneFriendsOnlineStatusControl.cs | 52 +++++++++++++++++++ .../Overlays/Home/Friends/FriendsBundle.cs | 48 +++++++++++++++++ .../Friends/FriendsOnlineStatusControl.cs | 10 ++++ .../Home/Friends/FriendsOnlineStatusItem.cs | 21 ++++++++ 4 files changed, 131 insertions(+) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs create mode 100644 osu.Game/Overlays/Home/Friends/FriendsBundle.cs create mode 100644 osu.Game/Overlays/Home/Friends/FriendsOnlineStatusControl.cs create mode 100644 osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs new file mode 100644 index 0000000000..bb64593088 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs @@ -0,0 +1,52 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Overlays; +using osu.Game.Overlays.Home.Friends; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public class TestSceneFriendsOnlineStatusControl : OsuTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(FriendsOnlineStatusControl), + typeof(FriendsOnlineStatusItem), + typeof(OverlayUpdateStreamControl<>), + typeof(OverlayUpdateStreamItem<>), + typeof(FriendsBundle) + }; + + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); + + private FriendsOnlineStatusControl control; + + [SetUp] + public void SetUp() => Schedule(() => + { + Clear(); + Add(control = new FriendsOnlineStatusControl + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + }); + + [Test] + public void Populate() + { + AddStep(@"Populate", () => control.Populate(new List + { + new FriendsBundle(FriendsOnlineStatus.All, 100), + new FriendsBundle(FriendsOnlineStatus.Online, 50), + new FriendsBundle(FriendsOnlineStatus.Offline, 50), + })); + } + } +} diff --git a/osu.Game/Overlays/Home/Friends/FriendsBundle.cs b/osu.Game/Overlays/Home/Friends/FriendsBundle.cs new file mode 100644 index 0000000000..e0f841da9a --- /dev/null +++ b/osu.Game/Overlays/Home/Friends/FriendsBundle.cs @@ -0,0 +1,48 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osuTK.Graphics; + +namespace osu.Game.Overlays.Home.Friends +{ + public class FriendsBundle + { + public FriendsOnlineStatus Status { get; } + + public int Amount { get; } + + public Color4 Colour => getColour(); + + public FriendsBundle(FriendsOnlineStatus status, int amount) + { + Status = status; + Amount = amount; + } + + private Color4 getColour() + { + switch (Status) + { + default: + throw new ArgumentException($@"{Status} status does not provide a colour in {nameof(getColour)}."); + + case FriendsOnlineStatus.All: + return Color4.White; + + case FriendsOnlineStatus.Online: + return Color4.Lime; + + case FriendsOnlineStatus.Offline: + return Color4.Black; + } + } + } + + public enum FriendsOnlineStatus + { + All, + Online, + Offline + } +} diff --git a/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusControl.cs b/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusControl.cs new file mode 100644 index 0000000000..abcd04bb0e --- /dev/null +++ b/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusControl.cs @@ -0,0 +1,10 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Overlays.Home.Friends +{ + public class FriendsOnlineStatusControl : OverlayUpdateStreamControl + { + protected override OverlayUpdateStreamItem CreateStreamItem(FriendsBundle value) => new FriendsOnlineStatusItem(value); + } +} diff --git a/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs b/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs new file mode 100644 index 0000000000..0c77ef20b9 --- /dev/null +++ b/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.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 osuTK.Graphics; + +namespace osu.Game.Overlays.Home.Friends +{ + public class FriendsOnlineStatusItem : OverlayUpdateStreamItem + { + public FriendsOnlineStatusItem(FriendsBundle value) + : base(value) + { + } + + protected override string GetMainText() => Value.Status.ToString(); + + protected override string GetAdditionalText() => Value.Amount.ToString(); + + protected override Color4 GetBarColour() => Value.Colour; + } +} From 83dad93b6d499555d8c015049599dc3df153dff6 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 3 Mar 2020 18:08:51 +0300 Subject: [PATCH 0133/2376] Make Populate() accept list of users --- .../TestSceneFriendsOnlineStatusControl.cs | 23 +++++++++++++++---- .../Friends/FriendsOnlineStatusControl.cs | 16 +++++++++++++ osu.Game/Overlays/OverlayUpdateStreamItem.cs | 2 +- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs index bb64593088..87e7d848a8 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs @@ -3,11 +3,13 @@ using System; using System.Collections.Generic; +using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Overlays; using osu.Game.Overlays.Home.Friends; +using osu.Game.Users; namespace osu.Game.Tests.Visual.UserInterface { @@ -41,12 +43,25 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void Populate() { - AddStep(@"Populate", () => control.Populate(new List + AddStep("Populate", () => control.Populate(new List { - new FriendsBundle(FriendsOnlineStatus.All, 100), - new FriendsBundle(FriendsOnlineStatus.Online, 50), - new FriendsBundle(FriendsOnlineStatus.Offline, 50), + new User + { + IsOnline = true + }, + new User + { + IsOnline = false + }, + new User + { + IsOnline = false + } })); + + AddAssert("3 users", () => control.Items.FirstOrDefault(item => item.Status == FriendsOnlineStatus.All)?.Amount == 3); + AddAssert("1 online user", () => control.Items.FirstOrDefault(item => item.Status == FriendsOnlineStatus.Online)?.Amount == 1); + AddAssert("2 offline users", () => control.Items.FirstOrDefault(item => item.Status == FriendsOnlineStatus.Offline)?.Amount == 2); } } } diff --git a/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusControl.cs b/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusControl.cs index abcd04bb0e..a92de9dbeb 100644 --- a/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusControl.cs +++ b/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusControl.cs @@ -1,10 +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 System.Linq; +using osu.Game.Users; + namespace osu.Game.Overlays.Home.Friends { public class FriendsOnlineStatusControl : OverlayUpdateStreamControl { protected override OverlayUpdateStreamItem CreateStreamItem(FriendsBundle value) => new FriendsOnlineStatusItem(value); + + public void Populate(List users) + { + var userCount = users.Count; + var onlineUsersCount = users.Count(user => user.IsOnline); + + AddItem(new FriendsBundle(FriendsOnlineStatus.All, userCount)); + AddItem(new FriendsBundle(FriendsOnlineStatus.Online, onlineUsersCount)); + AddItem(new FriendsBundle(FriendsOnlineStatus.Offline, userCount - onlineUsersCount)); + + Current.Value = Items.FirstOrDefault(); + } } } diff --git a/osu.Game/Overlays/OverlayUpdateStreamItem.cs b/osu.Game/Overlays/OverlayUpdateStreamItem.cs index 5014aac5b0..bf8e6ac3b9 100644 --- a/osu.Game/Overlays/OverlayUpdateStreamItem.cs +++ b/osu.Game/Overlays/OverlayUpdateStreamItem.cs @@ -40,7 +40,7 @@ namespace osu.Game.Overlays private FillFlowContainer text; private ExpandingBar expandingBar; - public OverlayUpdateStreamItem(T value) + protected OverlayUpdateStreamItem(T value) : base(value) { } From 06b23b626ecaab8754c602087e9af752979e51f2 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 4 Mar 2020 00:15:10 +0300 Subject: [PATCH 0134/2376] Simplify test scene setup --- .../TestSceneFriendsOnlineStatusControl.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs index 87e7d848a8..614e99ee0e 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs @@ -30,14 +30,10 @@ namespace osu.Game.Tests.Visual.UserInterface private FriendsOnlineStatusControl control; [SetUp] - public void SetUp() => Schedule(() => + public void SetUp() => Schedule(() => Child = control = new FriendsOnlineStatusControl { - Clear(); - Add(control = new FriendsOnlineStatusControl - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }); + Anchor = Anchor.Centre, + Origin = Anchor.Centre, }); [Test] From c22f61b2b1ad9ec6663727b79eddb3616d9799f3 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 4 Mar 2020 00:28:47 +0300 Subject: [PATCH 0135/2376] Move colour selection to the FriendsOnlineStatusItem --- .../Changelog/ChangelogUpdateStreamItem.cs | 3 ++- .../Overlays/Home/Friends/FriendsBundle.cs | 23 ------------------- .../Home/Friends/FriendsOnlineStatusItem.cs | 20 +++++++++++++++- osu.Game/Overlays/OverlayUpdateStreamItem.cs | 6 ++--- 4 files changed, 24 insertions(+), 28 deletions(-) diff --git a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs index b796348242..189c156b35 100644 --- a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs +++ b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using Humanizer; +using osu.Game.Graphics; using osu.Game.Online.API.Requests.Responses; using osuTK.Graphics; @@ -28,6 +29,6 @@ namespace osu.Game.Overlays.Changelog protected override string GetInfoText() => Value.LatestBuild.Users > 0 ? $"{"user".ToQuantity(Value.LatestBuild.Users, "N0")} online" : null; - protected override Color4 GetBarColour() => Value.Colour; + protected override Color4 GetBarColour(OsuColour colours) => Value.Colour; } } diff --git a/osu.Game/Overlays/Home/Friends/FriendsBundle.cs b/osu.Game/Overlays/Home/Friends/FriendsBundle.cs index e0f841da9a..aa403feffc 100644 --- a/osu.Game/Overlays/Home/Friends/FriendsBundle.cs +++ b/osu.Game/Overlays/Home/Friends/FriendsBundle.cs @@ -1,9 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using osuTK.Graphics; - namespace osu.Game.Overlays.Home.Friends { public class FriendsBundle @@ -12,31 +9,11 @@ namespace osu.Game.Overlays.Home.Friends public int Amount { get; } - public Color4 Colour => getColour(); - public FriendsBundle(FriendsOnlineStatus status, int amount) { Status = status; Amount = amount; } - - private Color4 getColour() - { - switch (Status) - { - default: - throw new ArgumentException($@"{Status} status does not provide a colour in {nameof(getColour)}."); - - case FriendsOnlineStatus.All: - return Color4.White; - - case FriendsOnlineStatus.Online: - return Color4.Lime; - - case FriendsOnlineStatus.Offline: - return Color4.Black; - } - } } public enum FriendsOnlineStatus diff --git a/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs b/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs index 0c77ef20b9..bd480aebe8 100644 --- a/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs +++ b/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.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.Game.Graphics; using osuTK.Graphics; namespace osu.Game.Overlays.Home.Friends @@ -16,6 +18,22 @@ namespace osu.Game.Overlays.Home.Friends protected override string GetAdditionalText() => Value.Amount.ToString(); - protected override Color4 GetBarColour() => Value.Colour; + protected override Color4 GetBarColour(OsuColour colours) + { + switch (Value.Status) + { + default: + throw new ArgumentException($@"{Value.Status} status does not provide a colour in {nameof(GetBarColour)}."); + + case FriendsOnlineStatus.All: + return Color4.White; + + case FriendsOnlineStatus.Online: + return colours.GreenLight; + + case FriendsOnlineStatus.Offline: + return Color4.Black; + } + } } } diff --git a/osu.Game/Overlays/OverlayUpdateStreamItem.cs b/osu.Game/Overlays/OverlayUpdateStreamItem.cs index bf8e6ac3b9..ce9aca6f1f 100644 --- a/osu.Game/Overlays/OverlayUpdateStreamItem.cs +++ b/osu.Game/Overlays/OverlayUpdateStreamItem.cs @@ -46,7 +46,7 @@ namespace osu.Game.Overlays } [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) + private void load(OverlayColourProvider colourProvider, OsuColour colours) { Size = new Vector2(GetWidth(), 60); Padding = new MarginPadding(5); @@ -81,7 +81,7 @@ namespace osu.Game.Overlays expandingBar = new ExpandingBar { Anchor = Anchor.TopCentre, - Colour = GetBarColour(), + Colour = GetBarColour(colours), ExpandedSize = 4, CollapsedSize = 2, Expanded = true @@ -98,7 +98,7 @@ namespace osu.Game.Overlays protected virtual string GetInfoText() => string.Empty; - protected abstract Color4 GetBarColour(); + protected abstract Color4 GetBarColour(OsuColour colours); protected virtual float GetWidth() => tab_width; From 4d5445b5dc63d8b2c3ff889c70cd5f573b37d25d Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 4 Mar 2020 00:31:06 +0300 Subject: [PATCH 0136/2376] Rename Amount to Count --- .../UserInterface/TestSceneFriendsOnlineStatusControl.cs | 6 +++--- osu.Game/Overlays/Home/Friends/FriendsBundle.cs | 6 +++--- osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs index 614e99ee0e..45f8a029a8 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs @@ -55,9 +55,9 @@ namespace osu.Game.Tests.Visual.UserInterface } })); - AddAssert("3 users", () => control.Items.FirstOrDefault(item => item.Status == FriendsOnlineStatus.All)?.Amount == 3); - AddAssert("1 online user", () => control.Items.FirstOrDefault(item => item.Status == FriendsOnlineStatus.Online)?.Amount == 1); - AddAssert("2 offline users", () => control.Items.FirstOrDefault(item => item.Status == FriendsOnlineStatus.Offline)?.Amount == 2); + AddAssert("3 users", () => control.Items.FirstOrDefault(item => item.Status == FriendsOnlineStatus.All)?.Count == 3); + AddAssert("1 online user", () => control.Items.FirstOrDefault(item => item.Status == FriendsOnlineStatus.Online)?.Count == 1); + AddAssert("2 offline users", () => control.Items.FirstOrDefault(item => item.Status == FriendsOnlineStatus.Offline)?.Count == 2); } } } diff --git a/osu.Game/Overlays/Home/Friends/FriendsBundle.cs b/osu.Game/Overlays/Home/Friends/FriendsBundle.cs index aa403feffc..75d00dfef8 100644 --- a/osu.Game/Overlays/Home/Friends/FriendsBundle.cs +++ b/osu.Game/Overlays/Home/Friends/FriendsBundle.cs @@ -7,12 +7,12 @@ namespace osu.Game.Overlays.Home.Friends { public FriendsOnlineStatus Status { get; } - public int Amount { get; } + public int Count { get; } - public FriendsBundle(FriendsOnlineStatus status, int amount) + public FriendsBundle(FriendsOnlineStatus status, int count) { Status = status; - Amount = amount; + Count = count; } } diff --git a/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs b/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs index bd480aebe8..4043c72bc4 100644 --- a/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs +++ b/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs @@ -16,7 +16,7 @@ namespace osu.Game.Overlays.Home.Friends protected override string GetMainText() => Value.Status.ToString(); - protected override string GetAdditionalText() => Value.Amount.ToString(); + protected override string GetAdditionalText() => Value.Count.ToString(); protected override Color4 GetBarColour(OsuColour colours) { From 17f2baf600fd703a1f6769bbc08c9298019eb07a Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 4 Mar 2020 00:35:32 +0300 Subject: [PATCH 0137/2376] Remove GetWidth function --- .../Changelog/ChangelogUpdateStreamItem.cs | 10 ++-------- osu.Game/Overlays/OverlayUpdateStreamItem.cs | 14 ++++---------- 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs index 189c156b35..5a16fc8ddf 100644 --- a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs +++ b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs @@ -13,14 +13,8 @@ namespace osu.Game.Overlays.Changelog public ChangelogUpdateStreamItem(APIUpdateStream stream) : base(stream) { - } - - protected override float GetWidth() - { - if (Value.IsFeatured) - return base.GetWidth() * 2; - - return base.GetWidth(); + if (stream.IsFeatured) + Width *= 2; } protected override string GetMainText() => Value.DisplayName; diff --git a/osu.Game/Overlays/OverlayUpdateStreamItem.cs b/osu.Game/Overlays/OverlayUpdateStreamItem.cs index ce9aca6f1f..0cdf894f73 100644 --- a/osu.Game/Overlays/OverlayUpdateStreamItem.cs +++ b/osu.Game/Overlays/OverlayUpdateStreamItem.cs @@ -11,16 +11,12 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Allocation; using osu.Game.Graphics.Sprites; using osu.Game.Graphics; -using osuTK; using osuTK.Graphics; namespace osu.Game.Overlays { public abstract class OverlayUpdateStreamItem : TabItem { - private const float transition_duration = 100; - private const float tab_width = 100; - public readonly Bindable SelectedItem = new Bindable(); private bool userHoveringArea; @@ -43,14 +39,14 @@ namespace osu.Game.Overlays protected OverlayUpdateStreamItem(T value) : base(value) { + Height = 60; + Width = 100; + Padding = new MarginPadding(5); } [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider, OsuColour colours) { - Size = new Vector2(GetWidth(), 60); - Padding = new MarginPadding(5); - AddRange(new Drawable[] { text = new FillFlowContainer @@ -100,8 +96,6 @@ namespace osu.Game.Overlays protected abstract Color4 GetBarColour(OsuColour colours); - protected virtual float GetWidth() => tab_width; - protected override void OnActivated() => updateState(); protected override void OnDeactivated() => updateState(); @@ -140,7 +134,7 @@ namespace osu.Game.Overlays } expandingBar.Expanded = barExpanded; - text.FadeTo(textHighlighted ? 1 : 0.5f, transition_duration, Easing.OutQuint); + text.FadeTo(textHighlighted ? 1 : 0.5f, 100, Easing.OutQuint); } } } From 6fca3e5a468d09d065a975e5d5eaab567ee6e5f7 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 4 Mar 2020 00:39:12 +0300 Subject: [PATCH 0138/2376] Remove functions with get-only properties --- .../Overlays/Changelog/ChangelogUpdateStreamItem.cs | 6 +++--- .../Overlays/Home/Friends/FriendsOnlineStatusItem.cs | 4 ++-- osu.Game/Overlays/OverlayUpdateStreamItem.cs | 12 ++++++------ 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs index 5a16fc8ddf..84b33fcde9 100644 --- a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs +++ b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs @@ -17,11 +17,11 @@ namespace osu.Game.Overlays.Changelog Width *= 2; } - protected override string GetMainText() => Value.DisplayName; + protected override string GetMainText => Value.DisplayName; - protected override string GetAdditionalText() => Value.LatestBuild.DisplayVersion; + protected override string GetAdditionalText => Value.LatestBuild.DisplayVersion; - protected override string GetInfoText() => Value.LatestBuild.Users > 0 ? $"{"user".ToQuantity(Value.LatestBuild.Users, "N0")} online" : null; + protected override string GetInfoText => Value.LatestBuild.Users > 0 ? $"{"user".ToQuantity(Value.LatestBuild.Users, "N0")} online" : null; protected override Color4 GetBarColour(OsuColour colours) => Value.Colour; } diff --git a/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs b/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs index 4043c72bc4..1025fc8146 100644 --- a/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs +++ b/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs @@ -14,9 +14,9 @@ namespace osu.Game.Overlays.Home.Friends { } - protected override string GetMainText() => Value.Status.ToString(); + protected override string GetMainText => Value.Status.ToString(); - protected override string GetAdditionalText() => Value.Count.ToString(); + protected override string GetAdditionalText => Value.Count.ToString(); protected override Color4 GetBarColour(OsuColour colours) { diff --git a/osu.Game/Overlays/OverlayUpdateStreamItem.cs b/osu.Game/Overlays/OverlayUpdateStreamItem.cs index 0cdf894f73..459daeb3a5 100644 --- a/osu.Game/Overlays/OverlayUpdateStreamItem.cs +++ b/osu.Game/Overlays/OverlayUpdateStreamItem.cs @@ -58,17 +58,17 @@ namespace osu.Game.Overlays { new OsuSpriteText { - Text = GetMainText(), + Text = GetMainText, Font = OsuFont.GetFont(size: 12, weight: FontWeight.Black), }, new OsuSpriteText { - Text = GetAdditionalText(), + Text = GetAdditionalText, Font = OsuFont.GetFont(size: 16, weight: FontWeight.Regular), }, new OsuSpriteText { - Text = GetInfoText(), + Text = GetInfoText, Font = OsuFont.GetFont(size: 10), Colour = colourProvider.Foreground1 }, @@ -88,11 +88,11 @@ namespace osu.Game.Overlays SelectedItem.BindValueChanged(_ => updateState(), true); } - protected abstract string GetMainText(); + protected abstract string GetMainText { get; } - protected abstract string GetAdditionalText(); + protected abstract string GetAdditionalText { get; } - protected virtual string GetInfoText() => string.Empty; + protected virtual string GetInfoText => string.Empty; protected abstract Color4 GetBarColour(OsuColour colours); From e2ed13b39248d10f52ffae58f4e51ed594a8951e Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 4 Mar 2020 00:40:10 +0300 Subject: [PATCH 0139/2376] Trim whitespace --- osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs index 84b33fcde9..79824db572 100644 --- a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs +++ b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs @@ -14,7 +14,7 @@ namespace osu.Game.Overlays.Changelog : base(stream) { if (stream.IsFeatured) - Width *= 2; + Width *= 2; } protected override string GetMainText => Value.DisplayName; From 5e218697c5a525d8289cfa3d9959b1e0df6ac2d6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 4 Mar 2020 09:46:53 +0900 Subject: [PATCH 0140/2376] Use stacked positions --- .../TestSceneFollowPoints.cs | 46 +++++++++++++++++++ .../Connections/FollowPointConnection.cs | 4 +- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs index 94ca2d4cd1..87da7ef417 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs @@ -2,9 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Osu.Objects; @@ -114,6 +117,22 @@ namespace osu.Game.Rulesets.Osu.Tests assertGroups(); } + [Test] + public void TestStackedObjects() + { + addObjectsStep(() => new OsuHitObject[] + { + new HitCircle { Position = new Vector2(300, 100) }, + new HitCircle + { + Position = new Vector2(300, 300), + StackHeight = 20 + }, + }); + + assertDirections(); + } + private void addMultipleObjectsStep() => addObjectsStep(() => new OsuHitObject[] { new HitCircle { Position = new Vector2(100, 100) }, @@ -207,6 +226,33 @@ namespace osu.Game.Rulesets.Osu.Tests }); } + private void assertDirections() + { + AddAssert("group directions are correct", () => + { + for (int i = 0; i < hitObjectContainer.Count; i++) + { + DrawableOsuHitObject expectedStart = getObject(i); + DrawableOsuHitObject expectedEnd = i < hitObjectContainer.Count - 1 ? getObject(i + 1) : null; + + if (expectedEnd == null) + continue; + + var points = getGroup(i).ChildrenOfType().ToArray(); + if (points.Length == 0) + continue; + + float expectedDirection = MathF.Atan2(expectedStart.Position.Y - expectedEnd.Position.Y, expectedStart.Position.X - expectedEnd.Position.X); + float realDirection = MathF.Atan2(expectedStart.Position.Y - points[^1].Position.Y, expectedStart.Position.X - points[^1].Position.X); + + if (!Precision.AlmostEquals(expectedDirection, realDirection)) + throw new AssertionException($"Expected group {i} in direction {expectedDirection}, but was {realDirection}."); + } + + return true; + }); + } + private DrawableOsuHitObject getObject(int index) => hitObjectContainer[index]; private FollowPointConnection getGroup(int index) => followPointRenderer.Connections[index]; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs index 921b23cb13..3e9c0f341b 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs @@ -104,8 +104,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections return; } - Vector2 startPosition = osuStart.EndPosition; - Vector2 endPosition = osuEnd.Position; + Vector2 startPosition = osuStart.StackedEndPosition; + Vector2 endPosition = osuEnd.StackedPosition; double endTime = osuEnd.StartTime; Vector2 distanceVector = endPosition - startPosition; From c3f840cc1a5071ee92c758315d5c41ee12cd1dec Mon Sep 17 00:00:00 2001 From: voidedWarranties Date: Tue, 3 Mar 2020 17:12:01 -0800 Subject: [PATCH 0141/2376] Fix Autoplay = false and AllowFail behavior --- .../Mods/TestSceneOsuModDifficultyAdjust.cs | 6 +++--- osu.Game/Tests/Visual/ModSandboxTestScene.cs | 13 ++++++++----- osu.Game/Tests/Visual/PlayerTestScene.cs | 2 ++ 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs index 1fc9ccccd1..20cb9ef05d 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs @@ -42,14 +42,14 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods PassCondition = () => ((ScoreAccessibleTestPlayer)Player).ScoreProcessor.JudgedHits >= 2 }); - protected override TestPlayer CreateReplayPlayer(Score score) => new ScoreAccessibleTestPlayer(score); + protected override TestPlayer CreateReplayPlayer(Score score, bool allowFail) => new ScoreAccessibleTestPlayer(score, allowFail); private class ScoreAccessibleTestPlayer : TestPlayer { public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; - public ScoreAccessibleTestPlayer(Score score) - : base(score) + public ScoreAccessibleTestPlayer(Score score, bool allowFail) + : base(score, allowFail) { } } diff --git a/osu.Game/Tests/Visual/ModSandboxTestScene.cs b/osu.Game/Tests/Visual/ModSandboxTestScene.cs index 11612d0eca..8a9cdf009b 100644 --- a/osu.Game/Tests/Visual/ModSandboxTestScene.cs +++ b/osu.Game/Tests/Visual/ModSandboxTestScene.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; using osu.Game.Beatmaps; -using osu.Game.Replays; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; @@ -56,22 +55,26 @@ namespace osu.Game.Tests.Visual var score = currentTest.Autoplay ? ruleset.GetAutoplayMod().CreateReplayScore(Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo, SelectedMods.Value)) - : new Score { Replay = new Replay() }; + : null; - return CreateReplayPlayer(score); + return CreateReplayPlayer(score, AllowFail); } /// /// Creates the for a test case. /// /// The . - protected virtual TestPlayer CreateReplayPlayer(Score score) => new TestPlayer(score); + /// Whether the player can fail. + protected virtual TestPlayer CreateReplayPlayer(Score score, bool allowFail) => new TestPlayer(score, allowFail); protected class TestPlayer : TestReplayPlayer { - public TestPlayer(Score score) + protected override bool AllowFail { get; } + + public TestPlayer(Score score, bool allowFail) : base(score, false, false) { + AllowFail = allowFail; } } diff --git a/osu.Game/Tests/Visual/PlayerTestScene.cs b/osu.Game/Tests/Visual/PlayerTestScene.cs index 0d5aac8cfd..17ad6e80df 100644 --- a/osu.Game/Tests/Visual/PlayerTestScene.cs +++ b/osu.Game/Tests/Visual/PlayerTestScene.cs @@ -68,6 +68,8 @@ namespace osu.Game.Tests.Visual Beatmap.Value = CreateWorkingBeatmap(beatmap); + SelectedMods.Value = Array.Empty(); + if (!AllowFail) { var noFailMod = ruleset.GetAllMods().FirstOrDefault(m => m is ModNoFail); From 4294ed4b6495e40bcb9c2eb9664f9e369cd949d7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Mar 2020 11:21:37 +0900 Subject: [PATCH 0142/2376] Better align fonts to weights --- osu.Game/Graphics/OsuFont.cs | 37 ++++++++++++++++---- osu.Game/Online/Leaderboards/DrawableRank.cs | 2 +- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/osu.Game/Graphics/OsuFont.cs b/osu.Game/Graphics/OsuFont.cs index 113816a14a..4dd7bcdf31 100644 --- a/osu.Game/Graphics/OsuFont.cs +++ b/osu.Game/Graphics/OsuFont.cs @@ -17,7 +17,7 @@ namespace osu.Game.Graphics /// public static FontUsage Default => GetFont(); - public static FontUsage Numeric => GetFont(Typeface.Venera); + public static FontUsage Numeric => GetFont(Typeface.Venera, weight: FontWeight.Bold); public static FontUsage Torus => GetFont(Typeface.Torus, weight: FontWeight.Regular); @@ -103,11 +103,34 @@ namespace osu.Game.Graphics public enum FontWeight { - Light, - Regular, - Medium, - SemiBold, - Bold, - Black + /// + /// equivalent to weight 300 + /// + Light = 300, + + /// + /// equivalent to weight 400 + /// + Regular = 400, + + /// + /// equivalent to weight 500 + /// + Medium = 500, + + /// + /// equivalent to weight 600 + /// + SemiBold = 600, + + /// + /// equivalent to weight 700 + /// + Bold = 700, + + /// + /// equivalent to weight 900 + /// + Black = 900 } } diff --git a/osu.Game/Online/Leaderboards/DrawableRank.cs b/osu.Game/Online/Leaderboards/DrawableRank.cs index 50cb58c6ab..20bda4601f 100644 --- a/osu.Game/Online/Leaderboards/DrawableRank.cs +++ b/osu.Game/Online/Leaderboards/DrawableRank.cs @@ -58,7 +58,7 @@ namespace osu.Game.Online.Leaderboards Spacing = new Vector2(-3, 0), Padding = new MarginPadding { Top = 5 }, Colour = getRankNameColour(), - Font = OsuFont.GetFont(Typeface.Venera, 25), + Font = OsuFont.Numeric.With(size: 25), Text = getRankName(), ShadowColour = Color4.Black.Opacity(0.3f), ShadowOffset = new Vector2(0, 0.08f), From 75968fb4baa8657c49f0bf9865a83c842553279b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Mar 2020 11:46:48 +0900 Subject: [PATCH 0143/2376] Update resources once more --- osu.Android.props | 2 +- osu.Game/OsuGameBase.cs | 4 ++-- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index d62011bf1d..1c4a6ffe75 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,7 +51,7 @@ - + diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 676b4b6c7a..a890331f05 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -162,9 +162,9 @@ namespace osu.Game AddFont(Resources, @"Fonts/Torus-Regular"); AddFont(Resources, @"Fonts/Torus-Light"); - AddFont(Resources, @"Fonts/Venera"); AddFont(Resources, @"Fonts/Venera-Light"); - AddFont(Resources, @"Fonts/Venera-Medium"); + AddFont(Resources, @"Fonts/Venera-Bold"); + AddFont(Resources, @"Fonts/Venera-Black"); runMigrations(); diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index e407fca8b7..4d59b709aa 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -22,7 +22,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index cc0b8074bb..6897d3e625 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + From 275d075acda463cafa23bb6ac97145d3a1262597 Mon Sep 17 00:00:00 2001 From: mcendu Date: Wed, 4 Mar 2020 10:53:30 +0800 Subject: [PATCH 0144/2376] comment out log locations in bug report template --- .github/ISSUE_TEMPLATE/01-bug-issues.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/01-bug-issues.md b/.github/ISSUE_TEMPLATE/01-bug-issues.md index 0aff276d03..0b80ce44dd 100644 --- a/.github/ISSUE_TEMPLATE/01-bug-issues.md +++ b/.github/ISSUE_TEMPLATE/01-bug-issues.md @@ -9,6 +9,8 @@ about: Issues regarding encountered bugs. **osu!lazer version:** **Logs:** + From 0f7316d41d54a6c1c522aa3bdbac2992095ebd45 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Mar 2020 12:05:23 +0900 Subject: [PATCH 0145/2376] Move file to other file --- .../Ladder/Components/LadderEditorSettings.cs | 62 ------------------- .../Components/LadderSettingsDropdown.cs | 26 ++++++++ .../Ladder/Components/SettingsTeamDropdown.cs | 55 ++++++++++++++++ 3 files changed, 81 insertions(+), 62 deletions(-) create mode 100644 osu.Game.Tournament/Screens/Ladder/Components/LadderSettingsDropdown.cs create mode 100644 osu.Game.Tournament/Screens/Ladder/Components/SettingsTeamDropdown.cs diff --git a/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs b/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs index 8ab083ddaf..4aea7ff4c0 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs @@ -9,7 +9,6 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Input.Events; -using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Settings; using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Tournament.Components; @@ -126,66 +125,5 @@ namespace osu.Game.Tournament.Screens.Ladder.Components }); } } - - private class SettingsTeamDropdown : LadderSettingsDropdown - { - public SettingsTeamDropdown(BindableList teams) - { - foreach (var t in teams.Prepend(new TournamentTeam())) - add(t); - - teams.CollectionChanged += (_, args) => - { - switch (args.Action) - { - case NotifyCollectionChangedAction.Add: - args.NewItems.Cast().ForEach(add); - break; - - case NotifyCollectionChangedAction.Remove: - args.OldItems.Cast().ForEach(i => Control.RemoveDropdownItem(i)); - break; - } - }; - } - - private readonly List refBindables = new List(); - - private T boundReference(T obj) - where T : IBindable - { - obj = (T)obj.GetBoundCopy(); - refBindables.Add(obj); - return obj; - } - - private void add(TournamentTeam team) - { - Control.AddDropdownItem(team); - boundReference(team.FullName).BindValueChanged(_ => - { - Control.RemoveDropdownItem(team); - Control.AddDropdownItem(team); - }); - } - } - - private class LadderSettingsDropdown : SettingsDropdown - { - protected override OsuDropdown CreateDropdown() => new DropdownControl(); - - private new class DropdownControl : SettingsDropdown.DropdownControl - { - protected override DropdownMenu CreateMenu() => new Menu(); - - private new class Menu : OsuDropdownMenu - { - public Menu() - { - MaxHeight = 200; - } - } - } - } } } diff --git a/osu.Game.Tournament/Screens/Ladder/Components/LadderSettingsDropdown.cs b/osu.Game.Tournament/Screens/Ladder/Components/LadderSettingsDropdown.cs new file mode 100644 index 0000000000..347e4d91e0 --- /dev/null +++ b/osu.Game.Tournament/Screens/Ladder/Components/LadderSettingsDropdown.cs @@ -0,0 +1,26 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays.Settings; + +namespace osu.Game.Tournament.Screens.Ladder.Components +{ + public class LadderSettingsDropdown : SettingsDropdown + { + protected override OsuDropdown CreateDropdown() => new DropdownControl(); + + private new class DropdownControl : SettingsDropdown.DropdownControl + { + protected override DropdownMenu CreateMenu() => new Menu(); + + private new class Menu : OsuDropdownMenu + { + public Menu() + { + MaxHeight = 200; + } + } + } + } +} diff --git a/osu.Game.Tournament/Screens/Ladder/Components/SettingsTeamDropdown.cs b/osu.Game.Tournament/Screens/Ladder/Components/SettingsTeamDropdown.cs new file mode 100644 index 0000000000..a630e51e44 --- /dev/null +++ b/osu.Game.Tournament/Screens/Ladder/Components/SettingsTeamDropdown.cs @@ -0,0 +1,55 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using osu.Framework.Bindables; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Game.Tournament.Models; + +namespace osu.Game.Tournament.Screens.Ladder.Components +{ + public class SettingsTeamDropdown : LadderSettingsDropdown + { + public SettingsTeamDropdown(BindableList teams) + { + foreach (var t in teams.Prepend(new TournamentTeam())) + add(t); + + teams.CollectionChanged += (_, args) => + { + switch (args.Action) + { + case NotifyCollectionChangedAction.Add: + args.NewItems.Cast().ForEach(add); + break; + + case NotifyCollectionChangedAction.Remove: + args.OldItems.Cast().ForEach(i => Control.RemoveDropdownItem(i)); + break; + } + }; + } + + private readonly List refBindables = new List(); + + private T boundReference(T obj) + where T : IBindable + { + obj = (T)obj.GetBoundCopy(); + refBindables.Add(obj); + return obj; + } + + private void add(TournamentTeam team) + { + Control.AddDropdownItem(team); + boundReference(team.FullName).BindValueChanged(_ => + { + Control.RemoveDropdownItem(team); + Control.AddDropdownItem(team); + }); + } + } +} From fc0821f1942d141854b9ff6823d0762256068a37 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 4 Mar 2020 12:37:42 +0900 Subject: [PATCH 0146/2376] Fix nullref when deleting teams in the ladder screen --- osu.Game.Tournament/Screens/Ladder/LadderScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs b/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs index 8ea366e1b4..86d1081241 100644 --- a/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs +++ b/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs @@ -80,7 +80,7 @@ namespace osu.Game.Tournament.Screens.Ladder break; case NotifyCollectionChangedAction.Remove: - foreach (var p in args.NewItems.Cast()) + foreach (var p in args.OldItems.Cast()) { foreach (var d in MatchesContainer.Where(d => d.Match == p)) d.Expire(); From e9b0770f6418e904686f0744b82475d9db41f026 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Mar 2020 12:44:44 +0900 Subject: [PATCH 0147/2376] Apply suggestions from code review Co-Authored-By: Dan Balasescu --- osu.Game/Graphics/OsuFont.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Graphics/OsuFont.cs b/osu.Game/Graphics/OsuFont.cs index 4dd7bcdf31..12e36871f0 100644 --- a/osu.Game/Graphics/OsuFont.cs +++ b/osu.Game/Graphics/OsuFont.cs @@ -109,27 +109,27 @@ namespace osu.Game.Graphics Light = 300, /// - /// equivalent to weight 400 + /// Equivalent to weight 400. /// Regular = 400, /// - /// equivalent to weight 500 + /// Equivalent to weight 500. /// Medium = 500, /// - /// equivalent to weight 600 + /// Equivalent to weight 600. /// SemiBold = 600, /// - /// equivalent to weight 700 + /// Equivalent to weight 700. /// Bold = 700, /// - /// equivalent to weight 900 + /// Equivalent to weight 900. /// Black = 900 } From aa17a64a0e383e7a6f7b011439a84f2cf1bef191 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 4 Mar 2020 12:47:01 +0900 Subject: [PATCH 0148/2376] Apply missed xmldoc suggestion --- osu.Game/Graphics/OsuFont.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/OsuFont.cs b/osu.Game/Graphics/OsuFont.cs index 12e36871f0..841936d2c5 100644 --- a/osu.Game/Graphics/OsuFont.cs +++ b/osu.Game/Graphics/OsuFont.cs @@ -104,7 +104,7 @@ namespace osu.Game.Graphics public enum FontWeight { /// - /// equivalent to weight 300 + /// Equivalent to weight 300. /// Light = 300, From 6b7144e21bf923b3f72af0a2980bab8c416ba718 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Mar 2020 13:19:52 +0900 Subject: [PATCH 0149/2376] Fix non-matching filename --- .../Screens/Editors/{SeedingEditor.cs => SeedingEditorScreen.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename osu.Game.Tournament/Screens/Editors/{SeedingEditor.cs => SeedingEditorScreen.cs} (100%) diff --git a/osu.Game.Tournament/Screens/Editors/SeedingEditor.cs b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs similarity index 100% rename from osu.Game.Tournament/Screens/Editors/SeedingEditor.cs rename to osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs From dade58adf39afb94677c26936adc9df2a36b3b2b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Mar 2020 13:21:14 +0900 Subject: [PATCH 0150/2376] Fix crash from editor screen test scene when trying to view seeding editor --- osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs index d6fa1a29a8..ca8bce1cca 100644 --- a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs @@ -154,7 +154,7 @@ namespace osu.Game.Tournament.Screens.Editors Text = "Edit seeding results", Action = () => { - sceneManager.SetScreen(new SeedingEditorScreen(team)); + sceneManager?.SetScreen(new SeedingEditorScreen(team)); } }, } From 003aeeb05294889a74d99dc59f63747b320c17e9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Mar 2020 13:26:25 +0900 Subject: [PATCH 0151/2376] Add test scene for seedings editor --- .../Screens/TestSceneSeedingEditorScreen.cs | 25 +++ .../Screens/TestSceneSeedingScreen.cs | 194 +++++++++--------- 2 files changed, 123 insertions(+), 96 deletions(-) create mode 100644 osu.Game.Tournament.Tests/Screens/TestSceneSeedingEditorScreen.cs diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneSeedingEditorScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneSeedingEditorScreen.cs new file mode 100644 index 0000000000..014cd4663b --- /dev/null +++ b/osu.Game.Tournament.Tests/Screens/TestSceneSeedingEditorScreen.cs @@ -0,0 +1,25 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Game.Tournament.Models; +using osu.Game.Tournament.Screens.Editors; + +namespace osu.Game.Tournament.Tests.Screens +{ + public class TestSceneSeedingEditorScreen : LadderTestScene + { + [Cached] + private readonly LadderInfo ladder = new LadderInfo(); + + public TestSceneSeedingEditorScreen() + { + var match = TestSceneSeedingScreen.CreateSampleSeededMatch(); + + Add(new SeedingEditorScreen(match.Team1.Value) + { + Width = 0.85f // create room for control panel + }); + } + } +} diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneSeedingScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneSeedingScreen.cs index eb46d75705..335a6c80a1 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneSeedingScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneSeedingScreen.cs @@ -18,102 +18,7 @@ namespace osu.Game.Tournament.Tests.Screens [BackgroundDependencyLoader] private void load() { - ladder.CurrentMatch.Value = new TournamentMatch - { - Team1 = - { - Value = new TournamentTeam - { - FlagName = { Value = "JP" }, - FullName = { Value = "Japan" }, - LastYearPlacing = { Value = 10 }, - Seed = { Value = "Low" }, - SeedingResults = - { - new SeedingResult - { - Mod = { Value = "NM" }, - Seed = { Value = 10 }, - Beatmaps = - { - new SeedingBeatmap - { - BeatmapInfo = new BeatmapInfo { Metadata = new BeatmapMetadata { Title = "Test Title", Artist = "Test Artist" } }, - Score = 12345672, - Seed = { Value = 24 }, - }, - new SeedingBeatmap - { - BeatmapInfo = new BeatmapInfo { Metadata = new BeatmapMetadata { Title = "Test Title", Artist = "Test Artist" } }, - Score = 1234567, - Seed = { Value = 12 }, - }, - new SeedingBeatmap - { - BeatmapInfo = new BeatmapInfo { Metadata = new BeatmapMetadata { Title = "Test Title", Artist = "Test Artist" } }, - Score = 1234567, - Seed = { Value = 16 }, - } - } - }, - new SeedingResult - { - Mod = { Value = "DT" }, - Seed = { Value = 5 }, - Beatmaps = - { - new SeedingBeatmap - { - BeatmapInfo = new BeatmapInfo { Metadata = new BeatmapMetadata { Title = "Test Title", Artist = "Test Artist" } }, - Score = 234567, - Seed = { Value = 3 }, - }, - new SeedingBeatmap - { - BeatmapInfo = new BeatmapInfo { Metadata = new BeatmapMetadata { Title = "Test Title", Artist = "Test Artist" } }, - Score = 234567, - Seed = { Value = 6 }, - }, - new SeedingBeatmap - { - BeatmapInfo = new BeatmapInfo { Metadata = new BeatmapMetadata { Title = "Test Title", Artist = "Test Artist" } }, - Score = 234567, - Seed = { Value = 12 }, - } - } - } - }, - Players = - { - new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 12 } } }, - new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 16 } } }, - new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 20 } } }, - new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 24 } } }, - new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 30 } } }, - } - } - }, - Team2 = - { - Value = new TournamentTeam - { - FlagName = { Value = "US" }, - FullName = { Value = "United States" }, - Players = - { - new User { Username = "Hello" }, - new User { Username = "Hello" }, - new User { Username = "Hello" }, - new User { Username = "Hello" }, - new User { Username = "Hello" }, - } - } - }, - Round = - { - Value = new TournamentRound { Name = { Value = "Quarterfinals" } } - } - }; + ladder.CurrentMatch.Value = CreateSampleSeededMatch(); Add(new SeedingScreen { @@ -121,5 +26,102 @@ namespace osu.Game.Tournament.Tests.Screens FillAspectRatio = 16 / 9f }); } + + public static TournamentMatch CreateSampleSeededMatch() => new TournamentMatch + { + Team1 = + { + Value = new TournamentTeam + { + FlagName = { Value = "JP" }, + FullName = { Value = "Japan" }, + LastYearPlacing = { Value = 10 }, + Seed = { Value = "Low" }, + SeedingResults = + { + new SeedingResult + { + Mod = { Value = "NM" }, + Seed = { Value = 10 }, + Beatmaps = + { + new SeedingBeatmap + { + BeatmapInfo = new BeatmapInfo { Metadata = new BeatmapMetadata { Title = "Test Title", Artist = "Test Artist" } }, + Score = 12345672, + Seed = { Value = 24 }, + }, + new SeedingBeatmap + { + BeatmapInfo = new BeatmapInfo { Metadata = new BeatmapMetadata { Title = "Test Title", Artist = "Test Artist" } }, + Score = 1234567, + Seed = { Value = 12 }, + }, + new SeedingBeatmap + { + BeatmapInfo = new BeatmapInfo { Metadata = new BeatmapMetadata { Title = "Test Title", Artist = "Test Artist" } }, + Score = 1234567, + Seed = { Value = 16 }, + } + } + }, + new SeedingResult + { + Mod = { Value = "DT" }, + Seed = { Value = 5 }, + Beatmaps = + { + new SeedingBeatmap + { + BeatmapInfo = new BeatmapInfo { Metadata = new BeatmapMetadata { Title = "Test Title", Artist = "Test Artist" } }, + Score = 234567, + Seed = { Value = 3 }, + }, + new SeedingBeatmap + { + BeatmapInfo = new BeatmapInfo { Metadata = new BeatmapMetadata { Title = "Test Title", Artist = "Test Artist" } }, + Score = 234567, + Seed = { Value = 6 }, + }, + new SeedingBeatmap + { + BeatmapInfo = new BeatmapInfo { Metadata = new BeatmapMetadata { Title = "Test Title", Artist = "Test Artist" } }, + Score = 234567, + Seed = { Value = 12 }, + } + } + } + }, + Players = + { + new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 12 } } }, + new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 16 } } }, + new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 20 } } }, + new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 24 } } }, + new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 30 } } }, + } + } + }, + Team2 = + { + Value = new TournamentTeam + { + FlagName = { Value = "US" }, + FullName = { Value = "United States" }, + Players = + { + new User { Username = "Hello" }, + new User { Username = "Hello" }, + new User { Username = "Hello" }, + new User { Username = "Hello" }, + new User { Username = "Hello" }, + } + } + }, + Round = + { + Value = new TournamentRound { Name = { Value = "Quarterfinals" } } + } + }; } } From a3709ae2684d5dae521a6ba6c798505bddc75613 Mon Sep 17 00:00:00 2001 From: mcendu Date: Wed, 4 Mar 2020 13:39:37 +0800 Subject: [PATCH 0152/2376] Also comment out log locations in crash template --- .github/ISSUE_TEMPLATE/02-crash-issues.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/02-crash-issues.md b/.github/ISSUE_TEMPLATE/02-crash-issues.md index 9c3ae33161..ada8de73c0 100644 --- a/.github/ISSUE_TEMPLATE/02-crash-issues.md +++ b/.github/ISSUE_TEMPLATE/02-crash-issues.md @@ -9,8 +9,10 @@ about: Issues regarding crashes or permanent freezes. **osu!lazer version:** **Logs:** + **Computer Specifications:** From f42523352735dff805a37713893107bd6ab7d2cd Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 4 Mar 2020 08:41:21 +0300 Subject: [PATCH 0153/2376] Basic UserCard implementation --- .../Visual/UserInterface/TestSceneUserCard.cs | 35 +++++++++ .../UserInterfaceV2/Users/UserCard.cs | 73 +++++++++++++++++++ .../UserInterfaceV2/Users/UserGridCard.cs | 18 +++++ 3 files changed, 126 insertions(+) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs create mode 100644 osu.Game/Graphics/UserInterfaceV2/Users/UserCard.cs create mode 100644 osu.Game/Graphics/UserInterfaceV2/Users/UserGridCard.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs new file mode 100644 index 0000000000..96a5a1e9ca --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs @@ -0,0 +1,35 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using osu.Framework.Graphics; +using osu.Game.Graphics.UserInterfaceV2.Users; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public class TestSceneUserCard : OsuTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(UserCard), + typeof(UserGridCard), + }; + + public TestSceneUserCard() + { + Add(new UserGridCard(new User + { + Username = @"flyte", + Id = 3103765, + Country = new Country { FlagName = @"JP" }, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg" + }) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + } + } +} diff --git a/osu.Game/Graphics/UserInterfaceV2/Users/UserCard.cs b/osu.Game/Graphics/UserInterfaceV2/Users/UserCard.cs new file mode 100644 index 0000000000..a83a523f9a --- /dev/null +++ b/osu.Game/Graphics/UserInterfaceV2/Users/UserCard.cs @@ -0,0 +1,73 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Game.Overlays; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics.UserInterface; +using osu.Framework.Graphics.Cursor; +using osu.Game.Graphics.Containers; +using osu.Game.Users; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using System.Collections.Generic; +using osu.Framework.Input.Events; + +namespace osu.Game.Graphics.UserInterfaceV2.Users +{ + public abstract class UserCard : OsuHoverContainer, IHasContextMenu + { + [Resolved(canBeNull:true)] + private UserProfileOverlay profileOverlay { get; set; } + + protected override IEnumerable EffectTargets => null; + + public User User { get; } + + public UserCard(User user) + { + if (user == null) + throw new ArgumentNullException(nameof(user)); + + User = user; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Action = () => profileOverlay?.ShowUser(User); + + Masking = true; + BorderColour = colours.GreyVioletLighter; + + Add(new DelayedLoadUnloadWrapper(() => new UserCoverBackground + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + User = User, + }, 300, 5000) + { + RelativeSizeAxes = Axes.Both, + }); + } + + protected override bool OnHover(HoverEvent e) + { + BorderThickness = 2; + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + BorderThickness = 0; + base.OnHoverLost(e); + } + + public MenuItem[] ContextMenuItems => new MenuItem[] + { + new OsuMenuItem("View Profile", MenuItemType.Highlighted, Action), + }; + } +} diff --git a/osu.Game/Graphics/UserInterfaceV2/Users/UserGridCard.cs b/osu.Game/Graphics/UserInterfaceV2/Users/UserGridCard.cs new file mode 100644 index 0000000000..8a322a20f3 --- /dev/null +++ b/osu.Game/Graphics/UserInterfaceV2/Users/UserGridCard.cs @@ -0,0 +1,18 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Users; +using osuTK; + +namespace osu.Game.Graphics.UserInterfaceV2.Users +{ + public class UserGridCard : UserCard + { + public UserGridCard(User user) + : base(user) + { + Size = new Vector2(290, 120); + CornerRadius = 10; + } + } +} From e2ea92e21f7d3a6fad4459a8cfdaadbbcfead410 Mon Sep 17 00:00:00 2001 From: mcendu Date: Wed, 4 Mar 2020 13:51:12 +0800 Subject: [PATCH 0154/2376] Use framework method to convert rad to deg --- osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index 30b9c84538..49c4e7fa45 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Framework.Utils; using osu.Game.Graphics; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; @@ -40,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Mods if (drawable is DrawableSpinner spinner) { if (spinner.Disc.Valid) - spinner.Disc.Rotate(180 / MathF.PI * (float)spinner.Clock.ElapsedFrameTime * 0.03f); + spinner.Disc.Rotate(MathUtils.RadiansToDegrees((float)spinner.Clock.ElapsedFrameTime * 0.03f)); if (!spinner.SpmCounter.IsPresent) spinner.SpmCounter.FadeIn(spinner.HitObject.TimeFadeIn); } From 1b5222f39638ee5fe0f9033902040d4e7646f716 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 4 Mar 2020 08:53:14 +0300 Subject: [PATCH 0155/2376] Baisc UserListCard implementation --- .../Visual/UserInterface/TestSceneUserCard.cs | 41 +++++++++++++++---- .../UserInterfaceV2/Users/UserListCard.cs | 19 +++++++++ 2 files changed, 53 insertions(+), 7 deletions(-) create mode 100644 osu.Game/Graphics/UserInterfaceV2/Users/UserListCard.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs index 96a5a1e9ca..9639aeb49e 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs @@ -4,8 +4,10 @@ using System; using System.Collections.Generic; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Graphics.UserInterfaceV2.Users; using osu.Game.Users; +using osuTK; namespace osu.Game.Tests.Visual.UserInterface { @@ -15,20 +17,45 @@ namespace osu.Game.Tests.Visual.UserInterface { typeof(UserCard), typeof(UserGridCard), + typeof(UserListCard) }; public TestSceneUserCard() { - Add(new UserGridCard(new User - { - Username = @"flyte", - Id = 3103765, - Country = new Country { FlagName = @"JP" }, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg" - }) + Add(new FillFlowContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Spacing = new Vector2(0, 10), + Children = new Drawable[] + { + new UserGridCard(new User + { + Username = @"flyte", + Id = 3103765, + Country = new Country { FlagName = @"JP" }, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg" + }) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + new UserListCard(new User + { + Username = @"peppy", + Id = 2, + Country = new Country { FlagName = @"AU" }, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + IsSupporter = true, + SupportLevel = 3, + }) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + } }); } } diff --git a/osu.Game/Graphics/UserInterfaceV2/Users/UserListCard.cs b/osu.Game/Graphics/UserInterfaceV2/Users/UserListCard.cs new file mode 100644 index 0000000000..c270690086 --- /dev/null +++ b/osu.Game/Graphics/UserInterfaceV2/Users/UserListCard.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.Game.Users; +using osu.Framework.Graphics; + +namespace osu.Game.Graphics.UserInterfaceV2.Users +{ + public class UserListCard : UserCard + { + public UserListCard(User user) + : base(user) + { + RelativeSizeAxes = Axes.X; + Height = 40; + CornerRadius = 6; + } + } +} From b7d34b399d9f6819c3e71d9b20a3718495e89cc1 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 4 Mar 2020 09:10:51 +0300 Subject: [PATCH 0156/2376] Adjust background presentation for UserListCard --- .../Visual/UserInterface/TestSceneUserCard.cs | 5 +++ .../UserInterfaceV2/Users/UserCard.cs | 37 +++++++++++++------ .../UserInterfaceV2/Users/UserListCard.cs | 11 ++++++ 3 files changed, 41 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs index 9639aeb49e..db934fc822 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs @@ -3,9 +3,11 @@ using System; using System.Collections.Generic; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.UserInterfaceV2.Users; +using osu.Game.Overlays; using osu.Game.Users; using osuTK; @@ -20,6 +22,9 @@ namespace osu.Game.Tests.Visual.UserInterface typeof(UserListCard) }; + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); + public TestSceneUserCard() { Add(new FillFlowContainer diff --git a/osu.Game/Graphics/UserInterfaceV2/Users/UserCard.cs b/osu.Game/Graphics/UserInterfaceV2/Users/UserCard.cs index a83a523f9a..9a89c0118b 100644 --- a/osu.Game/Graphics/UserInterfaceV2/Users/UserCard.cs +++ b/osu.Game/Graphics/UserInterfaceV2/Users/UserCard.cs @@ -13,17 +13,17 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using System.Collections.Generic; using osu.Framework.Input.Events; +using osu.Framework.Graphics.Shapes; namespace osu.Game.Graphics.UserInterfaceV2.Users { public abstract class UserCard : OsuHoverContainer, IHasContextMenu { - [Resolved(canBeNull:true)] - private UserProfileOverlay profileOverlay { get; set; } + public User User { get; } protected override IEnumerable EffectTargets => null; - public User User { get; } + protected DelayedLoadUnloadWrapper Background; public UserCard(User user) { @@ -33,23 +33,36 @@ namespace osu.Game.Graphics.UserInterfaceV2.Users User = user; } + [Resolved(canBeNull: true)] + private UserProfileOverlay profileOverlay { get; set; } + [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OsuColour colours, OverlayColourProvider colourProvider) { Action = () => profileOverlay?.ShowUser(User); Masking = true; BorderColour = colours.GreyVioletLighter; - Add(new DelayedLoadUnloadWrapper(() => new UserCoverBackground + AddRange(new Drawable[] { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - User = User, - }, 300, 5000) - { - RelativeSizeAxes = Axes.Both, + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background5 + }, + Background = new DelayedLoadUnloadWrapper(() => new UserCoverBackground + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + User = User, + }, 300, 5000) + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.Both, + } }); } diff --git a/osu.Game/Graphics/UserInterfaceV2/Users/UserListCard.cs b/osu.Game/Graphics/UserInterfaceV2/Users/UserListCard.cs index c270690086..e445404d40 100644 --- a/osu.Game/Graphics/UserInterfaceV2/Users/UserListCard.cs +++ b/osu.Game/Graphics/UserInterfaceV2/Users/UserListCard.cs @@ -3,6 +3,10 @@ using osu.Game.Users; using osu.Framework.Graphics; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Colour; +using osu.Framework.Extensions.Color4Extensions; +using osuTK.Graphics; namespace osu.Game.Graphics.UserInterfaceV2.Users { @@ -15,5 +19,12 @@ namespace osu.Game.Graphics.UserInterfaceV2.Users Height = 40; CornerRadius = 6; } + + [BackgroundDependencyLoader] + private void load() + { + Background.Width = 0.5f; + Background.Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(1), Color4.White); + } } } From 416b9e4e6f38d64113ed35654bf6435be44c0cda Mon Sep 17 00:00:00 2001 From: mcendu Date: Wed, 4 Mar 2020 14:28:18 +0800 Subject: [PATCH 0157/2376] fix beatmap status display --- osu.Game/Overlays/BeatmapSet/AuthorInfo.cs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs b/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs index 446a075ae4..e80034f494 100644 --- a/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs +++ b/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs @@ -59,7 +59,25 @@ namespace osu.Game.Overlays.BeatmapSet if (online.Ranked.HasValue) { - fields.Add(new Field("ranked", online.Ranked.Value, OsuFont.GetFont(weight: FontWeight.Bold))); + string verb = "ranked"; + + switch (online.Status) + { + case BeatmapSetOnlineStatus.Ranked: + verb = "ranked"; + break; + case BeatmapSetOnlineStatus.Approved: + verb = "approved"; + break; + case BeatmapSetOnlineStatus.Qualified: + verb = "qualified"; + break; + case BeatmapSetOnlineStatus.Loved: + verb = "loved"; + break; + } + + fields.Add(new Field(verb, online.Ranked.Value, OsuFont.GetFont(weight: FontWeight.Bold))); } else if (online.LastUpdated.HasValue) { From fbd0dfd71b6c889794197e6e747ef9a99a185273 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=9CNate?= Date: Wed, 4 Mar 2020 14:55:51 +0800 Subject: [PATCH 0158/2376] add blank lines --- osu.Game/Overlays/BeatmapSet/AuthorInfo.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs b/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs index e80034f494..f3bf0b8e5e 100644 --- a/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs +++ b/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs @@ -66,12 +66,15 @@ namespace osu.Game.Overlays.BeatmapSet case BeatmapSetOnlineStatus.Ranked: verb = "ranked"; break; + case BeatmapSetOnlineStatus.Approved: verb = "approved"; break; + case BeatmapSetOnlineStatus.Qualified: verb = "qualified"; break; + case BeatmapSetOnlineStatus.Loved: verb = "loved"; break; From c306d3de2e1839a8cdf058bc0448c80d8caf4ddd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Mar 2020 16:03:43 +0900 Subject: [PATCH 0159/2376] Fix typo in button Co-Authored-By: Dan Balasescu --- osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs index 68003a1c43..e68946aaf2 100644 --- a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs @@ -94,7 +94,7 @@ namespace osu.Game.Tournament.Screens.Editors Origin = Anchor.CentreRight, RelativeSizeAxes = Axes.None, Width = 150, - Text = "Delete SeeingResult", + Text = "Delete result", Action = () => { Expire(); From 6ea3af1951562c7f0720745abb7441b755412dc0 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 4 Mar 2020 10:35:43 +0300 Subject: [PATCH 0160/2376] Implement layout for UserListCard --- .../Visual/UserInterface/TestSceneUserCard.cs | 2 + .../UserInterfaceV2/Users/UserCard.cs | 76 ++++++++++++++++- .../UserInterfaceV2/Users/UserGridCard.cs | 7 ++ .../UserInterfaceV2/Users/UserListCard.cs | 81 ++++++++++++++++++- 4 files changed, 163 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs index db934fc822..8134af8208 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs @@ -55,6 +55,8 @@ namespace osu.Game.Tests.Visual.UserInterface CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", IsSupporter = true, SupportLevel = 3, + IsOnline = false, + LastVisit = DateTimeOffset.Now }) { Anchor = Anchor.Centre, diff --git a/osu.Game/Graphics/UserInterfaceV2/Users/UserCard.cs b/osu.Game/Graphics/UserInterfaceV2/Users/UserCard.cs index 9a89c0118b..916c0c2401 100644 --- a/osu.Game/Graphics/UserInterfaceV2/Users/UserCard.cs +++ b/osu.Game/Graphics/UserInterfaceV2/Users/UserCard.cs @@ -14,6 +14,13 @@ using osu.Framework.Graphics.Containers; using System.Collections.Generic; using osu.Framework.Input.Events; using osu.Framework.Graphics.Shapes; +using JetBrains.Annotations; +using osu.Game.Users.Drawables; +using osuTK; +using osu.Game.Graphics.Sprites; +using osu.Game.Overlays.Profile.Header.Components; +using osu.Framework.Graphics.Sprites; +using osuTK.Graphics; namespace osu.Game.Graphics.UserInterfaceV2.Users { @@ -36,8 +43,11 @@ namespace osu.Game.Graphics.UserInterfaceV2.Users [Resolved(canBeNull: true)] private UserProfileOverlay profileOverlay { get; set; } + [Resolved] + private OsuColour colours { get; set; } + [BackgroundDependencyLoader] - private void load(OsuColour colours, OverlayColourProvider colourProvider) + private void load(OverlayColourProvider colourProvider) { Action = () => profileOverlay?.ShowUser(User); @@ -62,10 +72,72 @@ namespace osu.Game.Graphics.UserInterfaceV2.Users Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, RelativeSizeAxes = Axes.Both, - } + }, + CreateLayout() }); } + [NotNull] + protected abstract Drawable CreateLayout(); + + protected UpdateableAvatar CreateAvatar() => new UpdateableAvatar + { + User = User, + Masking = true, + OpenOnClick = { Value = false } + }; + + protected UpdateableFlag CreateFlag() => new UpdateableFlag(User.Country) + { + Size = new Vector2(39, 26) + }; + + protected OsuSpriteText CreateUsername() => new OsuSpriteText + { + Font = OsuFont.GetFont(size: 20, weight: FontWeight.Bold, italics: true), + Text = User.Username, + }; + + protected SpriteIcon CreateStatusIcon() => new SpriteIcon + { + Icon = FontAwesome.Regular.Circle, + Size = new Vector2(25), + Colour = User.IsOnline ? colours.GreenLight : Color4.Black + }; + + protected FillFlowContainer CreateStatusMessage(bool rightAlignedChildren) + { + var status = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical + }; + + var alignment = rightAlignedChildren ? Anchor.x2 : Anchor.x0; + + if (!User.IsOnline && User.LastVisit.HasValue) + { + status.Add(new TextFlowContainer(t => t.Font = OsuFont.GetFont(size: 15)).With(text => + { + text.Anchor = Anchor.y1 | alignment; + text.Origin = Anchor.y1 | alignment; + text.AutoSizeAxes = Axes.Both; + text.AddText(@"Last seen "); + text.AddText(new DrawableDate(User.LastVisit.Value, italic: false)); + })); + } + + status.Add(new OsuSpriteText + { + Anchor = Anchor.y1 | alignment, + Origin = Anchor.y1 | alignment, + Font = OsuFont.GetFont(size: 17), + Text = User.IsOnline ? @"Online" : @"Offline" + }); + + return status; + } + protected override bool OnHover(HoverEvent e) { BorderThickness = 2; diff --git a/osu.Game/Graphics/UserInterfaceV2/Users/UserGridCard.cs b/osu.Game/Graphics/UserInterfaceV2/Users/UserGridCard.cs index 8a322a20f3..7390d33b4e 100644 --- a/osu.Game/Graphics/UserInterfaceV2/Users/UserGridCard.cs +++ b/osu.Game/Graphics/UserInterfaceV2/Users/UserGridCard.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Users; using osuTK; @@ -14,5 +16,10 @@ namespace osu.Game.Graphics.UserInterfaceV2.Users Size = new Vector2(290, 120); CornerRadius = 10; } + + protected override Drawable CreateLayout() => new Container + { + RelativeSizeAxes = Axes.Both, + }; } } diff --git a/osu.Game/Graphics/UserInterfaceV2/Users/UserListCard.cs b/osu.Game/Graphics/UserInterfaceV2/Users/UserListCard.cs index e445404d40..9be6ba9c65 100644 --- a/osu.Game/Graphics/UserInterfaceV2/Users/UserListCard.cs +++ b/osu.Game/Graphics/UserInterfaceV2/Users/UserListCard.cs @@ -7,6 +7,9 @@ using osu.Framework.Allocation; using osu.Framework.Graphics.Colour; using osu.Framework.Extensions.Color4Extensions; using osuTK.Graphics; +using osu.Framework.Graphics.Containers; +using osuTK; +using osu.Game.Overlays.Profile.Header.Components; namespace osu.Game.Graphics.UserInterfaceV2.Users { @@ -24,7 +27,83 @@ namespace osu.Game.Graphics.UserInterfaceV2.Users private void load() { Background.Width = 0.5f; - Background.Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(1), Color4.White); + Background.Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(1), Color4.White.Opacity(User.IsOnline ? 0.6f : 0.7f)); + } + + protected override Drawable CreateLayout() + { + FillFlowContainer details; + + var layout = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + details = new FillFlowContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), + Children = new Drawable[] + { + CreateAvatar().With(avatar => + { + avatar.Anchor = Anchor.CentreLeft; + avatar.Origin = Anchor.CentreLeft; + avatar.Size = new Vector2(40); + avatar.Masking = false; + }), + CreateFlag().With(flag => + { + flag.Anchor = Anchor.CentreLeft; + flag.Origin = Anchor.CentreLeft; + }), + CreateUsername().With(username => + { + username.Anchor = Anchor.CentreLeft; + username.Origin = Anchor.CentreLeft; + }) + } + }, + new FillFlowContainer + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), + Margin = new MarginPadding { Right = 10 }, + Children = new Drawable[] + { + CreateStatusIcon().With(icon => + { + icon.Anchor = Anchor.CentreRight; + icon.Origin = Anchor.CentreRight; + }), + CreateStatusMessage(true).With(message => + { + message.Anchor = Anchor.CentreRight; + message.Origin = Anchor.CentreRight; + }) + } + } + } + }; + + if (User.IsSupporter) + { + details.Add(new SupporterIcon + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Height = 20, + SupportLevel = User.SupportLevel + }); + } + + return layout; } } } From 7464a486d980a4180aa86f3404a77e8234688bcd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Mar 2020 16:35:45 +0900 Subject: [PATCH 0161/2376] Fix DummyWorkingBeatmap's track completion attempting to change game-wide beatmap --- osu.Game/OsuGame.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 5781a7fbc4..69d5a9a583 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -428,11 +428,17 @@ namespace osu.Game } } - private void currentTrackCompleted() => Schedule(() => + private void currentTrackCompleted() { - if (!Beatmap.Value.Track.Looping && !Beatmap.Disabled) - musicController.NextTrack(); - }); + if (Beatmap.Value is DummyWorkingBeatmap) + return; + + Schedule(() => + { + if (Beatmap.Value.Track.Looping && !Beatmap.Disabled) + musicController.NextTrack(); + }); + } #endregion From eaa77bce142cb905b8be09fdf5ecf2e5bd2e8242 Mon Sep 17 00:00:00 2001 From: mcendu Date: Wed, 4 Mar 2020 16:43:35 +0800 Subject: [PATCH 0162/2376] Use ToString().ToLowerInvariant() * https://github.com/ppy/osu/pull/8128#issuecomment-594360083 --- osu.Game/Overlays/BeatmapSet/AuthorInfo.cs | 23 +--------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs b/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs index f3bf0b8e5e..31c1439c8f 100644 --- a/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs +++ b/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs @@ -59,28 +59,7 @@ namespace osu.Game.Overlays.BeatmapSet if (online.Ranked.HasValue) { - string verb = "ranked"; - - switch (online.Status) - { - case BeatmapSetOnlineStatus.Ranked: - verb = "ranked"; - break; - - case BeatmapSetOnlineStatus.Approved: - verb = "approved"; - break; - - case BeatmapSetOnlineStatus.Qualified: - verb = "qualified"; - break; - - case BeatmapSetOnlineStatus.Loved: - verb = "loved"; - break; - } - - fields.Add(new Field(verb, online.Ranked.Value, OsuFont.GetFont(weight: FontWeight.Bold))); + fields.Add(new Field(online.Status.ToString().ToLowerInvariant(), online.Ranked.Value, OsuFont.GetFont(weight: FontWeight.Bold))); } else if (online.LastUpdated.HasValue) { From 184d10a75a9f0c4cdc839d8b62d1540ac2650781 Mon Sep 17 00:00:00 2001 From: recapitalverb <41869184+recapitalverb@users.noreply.github.com> Date: Wed, 4 Mar 2020 15:45:22 +0700 Subject: [PATCH 0163/2376] Revert "Reduce social overlay/direct overlay paddings" This reverts commit cb1129218135bd2b1e2d3a287d381c13e08725d6. --- osu.Game/Overlays/SearchableList/SearchableListOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs b/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs index 72796df6d5..0783c64c20 100644 --- a/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs +++ b/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs @@ -15,7 +15,7 @@ namespace osu.Game.Overlays.SearchableList { public abstract class SearchableListOverlay : FullscreenOverlay { - public const float WIDTH_PADDING = 10; + public const float WIDTH_PADDING = 80; protected SearchableListOverlay(OverlayColourScheme colourScheme) : base(colourScheme) From a1dc59500699d1d83875c58a48bf9b0e489ec187 Mon Sep 17 00:00:00 2001 From: recapitalverb <41869184+recapitalverb@users.noreply.github.com> Date: Wed, 4 Mar 2020 15:46:35 +0700 Subject: [PATCH 0164/2376] Change scroll container padding --- osu.Game/Overlays/SearchableList/SearchableListOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs b/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs index 0783c64c20..d6174e0733 100644 --- a/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs +++ b/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs @@ -80,7 +80,7 @@ namespace osu.Game.Overlays.SearchableList { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Horizontal = WIDTH_PADDING, Bottom = 50 }, + Padding = new MarginPadding { Horizontal = 10, Bottom = 50 }, Direction = FillDirection.Vertical, }, }, From 15e47d843295e1d22d9f13b5e72c546b362caed7 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 4 Mar 2020 12:20:49 +0300 Subject: [PATCH 0165/2376] Implement layout for UserGridCard --- .../Visual/UserInterface/TestSceneUserCard.cs | 47 ++++++--- .../UserInterfaceV2/Users/UserCard.cs | 1 - .../UserInterfaceV2/Users/UserGridCard.cs | 95 ++++++++++++++++++- .../UserInterfaceV2/Users/UserListCard.cs | 1 - 4 files changed, 126 insertions(+), 18 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs index 8134af8208..2966ee242a 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs @@ -36,16 +36,33 @@ namespace osu.Game.Tests.Visual.UserInterface Spacing = new Vector2(0, 10), Children = new Drawable[] { - new UserGridCard(new User + new FillFlowContainer { - Username = @"flyte", - Id = 3103765, - Country = new Country { FlagName = @"JP" }, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg" - }) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10), + Children = new Drawable[] + { + new UserGridCard(new User + { + Username = @"flyte", + Id = 3103765, + Country = new Country { FlagName = @"JP" }, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg", + IsOnline = true, + IsSupporter = true, + SupportLevel = 3, + }), + new UserGridCard(new User + { + Username = @"Evast", + Id = 8195163, + Country = new Country { FlagName = @"BY" }, + CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", + IsOnline = false, + LastVisit = DateTimeOffset.Now + }) + } }, new UserListCard(new User { @@ -57,11 +74,15 @@ namespace osu.Game.Tests.Visual.UserInterface SupportLevel = 3, IsOnline = false, LastVisit = DateTimeOffset.Now - }) + }), + new UserListCard(new User { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - } + Username = @"chocomint", + Id = 124493, + Country = new Country { FlagName = @"KR" }, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c5.jpg", + IsOnline = true, + }), } }); } diff --git a/osu.Game/Graphics/UserInterfaceV2/Users/UserCard.cs b/osu.Game/Graphics/UserInterfaceV2/Users/UserCard.cs index 916c0c2401..dc26518692 100644 --- a/osu.Game/Graphics/UserInterfaceV2/Users/UserCard.cs +++ b/osu.Game/Graphics/UserInterfaceV2/Users/UserCard.cs @@ -83,7 +83,6 @@ namespace osu.Game.Graphics.UserInterfaceV2.Users protected UpdateableAvatar CreateAvatar() => new UpdateableAvatar { User = User, - Masking = true, OpenOnClick = { Value = false } }; diff --git a/osu.Game/Graphics/UserInterfaceV2/Users/UserGridCard.cs b/osu.Game/Graphics/UserInterfaceV2/Users/UserGridCard.cs index 7390d33b4e..b6e704e901 100644 --- a/osu.Game/Graphics/UserInterfaceV2/Users/UserGridCard.cs +++ b/osu.Game/Graphics/UserInterfaceV2/Users/UserGridCard.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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Overlays.Profile.Header.Components; using osu.Game.Users; using osuTK; @@ -10,6 +12,8 @@ namespace osu.Game.Graphics.UserInterfaceV2.Users { public class UserGridCard : UserCard { + private const int margin = 10; + public UserGridCard(User user) : base(user) { @@ -17,9 +21,94 @@ namespace osu.Game.Graphics.UserInterfaceV2.Users CornerRadius = 10; } - protected override Drawable CreateLayout() => new Container + [BackgroundDependencyLoader] + private void load() { - RelativeSizeAxes = Axes.Both, - }; + Background.FadeTo(User.IsOnline ? 0.6f : 0.7f); + } + + protected override Drawable CreateLayout() + { + FillFlowContainer details; + + var layout = new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(margin), + Child = new GridContainer + { + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension() + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension() + }, + Content = new[] + { + new Drawable[] + { + CreateAvatar().With(avatar => + { + avatar.Size = new Vector2(60); + avatar.Margin = new MarginPadding { Bottom = margin }; + avatar.Masking = true; + avatar.CornerRadius = 6; + }), + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 7), + Margin = new MarginPadding { Left = margin }, + Children = new Drawable[] + { + details = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(6), + Children = new Drawable[] + { + CreateFlag(), + } + }, + CreateUsername(), + } + } + }, + new Drawable[] + { + CreateStatusIcon().With(icon => + { + icon.Anchor = Anchor.Centre; + icon.Origin = Anchor.Centre; + }), + CreateStatusMessage(false).With(message => + { + message.Anchor = Anchor.CentreLeft; + message.Origin = Anchor.CentreLeft; + message.Margin = new MarginPadding { Left = margin }; + }) + } + } + } + }; + + if (User.IsSupporter) + { + details.Add(new SupporterIcon + { + Height = 26, + SupportLevel = User.SupportLevel + }); + } + + return layout; + } } } diff --git a/osu.Game/Graphics/UserInterfaceV2/Users/UserListCard.cs b/osu.Game/Graphics/UserInterfaceV2/Users/UserListCard.cs index 9be6ba9c65..685556035e 100644 --- a/osu.Game/Graphics/UserInterfaceV2/Users/UserListCard.cs +++ b/osu.Game/Graphics/UserInterfaceV2/Users/UserListCard.cs @@ -53,7 +53,6 @@ namespace osu.Game.Graphics.UserInterfaceV2.Users avatar.Anchor = Anchor.CentreLeft; avatar.Origin = Anchor.CentreLeft; avatar.Size = new Vector2(40); - avatar.Masking = false; }), CreateFlag().With(flag => { From 8a437e1b5452bb14f5be2f1c66c20dd1af616467 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 4 Mar 2020 12:42:21 +0300 Subject: [PATCH 0166/2376] Add ability to send pm via context menu --- .../Visual/UserInterface/TestSceneUserCard.cs | 107 +++++++++--------- .../UserInterfaceV2/Users/UserCard.cs | 17 ++- 2 files changed, 70 insertions(+), 54 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs index 2966ee242a..0d5967b5a8 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterfaceV2.Users; using osu.Game.Overlays; using osu.Game.Users; @@ -27,62 +28,66 @@ namespace osu.Game.Tests.Visual.UserInterface public TestSceneUserCard() { - Add(new FillFlowContainer + Add(new OsuContextMenuContainer { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Spacing = new Vector2(0, 10), - Children = new Drawable[] + RelativeSizeAxes = Axes.Both, + Child = new FillFlowContainer { - new FillFlowContainer + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Spacing = new Vector2(0, 10), + Children = new Drawable[] { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10), - Children = new Drawable[] + new FillFlowContainer { - new UserGridCard(new User + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10), + Children = new Drawable[] { - Username = @"flyte", - Id = 3103765, - Country = new Country { FlagName = @"JP" }, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg", - IsOnline = true, - IsSupporter = true, - SupportLevel = 3, - }), - new UserGridCard(new User - { - Username = @"Evast", - Id = 8195163, - Country = new Country { FlagName = @"BY" }, - CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", - IsOnline = false, - LastVisit = DateTimeOffset.Now - }) - } - }, - new UserListCard(new User - { - Username = @"peppy", - Id = 2, - Country = new Country { FlagName = @"AU" }, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", - IsSupporter = true, - SupportLevel = 3, - IsOnline = false, - LastVisit = DateTimeOffset.Now - }), - new UserListCard(new User - { - Username = @"chocomint", - Id = 124493, - Country = new Country { FlagName = @"KR" }, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c5.jpg", - IsOnline = true, - }), + new UserGridCard(new User + { + Username = @"flyte", + Id = 3103765, + Country = new Country { FlagName = @"JP" }, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg", + IsOnline = true, + IsSupporter = true, + SupportLevel = 3, + }), + new UserGridCard(new User + { + Username = @"Evast", + Id = 8195163, + Country = new Country { FlagName = @"BY" }, + CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", + IsOnline = false, + LastVisit = DateTimeOffset.Now + }) + } + }, + new UserListCard(new User + { + Username = @"peppy", + Id = 2, + Country = new Country { FlagName = @"AU" }, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + IsSupporter = true, + SupportLevel = 3, + IsOnline = false, + LastVisit = DateTimeOffset.Now + }), + new UserListCard(new User + { + Username = @"chocomint", + Id = 124493, + Country = new Country { FlagName = @"KR" }, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c5.jpg", + IsOnline = true, + }), + } } }); } diff --git a/osu.Game/Graphics/UserInterfaceV2/Users/UserCard.cs b/osu.Game/Graphics/UserInterfaceV2/Users/UserCard.cs index dc26518692..4808990c70 100644 --- a/osu.Game/Graphics/UserInterfaceV2/Users/UserCard.cs +++ b/osu.Game/Graphics/UserInterfaceV2/Users/UserCard.cs @@ -18,9 +18,9 @@ using JetBrains.Annotations; using osu.Game.Users.Drawables; using osuTK; using osu.Game.Graphics.Sprites; -using osu.Game.Overlays.Profile.Header.Components; using osu.Framework.Graphics.Sprites; using osuTK.Graphics; +using osu.Game.Online.Chat; namespace osu.Game.Graphics.UserInterfaceV2.Users { @@ -32,7 +32,7 @@ namespace osu.Game.Graphics.UserInterfaceV2.Users protected DelayedLoadUnloadWrapper Background; - public UserCard(User user) + protected UserCard(User user) { if (user == null) throw new ArgumentNullException(nameof(user)); @@ -43,6 +43,12 @@ namespace osu.Game.Graphics.UserInterfaceV2.Users [Resolved(canBeNull: true)] private UserProfileOverlay profileOverlay { get; set; } + [Resolved(canBeNull: true)] + private ChannelManager channelManager { get; set; } + + [Resolved(canBeNull: true)] + private ChatOverlay chatOverlay { get; set; } + [Resolved] private OsuColour colours { get; set; } @@ -54,7 +60,7 @@ namespace osu.Game.Graphics.UserInterfaceV2.Users Masking = true; BorderColour = colours.GreyVioletLighter; - AddRange(new Drawable[] + AddRange(new[] { new Box { @@ -152,6 +158,11 @@ namespace osu.Game.Graphics.UserInterfaceV2.Users public MenuItem[] ContextMenuItems => new MenuItem[] { new OsuMenuItem("View Profile", MenuItemType.Highlighted, Action), + new OsuMenuItem("Send message", MenuItemType.Standard, () => + { + channelManager?.OpenPrivateChannel(User); + chatOverlay?.Show(); + }) }; } } From bac35b2a68f15a0abe079f94a17883e6c040ef68 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Mar 2020 19:09:52 +0900 Subject: [PATCH 0167/2376] Handle beatmap track changing in a saner way --- osu.Game/OsuGame.cs | 39 ++++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 69d5a9a583..0be9e6cdaa 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -396,18 +396,27 @@ namespace osu.Game private void beatmapChanged(ValueChangedEvent beatmap) { - var nextBeatmap = beatmap.NewValue; - if (nextBeatmap?.Track != null) - nextBeatmap.Track.Completed += currentTrackCompleted; - - var oldBeatmap = beatmap.OldValue; - if (oldBeatmap?.Track != null) - oldBeatmap.Track.Completed -= currentTrackCompleted; + beatmap.OldValue?.CancelAsyncLoad(); updateModDefaults(); - oldBeatmap?.CancelAsyncLoad(); - nextBeatmap?.BeginAsyncLoad(); + var newBeatmap = beatmap.NewValue; + + if (newBeatmap != null) + { + newBeatmap.Track.Completed += () => Scheduler.AddOnce(() => trackCompleted(newBeatmap)); + newBeatmap.BeginAsyncLoad(); + } + + void trackCompleted(WorkingBeatmap b) + { + // the source of track completion is the audio thread, so the beatmap may have changed before a firing. + if (Beatmap.Value != b) + return; + + if (Beatmap.Value.Track.Looping && !Beatmap.Disabled) + musicController.NextTrack(); + } } private void modsChanged(ValueChangedEvent> mods) @@ -428,18 +437,6 @@ namespace osu.Game } } - private void currentTrackCompleted() - { - if (Beatmap.Value is DummyWorkingBeatmap) - return; - - Schedule(() => - { - if (Beatmap.Value.Track.Looping && !Beatmap.Disabled) - musicController.NextTrack(); - }); - } - #endregion private ScheduledDelegate performFromMainMenuTask; From 38d91ccd0d20dcd4f9f4e016f03da00da2ba8152 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Mar 2020 19:07:34 +0900 Subject: [PATCH 0168/2376] Add comment regarding no-longer-required schedule --- osu.Game/OsuGameBase.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index a890331f05..67aa4a8d4d 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -211,6 +211,10 @@ namespace osu.Game Audio.Tracks.AddAdjustment(AdjustableProperty.Volume, new BindableDouble(0.8)); Beatmap = new NonNullableBindable(defaultBeatmap); + + // ScheduleAfterChildren is safety against something in the current frame accessing the previous beatmap's track + // and potentially causing a reload of it after just unloading. + // Note that the reason for this being added *has* been resolved, so it may be feasible to remover this if required. Beatmap.BindValueChanged(b => ScheduleAfterChildren(() => { // compare to last beatmap as sometimes the two may share a track representation (optimisation, see WorkingBeatmap.TransferTo) From a62550b3239791678ab70613ad8ac522101e5788 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Mar 2020 20:14:18 +0900 Subject: [PATCH 0169/2376] Reapply filters on next change after a forced beatmap display --- osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs | 7 +++++++ osu.Game/Screens/Select/Carousel/CarouselItem.cs | 2 +- osu.Game/Screens/Select/FilterCriteria.cs | 2 ++ osu.Game/Screens/Select/SongSelect.cs | 7 +++++-- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index 2ffb73f226..d78d407748 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -25,6 +25,13 @@ namespace osu.Game.Screens.Select.Carousel { base.Filter(criteria); + if (Beatmap.Equals(criteria.SelectedBeatmap)) + { + // bypass filtering for selected beatmap + Filtered.Value = false; + return; + } + bool match = criteria.Ruleset == null || Beatmap.RulesetID == criteria.Ruleset.ID || diff --git a/osu.Game/Screens/Select/Carousel/CarouselItem.cs b/osu.Game/Screens/Select/Carousel/CarouselItem.cs index 1108b72bd2..79c1a4cb6b 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselItem.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselItem.cs @@ -16,7 +16,7 @@ namespace osu.Game.Screens.Select.Carousel /// /// This item is not in a hidden state. /// - public bool Visible => State.Value == CarouselItemState.Selected || (State.Value != CarouselItemState.Collapsed && !Filtered.Value); + public bool Visible => State.Value != CarouselItemState.Collapsed && !Filtered.Value; public virtual List Drawables { diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index 9fa57af01d..a5c4e2961d 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -15,6 +15,8 @@ namespace osu.Game.Screens.Select public GroupMode Group; public SortMode Sort; + public BeatmapInfo SelectedBeatmap; + public OptionalRange StarDifficulty; public OptionalRange ApproachRate; public OptionalRange DrainRate; diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 67626d1e4f..90ad7a7fdd 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -392,8 +392,11 @@ namespace osu.Game.Screens.Select } // Even if a ruleset mismatch was not the cause (ie. a text filter is applied), - // we still want to forcefully show the new beatmap, bypassing filters. - Carousel.SelectBeatmap(e.NewValue.BeatmapInfo); + // we still want to temporarily show the new beatmap, bypassing filters. + // This will be undone the next time the user changes the filter. + var criteria = FilterControl.CreateCriteria(); + criteria.SelectedBeatmap = e.NewValue.BeatmapInfo; + Carousel.Filter(criteria); } } From 6631b074421aabf4e50f5a061c10475864a8244a Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 4 Mar 2020 14:58:15 +0300 Subject: [PATCH 0170/2376] Refactor to replace existing panels --- .../Online/TestSceneAccountCreationOverlay.cs | 2 +- .../Visual/Online/TestSceneSocialOverlay.cs | 5 +- .../Visual/Online/TestSceneUserPanel.cs | 25 +- .../Visual/UserInterface/TestSceneUserCard.cs | 95 ------ .../Screens/Editors/TeamEditorScreen.cs | 2 +- .../UserInterfaceV2/Users/UserCard.cs | 168 ---------- .../Sections/General/LoginSettings.cs | 2 +- osu.Game/Overlays/Social/SocialGridPanel.cs | 16 - osu.Game/Overlays/Social/SocialListPanel.cs | 17 - osu.Game/Overlays/Social/SocialPanel.cs | 61 ---- osu.Game/Overlays/SocialOverlay.cs | 13 +- .../UserGridPanel.cs} | 11 +- .../UserListPanel.cs} | 9 +- osu.Game/Users/UserPanel.cs | 305 ++++++++---------- osu.Game/Users/UserStatus.cs | 4 +- 15 files changed, 175 insertions(+), 560 deletions(-) delete mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs delete mode 100644 osu.Game/Graphics/UserInterfaceV2/Users/UserCard.cs delete mode 100644 osu.Game/Overlays/Social/SocialGridPanel.cs delete mode 100644 osu.Game/Overlays/Social/SocialListPanel.cs delete mode 100644 osu.Game/Overlays/Social/SocialPanel.cs rename osu.Game/{Graphics/UserInterfaceV2/Users/UserGridCard.cs => Users/UserGridPanel.cs} (93%) rename osu.Game/{Graphics/UserInterfaceV2/Users/UserListCard.cs => Users/UserListPanel.cs} (94%) diff --git a/osu.Game.Tests/Visual/Online/TestSceneAccountCreationOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneAccountCreationOverlay.cs index 31eab7f74e..a53a818065 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneAccountCreationOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneAccountCreationOverlay.cs @@ -54,7 +54,7 @@ namespace osu.Game.Tests.Visual.Online API.Logout(); localUser = API.LocalUser.GetBoundCopy(); - localUser.BindValueChanged(user => { userPanelArea.Child = new UserPanel(user.NewValue) { Width = 200 }; }, true); + localUser.BindValueChanged(user => { userPanelArea.Child = new UserGridPanel(user.NewValue) { Width = 200 }; }, true); AddStep("logout", API.Logout); } diff --git a/osu.Game.Tests/Visual/Online/TestSceneSocialOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneSocialOverlay.cs index dbd7544b38..24341cbd05 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneSocialOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneSocialOverlay.cs @@ -18,10 +18,9 @@ namespace osu.Game.Tests.Visual.Online public override IReadOnlyList RequiredTypes => new[] { typeof(UserPanel), - typeof(SocialPanel), typeof(FilterControl), - typeof(SocialGridPanel), - typeof(SocialListPanel) + typeof(UserGridPanel), + typeof(UserListPanel) }; public TestSceneSocialOverlay() diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs index 54f06d6ad2..f4c96ac251 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -13,6 +15,13 @@ namespace osu.Game.Tests.Visual.Online [TestFixture] public class TestSceneUserPanel : OsuTestScene { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(UserPanel), + typeof(UserListPanel), + typeof(UserGridPanel), + }; + private readonly UserPanel peppy; public TestSceneUserPanel() @@ -23,18 +32,19 @@ namespace osu.Game.Tests.Visual.Online { Anchor = Anchor.Centre, Origin = Anchor.Centre, - AutoSizeAxes = Axes.Both, + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, Spacing = new Vector2(10f), Children = new[] { - flyte = new UserPanel(new User + flyte = new UserGridPanel(new User { Username = @"flyte", Id = 3103765, Country = new Country { FlagName = @"JP" }, CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg" }) { Width = 300 }, - peppy = new UserPanel(new User + peppy = new UserGridPanel(new User { Username = @"peppy", Id = 2, @@ -43,6 +53,15 @@ namespace osu.Game.Tests.Visual.Online IsSupporter = true, SupportLevel = 3, }) { Width = 300 }, + new UserListPanel(new User + { + Username = @"Evast", + Id = 8195163, + Country = new Country { FlagName = @"BY" }, + CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", + IsOnline = false, + LastVisit = DateTimeOffset.Now + }) }, }); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs deleted file mode 100644 index 0d5967b5a8..0000000000 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneUserCard.cs +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Collections.Generic; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Graphics.Cursor; -using osu.Game.Graphics.UserInterfaceV2.Users; -using osu.Game.Overlays; -using osu.Game.Users; -using osuTK; - -namespace osu.Game.Tests.Visual.UserInterface -{ - public class TestSceneUserCard : OsuTestScene - { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(UserCard), - typeof(UserGridCard), - typeof(UserListCard) - }; - - [Cached] - private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); - - public TestSceneUserCard() - { - Add(new OsuContextMenuContainer - { - RelativeSizeAxes = Axes.Both, - Child = new FillFlowContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Spacing = new Vector2(0, 10), - Children = new Drawable[] - { - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10), - Children = new Drawable[] - { - new UserGridCard(new User - { - Username = @"flyte", - Id = 3103765, - Country = new Country { FlagName = @"JP" }, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg", - IsOnline = true, - IsSupporter = true, - SupportLevel = 3, - }), - new UserGridCard(new User - { - Username = @"Evast", - Id = 8195163, - Country = new Country { FlagName = @"BY" }, - CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", - IsOnline = false, - LastVisit = DateTimeOffset.Now - }) - } - }, - new UserListCard(new User - { - Username = @"peppy", - Id = 2, - Country = new Country { FlagName = @"AU" }, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", - IsSupporter = true, - SupportLevel = 3, - IsOnline = false, - LastVisit = DateTimeOffset.Now - }), - new UserListCard(new User - { - Username = @"chocomint", - Id = 124493, - Country = new Country { FlagName = @"KR" }, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c5.jpg", - IsOnline = true, - }), - } - } - }); - } - } -} diff --git a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs index ca8bce1cca..bbbc948811 100644 --- a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs @@ -311,7 +311,7 @@ namespace osu.Game.Tournament.Screens.Editors private void updatePanel() { - drawableContainer.Child = new UserPanel(user) { Width = 300 }; + drawableContainer.Child = new UserGridPanel(user) { Width = 300 }; } } } diff --git a/osu.Game/Graphics/UserInterfaceV2/Users/UserCard.cs b/osu.Game/Graphics/UserInterfaceV2/Users/UserCard.cs deleted file mode 100644 index 4808990c70..0000000000 --- a/osu.Game/Graphics/UserInterfaceV2/Users/UserCard.cs +++ /dev/null @@ -1,168 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using osu.Framework.Allocation; -using osu.Game.Overlays; -using osu.Framework.Graphics.UserInterface; -using osu.Game.Graphics.UserInterface; -using osu.Framework.Graphics.Cursor; -using osu.Game.Graphics.Containers; -using osu.Game.Users; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using System.Collections.Generic; -using osu.Framework.Input.Events; -using osu.Framework.Graphics.Shapes; -using JetBrains.Annotations; -using osu.Game.Users.Drawables; -using osuTK; -using osu.Game.Graphics.Sprites; -using osu.Framework.Graphics.Sprites; -using osuTK.Graphics; -using osu.Game.Online.Chat; - -namespace osu.Game.Graphics.UserInterfaceV2.Users -{ - public abstract class UserCard : OsuHoverContainer, IHasContextMenu - { - public User User { get; } - - protected override IEnumerable EffectTargets => null; - - protected DelayedLoadUnloadWrapper Background; - - protected UserCard(User user) - { - if (user == null) - throw new ArgumentNullException(nameof(user)); - - User = user; - } - - [Resolved(canBeNull: true)] - private UserProfileOverlay profileOverlay { get; set; } - - [Resolved(canBeNull: true)] - private ChannelManager channelManager { get; set; } - - [Resolved(canBeNull: true)] - private ChatOverlay chatOverlay { get; set; } - - [Resolved] - private OsuColour colours { get; set; } - - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) - { - Action = () => profileOverlay?.ShowUser(User); - - Masking = true; - BorderColour = colours.GreyVioletLighter; - - AddRange(new[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background5 - }, - Background = new DelayedLoadUnloadWrapper(() => new UserCoverBackground - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - User = User, - }, 300, 5000) - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - RelativeSizeAxes = Axes.Both, - }, - CreateLayout() - }); - } - - [NotNull] - protected abstract Drawable CreateLayout(); - - protected UpdateableAvatar CreateAvatar() => new UpdateableAvatar - { - User = User, - OpenOnClick = { Value = false } - }; - - protected UpdateableFlag CreateFlag() => new UpdateableFlag(User.Country) - { - Size = new Vector2(39, 26) - }; - - protected OsuSpriteText CreateUsername() => new OsuSpriteText - { - Font = OsuFont.GetFont(size: 20, weight: FontWeight.Bold, italics: true), - Text = User.Username, - }; - - protected SpriteIcon CreateStatusIcon() => new SpriteIcon - { - Icon = FontAwesome.Regular.Circle, - Size = new Vector2(25), - Colour = User.IsOnline ? colours.GreenLight : Color4.Black - }; - - protected FillFlowContainer CreateStatusMessage(bool rightAlignedChildren) - { - var status = new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical - }; - - var alignment = rightAlignedChildren ? Anchor.x2 : Anchor.x0; - - if (!User.IsOnline && User.LastVisit.HasValue) - { - status.Add(new TextFlowContainer(t => t.Font = OsuFont.GetFont(size: 15)).With(text => - { - text.Anchor = Anchor.y1 | alignment; - text.Origin = Anchor.y1 | alignment; - text.AutoSizeAxes = Axes.Both; - text.AddText(@"Last seen "); - text.AddText(new DrawableDate(User.LastVisit.Value, italic: false)); - })); - } - - status.Add(new OsuSpriteText - { - Anchor = Anchor.y1 | alignment, - Origin = Anchor.y1 | alignment, - Font = OsuFont.GetFont(size: 17), - Text = User.IsOnline ? @"Online" : @"Offline" - }); - - return status; - } - - protected override bool OnHover(HoverEvent e) - { - BorderThickness = 2; - return base.OnHover(e); - } - - protected override void OnHoverLost(HoverLostEvent e) - { - BorderThickness = 0; - base.OnHoverLost(e); - } - - public MenuItem[] ContextMenuItems => new MenuItem[] - { - new OsuMenuItem("View Profile", MenuItemType.Highlighted, Action), - new OsuMenuItem("Send message", MenuItemType.Standard, () => - { - channelManager?.OpenPrivateChannel(User); - chatOverlay?.Show(); - }) - }; - } -} diff --git a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs index bf0e073350..3ab64786a2 100644 --- a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs @@ -143,7 +143,7 @@ namespace osu.Game.Overlays.Settings.Sections.General }, }, }, - panel = new UserPanel(api.LocalUser.Value) + panel = new UserGridPanel(api.LocalUser.Value) { RelativeSizeAxes = Axes.X, Action = RequestHide diff --git a/osu.Game/Overlays/Social/SocialGridPanel.cs b/osu.Game/Overlays/Social/SocialGridPanel.cs deleted file mode 100644 index 6f707d640b..0000000000 --- a/osu.Game/Overlays/Social/SocialGridPanel.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Users; - -namespace osu.Game.Overlays.Social -{ - public class SocialGridPanel : SocialPanel - { - public SocialGridPanel(User user) - : base(user) - { - Width = 300; - } - } -} diff --git a/osu.Game/Overlays/Social/SocialListPanel.cs b/osu.Game/Overlays/Social/SocialListPanel.cs deleted file mode 100644 index 1ba91e9204..0000000000 --- a/osu.Game/Overlays/Social/SocialListPanel.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Graphics; -using osu.Game.Users; - -namespace osu.Game.Overlays.Social -{ - public class SocialListPanel : SocialPanel - { - public SocialListPanel(User user) - : base(user) - { - RelativeSizeAxes = Axes.X; - } - } -} diff --git a/osu.Game/Overlays/Social/SocialPanel.cs b/osu.Game/Overlays/Social/SocialPanel.cs deleted file mode 100644 index 555527670a..0000000000 --- a/osu.Game/Overlays/Social/SocialPanel.cs +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osuTK; -using osuTK.Graphics; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Effects; -using osu.Framework.Input.Events; -using osu.Game.Users; - -namespace osu.Game.Overlays.Social -{ - public class SocialPanel : UserPanel - { - private const double hover_transition_time = 400; - - public SocialPanel(User user) - : base(user) - { - } - - private readonly EdgeEffectParameters edgeEffectNormal = new EdgeEffectParameters - { - Type = EdgeEffectType.Shadow, - Offset = new Vector2(0f, 1f), - Radius = 2f, - Colour = Color4.Black.Opacity(0.25f), - }; - - private readonly EdgeEffectParameters edgeEffectHovered = new EdgeEffectParameters - { - Type = EdgeEffectType.Shadow, - Offset = new Vector2(0f, 5f), - Radius = 10f, - Colour = Color4.Black.Opacity(0.3f), - }; - - protected override bool OnHover(HoverEvent e) - { - Content.TweenEdgeEffectTo(edgeEffectHovered, hover_transition_time, Easing.OutQuint); - Content.MoveToY(-4, hover_transition_time, Easing.OutQuint); - - return base.OnHover(e); - } - - protected override void OnHoverLost(HoverLostEvent e) - { - Content.TweenEdgeEffectTo(edgeEffectNormal, hover_transition_time, Easing.OutQuint); - Content.MoveToY(0, hover_transition_time, Easing.OutQuint); - - base.OnHoverLost(e); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - this.FadeInFromZero(200, Easing.Out); - } - } -} diff --git a/osu.Game/Overlays/SocialOverlay.cs b/osu.Game/Overlays/SocialOverlay.cs index 54c978738d..5a9dd54d53 100644 --- a/osu.Game/Overlays/SocialOverlay.cs +++ b/osu.Game/Overlays/SocialOverlay.cs @@ -25,7 +25,7 @@ namespace osu.Game.Overlays public class SocialOverlay : SearchableListOverlay { private readonly LoadingSpinner loading; - private FillFlowContainer panels; + private FillFlowContainer panels; protected override Color4 BackgroundColour => OsuColour.FromHex(@"60284b"); protected override Color4 TrianglesColourLight => OsuColour.FromHex(@"672b51"); @@ -158,7 +158,7 @@ namespace osu.Game.Overlays if (Filter.DisplayStyleControl.Dropdown.Current.Value == SortDirection.Descending) sortedUsers = sortedUsers.Reverse(); - var newPanels = new FillFlowContainer + var newPanels = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, @@ -166,20 +166,21 @@ namespace osu.Game.Overlays Margin = new MarginPadding { Top = 10 }, ChildrenEnumerable = sortedUsers.Select(u => { - SocialPanel panel; + UserPanel panel; switch (Filter.DisplayStyleControl.DisplayStyle.Value) { case PanelDisplayStyle.Grid: - panel = new SocialGridPanel(u) + panel = new UserGridPanel(u) { Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre + Origin = Anchor.TopCentre, + Width = 290, }; break; default: - panel = new SocialListPanel(u); + panel = new UserListPanel(u); break; } diff --git a/osu.Game/Graphics/UserInterfaceV2/Users/UserGridCard.cs b/osu.Game/Users/UserGridPanel.cs similarity index 93% rename from osu.Game/Graphics/UserInterfaceV2/Users/UserGridCard.cs rename to osu.Game/Users/UserGridPanel.cs index b6e704e901..f9c5c2b0cc 100644 --- a/osu.Game/Graphics/UserInterfaceV2/Users/UserGridCard.cs +++ b/osu.Game/Users/UserGridPanel.cs @@ -5,26 +5,25 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Overlays.Profile.Header.Components; -using osu.Game.Users; using osuTK; -namespace osu.Game.Graphics.UserInterfaceV2.Users +namespace osu.Game.Users { - public class UserGridCard : UserCard + public class UserGridPanel : UserPanel { private const int margin = 10; - public UserGridCard(User user) + public UserGridPanel(User user) : base(user) { - Size = new Vector2(290, 120); + Height = 120; CornerRadius = 10; } [BackgroundDependencyLoader] private void load() { - Background.FadeTo(User.IsOnline ? 0.6f : 0.7f); + Background.FadeTo(0.6f); } protected override Drawable CreateLayout() diff --git a/osu.Game/Graphics/UserInterfaceV2/Users/UserListCard.cs b/osu.Game/Users/UserListPanel.cs similarity index 94% rename from osu.Game/Graphics/UserInterfaceV2/Users/UserListCard.cs rename to osu.Game/Users/UserListPanel.cs index 685556035e..087de13706 100644 --- a/osu.Game/Graphics/UserInterfaceV2/Users/UserListCard.cs +++ b/osu.Game/Users/UserListPanel.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.Game.Users; using osu.Framework.Graphics; using osu.Framework.Allocation; using osu.Framework.Graphics.Colour; @@ -11,11 +10,11 @@ using osu.Framework.Graphics.Containers; using osuTK; using osu.Game.Overlays.Profile.Header.Components; -namespace osu.Game.Graphics.UserInterfaceV2.Users +namespace osu.Game.Users { - public class UserListCard : UserCard + public class UserListPanel : UserPanel { - public UserListCard(User user) + public UserListPanel(User user) : base(user) { RelativeSizeAxes = Axes.X; @@ -27,7 +26,7 @@ namespace osu.Game.Graphics.UserInterfaceV2.Users private void load() { Background.Width = 0.5f; - Background.Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(1), Color4.White.Opacity(User.IsOnline ? 0.6f : 0.7f)); + Background.Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(1), Color4.White.Opacity(0.6f)); } protected override Drawable CreateLayout() diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index 6f34466e94..5a5f18dcfe 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -3,10 +3,8 @@ using System; using osuTK; -using osuTK.Graphics; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -16,32 +14,18 @@ using osu.Game.Overlays; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.UserInterface; using osu.Framework.Graphics.Cursor; -using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics.Containers; -using osu.Game.Overlays.Profile.Header.Components; using osu.Game.Users.Drawables; +using JetBrains.Annotations; +using osu.Framework.Input.Events; namespace osu.Game.Users { - public class UserPanel : OsuClickableContainer, IHasContextMenu + public abstract class UserPanel : OsuClickableContainer, IHasContextMenu { - private const float height = 100; - private const float content_padding = 10; - private const float status_height = 30; - public readonly User User; - [Resolved(canBeNull: true)] - private OsuColour colours { get; set; } - - private Container statusBar; - private Box statusBg; - private OsuSpriteText statusMessage; - - private Container content; - protected override Container Content => content; - public readonly Bindable Status = new Bindable(); public readonly IBindable Activity = new Bindable(); @@ -50,164 +34,63 @@ namespace osu.Game.Users protected Action ViewProfile; - public UserPanel(User user) + protected DelayedLoadUnloadWrapper Background; + + private SpriteIcon statusIcon; + private OsuSpriteText statusMessage; + private TextFlowContainer lastVisitMessage; + + protected UserPanel(User user) { if (user == null) throw new ArgumentNullException(nameof(user)); User = user; - - Height = height - status_height; } - [BackgroundDependencyLoader(permitNulls: true)] - private void load(UserProfileOverlay profile) + [Resolved(canBeNull: true)] + private UserProfileOverlay profileOverlay { get; set; } + + [Resolved] + private OsuColour colours { get; set; } + + [BackgroundDependencyLoader] + private void load() { - if (colours == null) - throw new InvalidOperationException($"{nameof(colours)} not initialized!"); + Action = () => profileOverlay?.ShowUser(User); - FillFlowContainer infoContainer; + Masking = true; + BorderColour = colours.GreyVioletLighter; - AddInternal(content = new Container + AddRange(new[] { - RelativeSizeAxes = Axes.Both, - Masking = true, - CornerRadius = 5, - EdgeEffect = new EdgeEffectParameters + new Box { - Type = EdgeEffectType.Shadow, - Colour = Color4.Black.Opacity(0.25f), - Radius = 4, + RelativeSizeAxes = Axes.Both, + Colour = colours.Gray1 }, - - Children = new Drawable[] + Background = new DelayedLoadUnloadWrapper(() => new UserCoverBackground { - new DelayedLoadUnloadWrapper(() => new UserCoverBackground - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - User = User, - }, 300, 5000) - { - RelativeSizeAxes = Axes.Both, - }, - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black.Opacity(0.7f), - }, - new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Top = content_padding, Horizontal = content_padding }, - Children = new Drawable[] - { - new UpdateableAvatar - { - Size = new Vector2(height - status_height - content_padding * 2), - User = User, - Masking = true, - CornerRadius = 5, - OpenOnClick = { Value = false }, - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Shadow, - Colour = Color4.Black.Opacity(0.25f), - Radius = 4, - }, - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Left = height - status_height - content_padding }, - Children = new Drawable[] - { - new OsuSpriteText - { - Text = User.Username, - Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 18, italics: true), - }, - infoContainer = new FillFlowContainer - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - AutoSizeAxes = Axes.X, - Height = 20f, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(5f, 0f), - Children = new Drawable[] - { - new UpdateableFlag(User.Country) - { - Width = 30f, - RelativeSizeAxes = Axes.Y, - }, - }, - }, - }, - }, - }, - }, - statusBar = new Container - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.X, - Alpha = 0f, - Children = new Drawable[] - { - statusBg = new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0.5f, - }, - new FillFlowContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Both, - Spacing = new Vector2(5f, 0f), - Children = new Drawable[] - { - new SpriteIcon - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Icon = FontAwesome.Regular.Circle, - Shadow = true, - Size = new Vector2(14), - }, - statusMessage = new OsuSpriteText - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Font = OsuFont.GetFont(weight: FontWeight.SemiBold), - }, - }, - }, - }, - }, - } + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + User = User, + }, 300, 5000) + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.Both, + }, + CreateLayout() }); - if (User.IsSupporter) - { - infoContainer.Add(new SupporterIcon - { - Height = 20f, - SupportLevel = User.SupportLevel - }); - } - Status.ValueChanged += status => displayStatus(status.NewValue, Activity.Value); Activity.ValueChanged += activity => displayStatus(Status.Value, activity.NewValue); base.Action = ViewProfile = () => { Action?.Invoke(); - profile?.ShowUser(User); + profileOverlay?.ShowUser(User); }; } @@ -217,33 +100,105 @@ namespace osu.Game.Users Status.TriggerChange(); } + protected override bool OnHover(HoverEvent e) + { + BorderThickness = 2; + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + BorderThickness = 0; + base.OnHoverLost(e); + } + + [NotNull] + protected abstract Drawable CreateLayout(); + + protected UpdateableAvatar CreateAvatar() => new UpdateableAvatar + { + User = User, + OpenOnClick = { Value = false } + }; + + protected UpdateableFlag CreateFlag() => new UpdateableFlag(User.Country) + { + Size = new Vector2(39, 26) + }; + + protected OsuSpriteText CreateUsername() => new OsuSpriteText + { + Font = OsuFont.GetFont(size: 20, weight: FontWeight.Bold, italics: true), + Text = User.Username, + }; + + protected SpriteIcon CreateStatusIcon() => statusIcon = new SpriteIcon + { + Icon = FontAwesome.Regular.Circle, + Size = new Vector2(25) + }; + + protected FillFlowContainer CreateStatusMessage(bool rightAlignedChildren) + { + var statusContainer = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical + }; + + var alignment = rightAlignedChildren ? Anchor.x2 : Anchor.x0; + + statusContainer.Add(lastVisitMessage = new TextFlowContainer(t => t.Font = OsuFont.GetFont(size: 15)).With(text => + { + text.Anchor = Anchor.y1 | alignment; + text.Origin = Anchor.y1 | alignment; + text.AutoSizeAxes = Axes.Both; + text.Alpha = 0; + + if (User.LastVisit.HasValue) + { + text.AddText(@"Last seen "); + text.AddText(new DrawableDate(User.LastVisit.Value, italic: false)); + } + })); + + statusContainer.Add(statusMessage = new OsuSpriteText + { + Anchor = Anchor.y1 | alignment, + Origin = Anchor.y1 | alignment, + Font = OsuFont.GetFont(size: 17) + }); + + return statusContainer; + } + private void displayStatus(UserStatus status, UserActivity activity = null) { - const float transition_duration = 500; + if (status != null) + { + if (activity != null && !(status is UserStatusOffline)) + { + statusMessage.Text = activity.Status; + statusIcon.FadeColour(activity.GetAppropriateColour(colours), 500, Easing.OutQuint); + } + else + { + lastVisitMessage.FadeTo(status is UserStatusOffline && User.LastVisit.HasValue ? 1 : 0); + statusMessage.Text = status.Message; + statusIcon.FadeColour(status.GetAppropriateColour(colours), 500, Easing.OutQuint); + } - if (status == null) - { - statusBar.ResizeHeightTo(0f, transition_duration, Easing.OutQuint); - statusBar.FadeOut(transition_duration, Easing.OutQuint); - this.ResizeHeightTo(height - status_height, transition_duration, Easing.OutQuint); - } - else - { - statusBar.ResizeHeightTo(status_height, transition_duration, Easing.OutQuint); - statusBar.FadeIn(transition_duration, Easing.OutQuint); - this.ResizeHeightTo(height, transition_duration, Easing.OutQuint); + return; } - if (status is UserStatusOnline && activity != null) + // Set local status according to web if it's null + if (User.IsOnline) { - statusMessage.Text = activity.Status; - statusBg.FadeColour(activity.GetAppropriateColour(colours), 500, Easing.OutQuint); - } - else - { - statusMessage.Text = status?.Message; - statusBg.FadeColour(status?.GetAppropriateColour(colours) ?? colours.Gray5, 500, Easing.OutQuint); + Status.Value = new UserStatusOnline(); + return; } + + Status.Value = new UserStatusOffline(); } public MenuItem[] ContextMenuItems => new MenuItem[] diff --git a/osu.Game/Users/UserStatus.cs b/osu.Game/Users/UserStatus.cs index cf372560af..21c18413f4 100644 --- a/osu.Game/Users/UserStatus.cs +++ b/osu.Game/Users/UserStatus.cs @@ -15,7 +15,7 @@ namespace osu.Game.Users public class UserStatusOnline : UserStatus { public override string Message => @"Online"; - public override Color4 GetAppropriateColour(OsuColour colours) => colours.BlueDarker; + public override Color4 GetAppropriateColour(OsuColour colours) => colours.GreenLight; } public abstract class UserStatusBusy : UserStatusOnline @@ -26,7 +26,7 @@ namespace osu.Game.Users public class UserStatusOffline : UserStatus { public override string Message => @"Offline"; - public override Color4 GetAppropriateColour(OsuColour colours) => colours.Gray7; + public override Color4 GetAppropriateColour(OsuColour colours) => Color4.Black; } public class UserStatusDoNotDisturb : UserStatus From 0b98bcc856582671025a333601c907198c0672b0 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 4 Mar 2020 15:22:08 +0300 Subject: [PATCH 0171/2376] Fix incorrect click action --- osu.Game/Users/UserPanel.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index 5a5f18dcfe..7692f70de0 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -57,8 +57,6 @@ namespace osu.Game.Users [BackgroundDependencyLoader] private void load() { - Action = () => profileOverlay?.ShowUser(User); - Masking = true; BorderColour = colours.GreyVioletLighter; From 11ffe6a072e935cbcfeaef5546754c0359770470 Mon Sep 17 00:00:00 2001 From: McEndu Date: Wed, 4 Mar 2020 21:45:01 +0800 Subject: [PATCH 0172/2376] Remove LD_LIBRARY_PATH from vscode launch.json --- .vscode/launch.json | 42 +----------------------------------------- 1 file changed, 1 insertion(+), 41 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 6480612b2e..4e8af405a2 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,11 +11,6 @@ ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build osu! (Debug)", - "linux": { - "env": { - "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Desktop/bin/Debug/netcoreapp3.1:${env:LD_LIBRARY_PATH}" - } - }, "console": "internalConsole" }, { @@ -28,11 +23,6 @@ ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build osu! (Release)", - "linux": { - "env": { - "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Desktop/bin/Release/netcoreapp3.1:${env:LD_LIBRARY_PATH}" - } - }, "console": "internalConsole" }, { @@ -45,11 +35,6 @@ ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build tests (Debug)", - "linux": { - "env": { - "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Game.Tests/bin/Debug/netcoreapp3.1:${env:LD_LIBRARY_PATH}" - } - }, "console": "internalConsole" }, { @@ -62,11 +47,6 @@ ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build tests (Release)", - "linux": { - "env": { - "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Game.Tests/bin/Release/netcoreapp3.1:${env:LD_LIBRARY_PATH}" - } - }, "console": "internalConsole" }, { @@ -80,11 +60,6 @@ ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build osu! (Debug)", - "linux": { - "env": { - "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Desktop/bin/Debug/netcoreapp3.1:${env:LD_LIBRARY_PATH}" - } - }, "console": "internalConsole" }, { @@ -98,11 +73,6 @@ ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build osu! (Release)", - "linux": { - "env": { - "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Desktop/bin/Release/netcoreapp3.1:${env:LD_LIBRARY_PATH}" - } - }, "console": "internalConsole" }, { @@ -116,11 +86,6 @@ ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build tournament tests (Debug)", - "linux": { - "env": { - "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Game.Tournament.Tests/bin/Debug/netcoreapp3.1:${env:LD_LIBRARY_PATH}" - } - }, "console": "internalConsole" }, { @@ -134,11 +99,6 @@ ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build tournament tests (Release)", - "linux": { - "env": { - "LD_LIBRARY_PATH": "${workspaceRoot}/osu.Game.Tournament.Tests/bin/Debug/netcoreapp3.1:${env:LD_LIBRARY_PATH}" - } - }, "console": "internalConsole" }, { @@ -169,4 +129,4 @@ "externalConsole": false } ] -} \ No newline at end of file +} From f8776a0be4924acfc5673b9683d4ef8fab702390 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Mar 2020 22:59:49 +0900 Subject: [PATCH 0173/2376] Display all difficulties from overriding selection --- osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs | 2 +- osu.Game/Screens/Select/FilterCriteria.cs | 2 +- osu.Game/Screens/Select/SongSelect.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index d78d407748..c110608a0e 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -25,7 +25,7 @@ namespace osu.Game.Screens.Select.Carousel { base.Filter(criteria); - if (Beatmap.Equals(criteria.SelectedBeatmap)) + if (Beatmap.BeatmapSet.Equals(criteria.SelectedBeatmapSet)) { // bypass filtering for selected beatmap Filtered.Value = false; diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index a5c4e2961d..18be4fcac8 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -15,7 +15,7 @@ namespace osu.Game.Screens.Select public GroupMode Group; public SortMode Sort; - public BeatmapInfo SelectedBeatmap; + public BeatmapSetInfo SelectedBeatmapSet; public OptionalRange StarDifficulty; public OptionalRange ApproachRate; diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 90ad7a7fdd..6577ed8506 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -395,7 +395,7 @@ namespace osu.Game.Screens.Select // we still want to temporarily show the new beatmap, bypassing filters. // This will be undone the next time the user changes the filter. var criteria = FilterControl.CreateCriteria(); - criteria.SelectedBeatmap = e.NewValue.BeatmapInfo; + criteria.SelectedBeatmapSet = e.NewValue.BeatmapInfo.BeatmapSet; Carousel.Filter(criteria); } } From 9aacc3f5ae1b27cf4564f9f1bf53bd8af0485f27 Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Wed, 4 Mar 2020 19:24:52 +0100 Subject: [PATCH 0174/2376] Replace Scores property with DisplayScores method Also adds null checks to prevent crashes in tests. --- .../Overlays/BeatmapSet/Scores/ScoreTable.cs | 32 ++++++++++--------- .../BeatmapSet/Scores/ScoresContainer.cs | 5 ++- .../Scores/TopScoreStatisticsSection.cs | 2 +- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs index af6bf8299f..4755522b9c 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs @@ -52,25 +52,27 @@ namespace osu.Game.Overlays.BeatmapSet.Scores highAccuracyColour = colours.GreenLight; } - public IReadOnlyList Scores + private bool showPerformancePoints; + + public void DisplayScores(IReadOnlyList scores, bool showPerformanceColumn) { - set - { - Content = null; - backgroundFlow.Clear(); + if (!scores.Any()) + return; - if (value?.Any() != true) - return; + showPerformancePoints = showPerformanceColumn; - for (int i = 0; i < value.Count; i++) - backgroundFlow.Add(new ScoreTableRowBackground(i, value[i], row_height)); + for (int i = 0; i < scores.Count; i++) + backgroundFlow.Add(new ScoreTableRowBackground(i, scores[i], row_height)); - Columns = createHeaders(value.First()); - Content = value.Select((s, i) => createContent(i, s)).ToArray().ToRectangular(); - } + Columns = createHeaders(scores.FirstOrDefault()); + Content = scores.Select((s, i) => createContent(i, s)).ToArray().ToRectangular(); } - public bool IsBeatmapRanked { get; set; } + public void ClearScores() + { + Content = null; + backgroundFlow.Clear(); + } private TableColumn[] createHeaders(ScoreInfo score) { @@ -90,7 +92,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores columns.Add(new TableColumn(score.SortedStatistics.LastOrDefault().Key.GetDescription(), Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 45, maxSize: 95))); - if (IsBeatmapRanked) + if (showPerformancePoints) columns.Add(new TableColumn("pp", Anchor.CentreLeft, new Dimension(GridSizeMode.Absolute, 30))); columns.Add(new TableColumn("mods", Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize))); @@ -151,7 +153,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores }); } - if (IsBeatmapRanked) + if (showPerformancePoints) { content.Add(new OsuSpriteText { diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs index 5a931fffcb..be1f8c2111 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs @@ -53,7 +53,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores if (value?.Scores.Any() != true) { - scoreTable.Scores = null; + scoreTable.ClearScores(); scoreTable.Hide(); return; } @@ -61,8 +61,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores var scoreInfos = value.Scores.Select(s => s.CreateScoreInfo(rulesets)).ToList(); var topScore = scoreInfos.First(); - scoreTable.Scores = scoreInfos; - scoreTable.IsBeatmapRanked = topScore.Beatmap.Status == BeatmapSetOnlineStatus.Ranked; + scoreTable.DisplayScores(scoreInfos, topScore.Beatmap?.Status == BeatmapSetOnlineStatus.Ranked); 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 9ecc40eed2..a92346e0fe 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs @@ -97,7 +97,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores totalScoreColumn.Text = $@"{value.TotalScore:N0}"; accuracyColumn.Text = value.DisplayAccuracy; maxComboColumn.Text = $@"{value.MaxCombo:N0}x"; - ppColumn.Alpha = value.Beatmap.Status == BeatmapSetOnlineStatus.Ranked ? 1 : 0; + ppColumn.Alpha = value.Beatmap?.Status == BeatmapSetOnlineStatus.Ranked ? 1 : 0; ppColumn.Text = $@"{value.PP:N0}"; statisticsColumns.ChildrenEnumerable = value.SortedStatistics.Select(kvp => createStatisticsColumn(kvp.Key, kvp.Value)); From 5628c5102dc791fa44c96e5c2480dcc40c726eaf Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Wed, 4 Mar 2020 20:01:15 +0100 Subject: [PATCH 0175/2376] Remove old scores before adding new ones --- osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs index 4755522b9c..097ca27bf7 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs @@ -56,6 +56,8 @@ namespace osu.Game.Overlays.BeatmapSet.Scores public void DisplayScores(IReadOnlyList scores, bool showPerformanceColumn) { + ClearScores(); + if (!scores.Any()) return; From 55a0586b13903999e5fd3ec04f8622ed077446c6 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 4 Mar 2020 23:03:02 +0300 Subject: [PATCH 0176/2376] Move exception handling below all the cases --- osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs b/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs index 1025fc8146..37652a7ecb 100644 --- a/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs +++ b/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs @@ -22,9 +22,6 @@ namespace osu.Game.Overlays.Home.Friends { switch (Value.Status) { - default: - throw new ArgumentException($@"{Value.Status} status does not provide a colour in {nameof(GetBarColour)}."); - case FriendsOnlineStatus.All: return Color4.White; @@ -33,6 +30,9 @@ namespace osu.Game.Overlays.Home.Friends case FriendsOnlineStatus.Offline: return Color4.Black; + + default: + throw new ArgumentException($@"{Value.Status} status does not provide a colour in {nameof(GetBarColour)}."); } } } From 63219a2357ebe94d9b96c4a74ac17bdc85e57a42 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 4 Mar 2020 23:06:16 +0300 Subject: [PATCH 0177/2376] Adjust properties naming --- .../Overlays/Changelog/ChangelogUpdateStreamItem.cs | 6 +++--- .../Overlays/Home/Friends/FriendsOnlineStatusItem.cs | 4 ++-- osu.Game/Overlays/OverlayUpdateStreamItem.cs | 12 ++++++------ 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs index 79824db572..9590aefa49 100644 --- a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs +++ b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs @@ -17,11 +17,11 @@ namespace osu.Game.Overlays.Changelog Width *= 2; } - protected override string GetMainText => Value.DisplayName; + protected override string MainText => Value.DisplayName; - protected override string GetAdditionalText => Value.LatestBuild.DisplayVersion; + protected override string AdditionalText => Value.LatestBuild.DisplayVersion; - protected override string GetInfoText => Value.LatestBuild.Users > 0 ? $"{"user".ToQuantity(Value.LatestBuild.Users, "N0")} online" : null; + protected override string InfoText => Value.LatestBuild.Users > 0 ? $"{"user".ToQuantity(Value.LatestBuild.Users, "N0")} online" : null; protected override Color4 GetBarColour(OsuColour colours) => Value.Colour; } diff --git a/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs b/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs index 37652a7ecb..5dd7ca2c18 100644 --- a/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs +++ b/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs @@ -14,9 +14,9 @@ namespace osu.Game.Overlays.Home.Friends { } - protected override string GetMainText => Value.Status.ToString(); + protected override string MainText => Value.Status.ToString(); - protected override string GetAdditionalText => Value.Count.ToString(); + protected override string AdditionalText => Value.Count.ToString(); protected override Color4 GetBarColour(OsuColour colours) { diff --git a/osu.Game/Overlays/OverlayUpdateStreamItem.cs b/osu.Game/Overlays/OverlayUpdateStreamItem.cs index 459daeb3a5..7bccb8da25 100644 --- a/osu.Game/Overlays/OverlayUpdateStreamItem.cs +++ b/osu.Game/Overlays/OverlayUpdateStreamItem.cs @@ -58,17 +58,17 @@ namespace osu.Game.Overlays { new OsuSpriteText { - Text = GetMainText, + Text = MainText, Font = OsuFont.GetFont(size: 12, weight: FontWeight.Black), }, new OsuSpriteText { - Text = GetAdditionalText, + Text = AdditionalText, Font = OsuFont.GetFont(size: 16, weight: FontWeight.Regular), }, new OsuSpriteText { - Text = GetInfoText, + Text = InfoText, Font = OsuFont.GetFont(size: 10), Colour = colourProvider.Foreground1 }, @@ -88,11 +88,11 @@ namespace osu.Game.Overlays SelectedItem.BindValueChanged(_ => updateState(), true); } - protected abstract string GetMainText { get; } + protected abstract string MainText { get; } - protected abstract string GetAdditionalText { get; } + protected abstract string AdditionalText { get; } - protected virtual string GetInfoText => string.Empty; + protected virtual string InfoText => string.Empty; protected abstract Color4 GetBarColour(OsuColour colours); From bd03dd9b707bba167a2ae8697626a3f6da50641a Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 4 Mar 2020 23:08:58 +0300 Subject: [PATCH 0178/2376] Adjust class naming --- .../TestSceneFriendsOnlineStatusControl.cs | 4 ++-- .../Overlays/Changelog/ChangelogUpdateStreamControl.cs | 4 ++-- .../Overlays/Changelog/ChangelogUpdateStreamItem.cs | 2 +- .../Home/Friends/FriendsOnlineStatusControl.cs | 4 ++-- .../Overlays/Home/Friends/FriendsOnlineStatusItem.cs | 2 +- ...yUpdateStreamControl.cs => OverlayStreamControl.cs} | 10 +++++----- ...OverlayUpdateStreamItem.cs => OverlayStreamItem.cs} | 4 ++-- 7 files changed, 15 insertions(+), 15 deletions(-) rename osu.Game/Overlays/{OverlayUpdateStreamControl.cs => OverlayStreamControl.cs} (84%) rename osu.Game/Overlays/{OverlayUpdateStreamItem.cs => OverlayStreamItem.cs} (97%) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs index 45f8a029a8..0d841dfef1 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs @@ -19,8 +19,8 @@ namespace osu.Game.Tests.Visual.UserInterface { typeof(FriendsOnlineStatusControl), typeof(FriendsOnlineStatusItem), - typeof(OverlayUpdateStreamControl<>), - typeof(OverlayUpdateStreamItem<>), + typeof(OverlayStreamControl<>), + typeof(OverlayStreamItem<>), typeof(FriendsBundle) }; diff --git a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamControl.cs b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamControl.cs index 555f0904d6..509a6dabae 100644 --- a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamControl.cs +++ b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamControl.cs @@ -5,8 +5,8 @@ using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Overlays.Changelog { - public class ChangelogUpdateStreamControl : OverlayUpdateStreamControl + public class ChangelogUpdateStreamControl : OverlayStreamControl { - protected override OverlayUpdateStreamItem CreateStreamItem(APIUpdateStream value) => new ChangelogUpdateStreamItem(value); + protected override OverlayStreamItem CreateStreamItem(APIUpdateStream value) => new ChangelogUpdateStreamItem(value); } } diff --git a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs index 9590aefa49..f8e1ac0c84 100644 --- a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs +++ b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamItem.cs @@ -8,7 +8,7 @@ using osuTK.Graphics; namespace osu.Game.Overlays.Changelog { - public class ChangelogUpdateStreamItem : OverlayUpdateStreamItem + public class ChangelogUpdateStreamItem : OverlayStreamItem { public ChangelogUpdateStreamItem(APIUpdateStream stream) : base(stream) diff --git a/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusControl.cs b/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusControl.cs index a92de9dbeb..196f01ab4a 100644 --- a/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusControl.cs +++ b/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusControl.cs @@ -7,9 +7,9 @@ using osu.Game.Users; namespace osu.Game.Overlays.Home.Friends { - public class FriendsOnlineStatusControl : OverlayUpdateStreamControl + public class FriendsOnlineStatusControl : OverlayStreamControl { - protected override OverlayUpdateStreamItem CreateStreamItem(FriendsBundle value) => new FriendsOnlineStatusItem(value); + protected override OverlayStreamItem CreateStreamItem(FriendsBundle value) => new FriendsOnlineStatusItem(value); public void Populate(List users) { diff --git a/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs b/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs index 5dd7ca2c18..d9b780ce46 100644 --- a/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs +++ b/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs @@ -7,7 +7,7 @@ using osuTK.Graphics; namespace osu.Game.Overlays.Home.Friends { - public class FriendsOnlineStatusItem : OverlayUpdateStreamItem + public class FriendsOnlineStatusItem : OverlayStreamItem { public FriendsOnlineStatusItem(FriendsBundle value) : base(value) diff --git a/osu.Game/Overlays/OverlayUpdateStreamControl.cs b/osu.Game/Overlays/OverlayStreamControl.cs similarity index 84% rename from osu.Game/Overlays/OverlayUpdateStreamControl.cs rename to osu.Game/Overlays/OverlayStreamControl.cs index 0fdf6c0111..8b6aca6d5d 100644 --- a/osu.Game/Overlays/OverlayUpdateStreamControl.cs +++ b/osu.Game/Overlays/OverlayStreamControl.cs @@ -10,9 +10,9 @@ using JetBrains.Annotations; namespace osu.Game.Overlays { - public abstract class OverlayUpdateStreamControl : TabControl + public abstract class OverlayStreamControl : TabControl { - protected OverlayUpdateStreamControl() + protected OverlayStreamControl() { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; @@ -28,7 +28,7 @@ namespace osu.Game.Overlays }); [NotNull] - protected abstract OverlayUpdateStreamItem CreateStreamItem(T value); + protected abstract OverlayStreamItem CreateStreamItem(T value); protected override TabFillFlowContainer CreateTabFlow() => new TabFillFlowContainer { @@ -39,7 +39,7 @@ namespace osu.Game.Overlays protected override bool OnHover(HoverEvent e) { - foreach (var streamBadge in TabContainer.Children.OfType>()) + foreach (var streamBadge in TabContainer.Children.OfType>()) streamBadge.UserHoveringArea = true; return base.OnHover(e); @@ -47,7 +47,7 @@ namespace osu.Game.Overlays protected override void OnHoverLost(HoverLostEvent e) { - foreach (var streamBadge in TabContainer.Children.OfType>()) + foreach (var streamBadge in TabContainer.Children.OfType>()) streamBadge.UserHoveringArea = false; base.OnHoverLost(e); diff --git a/osu.Game/Overlays/OverlayUpdateStreamItem.cs b/osu.Game/Overlays/OverlayStreamItem.cs similarity index 97% rename from osu.Game/Overlays/OverlayUpdateStreamItem.cs rename to osu.Game/Overlays/OverlayStreamItem.cs index 7bccb8da25..630d3a0a22 100644 --- a/osu.Game/Overlays/OverlayUpdateStreamItem.cs +++ b/osu.Game/Overlays/OverlayStreamItem.cs @@ -15,7 +15,7 @@ using osuTK.Graphics; namespace osu.Game.Overlays { - public abstract class OverlayUpdateStreamItem : TabItem + public abstract class OverlayStreamItem : TabItem { public readonly Bindable SelectedItem = new Bindable(); @@ -36,7 +36,7 @@ namespace osu.Game.Overlays private FillFlowContainer text; private ExpandingBar expandingBar; - protected OverlayUpdateStreamItem(T value) + protected OverlayStreamItem(T value) : base(value) { Height = 60; From 997be65be2329d3a3376f1ca16914622e767da0f Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Wed, 4 Mar 2020 21:19:26 +0100 Subject: [PATCH 0179/2376] Improve colouring logic --- .../Historical/DrawableMostPlayedBeatmap.cs | 17 +++++++++----- .../Profile/Sections/ProfileItemContainer.cs | 22 ++++++++++++++++--- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs b/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs index dc8492562a..5b7c5efbe2 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs @@ -37,10 +37,8 @@ namespace osu.Game.Overlays.Profile.Sections.Historical } [BackgroundDependencyLoader] - private void load(OsuColour colours, OverlayColourProvider colourProvider) + private void load(OverlayColourProvider colourProvider) { - ProfileItemContainer container; - AddRangeInternal(new Drawable[] { new UpdateableBeatmapSetCover @@ -63,7 +61,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical CornerRadius = corner_radius, Children = new Drawable[] { - container = new ProfileItemContainer + new MostPlayedBeatmapContainer { Child = new Container { @@ -108,9 +106,16 @@ namespace osu.Game.Overlays.Profile.Sections.Historical } } }); + } - container.IdleColour = colourProvider.Background4; - container.HoverColour = colourProvider.Background3; + private class MostPlayedBeatmapContainer : ProfileItemContainer + { + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + IdleColour = colourProvider.Background4; + HoverColour = colourProvider.Background3; + } } private class MostPlayedBeatmapMetadataContainer : BeatmapMetadataContainer diff --git a/osu.Game/Overlays/Profile/Sections/ProfileItemContainer.cs b/osu.Game/Overlays/Profile/Sections/ProfileItemContainer.cs index a48f21f52b..1ab9ed3f82 100644 --- a/osu.Game/Overlays/Profile/Sections/ProfileItemContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/ProfileItemContainer.cs @@ -21,13 +21,29 @@ namespace osu.Game.Overlays.Profile.Sections private Color4 idleColour; - public Color4 IdleColour + protected Color4 IdleColour { get => idleColour; - set => idleColour = background.Colour = value; + set + { + idleColour = value; + if (!IsHovered) + background.Colour = value; + } } - public Color4 HoverColour { get; set; } + private Color4 hoverColour; + + protected Color4 HoverColour + { + get => hoverColour; + set + { + hoverColour = value; + if (IsHovered) + background.Colour = value; + } + } public ProfileItemContainer() { From e3e66991b08c4f26b0b8f46a4ef8c414f775a6b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 4 Mar 2020 22:02:36 +0100 Subject: [PATCH 0180/2376] Move initialisation logic to [SetUp] --- .../Visual/Online/TestSceneUserPanel.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs index 54f06d6ad2..597ca00fb8 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs @@ -13,13 +13,16 @@ namespace osu.Game.Tests.Visual.Online [TestFixture] public class TestSceneUserPanel : OsuTestScene { - private readonly UserPanel peppy; + private readonly Bindable activity = new Bindable(); - public TestSceneUserPanel() + private UserPanel peppy; + + [SetUp] + public void SetUp() => Schedule(() => { UserPanel flyte; - Add(new FillFlowContainer + Child = new FillFlowContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -44,11 +47,12 @@ namespace osu.Game.Tests.Visual.Online SupportLevel = 3, }) { Width = 300 }, }, - }); + }; flyte.Status.Value = new UserStatusOnline(); peppy.Status.Value = null; - } + peppy.Activity.BindTo(activity); + }); [Test] public void UserStatusesTests() @@ -62,10 +66,6 @@ namespace osu.Game.Tests.Visual.Online [Test] public void UserActivitiesTests() { - Bindable activity = new Bindable(); - - peppy.Activity.BindTo(activity); - AddStep("idle", () => { activity.Value = null; }); AddStep("spectating", () => { activity.Value = new UserActivity.Spectating(); }); AddStep("solo", () => { activity.Value = new UserActivity.SoloGame(null, null); }); From 5b25b5dfabf972d05f4829fd55b363c575be4421 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 4 Mar 2020 22:04:49 +0100 Subject: [PATCH 0181/2376] Change brace style --- .../Visual/Online/TestSceneUserPanel.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs index 597ca00fb8..55017db479 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs @@ -57,21 +57,21 @@ namespace osu.Game.Tests.Visual.Online [Test] public void UserStatusesTests() { - AddStep("online", () => { peppy.Status.Value = new UserStatusOnline(); }); - AddStep(@"do not disturb", () => { peppy.Status.Value = new UserStatusDoNotDisturb(); }); - AddStep(@"offline", () => { peppy.Status.Value = new UserStatusOffline(); }); - AddStep(@"null status", () => { peppy.Status.Value = null; }); + AddStep("online", () => peppy.Status.Value = new UserStatusOnline()); + AddStep(@"do not disturb", () => peppy.Status.Value = new UserStatusDoNotDisturb()); + AddStep(@"offline", () => peppy.Status.Value = new UserStatusOffline()); + AddStep(@"null status", () => peppy.Status.Value = null); } [Test] public void UserActivitiesTests() { - AddStep("idle", () => { activity.Value = null; }); - AddStep("spectating", () => { activity.Value = new UserActivity.Spectating(); }); - AddStep("solo", () => { activity.Value = new UserActivity.SoloGame(null, null); }); - AddStep("choosing", () => { activity.Value = new UserActivity.ChoosingBeatmap(); }); - AddStep("editing", () => { activity.Value = new UserActivity.Editing(null); }); - AddStep("modding", () => { activity.Value = new UserActivity.Modding(); }); + AddStep("idle", () => activity.Value = null); + AddStep("spectating", () => activity.Value = new UserActivity.Spectating()); + AddStep("solo", () => activity.Value = new UserActivity.SoloGame(null, null)); + AddStep("choosing", () => activity.Value = new UserActivity.ChoosingBeatmap()); + AddStep("editing", () => activity.Value = new UserActivity.Editing(null)); + AddStep("modding", () => activity.Value = new UserActivity.Modding()); } } } From 1bd49d50c771846168209054257ae132e854e8e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 4 Mar 2020 22:05:08 +0100 Subject: [PATCH 0182/2376] Remove unnecessary raw string prefixes --- osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs index 55017db479..f128584b1a 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs @@ -58,9 +58,9 @@ namespace osu.Game.Tests.Visual.Online public void UserStatusesTests() { AddStep("online", () => peppy.Status.Value = new UserStatusOnline()); - AddStep(@"do not disturb", () => peppy.Status.Value = new UserStatusDoNotDisturb()); - AddStep(@"offline", () => peppy.Status.Value = new UserStatusOffline()); - AddStep(@"null status", () => peppy.Status.Value = null); + AddStep("do not disturb", () => peppy.Status.Value = new UserStatusDoNotDisturb()); + AddStep("offline", () => peppy.Status.Value = new UserStatusOffline()); + AddStep("null status", () => peppy.Status.Value = null); } [Test] From 5fa2638e81619fe0ba9fa726561972ea9886ac1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 4 Mar 2020 22:05:48 +0100 Subject: [PATCH 0183/2376] Rename tests to adhere to convention --- osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs index f128584b1a..ad5dfb371a 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs @@ -55,7 +55,7 @@ namespace osu.Game.Tests.Visual.Online }); [Test] - public void UserStatusesTests() + public void TestUserStatus() { AddStep("online", () => peppy.Status.Value = new UserStatusOnline()); AddStep("do not disturb", () => peppy.Status.Value = new UserStatusDoNotDisturb()); @@ -64,7 +64,7 @@ namespace osu.Game.Tests.Visual.Online } [Test] - public void UserActivitiesTests() + public void TestUserActivity() { AddStep("idle", () => activity.Value = null); AddStep("spectating", () => activity.Value = new UserActivity.Spectating()); From afa3ce494da1856810c549bf84efa262f59d259f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 4 Mar 2020 22:07:02 +0100 Subject: [PATCH 0184/2376] Set online status in activity test The test would check nothing otherwise. --- osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs index ad5dfb371a..fae3e9b17b 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs @@ -66,6 +66,8 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestUserActivity() { + AddStep("set online status", () => peppy.Status.Value = new UserStatusOnline()); + AddStep("idle", () => activity.Value = null); AddStep("spectating", () => activity.Value = new UserActivity.Spectating()); AddStep("solo", () => activity.Value = new UserActivity.SoloGame(null, null)); From b8889318dbcc6d7b6c8c28c08d4b063b7918f6cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 4 Mar 2020 22:13:31 +0100 Subject: [PATCH 0185/2376] Pass rulesets to solo game status --- osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs index fae3e9b17b..80fcef2ed2 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs @@ -2,9 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets; using osu.Game.Users; using osuTK; @@ -17,6 +19,9 @@ namespace osu.Game.Tests.Visual.Online private UserPanel peppy; + [Resolved] + private RulesetStore rulesetStore { get; set; } + [SetUp] public void SetUp() => Schedule(() => { @@ -70,10 +75,15 @@ namespace osu.Game.Tests.Visual.Online AddStep("idle", () => activity.Value = null); AddStep("spectating", () => activity.Value = new UserActivity.Spectating()); - AddStep("solo", () => activity.Value = new UserActivity.SoloGame(null, null)); + AddStep("solo (osu!)", () => activity.Value = soloGameStatusForRuleset(0)); + AddStep("solo (osu!taiko)", () => activity.Value = soloGameStatusForRuleset(1)); + AddStep("solo (osu!catch)", () => activity.Value = soloGameStatusForRuleset(2)); + AddStep("solo (osu!mania)", () => activity.Value = soloGameStatusForRuleset(3)); AddStep("choosing", () => activity.Value = new UserActivity.ChoosingBeatmap()); AddStep("editing", () => activity.Value = new UserActivity.Editing(null)); AddStep("modding", () => activity.Value = new UserActivity.Modding()); } + + private UserActivity soloGameStatusForRuleset(int rulesetId) => new UserActivity.SoloGame(null, rulesetStore.GetRuleset(rulesetId)); } } From d297bf69578cc58b276c89e213943f00d00e90b3 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 5 Mar 2020 01:41:55 +0300 Subject: [PATCH 0186/2376] Adjust background presentation --- osu.Game/Users/UserGridPanel.cs | 2 +- osu.Game/Users/UserListPanel.cs | 2 +- osu.Game/Users/UserPanel.cs | 7 +++++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game/Users/UserGridPanel.cs b/osu.Game/Users/UserGridPanel.cs index f9c5c2b0cc..4bd40b3e76 100644 --- a/osu.Game/Users/UserGridPanel.cs +++ b/osu.Game/Users/UserGridPanel.cs @@ -23,7 +23,7 @@ namespace osu.Game.Users [BackgroundDependencyLoader] private void load() { - Background.FadeTo(0.6f); + Background.FadeTo(0.4f); } protected override Drawable CreateLayout() diff --git a/osu.Game/Users/UserListPanel.cs b/osu.Game/Users/UserListPanel.cs index 087de13706..1c3ae20577 100644 --- a/osu.Game/Users/UserListPanel.cs +++ b/osu.Game/Users/UserListPanel.cs @@ -26,7 +26,7 @@ namespace osu.Game.Users private void load() { Background.Width = 0.5f; - Background.Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(1), Color4.White.Opacity(0.6f)); + Background.Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(1), Color4.White.Opacity(0.3f)); } protected override Drawable CreateLayout() diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index 7692f70de0..2606d669b7 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -51,6 +51,9 @@ namespace osu.Game.Users [Resolved(canBeNull: true)] private UserProfileOverlay profileOverlay { get; set; } + [Resolved(canBeNull: true)] + private OverlayColourProvider colourProvider { get; set; } + [Resolved] private OsuColour colours { get; set; } @@ -58,14 +61,14 @@ namespace osu.Game.Users private void load() { Masking = true; - BorderColour = colours.GreyVioletLighter; + BorderColour = colourProvider?.Light1 ?? colours.GreyVioletLighter; AddRange(new[] { new Box { RelativeSizeAxes = Axes.Both, - Colour = colours.Gray1 + Colour = colourProvider?.Background4 ?? colours.Gray1 }, Background = new DelayedLoadUnloadWrapper(() => new UserCoverBackground { From 06b2c70a04a76239127642573878d49322082ad1 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 5 Mar 2020 01:45:49 +0300 Subject: [PATCH 0187/2376] Simplify alignment setting for status message --- osu.Game/Users/UserPanel.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index 2606d669b7..265406c6d3 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -68,7 +68,7 @@ namespace osu.Game.Users new Box { RelativeSizeAxes = Axes.Both, - Colour = colourProvider?.Background4 ?? colours.Gray1 + Colour = colourProvider?.Background5 ?? colours.Gray1 }, Background = new DelayedLoadUnloadWrapper(() => new UserCoverBackground { @@ -147,12 +147,12 @@ namespace osu.Game.Users Direction = FillDirection.Vertical }; - var alignment = rightAlignedChildren ? Anchor.x2 : Anchor.x0; + var alignment = rightAlignedChildren ? Anchor.CentreRight : Anchor.CentreLeft; statusContainer.Add(lastVisitMessage = new TextFlowContainer(t => t.Font = OsuFont.GetFont(size: 15)).With(text => { - text.Anchor = Anchor.y1 | alignment; - text.Origin = Anchor.y1 | alignment; + text.Anchor = alignment; + text.Origin = alignment; text.AutoSizeAxes = Axes.Both; text.Alpha = 0; @@ -165,8 +165,8 @@ namespace osu.Game.Users statusContainer.Add(statusMessage = new OsuSpriteText { - Anchor = Anchor.y1 | alignment, - Origin = Anchor.y1 | alignment, + Anchor = alignment, + Origin = alignment, Font = OsuFont.GetFont(size: 17) }); From 22d43c26aace948380c694a68ce640202aca0425 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 5 Mar 2020 02:00:46 +0300 Subject: [PATCH 0188/2376] Adjust some text values --- osu.Game/Users/UserPanel.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index 265406c6d3..c06306c7b0 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -129,7 +129,7 @@ namespace osu.Game.Users protected OsuSpriteText CreateUsername() => new OsuSpriteText { - Font = OsuFont.GetFont(size: 20, weight: FontWeight.Bold, italics: true), + Font = OsuFont.GetFont(size: 19, weight: FontWeight.Bold, italics: true), Text = User.Username, }; @@ -149,7 +149,7 @@ namespace osu.Game.Users var alignment = rightAlignedChildren ? Anchor.CentreRight : Anchor.CentreLeft; - statusContainer.Add(lastVisitMessage = new TextFlowContainer(t => t.Font = OsuFont.GetFont(size: 15)).With(text => + statusContainer.Add(lastVisitMessage = new TextFlowContainer(t => t.Font = OsuFont.GetFont(size: 15, weight: FontWeight.SemiBold)).With(text => { text.Anchor = alignment; text.Origin = alignment; @@ -159,7 +159,10 @@ namespace osu.Game.Users if (User.LastVisit.HasValue) { text.AddText(@"Last seen "); - text.AddText(new DrawableDate(User.LastVisit.Value, italic: false)); + text.AddText(new DrawableDate(User.LastVisit.Value, italic: false) + { + Shadow = false + }); } })); @@ -167,7 +170,7 @@ namespace osu.Game.Users { Anchor = alignment, Origin = alignment, - Font = OsuFont.GetFont(size: 17) + Font = OsuFont.GetFont(size: 17, weight: FontWeight.SemiBold) }); return statusContainer; From 2e996eeb7e6f36082fad19f37dd655d1825a1007 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 5 Mar 2020 02:10:38 +0300 Subject: [PATCH 0189/2376] Refactor displayStatus function and add comments --- osu.Game/Users/UserPanel.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index c06306c7b0..9d577bcc2a 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -180,22 +180,23 @@ namespace osu.Game.Users { if (status != null) { + // Set status message based on activity (if we have one) and status is not offline if (activity != null && !(status is UserStatusOffline)) { statusMessage.Text = activity.Status; statusIcon.FadeColour(activity.GetAppropriateColour(colours), 500, Easing.OutQuint); + return; } - else - { - lastVisitMessage.FadeTo(status is UserStatusOffline && User.LastVisit.HasValue ? 1 : 0); - statusMessage.Text = status.Message; - statusIcon.FadeColour(status.GetAppropriateColour(colours), 500, Easing.OutQuint); - } + + // Otherwise use only status + lastVisitMessage.FadeTo(status is UserStatusOffline && User.LastVisit.HasValue ? 1 : 0); + statusMessage.Text = status.Message; + statusIcon.FadeColour(status.GetAppropriateColour(colours), 500, Easing.OutQuint); return; } - // Set local status according to web if it's null + // Fallback to web status if local one is null if (User.IsOnline) { Status.Value = new UserStatusOnline(); From 13752ffe9d86836affa22126bdf381e3b5cb9d33 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 5 Mar 2020 02:31:19 +0300 Subject: [PATCH 0190/2376] Revisit UserGridPanel layout --- osu.Game/Users/UserGridPanel.cs | 58 ++++++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 16 deletions(-) diff --git a/osu.Game/Users/UserGridPanel.cs b/osu.Game/Users/UserGridPanel.cs index 4bd40b3e76..b0ce557a19 100644 --- a/osu.Game/Users/UserGridPanel.cs +++ b/osu.Game/Users/UserGridPanel.cs @@ -45,6 +45,7 @@ namespace osu.Game.Users RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Absolute, margin), new Dimension() }, Content = new[] @@ -54,32 +55,57 @@ namespace osu.Game.Users CreateAvatar().With(avatar => { avatar.Size = new Vector2(60); - avatar.Margin = new MarginPadding { Bottom = margin }; avatar.Masking = true; avatar.CornerRadius = 6; }), - new FillFlowContainer + new Container { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 7), - Margin = new MarginPadding { Left = margin }, - Children = new Drawable[] + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = margin }, + Child = new GridContainer { - details = new FillFlowContainer + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(6), - Children = new Drawable[] - { - CreateFlag(), - } + new Dimension() }, - CreateUsername(), + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension() + }, + Content = new[] + { + new Drawable[] + { + details = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(6), + Children = new Drawable[] + { + CreateFlag(), + } + } + }, + new Drawable[] + { + CreateUsername().With(username => + { + username.Anchor = Anchor.CentreLeft; + username.Origin = Anchor.CentreLeft; + }) + } + } } } }, + new[] + { + Empty(), + Empty() + }, new Drawable[] { CreateStatusIcon().With(icon => From 6b1fdcf9a2f7a3d798f33f3870396416b838eb0a Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 5 Mar 2020 02:38:30 +0300 Subject: [PATCH 0191/2376] Minor adjustments --- osu.Game/Users/UserGridPanel.cs | 2 +- osu.Game/Users/UserPanel.cs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Users/UserGridPanel.cs b/osu.Game/Users/UserGridPanel.cs index b0ce557a19..e62a834d6d 100644 --- a/osu.Game/Users/UserGridPanel.cs +++ b/osu.Game/Users/UserGridPanel.cs @@ -23,7 +23,7 @@ namespace osu.Game.Users [BackgroundDependencyLoader] private void load() { - Background.FadeTo(0.4f); + Background.FadeTo(0.3f); } protected override Drawable CreateLayout() diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index 9d577bcc2a..f0a7895547 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -130,6 +130,7 @@ namespace osu.Game.Users protected OsuSpriteText CreateUsername() => new OsuSpriteText { Font = OsuFont.GetFont(size: 19, weight: FontWeight.Bold, italics: true), + Shadow = false, Text = User.Username, }; From 6b44021f4d0c61163190088ae68433d4694e3452 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 5 Mar 2020 03:33:14 +0300 Subject: [PATCH 0192/2376] Change username text size back to 20 --- osu.Game/Users/UserPanel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index f0a7895547..a900a55dab 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -129,7 +129,7 @@ namespace osu.Game.Users protected OsuSpriteText CreateUsername() => new OsuSpriteText { - Font = OsuFont.GetFont(size: 19, weight: FontWeight.Bold, italics: true), + Font = OsuFont.GetFont(size: 20, weight: FontWeight.Bold, italics: true), Shadow = false, Text = User.Username, }; From ce3786cfd912b2cff9e7431e13cb7761352de8af Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Mar 2020 10:11:27 +0900 Subject: [PATCH 0193/2376] Rename to ModTestScene (is no longer a sandbox) --- .../Mods/TestSceneOsuModDifficultyAdjust.cs | 2 +- .../Visual/{ModSandboxTestScene.cs => ModTestScene.cs} | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) rename osu.Game/Tests/Visual/{ModSandboxTestScene.cs => ModTestScene.cs} (95%) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs index 20cb9ef05d..c2a7a5003f 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs @@ -12,7 +12,7 @@ using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Osu.Tests.Mods { - public class TestSceneOsuModDifficultyAdjust : ModSandboxTestScene + public class TestSceneOsuModDifficultyAdjust : ModTestScene { public override IReadOnlyList RequiredTypes => base.RequiredTypes.Append(typeof(OsuModDifficultyAdjust)).ToList(); diff --git a/osu.Game/Tests/Visual/ModSandboxTestScene.cs b/osu.Game/Tests/Visual/ModTestScene.cs similarity index 95% rename from osu.Game/Tests/Visual/ModSandboxTestScene.cs rename to osu.Game/Tests/Visual/ModTestScene.cs index 8a9cdf009b..1ff061dfac 100644 --- a/osu.Game/Tests/Visual/ModSandboxTestScene.cs +++ b/osu.Game/Tests/Visual/ModTestScene.cs @@ -13,16 +13,16 @@ using osu.Game.Screens.Play; namespace osu.Game.Tests.Visual { - public abstract class ModSandboxTestScene : PlayerTestScene + public abstract class ModTestScene : PlayerTestScene { protected sealed override bool HasCustomSteps => true; public override IReadOnlyList RequiredTypes => new[] { - typeof(ModSandboxTestScene) + typeof(ModTestScene) }; - protected ModSandboxTestScene(Ruleset ruleset) + protected ModTestScene(Ruleset ruleset) : base(ruleset) { } From 2a581ef24786102d41709f08cde6747060418ed1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Mar 2020 10:15:17 +0900 Subject: [PATCH 0194/2376] Remove required types --- .../Mods/TestSceneOsuModDifficultyAdjust.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs index c2a7a5003f..4a284022e2 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs @@ -1,9 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; -using System.Linq; using NUnit.Framework; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Scoring; @@ -14,8 +11,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods { public class TestSceneOsuModDifficultyAdjust : ModTestScene { - public override IReadOnlyList RequiredTypes => base.RequiredTypes.Append(typeof(OsuModDifficultyAdjust)).ToList(); - public TestSceneOsuModDifficultyAdjust() : base(new OsuRuleset()) { From 0f1f1d1a6b91ce361913fbf2aa8bb6fe8a23fa01 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Mar 2020 10:18:37 +0900 Subject: [PATCH 0195/2376] Remove unused "name" parameter --- .../Mods/TestSceneOsuModDifficultyAdjust.cs | 6 +++--- osu.Game/Tests/Visual/ModTestScene.cs | 8 +------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs index 4a284022e2..8ff55c9728 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs @@ -17,21 +17,21 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods } [Test] - public void TestNoAdjustment() => CreateModTest(new ModTestCaseData("no adjustment", new OsuModDifficultyAdjust()) + public void TestNoAdjustment() => CreateModTest(new ModTestCaseData(new OsuModDifficultyAdjust()) { Autoplay = true, PassCondition = () => ((ScoreAccessibleTestPlayer)Player).ScoreProcessor.JudgedHits >= 2 }); [Test] - public void TestCircleSize10() => CreateModTest(new ModTestCaseData("cs = 10", new OsuModDifficultyAdjust { CircleSize = { Value = 10 } }) + public void TestCircleSize10() => CreateModTest(new ModTestCaseData(new OsuModDifficultyAdjust { CircleSize = { Value = 10 } }) { Autoplay = true, PassCondition = () => ((ScoreAccessibleTestPlayer)Player).ScoreProcessor.JudgedHits >= 2 }); [Test] - public void TestApproachRate10() => CreateModTest(new ModTestCaseData("ar = 10", new OsuModDifficultyAdjust { ApproachRate = { Value = 10 } }) + public void TestApproachRate10() => CreateModTest(new ModTestCaseData(new OsuModDifficultyAdjust { ApproachRate = { Value = 10 } }) { Autoplay = true, PassCondition = () => ((ScoreAccessibleTestPlayer)Player).ScoreProcessor.JudgedHits >= 2 diff --git a/osu.Game/Tests/Visual/ModTestScene.cs b/osu.Game/Tests/Visual/ModTestScene.cs index 1ff061dfac..04f93fc683 100644 --- a/osu.Game/Tests/Visual/ModTestScene.cs +++ b/osu.Game/Tests/Visual/ModTestScene.cs @@ -97,19 +97,13 @@ namespace osu.Game.Tests.Visual [CanBeNull] public Func PassCondition; - /// - /// The name of this test case, displayed in the test browser. - /// - public readonly string Name; - /// /// The this test case tests. /// public readonly Mod Mod; - public ModTestCaseData(string name, Mod mod) + public ModTestCaseData(Mod mod) { - Name = name; Mod = mod; } } From 3b19467eadbcdf0bec89b240a7d981a599391acd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Mar 2020 10:19:42 +0900 Subject: [PATCH 0196/2376] ModTestCaseData -> ModTestData --- .../Mods/TestSceneOsuModDifficultyAdjust.cs | 6 +++--- osu.Game/Tests/Visual/ModTestScene.cs | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs index 8ff55c9728..427f25fe11 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs @@ -17,21 +17,21 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods } [Test] - public void TestNoAdjustment() => CreateModTest(new ModTestCaseData(new OsuModDifficultyAdjust()) + public void TestNoAdjustment() => CreateModTest(new ModTestData(new OsuModDifficultyAdjust()) { Autoplay = true, PassCondition = () => ((ScoreAccessibleTestPlayer)Player).ScoreProcessor.JudgedHits >= 2 }); [Test] - public void TestCircleSize10() => CreateModTest(new ModTestCaseData(new OsuModDifficultyAdjust { CircleSize = { Value = 10 } }) + public void TestCircleSize10() => CreateModTest(new ModTestData(new OsuModDifficultyAdjust { CircleSize = { Value = 10 } }) { Autoplay = true, PassCondition = () => ((ScoreAccessibleTestPlayer)Player).ScoreProcessor.JudgedHits >= 2 }); [Test] - public void TestApproachRate10() => CreateModTest(new ModTestCaseData(new OsuModDifficultyAdjust { ApproachRate = { Value = 10 } }) + public void TestApproachRate10() => CreateModTest(new ModTestData(new OsuModDifficultyAdjust { ApproachRate = { Value = 10 } }) { Autoplay = true, PassCondition = () => ((ScoreAccessibleTestPlayer)Player).ScoreProcessor.JudgedHits >= 2 diff --git a/osu.Game/Tests/Visual/ModTestScene.cs b/osu.Game/Tests/Visual/ModTestScene.cs index 04f93fc683..a8b40a5a68 100644 --- a/osu.Game/Tests/Visual/ModTestScene.cs +++ b/osu.Game/Tests/Visual/ModTestScene.cs @@ -27,11 +27,11 @@ namespace osu.Game.Tests.Visual { } - private ModTestCaseData currentTest; + private ModTestData currentTest; - protected void CreateModTest(ModTestCaseData testCaseData) => CreateTest(() => + protected void CreateModTest(ModTestData testData) => CreateTest(() => { - AddStep("set test data", () => currentTest = testCaseData); + AddStep("set test data", () => currentTest = testData); }); public override void TearDownSteps() @@ -78,7 +78,7 @@ namespace osu.Game.Tests.Visual } } - protected class ModTestCaseData + protected class ModTestData { /// /// Whether to use a replay to simulate an auto-play. True by default. @@ -102,7 +102,7 @@ namespace osu.Game.Tests.Visual /// public readonly Mod Mod; - public ModTestCaseData(Mod mod) + public ModTestData(Mod mod) { Mod = mod; } From fadebcdc03188cb171f453153530e55c6be6c7a9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Mar 2020 10:21:25 +0900 Subject: [PATCH 0197/2376] Move all sets to object initialiser for code formatting reasons --- .../Mods/TestSceneOsuModDifficultyAdjust.cs | 9 ++++++--- osu.Game/Tests/Visual/ModTestScene.cs | 7 +------ 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs index 427f25fe11..e4b1e30bcd 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs @@ -17,22 +17,25 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods } [Test] - public void TestNoAdjustment() => CreateModTest(new ModTestData(new OsuModDifficultyAdjust()) + public void TestNoAdjustment() => CreateModTest(new ModTestData() { + Mod = new OsuModDifficultyAdjust(), Autoplay = true, PassCondition = () => ((ScoreAccessibleTestPlayer)Player).ScoreProcessor.JudgedHits >= 2 }); [Test] - public void TestCircleSize10() => CreateModTest(new ModTestData(new OsuModDifficultyAdjust { CircleSize = { Value = 10 } }) + public void TestCircleSize10() => CreateModTest(new ModTestData { + Mod = new OsuModDifficultyAdjust { CircleSize = { Value = 10 } }, Autoplay = true, PassCondition = () => ((ScoreAccessibleTestPlayer)Player).ScoreProcessor.JudgedHits >= 2 }); [Test] - public void TestApproachRate10() => CreateModTest(new ModTestData(new OsuModDifficultyAdjust { ApproachRate = { Value = 10 } }) + public void TestApproachRate10() => CreateModTest(new ModTestData { + Mod = new OsuModDifficultyAdjust { ApproachRate = { Value = 10 } }, Autoplay = true, PassCondition = () => ((ScoreAccessibleTestPlayer)Player).ScoreProcessor.JudgedHits >= 2 }); diff --git a/osu.Game/Tests/Visual/ModTestScene.cs b/osu.Game/Tests/Visual/ModTestScene.cs index a8b40a5a68..3d12001cca 100644 --- a/osu.Game/Tests/Visual/ModTestScene.cs +++ b/osu.Game/Tests/Visual/ModTestScene.cs @@ -100,12 +100,7 @@ namespace osu.Game.Tests.Visual /// /// The this test case tests. /// - public readonly Mod Mod; - - public ModTestData(Mod mod) - { - Mod = mod; - } + public Mod Mod; } } } From 5200633f9fa19b8f6994ed6b7ef0ec51d18befdf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Mar 2020 11:25:07 +0900 Subject: [PATCH 0198/2376] Centralise TestPlayer implementations as much as possible --- .../TestSceneAutoJuiceStream.cs | 3 +- .../TestSceneOsuFlashlight.cs | 4 +- .../TestSceneSkinFallbacks.cs | 5 +- .../TestSceneSpinnerRotation.cs | 5 +- .../TestSceneSwellJudgements.cs | 27 ---------- .../TestSceneTaikoSuddenDeath.cs | 18 ++----- .../Background/TestSceneUserDimBackgrounds.cs | 10 ++-- .../Visual/Gameplay/TestSceneAutoplay.cs | 24 +++------ .../Gameplay/TestSceneGameplayRewinding.cs | 49 ++++--------------- .../Visual/Gameplay/TestScenePause.cs | 7 +-- .../Gameplay/TestScenePauseWhenInactive.cs | 5 +- .../Visual/Gameplay/TestScenePlayerLoader.cs | 13 +---- osu.Game/Tests/Visual/PlayerTestScene.cs | 5 +- osu.Game/Tests/Visual/TestPlayer.cs | 28 +++++++++++ 14 files changed, 63 insertions(+), 140 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs index 74a9c05bf9..ed7bfb9a44 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs @@ -7,7 +7,6 @@ using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; -using osu.Game.Screens.Play; using osu.Game.Tests.Visual; using osuTK; @@ -51,7 +50,7 @@ namespace osu.Game.Rulesets.Catch.Tests return beatmap; } - protected override Player CreatePlayer(Ruleset ruleset) + protected override TestPlayer CreatePlayer(Ruleset ruleset) { SelectedMods.Value = SelectedMods.Value.Concat(new[] { ruleset.GetAutoplayMod() }).ToArray(); return base.CreatePlayer(ruleset); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuFlashlight.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuFlashlight.cs index 412effe176..19736a7709 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuFlashlight.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuFlashlight.cs @@ -3,13 +3,13 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; -using osu.Game.Screens.Play; +using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Osu.Tests { public class TestSceneOsuFlashlight : TestSceneOsuPlayer { - protected override Player CreatePlayer(Ruleset ruleset) + protected override TestPlayer CreatePlayer(Ruleset ruleset) { SelectedMods.Value = new Mod[] { new OsuModAutoplay(), new OsuModFlashlight(), }; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs index 4da1b1dae0..d39e24fc1f 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs @@ -18,7 +18,6 @@ using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Osu.Objects.Drawables; -using osu.Game.Screens.Play; using osu.Game.Skinning; using osu.Game.Storyboards; using osu.Game.Tests.Visual; @@ -56,7 +55,7 @@ namespace osu.Game.Rulesets.Osu.Tests private void checkNextHitObject(string skin) => AddUntilStep($"check skin from {skin}", () => { - var firstObject = ((TestPlayer)Player).DrawableRuleset.Playfield.HitObjectContainer.AliveObjects.OfType().FirstOrDefault(); + var firstObject = Player.DrawableRuleset.Playfield.HitObjectContainer.AliveObjects.OfType().FirstOrDefault(); if (firstObject == null) return false; @@ -75,7 +74,7 @@ namespace osu.Game.Rulesets.Osu.Tests [Resolved] private AudioManager audio { get; set; } - protected override Player CreatePlayer(Ruleset ruleset) => new SkinProvidingPlayer(testUserSkin); + protected override TestPlayer CreatePlayer(Ruleset ruleset) => new SkinProvidingPlayer(testUserSkin); protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) => new CustomSkinWorkingBeatmap(beatmap, storyboard, Clock, audio, testBeatmapSkin); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index 5cf571d961..ea006ec607 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -11,7 +11,6 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; -using osu.Game.Tests.Visual; using osuTK; using System.Collections.Generic; using System.Linq; @@ -44,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Tests base.SetUpSteps(); AddUntilStep("wait for track to start running", () => track.IsRunning); - AddStep("retrieve spinner", () => drawableSpinner = (DrawableSpinner)((TestPlayer)Player).DrawableRuleset.Playfield.AllHitObjects.First()); + AddStep("retrieve spinner", () => drawableSpinner = (DrawableSpinner)Player.DrawableRuleset.Playfield.AllHitObjects.First()); } [Test] @@ -89,7 +88,7 @@ namespace osu.Game.Rulesets.Osu.Tests { AddStep($"seek to {time}", () => track.Seek(time)); - AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, ((TestPlayer)Player).DrawableRuleset.FrameStableClock.CurrentTime, 100)); + AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100)); } protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneSwellJudgements.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneSwellJudgements.cs index ccacc50de1..303f0163b1 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneSwellJudgements.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneSwellJudgements.cs @@ -1,23 +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.Collections.Generic; using System.Linq; using NUnit.Framework; -using osu.Framework.Allocation; using osu.Game.Beatmaps; -using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects; -using osu.Game.Screens.Play; using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Taiko.Tests { public class TestSceneSwellJudgements : PlayerTestScene { - protected new TestPlayer Player => (TestPlayer)base.Player; - public TestSceneSwellJudgements() : base(new TaikoRuleset()) { @@ -49,25 +42,5 @@ namespace osu.Game.Rulesets.Taiko.Tests return beatmap; } - - protected override Player CreatePlayer(Ruleset ruleset) => new TestPlayer(); - - protected class TestPlayer : Player - { - public readonly List Results = new List(); - - public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; - - public TestPlayer() - : base(false, false) - { - } - - [BackgroundDependencyLoader] - private void load() - { - ScoreProcessor.NewJudgement += r => Results.Add(r); - } - } } } diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs index 140433a523..2ab041e191 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs @@ -4,11 +4,9 @@ using System.Linq; using NUnit.Framework; using osu.Game.Beatmaps; -using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Beatmaps; using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Rulesets.Taiko.Objects; -using osu.Game.Screens.Play; using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Taiko.Tests @@ -22,10 +20,10 @@ namespace osu.Game.Rulesets.Taiko.Tests protected override bool AllowFail => true; - protected override Player CreatePlayer(Ruleset ruleset) + protected override TestPlayer CreatePlayer(Ruleset ruleset) { SelectedMods.Value = SelectedMods.Value.Concat(new[] { new TaikoModSuddenDeath() }).ToArray(); - return new ScoreAccessiblePlayer(); + return base.CreatePlayer(ruleset); } protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => @@ -49,20 +47,10 @@ namespace osu.Game.Rulesets.Taiko.Tests AddStep("Setup judgements", () => { judged = false; - ((ScoreAccessiblePlayer)Player).ScoreProcessor.NewJudgement += b => judged = true; + Player.ScoreProcessor.NewJudgement += b => judged = true; }); AddUntilStep("swell judged", () => judged); AddAssert("not failed", () => !Player.HasFailed); } - - private class ScoreAccessiblePlayer : TestPlayer - { - public ScoreAccessiblePlayer() - : base(false, false) - { - } - - public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; - } } } diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs index 6d014ca1ca..06a155e78b 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs @@ -49,7 +49,7 @@ namespace osu.Game.Tests.Visual.Background private DummySongSelect songSelect; private TestPlayerLoader playerLoader; - private TestPlayer player; + private LoadBlockingTestPlayer player; private BeatmapManager manager; private RulesetStore rulesets; @@ -81,7 +81,7 @@ namespace osu.Game.Tests.Visual.Background public void PlayerLoaderSettingsHoverTest() { setupUserSettings(); - AddStep("Start player loader", () => songSelect.Push(playerLoader = new TestPlayerLoader(player = new TestPlayer { BlockLoad = true }))); + AddStep("Start player loader", () => songSelect.Push(playerLoader = new TestPlayerLoader(player = new LoadBlockingTestPlayer { BlockLoad = true }))); AddUntilStep("Wait for Player Loader to load", () => playerLoader?.IsLoaded ?? false); AddAssert("Background retained from song select", () => songSelect.IsBackgroundCurrent()); AddStep("Trigger background preview", () => @@ -268,7 +268,7 @@ namespace osu.Game.Tests.Visual.Background { setupUserSettings(); - AddStep("Start player loader", () => songSelect.Push(playerLoader = new TestPlayerLoader(player = new TestPlayer(allowPause)))); + AddStep("Start player loader", () => songSelect.Push(playerLoader = new TestPlayerLoader(player = new LoadBlockingTestPlayer(allowPause)))); AddUntilStep("Wait for Player Loader to load", () => playerLoader.IsLoaded); AddStep("Move mouse to center of screen", () => InputManager.MoveMouseTo(playerLoader.ScreenPos)); @@ -347,7 +347,7 @@ namespace osu.Game.Tests.Visual.Background public bool IsBlurCorrect() => ((FadeAccessibleBackground)Background).CurrentBlur == new Vector2(BACKGROUND_BLUR); } - private class TestPlayer : Visual.TestPlayer + private class LoadBlockingTestPlayer : TestPlayer { protected override BackgroundScreen CreateBackground() => new FadeAccessibleBackground(Beatmap.Value); @@ -360,7 +360,7 @@ namespace osu.Game.Tests.Visual.Background public readonly Bindable ReplacesBackground = new Bindable(); public readonly Bindable IsPaused = new Bindable(); - public TestPlayer(bool allowPause = true) + public LoadBlockingTestPlayer(bool allowPause = true) : base(allowPause) { } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs index 4daab8d137..756f31e0bf 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs @@ -5,7 +5,6 @@ using System.ComponentModel; using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets; -using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play; using osu.Game.Storyboards; @@ -14,20 +13,22 @@ namespace osu.Game.Tests.Visual.Gameplay [Description("Player instantiated with an autoplay mod.")] public class TestSceneAutoplay : TestSceneAllRulesetPlayers { + protected new TestPlayer Player => (TestPlayer)base.Player; + private ClockBackedTestWorkingBeatmap.TrackVirtualManual track; protected override Player CreatePlayer(Ruleset ruleset) { SelectedMods.Value = SelectedMods.Value.Concat(new[] { ruleset.GetAutoplayMod() }).ToArray(); - return new ScoreAccessiblePlayer(); + return new TestPlayer(false, false); } protected override void AddCheckSteps() { - AddUntilStep("score above zero", () => ((ScoreAccessiblePlayer)Player).ScoreProcessor.TotalScore.Value > 0); - AddUntilStep("key counter counted keys", () => ((ScoreAccessiblePlayer)Player).HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 2)); + AddUntilStep("score above zero", () => Player.ScoreProcessor.TotalScore.Value > 0); + AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 2)); AddStep("rewind", () => track.Seek(-10000)); - AddUntilStep("key counter reset", () => ((ScoreAccessiblePlayer)Player).HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0)); + AddUntilStep("key counter reset", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0)); } protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) @@ -38,18 +39,5 @@ namespace osu.Game.Tests.Visual.Gameplay return working; } - - private class ScoreAccessiblePlayer : TestPlayer - { - public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; - public new HUDOverlay HUDOverlay => base.HUDOverlay; - - public new GameplayClockContainer GameplayClockContainer => base.GameplayClockContainer; - - public ScoreAccessiblePlayer() - : base(false, false) - { - } - } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs index 78c3b22fb9..310746d179 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -11,12 +10,8 @@ using osu.Framework.Utils; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Rulesets; -using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Scoring; -using osu.Game.Rulesets.UI; -using osu.Game.Screens.Play; using osu.Game.Storyboards; using osuTK; @@ -24,8 +19,6 @@ namespace osu.Game.Tests.Visual.Gameplay { public class TestSceneGameplayRewinding : PlayerTestScene { - private RulesetExposingPlayer player => (RulesetExposingPlayer)Player; - [Resolved] private AudioManager audioManager { get; set; } @@ -48,13 +41,13 @@ namespace osu.Game.Tests.Visual.Gameplay { AddUntilStep("wait for track to start running", () => track.IsRunning); addSeekStep(3000); - AddAssert("all judged", () => player.DrawableRuleset.Playfield.AllHitObjects.All(h => h.Judged)); - AddUntilStep("key counter counted keys", () => player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses >= 7)); - AddStep("clear results", () => player.AppliedResults.Clear()); + AddAssert("all judged", () => Player.DrawableRuleset.Playfield.AllHitObjects.All(h => h.Judged)); + AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses >= 7)); + AddStep("clear results", () => Player.Results.Clear()); addSeekStep(0); - AddAssert("none judged", () => player.DrawableRuleset.Playfield.AllHitObjects.All(h => !h.Judged)); - AddUntilStep("key counters reset", () => player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0)); - AddAssert("no results triggered", () => player.AppliedResults.Count == 0); + AddAssert("none judged", () => Player.DrawableRuleset.Playfield.AllHitObjects.All(h => !h.Judged)); + AddUntilStep("key counters reset", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0)); + AddAssert("no results triggered", () => Player.Results.Count == 0); } private void addSeekStep(double time) @@ -62,13 +55,13 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep($"seek to {time}", () => track.Seek(time)); // Allow a few frames of lenience - AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, player.DrawableRuleset.FrameStableClock.CurrentTime, 100)); + AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100)); } - protected override Player CreatePlayer(Ruleset ruleset) + protected override TestPlayer CreatePlayer(Ruleset ruleset) { SelectedMods.Value = SelectedMods.Value.Concat(new[] { ruleset.GetAutoplayMod() }).ToArray(); - return new RulesetExposingPlayer(); + return base.CreatePlayer(ruleset); } protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) @@ -89,29 +82,5 @@ namespace osu.Game.Tests.Visual.Gameplay return beatmap; } - - private class RulesetExposingPlayer : Player - { - public readonly List AppliedResults = new List(); - - public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; - - public new HUDOverlay HUDOverlay => base.HUDOverlay; - - public new GameplayClockContainer GameplayClockContainer => base.GameplayClockContainer; - - public new DrawableRuleset DrawableRuleset => base.DrawableRuleset; - - public RulesetExposingPlayer() - : base(false, false) - { - } - - [BackgroundDependencyLoader] - private void load() - { - ScoreProcessor.NewJudgement += r => AppliedResults.Add(r); - } - } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index ad5bab4681..944e6ca6be 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -11,7 +11,6 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; -using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play; using osuTK; using osuTK.Input; @@ -282,14 +281,10 @@ namespace osu.Game.Tests.Visual.Gameplay protected override bool AllowFail => true; - protected override Player CreatePlayer(Ruleset ruleset) => new PausePlayer(); + protected override TestPlayer CreatePlayer(Ruleset ruleset) => new PausePlayer(); protected class PausePlayer : TestPlayer { - public new HealthProcessor HealthProcessor => base.HealthProcessor; - - public new HUDOverlay HUDOverlay => base.HUDOverlay; - public bool FailOverlayVisible => FailOverlay.State.Value == Visibility.Visible; public bool PauseOverlayVisible => PauseOverlay.State.Value == Visibility.Visible; diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs index 3513b6c25a..a83320048b 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs @@ -9,15 +9,12 @@ using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; -using osu.Game.Screens.Play; namespace osu.Game.Tests.Visual.Gameplay { [HeadlessTest] // we alter unsafe properties on the game host to test inactive window state. public class TestScenePauseWhenInactive : PlayerTestScene { - protected new TestPlayer Player => (TestPlayer)base.Player; - protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) { var beatmap = (Beatmap)base.CreateBeatmap(ruleset); @@ -46,6 +43,6 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("time of pause is after gameplay start time", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= Player.DrawableRuleset.GameplayStartTime); } - protected override Player CreatePlayer(Ruleset ruleset) => new TestPlayer(true, true, true); + protected override TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(true, true, true); } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index 100f99d130..175f909a5a 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -9,7 +9,6 @@ using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Utils; @@ -307,17 +306,7 @@ namespace osu.Game.Tests.Visual.Gameplay public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank; } - private class TestPlayer : Visual.TestPlayer - { - public new Bindable> Mods => base.Mods; - - public TestPlayer(bool allowPause = true, bool showResults = true) - : base(allowPause, showResults) - { - } - } - - protected class SlowLoadPlayer : Visual.TestPlayer + protected class SlowLoadPlayer : TestPlayer { public readonly ManualResetEventSlim AllowLoad = new ManualResetEventSlim(false); diff --git a/osu.Game/Tests/Visual/PlayerTestScene.cs b/osu.Game/Tests/Visual/PlayerTestScene.cs index 7c5ba7d30f..eee31fe014 100644 --- a/osu.Game/Tests/Visual/PlayerTestScene.cs +++ b/osu.Game/Tests/Visual/PlayerTestScene.cs @@ -8,7 +8,6 @@ using osu.Framework.Testing; using osu.Game.Configuration; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; -using osu.Game.Screens.Play; namespace osu.Game.Tests.Visual { @@ -16,7 +15,7 @@ namespace osu.Game.Tests.Visual { private readonly Ruleset ruleset; - protected Player Player; + protected TestPlayer Player; protected PlayerTestScene(Ruleset ruleset) { @@ -69,6 +68,6 @@ namespace osu.Game.Tests.Visual LoadScreen(Player); } - protected virtual Player CreatePlayer(Ruleset ruleset) => new TestPlayer(false, false); + protected virtual TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(false, false); } } diff --git a/osu.Game/Tests/Visual/TestPlayer.cs b/osu.Game/Tests/Visual/TestPlayer.cs index 8e3821f1a0..f016d29f38 100644 --- a/osu.Game/Tests/Visual/TestPlayer.cs +++ b/osu.Game/Tests/Visual/TestPlayer.cs @@ -1,23 +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 osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Screens.Play; namespace osu.Game.Tests.Visual { + /// + /// A player that exposes many components that would otherwise not be available, for testing purposes. + /// public class TestPlayer : Player { protected override bool PauseOnFocusLost { get; } public new DrawableRuleset DrawableRuleset => base.DrawableRuleset; + /// + /// Mods from *player* (not OsuScreen). + /// + public new Bindable> Mods => base.Mods; + + public new HUDOverlay HUDOverlay => base.HUDOverlay; + public new GameplayClockContainer GameplayClockContainer => base.GameplayClockContainer; + public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; + + public new HealthProcessor HealthProcessor => base.HealthProcessor; + + public readonly List Results = new List(); + public TestPlayer(bool allowPause = true, bool showResults = true, bool pauseOnFocusLost = false) : base(allowPause, showResults) { PauseOnFocusLost = pauseOnFocusLost; } + + [BackgroundDependencyLoader] + private void load() + { + ScoreProcessor.NewJudgement += r => Results.Add(r); + } } } From 26ce0d05d6f45f07cb6214b3f1036200f98513e6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Mar 2020 11:33:30 +0900 Subject: [PATCH 0199/2376] Use autoplay mod rather than local replay provider --- .../Mods/TestSceneOsuModDifficultyAdjust.cs | 22 ++-------- osu.Game/Tests/Visual/ModTestScene.cs | 41 ++++++++----------- osu.Game/Tests/Visual/TestReplayPlayer.cs | 24 ----------- 3 files changed, 21 insertions(+), 66 deletions(-) delete mode 100644 osu.Game/Tests/Visual/TestReplayPlayer.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs index e4b1e30bcd..0a98f49526 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs @@ -3,8 +3,6 @@ using NUnit.Framework; using osu.Game.Rulesets.Osu.Mods; -using osu.Game.Rulesets.Scoring; -using osu.Game.Scoring; using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Osu.Tests.Mods @@ -17,11 +15,11 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods } [Test] - public void TestNoAdjustment() => CreateModTest(new ModTestData() + public void TestNoAdjustment() => CreateModTest(new ModTestData { Mod = new OsuModDifficultyAdjust(), Autoplay = true, - PassCondition = () => ((ScoreAccessibleTestPlayer)Player).ScoreProcessor.JudgedHits >= 2 + PassCondition = () => Player.ScoreProcessor.JudgedHits >= 2 }); [Test] @@ -29,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods { Mod = new OsuModDifficultyAdjust { CircleSize = { Value = 10 } }, Autoplay = true, - PassCondition = () => ((ScoreAccessibleTestPlayer)Player).ScoreProcessor.JudgedHits >= 2 + PassCondition = () => Player.ScoreProcessor.JudgedHits >= 2 }); [Test] @@ -37,19 +35,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods { Mod = new OsuModDifficultyAdjust { ApproachRate = { Value = 10 } }, Autoplay = true, - PassCondition = () => ((ScoreAccessibleTestPlayer)Player).ScoreProcessor.JudgedHits >= 2 + PassCondition = () => Player.ScoreProcessor.JudgedHits >= 2 }); - - protected override TestPlayer CreateReplayPlayer(Score score, bool allowFail) => new ScoreAccessibleTestPlayer(score, allowFail); - - private class ScoreAccessibleTestPlayer : TestPlayer - { - public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; - - public ScoreAccessibleTestPlayer(Score score, bool allowFail) - : base(score, allowFail) - { - } - } } } diff --git a/osu.Game/Tests/Visual/ModTestScene.cs b/osu.Game/Tests/Visual/ModTestScene.cs index 3d12001cca..9abe543bf6 100644 --- a/osu.Game/Tests/Visual/ModTestScene.cs +++ b/osu.Game/Tests/Visual/ModTestScene.cs @@ -3,13 +3,10 @@ using System; using System.Collections.Generic; -using System.Linq; using JetBrains.Annotations; using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; -using osu.Game.Scoring; -using osu.Game.Screens.Play; namespace osu.Game.Tests.Visual { @@ -27,52 +24,48 @@ namespace osu.Game.Tests.Visual { } - private ModTestData currentTest; + private ModTestData currentTestData; protected void CreateModTest(ModTestData testData) => CreateTest(() => { - AddStep("set test data", () => currentTest = testData); + AddStep("set test data", () => currentTestData = testData); }); public override void TearDownSteps() { AddUntilStep("test passed", () => { - if (currentTest == null) + if (currentTestData == null) return true; - return currentTest.PassCondition?.Invoke() ?? false; + return currentTestData.PassCondition?.Invoke() ?? false; }); base.TearDownSteps(); } - protected sealed override IBeatmap CreateBeatmap(RulesetInfo ruleset) => currentTest?.Beatmap ?? base.CreateBeatmap(ruleset); + protected sealed override IBeatmap CreateBeatmap(RulesetInfo ruleset) => currentTestData?.Beatmap ?? base.CreateBeatmap(ruleset); - protected sealed override Player CreatePlayer(Ruleset ruleset) + protected sealed override TestPlayer CreatePlayer(Ruleset ruleset) { - SelectedMods.Value = SelectedMods.Value.Append(currentTest.Mod).ToArray(); + var mods = new List(SelectedMods.Value); - var score = currentTest.Autoplay - ? ruleset.GetAutoplayMod().CreateReplayScore(Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo, SelectedMods.Value)) - : null; + if (currentTestData.Mod != null) + mods.Add(currentTestData.Mod); + if (currentTestData.Autoplay) + mods.Add(ruleset.GetAutoplayMod()); - return CreateReplayPlayer(score, AllowFail); + SelectedMods.Value = mods; + + return new ModTestPlayer(AllowFail); } - /// - /// Creates the for a test case. - /// - /// The . - /// Whether the player can fail. - protected virtual TestPlayer CreateReplayPlayer(Score score, bool allowFail) => new TestPlayer(score, allowFail); - - protected class TestPlayer : TestReplayPlayer + protected class ModTestPlayer : TestPlayer { protected override bool AllowFail { get; } - public TestPlayer(Score score, bool allowFail) - : base(score, false, false) + public ModTestPlayer(bool allowFail) + : base(false, false) { AllowFail = allowFail; } diff --git a/osu.Game/Tests/Visual/TestReplayPlayer.cs b/osu.Game/Tests/Visual/TestReplayPlayer.cs deleted file mode 100644 index e99fcc1e37..0000000000 --- a/osu.Game/Tests/Visual/TestReplayPlayer.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.UI; -using osu.Game.Scoring; -using osu.Game.Screens.Play; - -namespace osu.Game.Tests.Visual -{ - public class TestReplayPlayer : ReplayPlayer - { - protected override bool PauseOnFocusLost { get; } - - public new DrawableRuleset DrawableRuleset => base.DrawableRuleset; - - public new GameplayClockContainer GameplayClockContainer => base.GameplayClockContainer; - - public TestReplayPlayer(Score score, bool allowPause = true, bool showResults = true, bool pauseOnFocusLost = false) - : base(score, allowPause, showResults) - { - PauseOnFocusLost = pauseOnFocusLost; - } - } -} From 9a12909f09a377f9348a496f0e95a52c2b51cedf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Mar 2020 11:53:04 +0900 Subject: [PATCH 0200/2376] Test ModDifficultyAdjust is actually taking effect --- .../Mods/TestSceneOsuModDifficultyAdjust.cs | 51 +++++++++++++++++-- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs index 0a98f49526..69415b70e3 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs @@ -1,8 +1,14 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using NUnit.Framework; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Graphics.Containers; using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Osu.Tests.Mods @@ -19,7 +25,15 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods { Mod = new OsuModDifficultyAdjust(), Autoplay = true, - PassCondition = () => Player.ScoreProcessor.JudgedHits >= 2 + PassCondition = checkSomeHit + }); + + [Test] + public void TestCircleSize1() => CreateModTest(new ModTestData + { + Mod = new OsuModDifficultyAdjust { CircleSize = { Value = 1 } }, + Autoplay = true, + PassCondition = () => checkSomeHit() && checkObjectsScale(0.78f) }); [Test] @@ -27,7 +41,15 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods { Mod = new OsuModDifficultyAdjust { CircleSize = { Value = 10 } }, Autoplay = true, - PassCondition = () => Player.ScoreProcessor.JudgedHits >= 2 + PassCondition = () => checkSomeHit() && checkObjectsScale(0.15f) + }); + + [Test] + public void TestApproachRate1() => CreateModTest(new ModTestData + { + Mod = new OsuModDifficultyAdjust { ApproachRate = { Value = 1 } }, + Autoplay = true, + PassCondition = () => checkSomeHit() && checkObjectsPreempt(1680) }); [Test] @@ -35,7 +57,30 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods { Mod = new OsuModDifficultyAdjust { ApproachRate = { Value = 10 } }, Autoplay = true, - PassCondition = () => Player.ScoreProcessor.JudgedHits >= 2 + PassCondition = () => checkSomeHit() && checkObjectsPreempt(450) }); + + private bool checkObjectsPreempt(double target) + { + var objects = Player.ChildrenOfType(); + if (!objects.Any()) + return false; + + return objects.All(o => o.HitObject.TimePreempt == target); + } + + private bool checkObjectsScale(float target) + { + var objects = Player.ChildrenOfType(); + if (!objects.Any()) + return false; + + return objects.All(o => Precision.AlmostEquals(o.ChildrenOfType().First().Children.OfType().Single().Scale.X, target)); + } + + private bool checkSomeHit() + { + return Player.ScoreProcessor.JudgedHits >= 2; + } } } From 7229131d369c19ac8b54ab840c125e3bc83ee9a6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Mar 2020 13:18:42 +0900 Subject: [PATCH 0201/2376] Fix song select max displayable star difficulty getting stuck at wrong maximum --- osu.Game/Configuration/OsuConfigManager.cs | 29 ++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index ce959e9057..c5d68e4efe 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Bindables; using osu.Framework.Configuration; using osu.Framework.Configuration.Tracking; using osu.Framework.Extensions; @@ -126,6 +127,34 @@ namespace osu.Game.Configuration public OsuConfigManager(Storage storage) : base(storage) { + Migrate(); + } + + public void Migrate() + { + // arrives as 2020.123.0 + var rawVersion = Get(OsuSetting.Version); + + if (rawVersion.Length < 6) + return; + + var pieces = rawVersion.Split('.'); + + if (!int.TryParse(pieces[0], out int year)) return; + if (!int.TryParse(pieces[1], out int monthDay)) return; + if (!int.TryParse(pieces[2], out int minor)) return; + + int combined = (year * 10000) + monthDay; + + if (combined < 20200305) + { + // the maximum value of this setting was changed. + // if we don't manually increase this, it causes song select to filter out beatmaps the user expects to see. + var maxStars = (BindableDouble)GetOriginalBindable(OsuSetting.DisplayStarsMaximum); + + if (maxStars.Value == 10) + maxStars.Value = maxStars.MaxValue; + } } public override TrackedSettings CreateTrackedSettings() => new TrackedSettings From 6477a7b73e1d7ede6f77cfb76b15e63d1be20bdb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Mar 2020 13:34:04 +0900 Subject: [PATCH 0202/2376] Centralise creation of UpdateManagers --- osu.Android/OsuGameAndroid.cs | 7 +------ osu.Desktop/OsuGameDesktop.cs | 19 ++++++++++++------- osu.Game/OsuGame.cs | 4 ++++ osu.iOS/OsuGameIOS.cs | 7 ------- 4 files changed, 17 insertions(+), 20 deletions(-) diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index a91c010809..84f215f930 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.cs @@ -30,11 +30,6 @@ namespace osu.Android } } - protected override void LoadComplete() - { - base.LoadComplete(); - - Add(new SimpleUpdateManager()); - } + protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager(); } } \ No newline at end of file diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index f70cc24159..f05ee48914 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -47,20 +47,25 @@ namespace osu.Desktop return null; } + protected override UpdateManager CreateUpdateManager() + { + switch (RuntimeInfo.OS) + { + case RuntimeInfo.Platform.Windows: + return new SquirrelUpdateManager(); + + default: + return new SimpleUpdateManager(); + } + } + protected override void LoadComplete() { base.LoadComplete(); if (!noVersionOverlay) - { LoadComponentAsync(versionManager = new VersionManager { Depth = int.MinValue }, Add); - if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows) - Add(new SquirrelUpdateManager()); - else - Add(new SimpleUpdateManager()); - } - LoadComponentAsync(new DiscordRichPresence(), Add); } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 5781a7fbc4..916464ff53 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -43,6 +43,7 @@ using osu.Game.Overlays.Volume; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Screens.Select; +using osu.Game.Updater; using osu.Game.Utils; using LogLevel = osu.Framework.Logging.LogLevel; @@ -390,6 +391,8 @@ namespace osu.Game protected virtual Loader CreateLoader() => new Loader(); + protected virtual UpdateManager CreateUpdateManager() => new UpdateManager(); + protected override Container CreateScalingContainer() => new ScalingContainer(ScalingMode.Everything); #region Beatmap progression @@ -528,6 +531,7 @@ namespace osu.Game AddRange(new Drawable[] { + CreateUpdateManager(), new VolumeControlReceptor { RelativeSizeAxes = Axes.Both, diff --git a/osu.iOS/OsuGameIOS.cs b/osu.iOS/OsuGameIOS.cs index e5ff4aec95..3a16f81530 100644 --- a/osu.iOS/OsuGameIOS.cs +++ b/osu.iOS/OsuGameIOS.cs @@ -11,12 +11,5 @@ namespace osu.iOS public class OsuGameIOS : OsuGame { public override Version AssemblyVersion => new Version(NSBundle.MainBundle.InfoDictionary["CFBundleVersion"].ToString()); - - protected override void LoadComplete() - { - base.LoadComplete(); - - Add(new UpdateManager()); - } } } From 74b5e76c0ebec3360fb998f4743e461329a12083 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Mar 2020 13:39:55 +0900 Subject: [PATCH 0203/2376] Fix dependency initialisation ordering --- osu.Game/OsuGame.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 916464ff53..50aefb4dd2 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -531,7 +531,6 @@ namespace osu.Game AddRange(new Drawable[] { - CreateUpdateManager(), new VolumeControlReceptor { RelativeSizeAxes = Axes.Both, @@ -632,6 +631,7 @@ namespace osu.Game chatOverlay.State.ValueChanged += state => channelManager.HighPollRate.Value = state.NewValue == Visibility.Visible; Add(externalLinkOpener = new ExternalLinkOpener()); + Add(CreateUpdateManager()); // dependency on notification overlay // side overlays which cancel each other. var singleDisplaySideOverlays = new OverlayContainer[] { Settings, notifications }; From 1e6710020e3adc3da750730ab6f66802dc50488f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Mar 2020 13:46:25 +0900 Subject: [PATCH 0204/2376] Remove minor version for now --- osu.Game/Configuration/OsuConfigManager.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index c5d68e4efe..5b20700086 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -142,7 +142,6 @@ namespace osu.Game.Configuration if (!int.TryParse(pieces[0], out int year)) return; if (!int.TryParse(pieces[1], out int monthDay)) return; - if (!int.TryParse(pieces[2], out int minor)) return; int combined = (year * 10000) + monthDay; From 1e1e8cbcb5915ad1d0db77d34926ebc54d0218aa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Mar 2020 14:46:38 +0900 Subject: [PATCH 0205/2376] Always update version --- osu.Game/Updater/UpdateManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs index 48505a9891..f7a7795d9b 100644 --- a/osu.Game/Updater/UpdateManager.cs +++ b/osu.Game/Updater/UpdateManager.cs @@ -34,12 +34,12 @@ namespace osu.Game.Updater if (game.IsDeployedBuild && version != lastVersion) { - config.Set(OsuSetting.Version, version); - // only show a notification if we've previously saved a version to the config file (ie. not the first run). if (!string.IsNullOrEmpty(lastVersion)) Notifications.Post(new UpdateCompleteNotification(version)); } + + config.Set(OsuSetting.Version, version); } private class UpdateCompleteNotification : SimpleNotification From a311ace62656d854b8136c93ad7f7dcb91e1e9ce Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Mar 2020 14:46:07 +0900 Subject: [PATCH 0206/2376] Add migration test --- .../Visual/Navigation/OsuGameTestScene.cs | 23 +++++++---- .../Navigation/TestSettingsMigration.cs | 41 +++++++++++++++++++ osu.Game/OsuGameBase.cs | 2 +- osu.Game/Tests/Visual/OsuTestScene.cs | 2 +- 4 files changed, 58 insertions(+), 10 deletions(-) create mode 100644 osu.Game.Tests/Visual/Navigation/TestSettingsMigration.cs diff --git a/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs b/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs index 70d71d0952..b0bfb64d61 100644 --- a/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs +++ b/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs @@ -62,14 +62,7 @@ namespace osu.Game.Tests.Visual.Navigation var frameworkConfig = host.Dependencies.Get(); frameworkConfig.GetBindable(FrameworkSetting.CursorSensitivity).Disabled = false; - Game = new TestOsuGame(LocalStorage, API); - Game.SetHost(host); - - // todo: this can be removed once we can run audio tracks without a device present - // see https://github.com/ppy/osu/issues/1302 - Game.LocalConfig.Set(OsuSetting.IntroSequence, IntroSequence.Circles); - - Add(Game); + CreateGame(); }); AddUntilStep("Wait for load", () => Game.IsLoaded); @@ -78,6 +71,18 @@ namespace osu.Game.Tests.Visual.Navigation ConfirmAtMainMenu(); } + protected void CreateGame() + { + Game = new TestOsuGame(LocalStorage, API); + Game.SetHost(host); + + // todo: this can be removed once we can run audio tracks without a device present + // see https://github.com/ppy/osu/issues/1302 + Game.LocalConfig.Set(OsuSetting.IntroSequence, IntroSequence.Circles); + + Add(Game); + } + protected void PushAndConfirm(Func newScreen) { Screen screen = null; @@ -103,6 +108,8 @@ namespace osu.Game.Tests.Visual.Navigation public new Bindable Ruleset => base.Ruleset; + public override string Version => "test game"; + protected override Loader CreateLoader() => new TestLoader(); public new void PerformFromScreen(Action action, IEnumerable validScreens = null) => base.PerformFromScreen(action, validScreens); diff --git a/osu.Game.Tests/Visual/Navigation/TestSettingsMigration.cs b/osu.Game.Tests/Visual/Navigation/TestSettingsMigration.cs new file mode 100644 index 0000000000..c0b77b580e --- /dev/null +++ b/osu.Game.Tests/Visual/Navigation/TestSettingsMigration.cs @@ -0,0 +1,41 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Utils; +using osu.Game.Configuration; + +namespace osu.Game.Tests.Visual.Navigation +{ + public class TestSettingsMigration : OsuGameTestScene + { + public override void RecycleLocalStorage() + { + base.RecycleLocalStorage(); + + using (var config = new OsuConfigManager(LocalStorage)) + { + config.Set(OsuSetting.Version, "2020.101.0"); + config.Set(OsuSetting.DisplayStarsMaximum, 10.0); + } + } + + [Test] + public void TestDisplayStarsMigration() + { + AddAssert("config has migrated value", () => Precision.AlmostEquals(Game.LocalConfig.Get(OsuSetting.DisplayStarsMaximum), 10.1)); + + AddStep("set value again", () => Game.LocalConfig.Set(OsuSetting.DisplayStarsMaximum, 10)); + + AddStep("force save config", () => Game.LocalConfig.Save()); + + AddStep("remove game", () => Remove(Game)); + + AddStep("create game again", CreateGame); + + AddUntilStep("Wait for load", () => Game.IsLoaded); + + AddAssert("config did not migrate value", () => Precision.AlmostEquals(Game.LocalConfig.Get(OsuSetting.DisplayStarsMaximum), 10)); + } + } +} diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index a890331f05..33333e592e 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -97,7 +97,7 @@ namespace osu.Game public bool IsDeployedBuild => AssemblyVersion.Major > 0; - public string Version + public virtual string Version { get { diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index b203557fab..f102e2ece3 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -104,7 +104,7 @@ namespace osu.Game.Tests.Visual base.Content.Add(content = new DrawSizePreservingFillContainer()); } - public void RecycleLocalStorage() + public virtual void RecycleLocalStorage() { if (localStorage?.IsValueCreated == true) { From 507af4fa72cb9c7e98733eb5925198ab4caac756 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Mar 2020 15:35:27 +0900 Subject: [PATCH 0207/2376] Add comment about rationale behind always updating version in config --- osu.Game/Updater/UpdateManager.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs index f7a7795d9b..28a295215f 100644 --- a/osu.Game/Updater/UpdateManager.cs +++ b/osu.Game/Updater/UpdateManager.cs @@ -39,6 +39,8 @@ namespace osu.Game.Updater Notifications.Post(new UpdateCompleteNotification(version)); } + // debug / local compilations will reset to a non-release string. + // can be useful to check when an install has transitioned between release and otherwise (see OsuConfigManager's migrations). config.Set(OsuSetting.Version, version); } From 5b8037ea7d2b69e29ae82eb54aba16e2bd07efe9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Mar 2020 15:36:36 +0900 Subject: [PATCH 0208/2376] Add note about early migration return on non-release transitions --- osu.Game/Configuration/OsuConfigManager.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 5b20700086..21de654670 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -140,6 +140,8 @@ namespace osu.Game.Configuration var pieces = rawVersion.Split('.'); + // on a fresh install or when coming from a non-release build, execution will end here. + // we don't want to run migrations in such cases. if (!int.TryParse(pieces[0], out int year)) return; if (!int.TryParse(pieces[1], out int monthDay)) return; From 646c8fe077eca5b6630b397e38fc5b3c6330e7ac Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Mar 2020 15:40:48 +0900 Subject: [PATCH 0209/2376] Add note about version override --- osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs b/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs index b0bfb64d61..ea8a06e990 100644 --- a/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs +++ b/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs @@ -108,6 +108,7 @@ namespace osu.Game.Tests.Visual.Navigation public new Bindable Ruleset => base.Ruleset; + // 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 9307caa3bfb60e7636033fe78311549da1653487 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 5 Mar 2020 16:58:07 +0900 Subject: [PATCH 0210/2376] Fix typos --- osu.Game/OsuGame.cs | 2 +- osu.Game/OsuGameBase.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 0be9e6cdaa..e54bbaabb2 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -410,7 +410,7 @@ namespace osu.Game void trackCompleted(WorkingBeatmap b) { - // the source of track completion is the audio thread, so the beatmap may have changed before a firing. + // the source of track completion is the audio thread, so the beatmap may have changed before firing. if (Beatmap.Value != b) return; diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 67aa4a8d4d..1048b37348 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -214,7 +214,7 @@ namespace osu.Game // ScheduleAfterChildren is safety against something in the current frame accessing the previous beatmap's track // and potentially causing a reload of it after just unloading. - // Note that the reason for this being added *has* been resolved, so it may be feasible to remover this if required. + // Note that the reason for this being added *has* been resolved, so it may be feasible to removed this if required. Beatmap.BindValueChanged(b => ScheduleAfterChildren(() => { // compare to last beatmap as sometimes the two may share a track representation (optimisation, see WorkingBeatmap.TransferTo) From 0c1775b52281517e8f6f0518fc930b42b0f26aca Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 5 Mar 2020 17:12:14 +0900 Subject: [PATCH 0211/2376] Fix incorrect condition and add test --- .../Visual/Navigation/OsuGameTestScene.cs | 2 ++ .../Navigation/TestSceneScreenNavigation.cs | 16 ++++++++++++++++ osu.Game/OsuGame.cs | 10 +++++----- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs b/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs index 70d71d0952..e984806dc9 100644 --- a/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs +++ b/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs @@ -97,6 +97,8 @@ namespace osu.Game.Tests.Visual.Navigation public new SettingsPanel Settings => base.Settings; + public new MusicController MusicController => base.MusicController; + public new OsuConfigManager LocalConfig => base.LocalConfig; public new Bindable Beatmap => base.Beatmap; diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 8258cc9465..9d603ac471 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -114,6 +114,22 @@ namespace osu.Game.Tests.Visual.Navigation AddAssert("Options overlay was closed", () => Game.Settings.State.Value == Visibility.Hidden); } + [Test] + public void TestWaitForNextTrackInMenu() + { + bool trackCompleted = false; + + AddUntilStep("Wait for music controller", () => Game.MusicController.IsLoaded); + AddStep("Seek close to end", () => + { + Game.MusicController.SeekTo(Game.Beatmap.Value.Track.Length - 1000); + Game.Beatmap.Value.Track.Completed += () => trackCompleted = true; + }); + + AddUntilStep("Track was completed", () => trackCompleted); + AddUntilStep("Track was restarted", () => Game.Beatmap.Value.Track.IsRunning); + } + private void pushEscape() => AddStep("Press escape", () => pressAndRelease(Key.Escape)); diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index e54bbaabb2..19602d524e 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -414,8 +414,8 @@ namespace osu.Game if (Beatmap.Value != b) return; - if (Beatmap.Value.Track.Looping && !Beatmap.Disabled) - musicController.NextTrack(); + if (!Beatmap.Value.Track.Looping && !Beatmap.Disabled) + MusicController.NextTrack(); } } @@ -588,7 +588,7 @@ namespace osu.Game loadComponentSingleFile(new OnScreenDisplay(), Add, true); - loadComponentSingleFile(musicController = new MusicController(), Add, true); + loadComponentSingleFile(MusicController = new MusicController(), Add, true); loadComponentSingleFile(notifications = new NotificationOverlay { @@ -896,7 +896,7 @@ namespace osu.Game private ScalingContainer screenContainer; - private MusicController musicController; + protected MusicController MusicController { get; private set; } protected override bool OnExiting() { @@ -954,7 +954,7 @@ namespace osu.Game { OverlayActivationMode.Value = newOsuScreen.InitialOverlayActivationMode; - musicController.AllowRateAdjustments = newOsuScreen.AllowRateAdjustments; + MusicController.AllowRateAdjustments = newOsuScreen.AllowRateAdjustments; if (newOsuScreen.HideOverlaysOnEnter) CloseAllOverlays(); From 371a54364508eb84a37f1e098bded8584e2c1d8c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Mar 2020 20:25:38 +0900 Subject: [PATCH 0212/2376] 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 1c4a6ffe75..97f7a7edb1 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 4d59b709aa..855bda3679 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -23,7 +23,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 6897d3e625..e2c4c09047 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + @@ -79,7 +79,7 @@ - + From 332f56a7f8f62fa426e1f153cb184a7030658961 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Mar 2020 20:34:24 +0900 Subject: [PATCH 0213/2376] Fix nullref in tests --- osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index c110608a0e..8c264ce974 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -25,7 +25,7 @@ namespace osu.Game.Screens.Select.Carousel { base.Filter(criteria); - if (Beatmap.BeatmapSet.Equals(criteria.SelectedBeatmapSet)) + if (Beatmap.BeatmapSet?.Equals(criteria.SelectedBeatmapSet) == true) { // bypass filtering for selected beatmap Filtered.Value = false; From 0477ef6c130052d0dc95d7fc78e0c2cc2e8a9a30 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Mar 2020 21:45:19 +0900 Subject: [PATCH 0214/2376] Force a selection after filtering to ensure correct difficulty is selected --- osu.Game/Screens/Select/SongSelect.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 6577ed8506..528222a89c 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -397,6 +397,8 @@ namespace osu.Game.Screens.Select var criteria = FilterControl.CreateCriteria(); criteria.SelectedBeatmapSet = e.NewValue.BeatmapInfo.BeatmapSet; Carousel.Filter(criteria); + + Carousel.SelectBeatmap(e.NewValue.BeatmapInfo); } } From 583e2c3f4a616fea8d2b9abc7065135e917217d3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Mar 2020 00:10:05 +0900 Subject: [PATCH 0215/2376] Actually check rate is applied --- .../Mods/TestSceneOsuModDoubleTime.cs | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDoubleTime.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDoubleTime.cs index deb733c581..dcf19ad993 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDoubleTime.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDoubleTime.cs @@ -2,14 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; +using osu.Framework.Utils; using osu.Game.Rulesets.Osu.Mods; -using osu.Game.Rulesets.Scoring; -using osu.Game.Scoring; using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Osu.Tests.Mods { - public class TestSceneOsuModDoubleTime : ModSandboxTestScene + public class TestSceneOsuModDoubleTime : ModTestScene { public TestSceneOsuModDoubleTime() : base(new OsuRuleset()) @@ -21,21 +20,16 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods [TestCase(1.5)] [TestCase(2)] [TestCase(5)] - public void TestDefaultRate(double rate) => CreateModTest(new ModTestCaseData("1.5x", new OsuModDoubleTime { SpeedChange = { Value = rate } }) + public void TestSpeedChangeCustomisation(double rate) { - PassCondition = () => ((ScoreAccessibleTestPlayer)Player).ScoreProcessor.JudgedHits >= 2 - }); + var mod = new OsuModDoubleTime { SpeedChange = { Value = rate } }; - protected override TestPlayer CreateReplayPlayer(Score score) => new ScoreAccessibleTestPlayer(score); - - private class ScoreAccessibleTestPlayer : TestPlayer - { - public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; - - public ScoreAccessibleTestPlayer(Score score) - : base(score) + CreateModTest(new ModTestData { - } + Mod = mod, + PassCondition = () => Player.ScoreProcessor.JudgedHits >= 2 && + Precision.AlmostEquals(Player.GameplayClockContainer.GameplayClock.Rate, mod.SpeedChange.Value) + }); } } } From ece263131b6c4d89a9580c70f34fdf36837b4c4a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Mar 2020 00:36:05 +0900 Subject: [PATCH 0216/2376] Update to follow new naming/structure --- osu.Game/Tests/Visual/ModPerfectTestScene.cs | 20 +++++++------------- osu.Game/Tests/Visual/ModTestScene.cs | 2 +- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/osu.Game/Tests/Visual/ModPerfectTestScene.cs b/osu.Game/Tests/Visual/ModPerfectTestScene.cs index 31d2ce9281..4bc00425bf 100644 --- a/osu.Game/Tests/Visual/ModPerfectTestScene.cs +++ b/osu.Game/Tests/Visual/ModPerfectTestScene.cs @@ -1,29 +1,28 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Extensions.TypeExtensions; using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; -using osu.Game.Scoring; namespace osu.Game.Tests.Visual { - public abstract class ModPerfectTestScene : ModSandboxTestScene + public abstract class ModPerfectTestScene : ModTestScene { private readonly Ruleset ruleset; - private readonly ModPerfect perfectMod; + private readonly ModPerfect mod; - protected ModPerfectTestScene(Ruleset ruleset, ModPerfect perfectMod) + protected ModPerfectTestScene(Ruleset ruleset, ModPerfect mod) : base(ruleset) { this.ruleset = ruleset; - this.perfectMod = perfectMod; + this.mod = mod; } - protected void CreateHitObjectTest(HitObjectTestCase testCaseData, bool shouldMiss) => CreateModTest(new ModTestCaseData(testCaseData.HitObject.GetType().ReadableName(), perfectMod) + protected void CreateHitObjectTest(HitObjectTestCase testCaseData, bool shouldMiss) => CreateModTest(new ModTestData { + Mod = mod, Beatmap = new Beatmap { BeatmapInfo = { Ruleset = ruleset.RulesetInfo }, @@ -33,15 +32,10 @@ namespace osu.Game.Tests.Visual PassCondition = () => ((PerfectModTestPlayer)Player).CheckFailed(shouldMiss && testCaseData.FailOnMiss) }); - protected sealed override TestPlayer CreateReplayPlayer(Score score) => new PerfectModTestPlayer(score); + protected override TestPlayer CreatePlayer(Ruleset ruleset) => new PerfectModTestPlayer(); private class PerfectModTestPlayer : TestPlayer { - public PerfectModTestPlayer(Score score) - : base(score) - { - } - protected override bool AllowFail => true; public bool CheckFailed(bool failed) diff --git a/osu.Game/Tests/Visual/ModTestScene.cs b/osu.Game/Tests/Visual/ModTestScene.cs index 9abe543bf6..eb418304d9 100644 --- a/osu.Game/Tests/Visual/ModTestScene.cs +++ b/osu.Game/Tests/Visual/ModTestScene.cs @@ -46,7 +46,7 @@ namespace osu.Game.Tests.Visual protected sealed override IBeatmap CreateBeatmap(RulesetInfo ruleset) => currentTestData?.Beatmap ?? base.CreateBeatmap(ruleset); - protected sealed override TestPlayer CreatePlayer(Ruleset ruleset) + protected override TestPlayer CreatePlayer(Ruleset ruleset) { var mods = new List(SelectedMods.Value); From e3509c742c4ec1bb9df56d4a3c1f733ad4587f58 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Mar 2020 01:28:59 +0900 Subject: [PATCH 0217/2376] Track time in a simpler way in TrackVirtualManual --- osu.Game/Tests/Visual/OsuTestScene.cs | 47 ++++++++++++++++++++------- 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index 917d12ebb1..3c95b990e1 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -229,10 +229,10 @@ namespace osu.Game.Tests.Visual /// public class TrackVirtualManual : Track { - private readonly StopwatchClock stopwatchClock = new StopwatchClock(); - private readonly IFrameBasedClock referenceClock; + private bool running; + public TrackVirtualManual(IFrameBasedClock referenceClock) { this.referenceClock = referenceClock; @@ -241,32 +241,55 @@ namespace osu.Game.Tests.Visual public override bool Seek(double seek) { - var offset = Math.Clamp(seek, 0, Length); + accumulated = Math.Min(seek, Length); + lastReferenceTime = null; - stopwatchClock.Seek(offset); - - return offset == seek; + return accumulated == seek; } - public override void Start() => stopwatchClock.Start(); + public override void Start() + { + running = true; + } public override void Reset() { - stopwatchClock.Seek(0); + Seek(0); base.Reset(); } - public override void Stop() => stopwatchClock.Stop(); + public override void Stop() + { + if (running) + { + running = false; + lastReferenceTime = null; + } + } - public override bool IsRunning => stopwatchClock.IsRunning; + public override bool IsRunning => running; - public override double CurrentTime => stopwatchClock.CurrentTime; + private double? lastReferenceTime; + + private double accumulated; + + public override double CurrentTime => Math.Min(accumulated, Length); protected override void UpdateState() { base.UpdateState(); - stopwatchClock.Rate = Rate * referenceClock.Rate; + if (running) + { + double refTime = referenceClock.CurrentTime; + + if (lastReferenceTime.HasValue) + accumulated += (refTime - lastReferenceTime.Value) * Rate; + + lastReferenceTime = refTime; + } + + Console.WriteLine($"t={CurrentTime}"); if (CurrentTime >= Length) { From ebc86c10754ede8686e06b06682c272ff117b562 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Mar 2020 02:08:49 +0900 Subject: [PATCH 0218/2376] Fix random test failure --- osu.Game/Screens/Play/SkipOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/SkipOverlay.cs b/osu.Game/Screens/Play/SkipOverlay.cs index 3daf5b1ff1..ac7e509c2c 100644 --- a/osu.Game/Screens/Play/SkipOverlay.cs +++ b/osu.Game/Screens/Play/SkipOverlay.cs @@ -116,7 +116,7 @@ namespace osu.Game.Screens.Play { base.Update(); - var progress = Math.Max(0, 1 - (gameplayClock.CurrentTime - displayTime) / (fadeOutBeginTime - displayTime)); + var progress = fadeOutBeginTime <= displayTime ? 1 : Math.Max(0, 1 - (gameplayClock.CurrentTime - displayTime) / (fadeOutBeginTime - displayTime)); remainingTimeBox.Width = (float)Interpolation.Lerp(remainingTimeBox.Width, progress, Math.Clamp(Time.Elapsed / 40, 0, 1)); From c3ad08f230b635f4dcc8582c5ee2028f04cd25c8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Mar 2020 02:18:35 +0900 Subject: [PATCH 0219/2376] Remove wild writeline --- osu.Game/Tests/Visual/OsuTestScene.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index 3c95b990e1..5623435da4 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -289,8 +289,6 @@ namespace osu.Game.Tests.Visual lastReferenceTime = refTime; } - Console.WriteLine($"t={CurrentTime}"); - if (CurrentTime >= Length) { Stop(); From bd1dbea6f4b35937777ef3f5b017d2bf787e35e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 5 Mar 2020 23:10:14 +0100 Subject: [PATCH 0220/2376] Centralise background colour updates --- .../Profile/Sections/ProfileItemContainer.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/ProfileItemContainer.cs b/osu.Game/Overlays/Profile/Sections/ProfileItemContainer.cs index 1ab9ed3f82..c057ebe12b 100644 --- a/osu.Game/Overlays/Profile/Sections/ProfileItemContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/ProfileItemContainer.cs @@ -27,8 +27,7 @@ namespace osu.Game.Overlays.Profile.Sections set { idleColour = value; - if (!IsHovered) - background.Colour = value; + fadeBackgroundColour(); } } @@ -40,8 +39,7 @@ namespace osu.Game.Overlays.Profile.Sections set { hoverColour = value; - if (IsHovered) - background.Colour = value; + fadeBackgroundColour(); } } @@ -73,14 +71,19 @@ namespace osu.Game.Overlays.Profile.Sections protected override bool OnHover(HoverEvent e) { - background.FadeColour(HoverColour, hover_duration, Easing.OutQuint); + fadeBackgroundColour(hover_duration); return base.OnHover(e); } protected override void OnHoverLost(HoverLostEvent e) { base.OnHoverLost(e); - background.FadeColour(IdleColour, hover_duration, Easing.OutQuint); + fadeBackgroundColour(hover_duration); + } + + private void fadeBackgroundColour(double fadeDuration = 0) + { + background.FadeColour(IsHovered ? HoverColour : IdleColour, fadeDuration, Easing.OutQuint); } } } From 5b0846cb69a2f5aca2da311d179169fc1c0f5deb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 5 Mar 2020 23:15:53 +0100 Subject: [PATCH 0221/2376] Handle hover explicitly --- osu.Game/Overlays/Profile/Sections/ProfileItemContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/Sections/ProfileItemContainer.cs b/osu.Game/Overlays/Profile/Sections/ProfileItemContainer.cs index c057ebe12b..afa6bd9f79 100644 --- a/osu.Game/Overlays/Profile/Sections/ProfileItemContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/ProfileItemContainer.cs @@ -72,7 +72,7 @@ namespace osu.Game.Overlays.Profile.Sections protected override bool OnHover(HoverEvent e) { fadeBackgroundColour(hover_duration); - return base.OnHover(e); + return true; } protected override void OnHoverLost(HoverLostEvent e) From 1318f242c1cdd710b52ace6de9ce881ec24cb1fb Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Fri, 6 Mar 2020 02:12:30 +0300 Subject: [PATCH 0222/2376] Revert changes to basic implementation and remove redundant stuff --- .../Online/TestSceneBeatmapListingOverlay.cs | 19 -------- .../TestSceneBeatmapListingSearchSection.cs | 25 ++-------- .../BeatmapListingSearchSection.cs | 33 ++++--------- .../BeatmapListing/BeatmapSearchParameters.cs | 30 ------------ osu.Game/Overlays/BeatmapListingOverlay.cs | 47 +++++-------------- 5 files changed, 28 insertions(+), 126 deletions(-) delete mode 100644 osu.Game/Overlays/BeatmapListing/BeatmapSearchParameters.cs diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs index 4dceb57129..7c05d99c59 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using osu.Game.Overlays; using NUnit.Framework; -using osu.Game.Online.API.Requests; namespace osu.Game.Tests.Visual.Online { @@ -25,24 +24,6 @@ namespace osu.Game.Tests.Visual.Online Add(overlay = new BeatmapListingOverlay()); } - [Test] - public void TestShowTag() - { - AddStep("Show Rem tag", () => overlay.ShowTag("Rem")); - } - - [Test] - public void TestShowGenre() - { - AddStep("Show Anime genre", () => overlay.ShowGenre(BeatmapSearchGenre.Anime)); - } - - [Test] - public void TestShowLanguage() - { - AddStep("Show Japanese language", () => overlay.ShowLanguage(BeatmapSearchLanguage.Japanese)); - } - [Test] public void TestShow() { diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchSection.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchSection.cs index f809c780f1..69e3fbd75f 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchSection.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchSection.cs @@ -11,7 +11,6 @@ using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; using osu.Game.Overlays.BeatmapListing; -using osu.Game.Online.API.Requests; using osuTK; namespace osu.Game.Tests.Visual.UserInterface @@ -57,14 +56,11 @@ namespace osu.Game.Tests.Visual.UserInterface } }); - section.SearchParameters.BindValueChanged(parameters => - { - query.Text = $"Query: {parameters.NewValue.Query}"; - ruleset.Text = $"Ruleset: {parameters.NewValue.Ruleset}"; - category.Text = $"Category: {parameters.NewValue.Category}"; - genre.Text = $"Genre: {parameters.NewValue.Genre}"; - language.Text = $"Language: {parameters.NewValue.Language}"; - }, true); + section.Query.BindValueChanged(q => query.Text = $"Query: {q.NewValue}", true); + section.Ruleset.BindValueChanged(r => ruleset.Text = $"Ruleset: {r.NewValue}", true); + section.Category.BindValueChanged(c => category.Text = $"Category: {c.NewValue}", true); + section.Genre.BindValueChanged(g => genre.Text = $"Genre: {g.NewValue}", true); + section.Language.BindValueChanged(l => language.Text = $"Language: {l.NewValue}", true); } [Test] @@ -75,17 +71,6 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("Set null beatmap", () => section.BeatmapSet = null); } - [Test] - public void TestParametersSet() - { - AddStep("Set big black tag", () => section.SetTag("big black")); - AddAssert("Check query is big black", () => section.SearchParameters.Value.Query == "big black"); - AddStep("Set anime genre", () => section.SetGenre(BeatmapSearchGenre.Anime)); - AddAssert("Check genre is anime", () => section.SearchParameters.Value.Genre == BeatmapSearchGenre.Anime); - AddStep("Set japanese language", () => section.SetLanguage(BeatmapSearchLanguage.Japanese)); - AddAssert("Check language is japanese", () => section.SearchParameters.Value.Language == BeatmapSearchLanguage.Japanese); - } - private static readonly BeatmapSetInfo beatmap_set = new BeatmapSetInfo { OnlineInfo = new BeatmapSetOnlineInfo diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchSection.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchSection.cs index 121b101861..501abbf2c8 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchSection.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchSection.cs @@ -13,12 +13,21 @@ using osu.Game.Beatmaps; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osuTK.Graphics; +using osu.Game.Rulesets; namespace osu.Game.Overlays.BeatmapListing { public class BeatmapListingSearchSection : CompositeDrawable { - public Bindable SearchParameters = new Bindable(); + public Bindable Query => textBox.Current; + + public Bindable Ruleset => modeFilter.Current; + + public Bindable Category => categoryFilter.Current; + + public Bindable Genre => genreFilter.Current; + + public Bindable Language => languageFilter.Current; public BeatmapSetInfo BeatmapSet { @@ -105,12 +114,6 @@ namespace osu.Game.Overlays.BeatmapListing }); categoryFilter.Current.Value = BeatmapSearchCategory.Leaderboard; - - textBox.Current.BindValueChanged(_ => changeSearchParameters()); - modeFilter.Current.BindValueChanged(_ => changeSearchParameters()); - categoryFilter.Current.BindValueChanged(_ => changeSearchParameters()); - genreFilter.Current.BindValueChanged(_ => changeSearchParameters()); - languageFilter.Current.BindValueChanged(_ => changeSearchParameters(), true); } [BackgroundDependencyLoader] @@ -119,22 +122,6 @@ namespace osu.Game.Overlays.BeatmapListing background.Colour = colourProvider.Dark6; } - public void SetTag(string tag) => textBox.Current.Value = tag; - - public void SetGenre(BeatmapSearchGenre genre) => genreFilter.Current.Value = genre; - - public void SetLanguage(BeatmapSearchLanguage language) => languageFilter.Current.Value = language; - - private void changeSearchParameters() - { - SearchParameters.Value = new BeatmapSearchParameters( - textBox.Current.Value, - modeFilter.Current.Value, - categoryFilter.Current.Value, - genreFilter.Current.Value, - languageFilter.Current.Value); - } - private class BeatmapSearchTextBox : SearchTextBox { protected override Color4 SelectionColour => Color4.Gray; diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchParameters.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchParameters.cs deleted file mode 100644 index 6a681503f5..0000000000 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchParameters.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Online.API.Requests; -using osu.Game.Rulesets; - -namespace osu.Game.Overlays.BeatmapListing -{ - public class BeatmapSearchParameters - { - public readonly string Query; - - public readonly RulesetInfo Ruleset; - - public readonly BeatmapSearchCategory Category; - - public readonly BeatmapSearchGenre Genre; - - public readonly BeatmapSearchLanguage Language; - - public BeatmapSearchParameters(string query, RulesetInfo ruleset, BeatmapSearchCategory category, BeatmapSearchGenre genre, BeatmapSearchLanguage language) - { - Query = query; - Ruleset = ruleset; - Category = category; - Genre = genre; - Language = language; - } - } -} diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index ebe4b7fe61..1a5257457f 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -153,43 +153,22 @@ namespace osu.Game.Overlays var sortCriteria = sortControl.Current; var sortDirection = sortControl.SortDirection; - searchSection.SearchParameters.BindValueChanged(parameters => + searchSection.Query.BindValueChanged(query => { - if (parameters.OldValue.Query != parameters.NewValue.Query) - { - sortCriteria.Value = string.IsNullOrEmpty(parameters.NewValue.Query) ? DirectSortCriteria.Ranked : DirectSortCriteria.Relevance; - sortDirection.Value = SortDirection.Descending; - - queueUpdateSearch(true); - } - else - { - queueUpdateSearch(); - } + sortCriteria.Value = string.IsNullOrEmpty(query.NewValue) ? DirectSortCriteria.Ranked : DirectSortCriteria.Relevance; + sortDirection.Value = SortDirection.Descending; + queueUpdateSearch(true); }); + searchSection.Ruleset.BindValueChanged(_ => queueUpdateSearch()); + searchSection.Category.BindValueChanged(_ => queueUpdateSearch()); + searchSection.Genre.BindValueChanged(_ => queueUpdateSearch()); + searchSection.Language.BindValueChanged(_ => queueUpdateSearch()); + sortCriteria.BindValueChanged(_ => queueUpdateSearch()); sortDirection.BindValueChanged(_ => queueUpdateSearch()); } - public void ShowTag(string tag) - { - searchSection.SetTag(tag); - Show(); - } - - public void ShowGenre(BeatmapSearchGenre genre) - { - searchSection.SetGenre(genre); - Show(); - } - - public void ShowLanguage(BeatmapSearchLanguage language) - { - searchSection.SetLanguage(language); - Show(); - } - private ScheduledDelegate queryChangedDebounce; private LoadingLayer loadingLayer; @@ -218,13 +197,13 @@ namespace osu.Game.Overlays loadingLayer.Show(); - getSetsRequest = new SearchBeatmapSetsRequest(searchSection.SearchParameters.Value.Query, searchSection.SearchParameters.Value.Ruleset) + getSetsRequest = new SearchBeatmapSetsRequest(searchSection.Query.Value, searchSection.Ruleset.Value) { - SearchCategory = searchSection.SearchParameters.Value.Category, + SearchCategory = searchSection.Category.Value, SortCriteria = sortControl.Current.Value, SortDirection = sortControl.SortDirection.Value, - Genre = searchSection.SearchParameters.Value.Genre, - Language = searchSection.SearchParameters.Value.Language + Genre = searchSection.Genre.Value, + Language = searchSection.Language.Value }; getSetsRequest.Success += response => Schedule(() => recreatePanels(response)); From c7384b9717a7cf9d063a502a8b219c04f98cc991 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Fri, 6 Mar 2020 03:09:43 +0300 Subject: [PATCH 0223/2376] Implement BeatmapListingSearchHandler component --- .../Online/TestSceneBeatmapListingOverlay.cs | 2 + .../BeatmapListingSearchHandler.cs | 163 +++++++++++++++++ osu.Game/Overlays/BeatmapListingOverlay.cs | 172 +++--------------- 3 files changed, 191 insertions(+), 146 deletions(-) create mode 100644 osu.Game/Overlays/BeatmapListing/BeatmapListingSearchHandler.cs diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs index 7c05d99c59..f80687e142 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using osu.Game.Overlays; using NUnit.Framework; +using osu.Game.Overlays.BeatmapListing; namespace osu.Game.Tests.Visual.Online { @@ -13,6 +14,7 @@ namespace osu.Game.Tests.Visual.Online public override IReadOnlyList RequiredTypes => new[] { typeof(BeatmapListingOverlay), + typeof(BeatmapListingSearchHandler) }; protected override bool UseOnlineAPI => true; diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchHandler.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchHandler.cs new file mode 100644 index 0000000000..ce3d37fb98 --- /dev/null +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchHandler.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.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Threading; +using osu.Game.Beatmaps; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Overlays.Direct; +using osu.Game.Rulesets; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Overlays.BeatmapListing +{ + public class BeatmapListingSearchHandler : CompositeDrawable + { + public Action> SearchFinished; + public Action SearchStarted; + + [Resolved] + private IAPIProvider api { get; set; } + + [Resolved] + private RulesetStore rulesets { get; set; } + + private readonly BeatmapListingSearchSection searchSection; + private readonly BeatmapListingSortTabControl sortControl; + private readonly Box sortControlBackground; + + private SearchBeatmapSetsRequest getSetsRequest; + + public BeatmapListingSearchHandler() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + InternalChild = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 10), + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Masking = true, + EdgeEffect = new EdgeEffectParameters + { + Colour = Color4.Black.Opacity(0.25f), + Type = EdgeEffectType.Shadow, + Radius = 3, + Offset = new Vector2(0f, 1f), + }, + Child = searchSection = new BeatmapListingSearchSection(), + }, + new Container + { + RelativeSizeAxes = Axes.X, + Height = 40, + Children = new Drawable[] + { + sortControlBackground = new Box + { + RelativeSizeAxes = Axes.Both + }, + sortControl = new BeatmapListingSortTabControl + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Margin = new MarginPadding { Left = 20 } + } + } + } + } + }; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + sortControlBackground.Colour = colourProvider.Background5; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + var sortCriteria = sortControl.Current; + var sortDirection = sortControl.SortDirection; + + searchSection.Query.BindValueChanged(query => + { + sortCriteria.Value = string.IsNullOrEmpty(query.NewValue) ? DirectSortCriteria.Ranked : DirectSortCriteria.Relevance; + sortDirection.Value = SortDirection.Descending; + queueUpdateSearch(true); + }); + + searchSection.Ruleset.BindValueChanged(_ => queueUpdateSearch()); + searchSection.Category.BindValueChanged(_ => queueUpdateSearch()); + searchSection.Genre.BindValueChanged(_ => queueUpdateSearch()); + searchSection.Language.BindValueChanged(_ => queueUpdateSearch()); + + sortCriteria.BindValueChanged(_ => queueUpdateSearch()); + sortDirection.BindValueChanged(_ => queueUpdateSearch()); + } + + private ScheduledDelegate queryChangedDebounce; + + private void queueUpdateSearch(bool queryTextChanged = false) + { + SearchStarted?.Invoke(); + + getSetsRequest?.Cancel(); + + queryChangedDebounce?.Cancel(); + queryChangedDebounce = Scheduler.AddDelayed(updateSearch, queryTextChanged ? 500 : 100); + } + + private void updateSearch() + { + getSetsRequest = new SearchBeatmapSetsRequest(searchSection.Query.Value, searchSection.Ruleset.Value) + { + SearchCategory = searchSection.Category.Value, + SortCriteria = sortControl.Current.Value, + SortDirection = sortControl.SortDirection.Value, + Genre = searchSection.Genre.Value, + Language = searchSection.Language.Value + }; + + getSetsRequest.Success += response => Schedule(() => onSearchFinished(response)); + + api.Queue(getSetsRequest); + } + + private void onSearchFinished(SearchBeatmapSetsResponse response) + { + var beatmaps = response.BeatmapSets.Select(r => r.ToBeatmapSet(rulesets)).ToList(); + + searchSection.BeatmapSet = response.Total == 0 ? null : beatmaps.First(); + + SearchFinished?.Invoke(beatmaps); + } + + protected override void Dispose(bool isDisposing) + { + getSetsRequest?.Cancel(); + queryChangedDebounce?.Cancel(); + + base.Dispose(isDisposing); + } + } +} diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index 1a5257457f..dd8dc4a79d 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -1,27 +1,23 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using System.Linq; +using System.Threading; using osu.Framework.Allocation; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; -using osu.Framework.Threading; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; -using osu.Game.Online.API.Requests; using osu.Game.Overlays.BeatmapListing; using osu.Game.Overlays.Direct; -using osu.Game.Rulesets; using osuTK; -using osuTK.Graphics; namespace osu.Game.Overlays { @@ -30,14 +26,9 @@ namespace osu.Game.Overlays [Resolved] private PreviewTrackManager previewTrackManager { get; set; } - [Resolved] - private RulesetStore rulesets { get; set; } - - private SearchBeatmapSetsRequest getSetsRequest; - private Drawable currentContent; - private BeatmapListingSearchSection searchSection; - private BeatmapListingSortTabControl sortControl; + private LoadingLayer loadingLayer; + private Container panelTarget; public BeatmapListingOverlay() : base(OverlayColourScheme.Blue) @@ -63,27 +54,13 @@ namespace osu.Game.Overlays AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 10), Children = new Drawable[] { - new FillFlowContainer + new BeatmapListingHeader(), + new BeatmapListingSearchHandler { - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Direction = FillDirection.Vertical, - Masking = true, - EdgeEffect = new EdgeEffectParameters - { - Colour = Color4.Black.Opacity(0.25f), - Type = EdgeEffectType.Shadow, - Radius = 3, - Offset = new Vector2(0f, 1f), - }, - Children = new Drawable[] - { - new BeatmapListingHeader(), - searchSection = new BeatmapListingSearchSection(), - } + SearchStarted = onSearchStarted, + SearchFinished = onSearchFinished, }, new Container { @@ -96,132 +73,41 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.Both, Colour = ColourProvider.Background4, }, - new FillFlowContainer + panelTarget = new Container { - RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Children = new Drawable[] - { - new Container - { - RelativeSizeAxes = Axes.X, - Height = 40, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = ColourProvider.Background5 - }, - sortControl = new BeatmapListingSortTabControl - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Margin = new MarginPadding { Left = 20 } - } - } - }, - new Container - { - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Padding = new MarginPadding { Horizontal = 20 }, - Children = new Drawable[] - { - panelTarget = new Container - { - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - }, - loadingLayer = new LoadingLayer(panelTarget), - } - }, - } - } + RelativeSizeAxes = Axes.X, + Padding = new MarginPadding { Horizontal = 20 } + }, + loadingLayer = new LoadingLayer(panelTarget) } - } + }, } } } }; } - protected override void LoadComplete() + private CancellationTokenSource cancellationToken; + + private void onSearchStarted() { - base.LoadComplete(); - - var sortCriteria = sortControl.Current; - var sortDirection = sortControl.SortDirection; - - searchSection.Query.BindValueChanged(query => - { - sortCriteria.Value = string.IsNullOrEmpty(query.NewValue) ? DirectSortCriteria.Ranked : DirectSortCriteria.Relevance; - sortDirection.Value = SortDirection.Descending; - queueUpdateSearch(true); - }); - - searchSection.Ruleset.BindValueChanged(_ => queueUpdateSearch()); - searchSection.Category.BindValueChanged(_ => queueUpdateSearch()); - searchSection.Genre.BindValueChanged(_ => queueUpdateSearch()); - searchSection.Language.BindValueChanged(_ => queueUpdateSearch()); - - sortCriteria.BindValueChanged(_ => queueUpdateSearch()); - sortDirection.BindValueChanged(_ => queueUpdateSearch()); - } - - private ScheduledDelegate queryChangedDebounce; - - private LoadingLayer loadingLayer; - private Container panelTarget; - - private void queueUpdateSearch(bool queryTextChanged = false) - { - getSetsRequest?.Cancel(); - - queryChangedDebounce?.Cancel(); - queryChangedDebounce = Scheduler.AddDelayed(updateSearch, queryTextChanged ? 500 : 100); - } - - private void updateSearch() - { - if (!IsLoaded) - return; - - if (State.Value == Visibility.Hidden) - return; - - if (API == null) - return; + cancellationToken?.Cancel(); previewTrackManager.StopAnyPlaying(this); - loadingLayer.Show(); - - getSetsRequest = new SearchBeatmapSetsRequest(searchSection.Query.Value, searchSection.Ruleset.Value) - { - SearchCategory = searchSection.Category.Value, - SortCriteria = sortControl.Current.Value, - SortDirection = sortControl.SortDirection.Value, - Genre = searchSection.Genre.Value, - Language = searchSection.Language.Value - }; - - getSetsRequest.Success += response => Schedule(() => recreatePanels(response)); - - API.Queue(getSetsRequest); + if (panelTarget.Any()) + loadingLayer.Show(); } - private void recreatePanels(SearchBeatmapSetsResponse response) + private void onSearchFinished(List beatmaps) { - if (response.Total == 0) + if (!beatmaps.Any()) { - searchSection.BeatmapSet = null; - LoadComponentAsync(new NotFoundDrawable(), addContentToPlaceholder); + LoadComponentAsync(new NotFoundDrawable(), addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token); return; } - var beatmaps = response.BeatmapSets.Select(r => r.ToBeatmapSet(rulesets)).ToList(); - var newPanels = new FillFlowContainer { RelativeSizeAxes = Axes.X, @@ -236,18 +122,14 @@ namespace osu.Game.Overlays }) }; - LoadComponentAsync(newPanels, loaded => - { - addContentToPlaceholder(loaded); - searchSection.BeatmapSet = beatmaps.First(); - }); + LoadComponentAsync(newPanels, addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token); } private void addContentToPlaceholder(Drawable content) { loadingLayer.Hide(); - Drawable lastContent = currentContent; + var lastContent = currentContent; if (lastContent != null) { @@ -266,9 +148,7 @@ namespace osu.Game.Overlays protected override void Dispose(bool isDisposing) { - getSetsRequest?.Cancel(); - queryChangedDebounce?.Cancel(); - + cancellationToken?.Cancel(); base.Dispose(isDisposing); } From ac88ba717b7f6589b01950ac915a4801eac4a007 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Mar 2020 16:03:26 +0900 Subject: [PATCH 0224/2376] Ensure screens respect aspect ratio in tests --- osu.Game.Tournament/Screens/TournamentScreen.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Tournament/Screens/TournamentScreen.cs b/osu.Game.Tournament/Screens/TournamentScreen.cs index 0b5b3e728b..5da7c7a5d2 100644 --- a/osu.Game.Tournament/Screens/TournamentScreen.cs +++ b/osu.Game.Tournament/Screens/TournamentScreen.cs @@ -18,6 +18,9 @@ namespace osu.Game.Tournament.Screens protected TournamentScreen() { RelativeSizeAxes = Axes.Both; + + FillMode = FillMode.Fit; + FillAspectRatio = 16 / 9f; } public override void Hide() => this.FadeOut(FADE_DELAY); From 8ff3370273fb9c857e0117bab3235b6408caf6c7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Mar 2020 16:27:38 +0900 Subject: [PATCH 0225/2376] Add a short load delay for avatars to avoid unnecessary fetching --- osu.Game/Users/Drawables/UpdateableAvatar.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Users/Drawables/UpdateableAvatar.cs b/osu.Game/Users/Drawables/UpdateableAvatar.cs index 59fbb5f910..171462f3fc 100644 --- a/osu.Game/Users/Drawables/UpdateableAvatar.cs +++ b/osu.Game/Users/Drawables/UpdateableAvatar.cs @@ -43,6 +43,8 @@ namespace osu.Game.Users.Drawables set => base.EdgeEffect = value; } + protected override double LoadDelay => 200; + /// /// Whether to show a default guest representation on null user (as opposed to nothing). /// From 88759e65a0b2702defc8e036561ad2d18fbb02dc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Mar 2020 16:31:36 +0900 Subject: [PATCH 0226/2376] Remove layout durations from tournament editor screns for better performance --- osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs | 2 -- osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs | 2 -- osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs | 2 -- osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs | 2 -- 4 files changed, 8 deletions(-) diff --git a/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs index 7119533743..8b8078e119 100644 --- a/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs @@ -129,8 +129,6 @@ namespace osu.Game.Tournament.Screens.Editors RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, - LayoutDuration = 200, - LayoutEasing = Easing.OutQuint, ChildrenEnumerable = round.Beatmaps.Select(p => new RoundBeatmapRow(round, p)) }; } diff --git a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs index e68946aaf2..46bb7b83e3 100644 --- a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs @@ -124,8 +124,6 @@ namespace osu.Game.Tournament.Screens.Editors RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, - LayoutDuration = 200, - LayoutEasing = Easing.OutQuint, ChildrenEnumerable = round.Beatmaps.Select(p => new SeedingBeatmapRow(round, p)) }; } diff --git a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs index ca8bce1cca..631393c6f4 100644 --- a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs @@ -202,8 +202,6 @@ namespace osu.Game.Tournament.Screens.Editors RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, - LayoutDuration = 200, - LayoutEasing = Easing.OutQuint, ChildrenEnumerable = team.Players.Select(p => new PlayerRow(team, p)) }; } diff --git a/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs index 5598910824..e4256e727d 100644 --- a/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs @@ -48,8 +48,6 @@ namespace osu.Game.Tournament.Screens.Editors Direction = FillDirection.Vertical, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - LayoutDuration = 200, - LayoutEasing = Easing.OutQuint, Spacing = new Vector2(20) }, }, From 40074f10dbd5999dca3f38dc0a6de093c28de463 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Mar 2020 17:55:05 +0900 Subject: [PATCH 0227/2376] Remove unnecessary override --- osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs index 9140cccafd..2a8f77210a 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs @@ -32,12 +32,5 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("rewind", () => Player.GameplayClockContainer.Seek(-80000)); AddUntilStep("key counter reset", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0)); } - - protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) - { - var working = base.CreateWorkingBeatmap(beatmap, storyboard); - - return working; - } } } From 3b0e3cd71a3ae257ec3f21f774c1ba2c63cf8e53 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Mar 2020 17:55:57 +0900 Subject: [PATCH 0228/2376] Remove using statements --- osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs index 2a8f77210a..afeda5fb7c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs @@ -3,10 +3,8 @@ using System.ComponentModel; using System.Linq; -using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Screens.Play; -using osu.Game.Storyboards; namespace osu.Game.Tests.Visual.Gameplay { From 0ccf691c972f3de8253c357b101e2f5c8ba5e93d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Mar 2020 18:00:07 +0900 Subject: [PATCH 0229/2376] Remove unnecessary interpolation --- osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs index 10827bc0b9..227ada70fe 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs @@ -60,7 +60,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses == 1); addPressKeyStep(); AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses == 2); - AddStep($"Disable counting", () => testCounter.IsCounting = false); + AddStep("Disable counting", () => testCounter.IsCounting = false); addPressKeyStep(); AddAssert($"Check {testKey} count has not changed", () => testCounter.CountPresses == 2); From a59c3d997da57bcfa33e2f3342222ccaa825d674 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Mar 2020 18:00:17 +0900 Subject: [PATCH 0230/2376] Refactor implementation to better match what already existed --- osu.Game/Screens/Play/Player.cs | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 2d49c707ec..bcadba14af 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -157,10 +157,7 @@ namespace osu.Game.Screens.Play addGameplayComponents(GameplayClockContainer, Beatmap.Value); addOverlayComponents(GameplayClockContainer, Beatmap.Value); - DrawableRuleset.HasReplayLoaded.BindValueChanged(e => - { - updatePauseOnFocusLostState(e.NewValue, BreakOverlay.IsBreakTime.Value); - }, true); + DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updatePauseOnFocusLostState(), true); // bind clock into components that require it DrawableRuleset.IsPaused.BindTo(GameplayClockContainer.IsPaused); @@ -232,7 +229,11 @@ namespace osu.Game.Screens.Play IsPaused = { BindTarget = GameplayClockContainer.IsPaused } }, PlayerSettingsOverlay = { PlaybackSettings = { UserPlaybackRate = { BindTarget = GameplayClockContainer.UserPlaybackRate } } }, - KeyCounter = { AlwaysVisible = { BindTarget = DrawableRuleset.HasReplayLoaded }, IsCounting = false }, + KeyCounter = + { + AlwaysVisible = { BindTarget = DrawableRuleset.HasReplayLoaded }, + IsCounting = false + }, RequestSeek = GameplayClockContainer.Seek, Anchor = Anchor.Centre, Origin = Anchor.Centre @@ -289,16 +290,16 @@ namespace osu.Game.Screens.Play HealthProcessor.IsBreakTime.BindTo(BreakOverlay.IsBreakTime); } - private void onBreakTimeChanged(ValueChangedEvent changeEvent) + private void onBreakTimeChanged(ValueChangedEvent isBreakTime) { - updatePauseOnFocusLostState(DrawableRuleset.HasReplayLoaded.Value, changeEvent.NewValue); - HUDOverlay.KeyCounter.IsCounting = !changeEvent.NewValue; + updatePauseOnFocusLostState(); + HUDOverlay.KeyCounter.IsCounting = !isBreakTime.NewValue; } - private void updatePauseOnFocusLostState(bool replayLoaded, bool isBreakTime) => + private void updatePauseOnFocusLostState() => HUDOverlay.HoldToQuit.PauseOnFocusLost = PauseOnFocusLost - && !replayLoaded - && !isBreakTime; + && !DrawableRuleset.HasReplayLoaded.Value + && !BreakOverlay.IsBreakTime.Value; private IBeatmap loadPlayableBeatmap() { From 2d95f2992534444949ebbf6a66dd53b333a94c9f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Mar 2020 16:06:23 +0900 Subject: [PATCH 0231/2376] Add gameplay screen specific video --- osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs index 6a3095d42d..c74302a869 100644 --- a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs +++ b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs @@ -6,6 +6,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Platform; using osu.Framework.Threading; using osu.Game.Graphics.UserInterface; using osu.Game.Tournament.Components; @@ -19,7 +20,7 @@ using osuTK.Graphics; namespace osu.Game.Tournament.Screens.Gameplay { - public class GameplayScreen : BeatmapInfoScreen + public class GameplayScreen : BeatmapInfoScreen, IProvideVideo { private readonly BindableBool warmup = new BindableBool(); @@ -39,12 +40,17 @@ namespace osu.Game.Tournament.Screens.Gameplay private TournamentMatchChatDisplay chat { get; set; } [BackgroundDependencyLoader] - private void load(LadderInfo ladder, MatchIPCInfo ipc) + private void load(LadderInfo ladder, MatchIPCInfo ipc, Storage storage) { this.ipc = ipc; AddRangeInternal(new Drawable[] { + new TourneyVideo(storage.GetStream("videos/gameplay.m4v")) + { + Loop = true, + RelativeSizeAxes = Axes.Both, + }, new MatchHeader(), new Container { From 0a72fa69ab70bbb14fa7742e19a6d0075aefa21f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Mar 2020 18:38:29 +0900 Subject: [PATCH 0232/2376] Simplify video creation (and handle fallback better) --- .../Components/TourneyVideo.cs | 38 ++++++++++++------- .../Screens/Gameplay/GameplayScreen.cs | 2 +- .../Screens/Ladder/LadderScreen.cs | 2 +- .../Screens/Schedule/ScheduleScreen.cs | 4 +- .../Screens/TeamIntro/SeedingScreen.cs | 2 +- .../Screens/TeamIntro/TeamIntroScreen.cs | 2 +- .../Screens/TeamWin/TeamWinScreen.cs | 4 +- osu.Game.Tournament/TournamentSceneManager.cs | 2 +- 8 files changed, 34 insertions(+), 22 deletions(-) diff --git a/osu.Game.Tournament/Components/TourneyVideo.cs b/osu.Game.Tournament/Components/TourneyVideo.cs index 206689ca1a..7d2eaff515 100644 --- a/osu.Game.Tournament/Components/TourneyVideo.cs +++ b/osu.Game.Tournament/Components/TourneyVideo.cs @@ -1,12 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.IO; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Video; +using osu.Framework.Platform; using osu.Framework.Timing; using osu.Game.Graphics; @@ -14,21 +15,24 @@ namespace osu.Game.Tournament.Components { public class TourneyVideo : CompositeDrawable { - private readonly VideoSprite video; + private readonly string filename; + private readonly bool drawFallbackGradient; + private VideoSprite video; - private readonly ManualClock manualClock; + private ManualClock manualClock; - public TourneyVideo(Stream stream) + public TourneyVideo(string filename, bool drawFallbackGradient = false) { - if (stream == null) - { - InternalChild = new Box - { - Colour = ColourInfo.GradientVertical(OsuColour.Gray(0.3f), OsuColour.Gray(0.6f)), - RelativeSizeAxes = Axes.Both, - }; - } - else + this.filename = filename; + this.drawFallbackGradient = drawFallbackGradient; + } + + [BackgroundDependencyLoader] + private void load(Storage storage) + { + var stream = storage.GetStream($@"videos/{filename}.m4v"); + + if (stream != null) { InternalChild = video = new VideoSprite(stream) { @@ -37,6 +41,14 @@ namespace osu.Game.Tournament.Components Clock = new FramedClock(manualClock = new ManualClock()) }; } + else if (drawFallbackGradient) + { + InternalChild = new Box + { + Colour = ColourInfo.GradientVertical(OsuColour.Gray(0.3f), OsuColour.Gray(0.6f)), + RelativeSizeAxes = Axes.Both, + }; + } } public bool Loop diff --git a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs index c74302a869..d632e7c5f3 100644 --- a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs +++ b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs @@ -46,7 +46,7 @@ namespace osu.Game.Tournament.Screens.Gameplay AddRangeInternal(new Drawable[] { - new TourneyVideo(storage.GetStream("videos/gameplay.m4v")) + new TourneyVideo("gameplay") { Loop = true, RelativeSizeAxes = Axes.Both, diff --git a/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs b/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs index 293f6e0068..7b265ded32 100644 --- a/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs +++ b/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs @@ -42,7 +42,7 @@ namespace osu.Game.Tournament.Screens.Ladder RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - new TourneyVideo(storage.GetStream(@"videos/ladder.m4v")) + new TourneyVideo("ladder") { RelativeSizeAxes = Axes.Both, Loop = true, diff --git a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs index 080570eac4..4c93c04fcf 100644 --- a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs +++ b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs @@ -18,7 +18,7 @@ using osuTK.Graphics; namespace osu.Game.Tournament.Screens.Schedule { - public class ScheduleScreen : TournamentScreen, IProvideVideo + public class ScheduleScreen : TournamentScreen { private readonly Bindable currentMatch = new Bindable(); private Container mainContainer; @@ -33,7 +33,7 @@ namespace osu.Game.Tournament.Screens.Schedule InternalChildren = new Drawable[] { - new TourneyVideo(storage.GetStream(@"videos/schedule.m4v")) + new TourneyVideo("schedule") { RelativeSizeAxes = Axes.Both, Loop = true, diff --git a/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs b/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs index db5363c155..513d84b594 100644 --- a/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs +++ b/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs @@ -34,7 +34,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro InternalChildren = new Drawable[] { - new TourneyVideo(storage.GetStream(@"videos/seeding.m4v")) + new TourneyVideo("seeding") { RelativeSizeAxes = Axes.Both, Loop = true, diff --git a/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs b/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs index 6559113f55..d584c21058 100644 --- a/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs +++ b/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs @@ -26,7 +26,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro InternalChildren = new Drawable[] { - new TourneyVideo(storage.GetStream(@"videos/teamintro.m4v")) + new TourneyVideo("teamintro") { RelativeSizeAxes = Axes.Both, Loop = true, diff --git a/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs b/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs index 30b86f8421..1765ab7ba2 100644 --- a/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs +++ b/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs @@ -31,13 +31,13 @@ namespace osu.Game.Tournament.Screens.TeamWin InternalChildren = new Drawable[] { - blueWinVideo = new TourneyVideo(storage.GetStream(@"videos/teamwin-blue.m4v")) + blueWinVideo = new TourneyVideo("teamwin-blue") { Alpha = 1, RelativeSizeAxes = Axes.Both, Loop = true, }, - redWinVideo = new TourneyVideo(storage.GetStream(@"videos/teamwin-red.m4v")) + redWinVideo = new TourneyVideo("teamwin-red") { Alpha = 0, RelativeSizeAxes = Axes.Both, diff --git a/osu.Game.Tournament/TournamentSceneManager.cs b/osu.Game.Tournament/TournamentSceneManager.cs index 9f5f2b6827..287e25b1fb 100644 --- a/osu.Game.Tournament/TournamentSceneManager.cs +++ b/osu.Game.Tournament/TournamentSceneManager.cs @@ -61,7 +61,7 @@ namespace osu.Game.Tournament //Masking = true, Children = new Drawable[] { - video = new TourneyVideo(storage.GetStream("videos/main.m4v")) + video = new TourneyVideo("main", true) { Loop = true, RelativeSizeAxes = Axes.Both, From 16cc49daa00d3c4af18130162ccee7f74a1bf098 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Mar 2020 18:47:31 +0900 Subject: [PATCH 0233/2376] 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 1c4a6ffe75..97f7a7edb1 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 4d59b709aa..855bda3679 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -23,7 +23,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 6897d3e625..e2c4c09047 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + @@ -79,7 +79,7 @@ - + From 3295f8657ac3d74a95c10016273068fbbdedde26 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Mar 2020 22:44:11 +0900 Subject: [PATCH 0234/2376] Restore clamp behaviour --- osu.Game/Tests/Visual/OsuTestScene.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index 5623435da4..d1d8059cb1 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -241,7 +241,7 @@ namespace osu.Game.Tests.Visual public override bool Seek(double seek) { - accumulated = Math.Min(seek, Length); + accumulated = Math.Clamp(seek, 0, Length); lastReferenceTime = null; return accumulated == seek; From 491840b17d215cd983dca0badaac0a502e6d0d19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 6 Mar 2020 21:10:23 +0100 Subject: [PATCH 0235/2376] Add failing tests --- .../TestSceneHitCircleArea.cs | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs new file mode 100644 index 0000000000..06eccdafdb --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.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 NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Scoring; +using osu.Game.Skinning; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests +{ + public class TestSceneHitCircleArea : ManualInputManagerTestScene + { + private HitCircle hitCircle; + private DrawableHitCircle drawableHitCircle; + private DrawableHitCircle.HitReceptor hitAreaReceptor => drawableHitCircle.HitArea; + + [SetUp] + public new void SetUp() + { + base.SetUp(); + + Schedule(() => + { + hitCircle = new HitCircle + { + Position = new Vector2(100, 100), + StartTime = Time.Current + 500 + }; + + hitCircle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + + Child = new SkinProvidingContainer(new DefaultSkin()) + { + RelativeSizeAxes = Axes.Both, + Child = drawableHitCircle = new DrawableHitCircle(hitCircle) + { + Size = new Vector2(100) + } + }; + }); + } + + [Test] + public void TestCircleHitCentre() + { + AddStep("move mouse to centre", () => InputManager.MoveMouseTo(hitAreaReceptor.ScreenSpaceDrawQuad.Centre)); + scheduleHit(); + + AddAssert("hit registered", () => hitAreaReceptor.HitAction == OsuAction.LeftButton); + } + + [Test] + public void TestCircleHitLeftEdge() + { + AddStep("move mouse to left edge", () => + { + var drawQuad = hitAreaReceptor.ScreenSpaceDrawQuad; + var mousePosition = new Vector2(drawQuad.TopLeft.X, drawQuad.Centre.Y); + + InputManager.MoveMouseTo(mousePosition); + }); + scheduleHit(); + + AddAssert("hit registered", () => hitAreaReceptor.HitAction == OsuAction.LeftButton); + } + + [Test] + public void TestCircleHitTopLeftEdge() + { + AddStep("move mouse to top left circle edge", () => + { + var drawQuad = hitAreaReceptor.ScreenSpaceDrawQuad; + // sqrt(2) / 2 = sin(45deg) = cos(45deg) + // draw width halved to get radius + // 0.95f taken for leniency + float correction = 0.95f * (float)Math.Sqrt(2) / 2 * (drawQuad.Width / 2); + var mousePosition = new Vector2(drawQuad.Centre.X - correction, drawQuad.Centre.Y - correction); + + InputManager.MoveMouseTo(mousePosition); + }); + scheduleHit(); + + AddAssert("hit registered", () => hitAreaReceptor.HitAction == OsuAction.LeftButton); + } + + [Test] + public void TestCircleMissBoundingBoxCorner() + { + AddStep("move mouse to top left corner of bounding box", () => InputManager.MoveMouseTo(hitAreaReceptor.ScreenSpaceDrawQuad.TopLeft)); + scheduleHit(); + + AddAssert("hit not registered", () => hitAreaReceptor.HitAction == null); + } + + private void scheduleHit() => AddStep("schedule action", () => + { + var delay = hitCircle.StartTime - hitCircle.HitWindows.WindowFor(HitResult.Great) - Time.Current; + Scheduler.AddDelayed(() => hitAreaReceptor.OnPressed(OsuAction.LeftButton), delay); + }); + } +} From 77fd7480352b3dbc834fe7fa85b01d17491466c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 6 Mar 2020 21:21:20 +0100 Subject: [PATCH 0236/2376] Fix incorrect circle piece hitbox Hitboxes of circle pieces in osu! have regressed with commit 8592335. The reason for the regression was that hit detection was moved from DrawableHitCircle itself to a newly-introduced private HitArea class (now named HitReceptor). As HitArea inherited from Drawable, it would return IsHovered == true over its entire bounding box. This meant that the hit area could wrongly pick up actions that are not within circle radius and make them into hits. To resolve, make HitReceptor a CompositeDrawable and set its corner radius to match the circle piece. This fixes the invalid hitbox, as IsHovered takes radius into account. --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index 4ef63bb2a0..da1e666aba 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -170,7 +170,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public Drawable ProxiedLayer => ApproachCircle; - public class HitReceptor : Drawable, IKeyBindingHandler + public class HitReceptor : CompositeDrawable, IKeyBindingHandler { // IsHovered is used public override bool HandlePositionalInput => true; @@ -185,6 +185,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Anchor = Anchor.Centre; Origin = Anchor.Centre; + + CornerRadius = OsuHitObject.OBJECT_RADIUS; + CornerExponent = 2; } public bool OnPressed(OsuAction action) From b60876455481d8dd756d938fe07865628b872128 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 6 Mar 2020 22:09:02 +0100 Subject: [PATCH 0237/2376] Cover area just outside circle in test --- osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs index 06eccdafdb..67b6dac787 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs @@ -71,23 +71,23 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("hit registered", () => hitAreaReceptor.HitAction == OsuAction.LeftButton); } - [Test] - public void TestCircleHitTopLeftEdge() + [TestCase(0.95f, OsuAction.LeftButton)] + [TestCase(1.05f, null)] + public void TestHitsCloseToEdge(float relativeDistanceFromCentre, OsuAction? expectedAction) { AddStep("move mouse to top left circle edge", () => { var drawQuad = hitAreaReceptor.ScreenSpaceDrawQuad; // sqrt(2) / 2 = sin(45deg) = cos(45deg) // draw width halved to get radius - // 0.95f taken for leniency - float correction = 0.95f * (float)Math.Sqrt(2) / 2 * (drawQuad.Width / 2); + float correction = relativeDistanceFromCentre * (float)Math.Sqrt(2) / 2 * (drawQuad.Width / 2); var mousePosition = new Vector2(drawQuad.Centre.X - correction, drawQuad.Centre.Y - correction); InputManager.MoveMouseTo(mousePosition); }); scheduleHit(); - AddAssert("hit registered", () => hitAreaReceptor.HitAction == OsuAction.LeftButton); + AddAssert($"hit {(expectedAction == null ? "not " : string.Empty)}registered", () => hitAreaReceptor.HitAction == expectedAction); } [Test] From e886c155e60d103dcc232bc210d0eb7017f6664f Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 7 Mar 2020 04:05:17 +0300 Subject: [PATCH 0238/2376] Adjust text size values --- osu.Game/Users/UserPanel.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index a900a55dab..5676113aad 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -129,7 +129,7 @@ namespace osu.Game.Users protected OsuSpriteText CreateUsername() => new OsuSpriteText { - Font = OsuFont.GetFont(size: 20, weight: FontWeight.Bold, italics: true), + Font = OsuFont.GetFont(size: 16, weight: FontWeight.Bold), Shadow = false, Text = User.Username, }; @@ -150,7 +150,7 @@ namespace osu.Game.Users var alignment = rightAlignedChildren ? Anchor.CentreRight : Anchor.CentreLeft; - statusContainer.Add(lastVisitMessage = new TextFlowContainer(t => t.Font = OsuFont.GetFont(size: 15, weight: FontWeight.SemiBold)).With(text => + statusContainer.Add(lastVisitMessage = new TextFlowContainer(t => t.Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold)).With(text => { text.Anchor = alignment; text.Origin = alignment; @@ -171,7 +171,7 @@ namespace osu.Game.Users { Anchor = alignment, Origin = alignment, - Font = OsuFont.GetFont(size: 17, weight: FontWeight.SemiBold) + Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold) }); return statusContainer; From d68d7edea332e37df2610fa91316ceb7692dabd4 Mon Sep 17 00:00:00 2001 From: voidedWarranties Date: Sat, 7 Mar 2020 14:08:13 -0800 Subject: [PATCH 0239/2376] Start background video playback based on provided offset --- osu.Game/Beatmaps/BeatmapMetadata.cs | 2 + .../Beatmaps/Formats/LegacyBeatmapDecoder.cs | 7 +- .../Beatmaps/Formats/LegacyBeatmapEncoder.cs | 2 +- .../20200307015200_AddVideoOffset.Designer.cs | 508 ++++++++++++++++++ .../20200307015200_AddVideoOffset.cs | 23 + .../Migrations/OsuDbContextModelSnapshot.cs | 2 + osu.Game/Screens/Play/DimmableVideo.cs | 42 +- .../Screens/Play/GameplayClockContainer.cs | 4 + osu.Game/Screens/Play/Player.cs | 2 +- 9 files changed, 581 insertions(+), 11 deletions(-) create mode 100644 osu.Game/Migrations/20200307015200_AddVideoOffset.Designer.cs create mode 100644 osu.Game/Migrations/20200307015200_AddVideoOffset.cs diff --git a/osu.Game/Beatmaps/BeatmapMetadata.cs b/osu.Game/Beatmaps/BeatmapMetadata.cs index 9267527d79..a353b1a0b6 100644 --- a/osu.Game/Beatmaps/BeatmapMetadata.cs +++ b/osu.Game/Beatmaps/BeatmapMetadata.cs @@ -52,6 +52,7 @@ namespace osu.Game.Beatmaps public int PreviewTime { get; set; } public string AudioFile { get; set; } public string BackgroundFile { get; set; } + public int VideoOffset { get; set; } public string VideoFile { get; set; } public override string ToString() => $"{Artist} - {Title} ({Author})"; @@ -83,6 +84,7 @@ namespace osu.Game.Beatmaps && PreviewTime == other.PreviewTime && AudioFile == other.AudioFile && BackgroundFile == other.BackgroundFile + && VideoOffset == other.VideoOffset && VideoFile == other.VideoFile; } } diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 4b01b2490e..8fe08a61b7 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -6,11 +6,11 @@ using System.Collections.Generic; using System.IO; using System.Linq; using osu.Framework.Extensions; -using osu.Game.Beatmaps.Timing; -using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.IO; using osu.Game.Beatmaps.Legacy; +using osu.Game.Beatmaps.Timing; +using osu.Game.IO; +using osu.Game.Rulesets.Objects.Legacy; namespace osu.Game.Beatmaps.Formats { @@ -304,6 +304,7 @@ namespace osu.Game.Beatmaps.Formats break; case LegacyEventType.Video: + beatmap.BeatmapInfo.Metadata.VideoOffset = Parsing.ParseInt(split[1]); beatmap.BeatmapInfo.Metadata.VideoFile = CleanFilename(split[2]); break; diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 09f40ce7b6..7e3e3aacd8 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -134,7 +134,7 @@ namespace osu.Game.Beatmaps.Formats writer.WriteLine(FormattableString.Invariant($"{(int)LegacyEventType.Background},0,\"{beatmap.BeatmapInfo.Metadata.BackgroundFile}\",0,0")); if (!string.IsNullOrEmpty(beatmap.BeatmapInfo.Metadata.VideoFile)) - writer.WriteLine(FormattableString.Invariant($"{(int)LegacyEventType.Video},0,\"{beatmap.BeatmapInfo.Metadata.VideoFile}\",0,0")); + writer.WriteLine(FormattableString.Invariant($"{(int)LegacyEventType.Video},{beatmap.BeatmapInfo.Metadata.VideoOffset},\"{beatmap.BeatmapInfo.Metadata.VideoFile}\",0,0")); foreach (var b in beatmap.Breaks) writer.WriteLine(FormattableString.Invariant($"{(int)LegacyEventType.Break},{b.StartTime},{b.EndTime}")); diff --git a/osu.Game/Migrations/20200307015200_AddVideoOffset.Designer.cs b/osu.Game/Migrations/20200307015200_AddVideoOffset.Designer.cs new file mode 100644 index 0000000000..10fea5a8bc --- /dev/null +++ b/osu.Game/Migrations/20200307015200_AddVideoOffset.Designer.cs @@ -0,0 +1,508 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using osu.Game.Database; + +namespace osu.Game.Migrations +{ + [DbContext(typeof(OsuDbContext))] + [Migration("20200307015200_AddVideoOffset")] + partial class AddVideoOffset + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.2.6-servicing-10079"); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("ApproachRate"); + + b.Property("CircleSize"); + + b.Property("DrainRate"); + + b.Property("OverallDifficulty"); + + b.Property("SliderMultiplier"); + + b.Property("SliderTickRate"); + + b.HasKey("ID"); + + b.ToTable("BeatmapDifficulty"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("AudioLeadIn"); + + b.Property("BPM"); + + b.Property("BaseDifficultyID"); + + b.Property("BeatDivisor"); + + b.Property("BeatmapSetInfoID"); + + b.Property("Countdown"); + + b.Property("DistanceSpacing"); + + b.Property("GridSize"); + + b.Property("Hash"); + + b.Property("Hidden"); + + b.Property("Length"); + + b.Property("LetterboxInBreaks"); + + b.Property("MD5Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapID"); + + b.Property("Path"); + + b.Property("RulesetID"); + + b.Property("SpecialStyle"); + + b.Property("StackLeniency"); + + b.Property("StarDifficulty"); + + b.Property("Status"); + + b.Property("StoredBookmarks"); + + b.Property("TimelineZoom"); + + b.Property("Version"); + + b.Property("WidescreenStoryboard"); + + b.HasKey("ID"); + + b.HasIndex("BaseDifficultyID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("Hash"); + + b.HasIndex("MD5Hash"); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("BeatmapInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Artist"); + + b.Property("ArtistUnicode"); + + b.Property("AudioFile"); + + b.Property("AuthorString") + .HasColumnName("Author"); + + b.Property("BackgroundFile"); + + b.Property("PreviewTime"); + + b.Property("Source"); + + b.Property("Tags"); + + b.Property("Title"); + + b.Property("TitleUnicode"); + + b.Property("VideoFile"); + + b.Property("VideoOffset"); + + b.HasKey("ID"); + + b.ToTable("BeatmapMetadata"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("BeatmapSetInfoID"); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.HasKey("ID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("FileInfoID"); + + b.ToTable("BeatmapSetFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("DateAdded"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapSetID"); + + b.Property("Protected"); + + b.Property("Status"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapSetID") + .IsUnique(); + + b.ToTable("BeatmapSetInfo"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Key") + .HasColumnName("Key"); + + b.Property("RulesetID"); + + b.Property("SkinInfoID"); + + b.Property("StringValue") + .HasColumnName("Value"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("SkinInfoID"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("osu.Game.IO.FileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Hash"); + + b.Property("ReferenceCount"); + + b.HasKey("ID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("ReferenceCount"); + + b.ToTable("FileInfo"); + }); + + modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntAction") + .HasColumnName("Action"); + + b.Property("KeysString") + .HasColumnName("Keys"); + + b.Property("RulesetID"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("IntAction"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("KeyBinding"); + }); + + modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Available"); + + b.Property("InstantiationInfo"); + + b.Property("Name"); + + b.Property("ShortName"); + + b.HasKey("ID"); + + b.HasIndex("Available"); + + b.HasIndex("ShortName") + .IsUnique(); + + b.ToTable("RulesetInfo"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("ScoreInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("ScoreInfoID"); + + b.ToTable("ScoreFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Accuracy") + .HasColumnType("DECIMAL(1,4)"); + + b.Property("BeatmapInfoID"); + + b.Property("Combo"); + + b.Property("Date"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MaxCombo"); + + b.Property("ModsJson") + .HasColumnName("Mods"); + + b.Property("OnlineScoreID"); + + b.Property("PP"); + + b.Property("Rank"); + + b.Property("RulesetID"); + + b.Property("StatisticsJson") + .HasColumnName("Statistics"); + + b.Property("TotalScore"); + + b.Property("UserID") + .HasColumnName("UserID"); + + b.Property("UserString") + .HasColumnName("User"); + + b.HasKey("ID"); + + b.HasIndex("BeatmapInfoID"); + + b.HasIndex("OnlineScoreID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("ScoreInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("SkinInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("SkinInfoID"); + + b.ToTable("SkinFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Creator"); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("Name"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.ToTable("SkinInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty") + .WithMany() + .HasForeignKey("BaseDifficultyID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet") + .WithMany("Beatmaps") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("Beatmaps") + .HasForeignKey("MetadataID"); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo") + .WithMany("Files") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("BeatmapSets") + .HasForeignKey("MetadataID"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Settings") + .HasForeignKey("SkinInfoID"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Scoring.ScoreInfo") + .WithMany("Files") + .HasForeignKey("ScoreInfoID"); + }); + + modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapInfo", "Beatmap") + .WithMany("Scores") + .HasForeignKey("BeatmapInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Files") + .HasForeignKey("SkinInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/osu.Game/Migrations/20200307015200_AddVideoOffset.cs b/osu.Game/Migrations/20200307015200_AddVideoOffset.cs new file mode 100644 index 0000000000..06c456c551 --- /dev/null +++ b/osu.Game/Migrations/20200307015200_AddVideoOffset.cs @@ -0,0 +1,23 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace osu.Game.Migrations +{ + public partial class AddVideoOffset : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "VideoOffset", + table: "BeatmapMetadata", + nullable: false, + defaultValue: 0); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "VideoOffset", + table: "BeatmapMetadata"); + } + } +} diff --git a/osu.Game/Migrations/OsuDbContextModelSnapshot.cs b/osu.Game/Migrations/OsuDbContextModelSnapshot.cs index bc4fc3342d..6f91688ddb 100644 --- a/osu.Game/Migrations/OsuDbContextModelSnapshot.cs +++ b/osu.Game/Migrations/OsuDbContextModelSnapshot.cs @@ -141,6 +141,8 @@ namespace osu.Game.Migrations b.Property("VideoFile"); + b.Property("VideoOffset"); + b.HasKey("ID"); b.ToTable("BeatmapMetadata"); diff --git a/osu.Game/Screens/Play/DimmableVideo.cs b/osu.Game/Screens/Play/DimmableVideo.cs index 1a01cace17..2e080d9c2b 100644 --- a/osu.Game/Screens/Play/DimmableVideo.cs +++ b/osu.Game/Screens/Play/DimmableVideo.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Video; +using osu.Framework.Timing; using osu.Game.Graphics.Containers; using osuTK.Graphics; @@ -14,11 +15,13 @@ namespace osu.Game.Screens.Play public class DimmableVideo : UserDimContainer { private readonly VideoSprite video; + private readonly int offset; private DrawableVideo drawableVideo; - public DimmableVideo(VideoSprite video) + public DimmableVideo(VideoSprite video, int offset) { this.video = video; + this.offset = offset; } [BackgroundDependencyLoader] @@ -46,7 +49,7 @@ namespace osu.Game.Screens.Play if (!ShowVideo.Value && !IgnoreUserSettings.Value) return; - drawableVideo = new DrawableVideo(video); + drawableVideo = new DrawableVideo(video, offset); if (async) LoadComponentAsync(drawableVideo, Add); @@ -56,8 +59,15 @@ namespace osu.Game.Screens.Play private class DrawableVideo : Container { - public DrawableVideo(VideoSprite video) + private readonly Drawable cover; + private readonly int offset; + private readonly ManualClock videoClock; + private bool videoStarted; + + public DrawableVideo(VideoSprite video, int offset) { + this.offset = offset; + RelativeSizeAxes = Axes.Both; Masking = true; @@ -66,14 +76,17 @@ namespace osu.Game.Screens.Play video.Anchor = Anchor.Centre; video.Origin = Anchor.Centre; - AddRangeInternal(new Drawable[] + videoClock = new ManualClock(); + video.Clock = new FramedClock(videoClock); + + AddRangeInternal(new[] { - new Box + video, + cover = new Box { RelativeSizeAxes = Axes.Both, Colour = Color4.Black, }, - video, }); } @@ -83,6 +96,23 @@ namespace osu.Game.Screens.Play if (clock != null) Clock = clock; } + + protected override void Update() + { + if (videoClock != null && Clock.CurrentTime > offset) + { + if (!videoStarted) + { + cover.FadeOut(500); + videoStarted = true; + } + + // handle seeking before the video starts (break skipping, replay seek) + videoClock.CurrentTime = Clock.CurrentTime - offset; + } + + base.Update(); + } } } } diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 591e969ad8..da4829d484 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -116,6 +116,10 @@ namespace osu.Game.Screens.Play if (beatmap.BeatmapInfo.AudioLeadIn > 0) startTime = Math.Min(startTime, firstHitObjectTime - beatmap.BeatmapInfo.AudioLeadIn); + // some beatmaps have no AudioLeadIn but the video starts before the first object + if (beatmap.Video != null && beatmap.Metadata.VideoOffset != 0) + startTime = Math.Min(startTime, beatmap.Metadata.VideoOffset); + Seek(startTime); adjustableClock.ProcessFrame(); diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index bcadba14af..22d90d4ac1 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -189,7 +189,7 @@ namespace osu.Game.Screens.Play private void addUnderlayComponents(Container target) { - target.Add(DimmableVideo = new DimmableVideo(Beatmap.Value.Video) { RelativeSizeAxes = Axes.Both }); + target.Add(DimmableVideo = new DimmableVideo(Beatmap.Value.Video, Beatmap.Value.Metadata.VideoOffset) { RelativeSizeAxes = Axes.Both }); target.Add(DimmableStoryboard = new DimmableStoryboard(Beatmap.Value.Storyboard) { RelativeSizeAxes = Axes.Both }); } From 9f44a7b2ce404a7f4c363a7643e779896691924d Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 8 Mar 2020 03:07:14 +0300 Subject: [PATCH 0240/2376] Simplify status assignment in the test scene --- osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs index f02a570f4f..ccae778745 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs @@ -25,6 +25,7 @@ namespace osu.Game.Tests.Visual.Online }; private readonly Bindable activity = new Bindable(); + private readonly Bindable status = new Bindable(); private UserGridPanel peppy; private UserListPanel evast; @@ -76,20 +77,20 @@ namespace osu.Game.Tests.Visual.Online flyte.Status.Value = new UserStatusOnline(); - peppy.Status.Value = null; + peppy.Status.BindTo(status); peppy.Activity.BindTo(activity); - evast.Status.Value = null; + evast.Status.BindTo(status); evast.Activity.BindTo(activity); }); [Test] public void TestUserStatus() { - AddStep("online", () => peppy.Status.Value = evast.Status.Value = new UserStatusOnline()); - AddStep("do not disturb", () => peppy.Status.Value = evast.Status.Value = new UserStatusDoNotDisturb()); - AddStep("offline", () => peppy.Status.Value = evast.Status.Value = new UserStatusOffline()); - AddStep("null status", () => peppy.Status.Value = evast.Status.Value = null); + AddStep("online", () => status.Value = new UserStatusOnline()); + AddStep("do not disturb", () => status.Value = new UserStatusDoNotDisturb()); + AddStep("offline", () => status.Value = new UserStatusOffline()); + AddStep("null status", () => status.Value = null); } [Test] From 76c832518fcf15f53581c5972efbfd7451584884 Mon Sep 17 00:00:00 2001 From: voidedWarranties Date: Sat, 7 Mar 2020 21:32:03 -0800 Subject: [PATCH 0241/2376] Render video as a part of the storyboard --- osu.Game.Tests/WaveformTestBeatmap.cs | 3 - osu.Game/Beatmaps/BeatmapManager.cs | 2 - .../Beatmaps/BeatmapManager_WorkingBeatmap.cs | 19 - osu.Game/Beatmaps/BeatmapMetadata.cs | 2 - osu.Game/Beatmaps/DummyWorkingBeatmap.cs | 3 - .../Beatmaps/Formats/LegacyBeatmapDecoder.cs | 5 - .../Beatmaps/Formats/LegacyBeatmapEncoder.cs | 3 - .../Formats/LegacyStoryboardDecoder.cs | 17 +- osu.Game/Beatmaps/IWorkingBeatmap.cs | 6 - osu.Game/Beatmaps/WorkingBeatmap.cs | 15 +- .../20200307015200_AddVideoOffset.Designer.cs | 508 ------------------ .../20200307015200_AddVideoOffset.cs | 23 - osu.Game/Rulesets/Mods/ModCinema.cs | 1 - osu.Game/Screens/Play/DimmableVideo.cs | 118 ---- .../Screens/Play/GameplayClockContainer.cs | 11 +- osu.Game/Screens/Play/Player.cs | 3 - .../Drawables/DrawableStoryboardVideo.cs | 82 +++ osu.Game/Storyboards/Storyboard.cs | 3 +- osu.Game/Storyboards/StoryboardVideo.cs | 25 + .../Tests/Beatmaps/BeatmapConversionTest.cs | 3 - osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs | 3 - 21 files changed, 136 insertions(+), 719 deletions(-) delete mode 100644 osu.Game/Migrations/20200307015200_AddVideoOffset.Designer.cs delete mode 100644 osu.Game/Migrations/20200307015200_AddVideoOffset.cs delete mode 100644 osu.Game/Screens/Play/DimmableVideo.cs create mode 100644 osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs create mode 100644 osu.Game/Storyboards/StoryboardVideo.cs diff --git a/osu.Game.Tests/WaveformTestBeatmap.cs b/osu.Game.Tests/WaveformTestBeatmap.cs index 53ce5def32..90c91eb007 100644 --- a/osu.Game.Tests/WaveformTestBeatmap.cs +++ b/osu.Game.Tests/WaveformTestBeatmap.cs @@ -6,7 +6,6 @@ using System.Linq; using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Graphics.Textures; -using osu.Framework.Graphics.Video; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; using osu.Game.IO; @@ -51,8 +50,6 @@ namespace osu.Game.Tests protected override Texture GetBackground() => null; - protected override VideoSprite GetVideo() => null; - protected override Waveform GetWaveform() => new Waveform(trackStore.GetStream(firstAudioFile)); protected override Track GetTrack() => trackStore.Get(firstAudioFile); diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 31869f9310..abb3f8ac42 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -14,7 +14,6 @@ using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Extensions; using osu.Framework.Graphics.Textures; -using osu.Framework.Graphics.Video; using osu.Framework.Lists; using osu.Framework.Logging; using osu.Framework.Platform; @@ -403,7 +402,6 @@ namespace osu.Game.Beatmaps protected override IBeatmap GetBeatmap() => beatmap; protected override Texture GetBackground() => null; - protected override VideoSprite GetVideo() => null; protected override Track GetTrack() => null; } diff --git a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs index 1991770518..e62a9bb39d 100644 --- a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs @@ -6,7 +6,6 @@ using System.Linq; using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Graphics.Textures; -using osu.Framework.Graphics.Video; using osu.Framework.IO.Stores; using osu.Framework.Logging; using osu.Game.Beatmaps.Formats; @@ -67,24 +66,6 @@ namespace osu.Game.Beatmaps } } - protected override VideoSprite GetVideo() - { - if (Metadata?.VideoFile == null) - return null; - - try - { - var stream = textureStore.GetStream(getPathForFile(Metadata.VideoFile)); - - return stream == null ? null : new VideoSprite(stream); - } - catch (Exception e) - { - Logger.Error(e, "Video failed to load"); - return null; - } - } - protected override Track GetTrack() { try diff --git a/osu.Game/Beatmaps/BeatmapMetadata.cs b/osu.Game/Beatmaps/BeatmapMetadata.cs index a353b1a0b6..9267527d79 100644 --- a/osu.Game/Beatmaps/BeatmapMetadata.cs +++ b/osu.Game/Beatmaps/BeatmapMetadata.cs @@ -52,7 +52,6 @@ namespace osu.Game.Beatmaps public int PreviewTime { get; set; } public string AudioFile { get; set; } public string BackgroundFile { get; set; } - public int VideoOffset { get; set; } public string VideoFile { get; set; } public override string ToString() => $"{Artist} - {Title} ({Author})"; @@ -84,7 +83,6 @@ namespace osu.Game.Beatmaps && PreviewTime == other.PreviewTime && AudioFile == other.AudioFile && BackgroundFile == other.BackgroundFile - && VideoOffset == other.VideoOffset && VideoFile == other.VideoFile; } } diff --git a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs index bfcc38e4a9..8080e94075 100644 --- a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs @@ -7,7 +7,6 @@ using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics.Textures; -using osu.Framework.Graphics.Video; using osu.Game.Rulesets; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; @@ -45,8 +44,6 @@ namespace osu.Game.Beatmaps protected override Texture GetBackground() => textures?.Get(@"Backgrounds/bg4"); - protected override VideoSprite GetVideo() => null; - protected override Track GetTrack() => GetVirtualTrack(); private class DummyRulesetInfo : RulesetInfo diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 8fe08a61b7..f5b27eddd2 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -303,11 +303,6 @@ namespace osu.Game.Beatmaps.Formats beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[2]); break; - case LegacyEventType.Video: - beatmap.BeatmapInfo.Metadata.VideoOffset = Parsing.ParseInt(split[1]); - beatmap.BeatmapInfo.Metadata.VideoFile = CleanFilename(split[2]); - break; - case LegacyEventType.Break: double start = getOffsetTime(Parsing.ParseDouble(split[1])); diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 7e3e3aacd8..ec2ca30535 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -133,9 +133,6 @@ namespace osu.Game.Beatmaps.Formats if (!string.IsNullOrEmpty(beatmap.BeatmapInfo.Metadata.BackgroundFile)) writer.WriteLine(FormattableString.Invariant($"{(int)LegacyEventType.Background},0,\"{beatmap.BeatmapInfo.Metadata.BackgroundFile}\",0,0")); - if (!string.IsNullOrEmpty(beatmap.BeatmapInfo.Metadata.VideoFile)) - writer.WriteLine(FormattableString.Invariant($"{(int)LegacyEventType.Video},{beatmap.BeatmapInfo.Metadata.VideoOffset},\"{beatmap.BeatmapInfo.Metadata.VideoFile}\",0,0")); - foreach (var b in beatmap.Breaks) writer.WriteLine(FormattableString.Invariant($"{(int)LegacyEventType.Break},{b.StartTime},{b.EndTime}")); } diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs index 6569f76b2d..b44d4947d4 100644 --- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs @@ -5,13 +5,13 @@ using System; using System.Collections.Generic; using System.Globalization; using System.IO; -using osuTK; -using osuTK.Graphics; using osu.Framework.Graphics; +using osu.Framework.Utils; +using osu.Game.Beatmaps.Legacy; using osu.Game.IO; using osu.Game.Storyboards; -using osu.Game.Beatmaps.Legacy; -using osu.Framework.Utils; +using osuTK; +using osuTK.Graphics; namespace osu.Game.Beatmaps.Formats { @@ -88,6 +88,15 @@ namespace osu.Game.Beatmaps.Formats switch (type) { + case LegacyEventType.Video: + { + var offset = Parsing.ParseInt(split[1]); + var filename = CleanFilename(split[2]); + + storyboard.GetLayer("Video").Add(new StoryboardVideo(filename, offset)); + break; + } + case LegacyEventType.Sprite: { var layer = parseLayer(split[1]); diff --git a/osu.Game/Beatmaps/IWorkingBeatmap.cs b/osu.Game/Beatmaps/IWorkingBeatmap.cs index 5f1f0d1e40..155e603d30 100644 --- a/osu.Game/Beatmaps/IWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/IWorkingBeatmap.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using osu.Framework.Audio.Track; using osu.Framework.Graphics.Textures; -using osu.Framework.Graphics.Video; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; @@ -26,11 +25,6 @@ namespace osu.Game.Beatmaps /// Texture Background { get; } - /// - /// Retrieves the video background file for this . - /// - VideoSprite Video { get; } - /// /// Retrieves the audio track for this . /// diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 1e1ffad81e..ad94ae8050 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -1,23 +1,22 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Audio.Track; -using osu.Framework.Graphics.Textures; -using osu.Game.Rulesets.Mods; using System; using System.Collections.Generic; -using osu.Game.Storyboards; using System.Linq; using System.Threading; using System.Threading.Tasks; using osu.Framework.Audio; +using osu.Framework.Audio.Track; +using osu.Framework.Graphics.Textures; +using osu.Framework.Logging; using osu.Framework.Statistics; using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.UI; using osu.Game.Skinning; -using osu.Framework.Graphics.Video; -using osu.Framework.Logging; +using osu.Game.Storyboards; namespace osu.Game.Beatmaps { @@ -208,10 +207,6 @@ namespace osu.Game.Beatmaps protected abstract Texture GetBackground(); private readonly RecyclableLazy background; - public VideoSprite Video => GetVideo(); - - protected abstract VideoSprite GetVideo(); - public bool TrackLoaded => track.IsResultAvailable; public Track Track => track.Value; protected abstract Track GetTrack(); diff --git a/osu.Game/Migrations/20200307015200_AddVideoOffset.Designer.cs b/osu.Game/Migrations/20200307015200_AddVideoOffset.Designer.cs deleted file mode 100644 index 10fea5a8bc..0000000000 --- a/osu.Game/Migrations/20200307015200_AddVideoOffset.Designer.cs +++ /dev/null @@ -1,508 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using osu.Game.Database; - -namespace osu.Game.Migrations -{ - [DbContext(typeof(OsuDbContext))] - [Migration("20200307015200_AddVideoOffset")] - partial class AddVideoOffset - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "2.2.6-servicing-10079"); - - modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b => - { - b.Property("ID") - .ValueGeneratedOnAdd(); - - b.Property("ApproachRate"); - - b.Property("CircleSize"); - - b.Property("DrainRate"); - - b.Property("OverallDifficulty"); - - b.Property("SliderMultiplier"); - - b.Property("SliderTickRate"); - - b.HasKey("ID"); - - b.ToTable("BeatmapDifficulty"); - }); - - modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => - { - b.Property("ID") - .ValueGeneratedOnAdd(); - - b.Property("AudioLeadIn"); - - b.Property("BPM"); - - b.Property("BaseDifficultyID"); - - b.Property("BeatDivisor"); - - b.Property("BeatmapSetInfoID"); - - b.Property("Countdown"); - - b.Property("DistanceSpacing"); - - b.Property("GridSize"); - - b.Property("Hash"); - - b.Property("Hidden"); - - b.Property("Length"); - - b.Property("LetterboxInBreaks"); - - b.Property("MD5Hash"); - - b.Property("MetadataID"); - - b.Property("OnlineBeatmapID"); - - b.Property("Path"); - - b.Property("RulesetID"); - - b.Property("SpecialStyle"); - - b.Property("StackLeniency"); - - b.Property("StarDifficulty"); - - b.Property("Status"); - - b.Property("StoredBookmarks"); - - b.Property("TimelineZoom"); - - b.Property("Version"); - - b.Property("WidescreenStoryboard"); - - b.HasKey("ID"); - - b.HasIndex("BaseDifficultyID"); - - b.HasIndex("BeatmapSetInfoID"); - - b.HasIndex("Hash"); - - b.HasIndex("MD5Hash"); - - b.HasIndex("MetadataID"); - - b.HasIndex("OnlineBeatmapID") - .IsUnique(); - - b.HasIndex("RulesetID"); - - b.ToTable("BeatmapInfo"); - }); - - modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b => - { - b.Property("ID") - .ValueGeneratedOnAdd(); - - b.Property("Artist"); - - b.Property("ArtistUnicode"); - - b.Property("AudioFile"); - - b.Property("AuthorString") - .HasColumnName("Author"); - - b.Property("BackgroundFile"); - - b.Property("PreviewTime"); - - b.Property("Source"); - - b.Property("Tags"); - - b.Property("Title"); - - b.Property("TitleUnicode"); - - b.Property("VideoFile"); - - b.Property("VideoOffset"); - - b.HasKey("ID"); - - b.ToTable("BeatmapMetadata"); - }); - - modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => - { - b.Property("ID") - .ValueGeneratedOnAdd(); - - b.Property("BeatmapSetInfoID"); - - b.Property("FileInfoID"); - - b.Property("Filename") - .IsRequired(); - - b.HasKey("ID"); - - b.HasIndex("BeatmapSetInfoID"); - - b.HasIndex("FileInfoID"); - - b.ToTable("BeatmapSetFileInfo"); - }); - - modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => - { - b.Property("ID") - .ValueGeneratedOnAdd(); - - b.Property("DateAdded"); - - b.Property("DeletePending"); - - b.Property("Hash"); - - b.Property("MetadataID"); - - b.Property("OnlineBeatmapSetID"); - - b.Property("Protected"); - - b.Property("Status"); - - b.HasKey("ID"); - - b.HasIndex("DeletePending"); - - b.HasIndex("Hash") - .IsUnique(); - - b.HasIndex("MetadataID"); - - b.HasIndex("OnlineBeatmapSetID") - .IsUnique(); - - b.ToTable("BeatmapSetInfo"); - }); - - modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => - { - b.Property("ID") - .ValueGeneratedOnAdd(); - - b.Property("Key") - .HasColumnName("Key"); - - b.Property("RulesetID"); - - b.Property("SkinInfoID"); - - b.Property("StringValue") - .HasColumnName("Value"); - - b.Property("Variant"); - - b.HasKey("ID"); - - b.HasIndex("SkinInfoID"); - - b.HasIndex("RulesetID", "Variant"); - - b.ToTable("Settings"); - }); - - modelBuilder.Entity("osu.Game.IO.FileInfo", b => - { - b.Property("ID") - .ValueGeneratedOnAdd(); - - b.Property("Hash"); - - b.Property("ReferenceCount"); - - b.HasKey("ID"); - - b.HasIndex("Hash") - .IsUnique(); - - b.HasIndex("ReferenceCount"); - - b.ToTable("FileInfo"); - }); - - modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b => - { - b.Property("ID") - .ValueGeneratedOnAdd(); - - b.Property("IntAction") - .HasColumnName("Action"); - - b.Property("KeysString") - .HasColumnName("Keys"); - - b.Property("RulesetID"); - - b.Property("Variant"); - - b.HasKey("ID"); - - b.HasIndex("IntAction"); - - b.HasIndex("RulesetID", "Variant"); - - b.ToTable("KeyBinding"); - }); - - modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b => - { - b.Property("ID") - .ValueGeneratedOnAdd(); - - b.Property("Available"); - - b.Property("InstantiationInfo"); - - b.Property("Name"); - - b.Property("ShortName"); - - b.HasKey("ID"); - - b.HasIndex("Available"); - - b.HasIndex("ShortName") - .IsUnique(); - - b.ToTable("RulesetInfo"); - }); - - modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b => - { - b.Property("ID") - .ValueGeneratedOnAdd(); - - b.Property("FileInfoID"); - - b.Property("Filename") - .IsRequired(); - - b.Property("ScoreInfoID"); - - b.HasKey("ID"); - - b.HasIndex("FileInfoID"); - - b.HasIndex("ScoreInfoID"); - - b.ToTable("ScoreFileInfo"); - }); - - modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b => - { - b.Property("ID") - .ValueGeneratedOnAdd(); - - b.Property("Accuracy") - .HasColumnType("DECIMAL(1,4)"); - - b.Property("BeatmapInfoID"); - - b.Property("Combo"); - - b.Property("Date"); - - b.Property("DeletePending"); - - b.Property("Hash"); - - b.Property("MaxCombo"); - - b.Property("ModsJson") - .HasColumnName("Mods"); - - b.Property("OnlineScoreID"); - - b.Property("PP"); - - b.Property("Rank"); - - b.Property("RulesetID"); - - b.Property("StatisticsJson") - .HasColumnName("Statistics"); - - b.Property("TotalScore"); - - b.Property("UserID") - .HasColumnName("UserID"); - - b.Property("UserString") - .HasColumnName("User"); - - b.HasKey("ID"); - - b.HasIndex("BeatmapInfoID"); - - b.HasIndex("OnlineScoreID") - .IsUnique(); - - b.HasIndex("RulesetID"); - - b.ToTable("ScoreInfo"); - }); - - modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => - { - b.Property("ID") - .ValueGeneratedOnAdd(); - - b.Property("FileInfoID"); - - b.Property("Filename") - .IsRequired(); - - b.Property("SkinInfoID"); - - b.HasKey("ID"); - - b.HasIndex("FileInfoID"); - - b.HasIndex("SkinInfoID"); - - b.ToTable("SkinFileInfo"); - }); - - modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b => - { - b.Property("ID") - .ValueGeneratedOnAdd(); - - b.Property("Creator"); - - b.Property("DeletePending"); - - b.Property("Hash"); - - b.Property("Name"); - - b.HasKey("ID"); - - b.HasIndex("DeletePending"); - - b.HasIndex("Hash") - .IsUnique(); - - b.ToTable("SkinInfo"); - }); - - modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => - { - b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty") - .WithMany() - .HasForeignKey("BaseDifficultyID") - .OnDelete(DeleteBehavior.Cascade); - - b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet") - .WithMany("Beatmaps") - .HasForeignKey("BeatmapSetInfoID") - .OnDelete(DeleteBehavior.Cascade); - - b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") - .WithMany("Beatmaps") - .HasForeignKey("MetadataID"); - - b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") - .WithMany() - .HasForeignKey("RulesetID") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => - { - b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo") - .WithMany("Files") - .HasForeignKey("BeatmapSetInfoID") - .OnDelete(DeleteBehavior.Cascade); - - b.HasOne("osu.Game.IO.FileInfo", "FileInfo") - .WithMany() - .HasForeignKey("FileInfoID") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => - { - b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") - .WithMany("BeatmapSets") - .HasForeignKey("MetadataID"); - }); - - modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => - { - b.HasOne("osu.Game.Skinning.SkinInfo") - .WithMany("Settings") - .HasForeignKey("SkinInfoID"); - }); - - modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b => - { - b.HasOne("osu.Game.IO.FileInfo", "FileInfo") - .WithMany() - .HasForeignKey("FileInfoID") - .OnDelete(DeleteBehavior.Cascade); - - b.HasOne("osu.Game.Scoring.ScoreInfo") - .WithMany("Files") - .HasForeignKey("ScoreInfoID"); - }); - - modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b => - { - b.HasOne("osu.Game.Beatmaps.BeatmapInfo", "Beatmap") - .WithMany("Scores") - .HasForeignKey("BeatmapInfoID") - .OnDelete(DeleteBehavior.Cascade); - - b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") - .WithMany() - .HasForeignKey("RulesetID") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => - { - b.HasOne("osu.Game.IO.FileInfo", "FileInfo") - .WithMany() - .HasForeignKey("FileInfoID") - .OnDelete(DeleteBehavior.Cascade); - - b.HasOne("osu.Game.Skinning.SkinInfo") - .WithMany("Files") - .HasForeignKey("SkinInfoID") - .OnDelete(DeleteBehavior.Cascade); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/osu.Game/Migrations/20200307015200_AddVideoOffset.cs b/osu.Game/Migrations/20200307015200_AddVideoOffset.cs deleted file mode 100644 index 06c456c551..0000000000 --- a/osu.Game/Migrations/20200307015200_AddVideoOffset.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -namespace osu.Game.Migrations -{ - public partial class AddVideoOffset : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "VideoOffset", - table: "BeatmapMetadata", - nullable: false, - defaultValue: 0); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "VideoOffset", - table: "BeatmapMetadata"); - } - } -} diff --git a/osu.Game/Rulesets/Mods/ModCinema.cs b/osu.Game/Rulesets/Mods/ModCinema.cs index cd08aee453..cf8128301c 100644 --- a/osu.Game/Rulesets/Mods/ModCinema.cs +++ b/osu.Game/Rulesets/Mods/ModCinema.cs @@ -39,7 +39,6 @@ namespace osu.Game.Rulesets.Mods { player.Background.EnableUserDim.Value = false; - player.DimmableVideo.IgnoreUserSettings.Value = true; player.DimmableStoryboard.IgnoreUserSettings.Value = true; player.BreakOverlay.Hide(); diff --git a/osu.Game/Screens/Play/DimmableVideo.cs b/osu.Game/Screens/Play/DimmableVideo.cs deleted file mode 100644 index 2e080d9c2b..0000000000 --- a/osu.Game/Screens/Play/DimmableVideo.cs +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Video; -using osu.Framework.Timing; -using osu.Game.Graphics.Containers; -using osuTK.Graphics; - -namespace osu.Game.Screens.Play -{ - public class DimmableVideo : UserDimContainer - { - private readonly VideoSprite video; - private readonly int offset; - private DrawableVideo drawableVideo; - - public DimmableVideo(VideoSprite video, int offset) - { - this.video = video; - this.offset = offset; - } - - [BackgroundDependencyLoader] - private void load() - { - initializeVideo(false); - } - - protected override void LoadComplete() - { - ShowVideo.BindValueChanged(_ => initializeVideo(true), true); - base.LoadComplete(); - } - - protected override bool ShowDimContent => IgnoreUserSettings.Value || (ShowVideo.Value && DimLevel < 1); - - private void initializeVideo(bool async) - { - if (video == null) - return; - - if (drawableVideo != null) - return; - - if (!ShowVideo.Value && !IgnoreUserSettings.Value) - return; - - drawableVideo = new DrawableVideo(video, offset); - - if (async) - LoadComponentAsync(drawableVideo, Add); - else - Add(drawableVideo); - } - - private class DrawableVideo : Container - { - private readonly Drawable cover; - private readonly int offset; - private readonly ManualClock videoClock; - private bool videoStarted; - - public DrawableVideo(VideoSprite video, int offset) - { - this.offset = offset; - - RelativeSizeAxes = Axes.Both; - Masking = true; - - video.RelativeSizeAxes = Axes.Both; - video.FillMode = FillMode.Fit; - video.Anchor = Anchor.Centre; - video.Origin = Anchor.Centre; - - videoClock = new ManualClock(); - video.Clock = new FramedClock(videoClock); - - AddRangeInternal(new[] - { - video, - cover = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black, - }, - }); - } - - [BackgroundDependencyLoader] - private void load(GameplayClock clock) - { - if (clock != null) - Clock = clock; - } - - protected override void Update() - { - if (videoClock != null && Clock.CurrentTime > offset) - { - if (!videoStarted) - { - cover.FadeOut(500); - videoStarted = true; - } - - // handle seeking before the video starts (break skipping, replay seek) - videoClock.CurrentTime = Clock.CurrentTime - offset; - } - - base.Update(); - } - } - } -} diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index da4829d484..ac0a4bcadc 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -117,8 +117,15 @@ namespace osu.Game.Screens.Play startTime = Math.Min(startTime, firstHitObjectTime - beatmap.BeatmapInfo.AudioLeadIn); // some beatmaps have no AudioLeadIn but the video starts before the first object - if (beatmap.Video != null && beatmap.Metadata.VideoOffset != 0) - startTime = Math.Min(startTime, beatmap.Metadata.VideoOffset); + var videoLayer = beatmap.Storyboard.GetLayer("Video"); + + if (videoLayer.Elements.Any()) + { + var videoOffset = videoLayer.Elements.First().StartTime; + + if (videoOffset != 0) + startTime = Math.Min(startTime, videoOffset); + } Seek(startTime); diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 22d90d4ac1..b90d9d982a 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -85,7 +85,6 @@ namespace osu.Game.Screens.Play protected GameplayClockContainer GameplayClockContainer { get; private set; } public DimmableStoryboard DimmableStoryboard { get; private set; } - public DimmableVideo DimmableVideo { get; private set; } [Cached] [Cached(Type = typeof(IBindable>))] @@ -189,7 +188,6 @@ namespace osu.Game.Screens.Play private void addUnderlayComponents(Container target) { - target.Add(DimmableVideo = new DimmableVideo(Beatmap.Value.Video, Beatmap.Value.Metadata.VideoOffset) { RelativeSizeAxes = Axes.Both }); target.Add(DimmableStoryboard = new DimmableStoryboard(Beatmap.Value.Storyboard) { RelativeSizeAxes = Axes.Both }); } @@ -549,7 +547,6 @@ namespace osu.Game.Screens.Play // bind component bindables. Background.IsBreakTime.BindTo(BreakOverlay.IsBreakTime); DimmableStoryboard.IsBreakTime.BindTo(BreakOverlay.IsBreakTime); - DimmableVideo.IsBreakTime.BindTo(BreakOverlay.IsBreakTime); Background.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground); DimmableStoryboard.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground); diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs new file mode 100644 index 0000000000..2c887a1553 --- /dev/null +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs @@ -0,0 +1,82 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Textures; +using osu.Framework.Graphics.Video; +using osu.Framework.Timing; +using osu.Game.Beatmaps; +using osu.Game.Screens.Play; + +namespace osu.Game.Storyboards.Drawables +{ + public class DrawableStoryboardVideo : Container + { + public readonly StoryboardVideo Video; + private VideoSprite videoSprite; + private ManualClock videoClock; + private GameplayClock clock; + + private bool videoStarted; + + public override bool RemoveWhenNotAlive => false; + + public DrawableStoryboardVideo(StoryboardVideo video) + { + Video = video; + + RelativeSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load(GameplayClock clock, IBindable beatmap, TextureStore textureStore) + { + if (clock != null) + this.clock = clock; + + var path = beatmap.Value.BeatmapSetInfo?.Files?.Find(f => f.Filename.Equals(Video.Path, StringComparison.OrdinalIgnoreCase))?.FileInfo.StoragePath; + + if (path == null) + return; + + var stream = textureStore.GetStream(path); + + if (stream == null) + return; + + AddInternal(videoSprite = new VideoSprite(stream) + { + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fill, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AlwaysPresent = true, + Alpha = 0 + }); + + videoClock = new ManualClock(); + videoSprite.Clock = new FramedClock(videoClock); + } + + protected override void Update() + { + if (clock.CurrentTime > Video.StartTime) + { + if (!videoStarted) + { + videoSprite.FadeIn(500); + videoStarted = true; + } + + // handle seeking before the video starts (break skipping, replay seek) + videoClock.CurrentTime = clock.CurrentTime - Video.StartTime; + } + + base.Update(); + } + } +} diff --git a/osu.Game/Storyboards/Storyboard.cs b/osu.Game/Storyboards/Storyboard.cs index 35bfe8c229..e58c422c6d 100644 --- a/osu.Game/Storyboards/Storyboard.cs +++ b/osu.Game/Storyboards/Storyboard.cs @@ -21,6 +21,7 @@ namespace osu.Game.Storyboards public Storyboard() { + layers.Add("Video", new StoryboardLayer("Video", 4)); layers.Add("Background", new StoryboardLayer("Background", 3)); layers.Add("Fail", new StoryboardLayer("Fail", 2) { EnabledWhenPassing = false, }); layers.Add("Pass", new StoryboardLayer("Pass", 1) { EnabledWhenFailing = false, }); @@ -53,7 +54,7 @@ namespace osu.Game.Storyboards public DrawableStoryboard CreateDrawable(WorkingBeatmap working = null) { var drawable = new DrawableStoryboard(this); - drawable.Width = drawable.Height * (BeatmapInfo.WidescreenStoryboard ? 16 / 9f : 4 / 3f); + drawable.Width = drawable.Height * (BeatmapInfo.WidescreenStoryboard || GetLayer("Video").Elements.Any() ? 16 / 9f : 4 / 3f); return drawable; } } diff --git a/osu.Game/Storyboards/StoryboardVideo.cs b/osu.Game/Storyboards/StoryboardVideo.cs new file mode 100644 index 0000000000..4652e45852 --- /dev/null +++ b/osu.Game/Storyboards/StoryboardVideo.cs @@ -0,0 +1,25 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Game.Storyboards.Drawables; + +namespace osu.Game.Storyboards +{ + public class StoryboardVideo : IStoryboardElement + { + public string Path { get; } + + public bool IsDrawable => true; + + public double StartTime { get; } + + public StoryboardVideo(string path, int offset) + { + Path = path; + StartTime = offset; + } + + public Drawable CreateDrawable() => new DrawableStoryboardVideo(this); + } +} diff --git a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs index ef86186e41..b60add6e3b 100644 --- a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs +++ b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs @@ -10,7 +10,6 @@ using Newtonsoft.Json; using NUnit.Framework; using osu.Framework.Audio.Track; using osu.Framework.Graphics.Textures; -using osu.Framework.Graphics.Video; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; using osu.Game.IO; @@ -207,8 +206,6 @@ namespace osu.Game.Tests.Beatmaps protected override Texture GetBackground() => throw new NotImplementedException(); - protected override VideoSprite GetVideo() => throw new NotImplementedException(); - protected override Track GetTrack() => throw new NotImplementedException(); protected override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap, Ruleset ruleset) diff --git a/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs b/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs index 871d8ee3f1..6db34af20c 100644 --- a/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs +++ b/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs @@ -3,7 +3,6 @@ using osu.Framework.Audio.Track; using osu.Framework.Graphics.Textures; -using osu.Framework.Graphics.Video; using osu.Game.Beatmaps; using osu.Game.Storyboards; @@ -32,8 +31,6 @@ namespace osu.Game.Tests.Beatmaps protected override Texture GetBackground() => null; - protected override VideoSprite GetVideo() => null; - protected override Track GetTrack() => null; } } From 48282dea8bd4fbd172efa6848aa88c87b4a103e9 Mon Sep 17 00:00:00 2001 From: voidedWarranties Date: Sat, 7 Mar 2020 22:08:38 -0800 Subject: [PATCH 0242/2376] Remove individual setting to disable videos, fix tests --- .../Beatmaps/Formats/LegacyStoryboardDecoderTest.cs | 2 +- osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs | 5 +++++ osu.Game/Configuration/OsuConfigManager.cs | 2 -- osu.Game/Graphics/Containers/UserDimContainer.cs | 4 ---- .../Settings/Sections/Graphics/DetailSettings.cs | 5 ----- .../Screens/Backgrounds/BackgroundScreenBeatmap.cs | 2 +- osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs | 3 --- .../Storyboards/Drawables/DrawableStoryboardVideo.cs | 10 ++++++---- 8 files changed, 13 insertions(+), 20 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs index 96ff6b81e3..edb0d9f04b 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs @@ -26,7 +26,7 @@ namespace osu.Game.Tests.Beatmaps.Formats var storyboard = decoder.Decode(stream); Assert.IsTrue(storyboard.HasDrawable); - Assert.AreEqual(4, storyboard.Layers.Count()); + Assert.AreEqual(5, storyboard.Layers.Count()); StoryboardLayer background = storyboard.Layers.FirstOrDefault(l => l.Depth == 3); Assert.IsNotNull(background); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs index ff8437311e..16a985c796 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Overlays; +using osu.Game.Screens.Play; using osu.Game.Storyboards.Drawables; using osuTK.Graphics; @@ -24,9 +25,13 @@ namespace osu.Game.Tests.Visual.Gameplay [Cached] private MusicController musicController = new MusicController(); + [Cached] + private GameplayClock gameplayClock; + public TestSceneStoryboard() { Clock = new FramedClock(); + gameplayClock = new GameplayClock(Clock); AddRange(new Drawable[] { diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 21de654670..41f6747b74 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -70,7 +70,6 @@ namespace osu.Game.Configuration Set(OsuSetting.ShowFpsDisplay, false); Set(OsuSetting.ShowStoryboard, true); - Set(OsuSetting.ShowVideoBackground, true); Set(OsuSetting.BeatmapSkins, true); Set(OsuSetting.BeatmapHitsounds, true); @@ -176,7 +175,6 @@ namespace osu.Game.Configuration BlurLevel, LightenDuringBreaks, ShowStoryboard, - ShowVideoBackground, KeyOverlay, ScoreMeter, FloatingComments, diff --git a/osu.Game/Graphics/Containers/UserDimContainer.cs b/osu.Game/Graphics/Containers/UserDimContainer.cs index 65c104b92f..4485ce3447 100644 --- a/osu.Game/Graphics/Containers/UserDimContainer.cs +++ b/osu.Game/Graphics/Containers/UserDimContainer.cs @@ -55,8 +55,6 @@ namespace osu.Game.Graphics.Containers protected Bindable ShowStoryboard { get; private set; } - protected Bindable ShowVideo { get; private set; } - private float breakLightening => LightenDuringBreaks.Value && IsBreakTime.Value ? BREAK_LIGHTEN_AMOUNT : 0; protected float DimLevel => Math.Max(EnableUserDim.Value && !IgnoreUserSettings.Value ? (float)UserDimLevel.Value - breakLightening : 0, 0); @@ -79,14 +77,12 @@ namespace osu.Game.Graphics.Containers UserDimLevel = config.GetBindable(OsuSetting.DimLevel); LightenDuringBreaks = config.GetBindable(OsuSetting.LightenDuringBreaks); ShowStoryboard = config.GetBindable(OsuSetting.ShowStoryboard); - ShowVideo = config.GetBindable(OsuSetting.ShowVideoBackground); EnableUserDim.ValueChanged += _ => UpdateVisuals(); UserDimLevel.ValueChanged += _ => UpdateVisuals(); LightenDuringBreaks.ValueChanged += _ => UpdateVisuals(); IsBreakTime.ValueChanged += _ => UpdateVisuals(); ShowStoryboard.ValueChanged += _ => UpdateVisuals(); - ShowVideo.ValueChanged += _ => UpdateVisuals(); StoryboardReplacesBackground.ValueChanged += _ => UpdateVisuals(); IgnoreUserSettings.ValueChanged += _ => UpdateVisuals(); } diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs index ea2811e5cd..acf33f00b3 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs @@ -22,11 +22,6 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics Bindable = config.GetBindable(OsuSetting.ShowStoryboard) }, new SettingsCheckbox - { - LabelText = "Video", - Bindable = config.GetBindable(OsuSetting.ShowVideoBackground) - }, - new SettingsCheckbox { LabelText = "Hit Lighting", Bindable = config.GetBindable(OsuSetting.HitLighting) diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs index 50fd127093..b08455be95 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs @@ -166,7 +166,7 @@ namespace osu.Game.Screens.Backgrounds BlurAmount.ValueChanged += _ => UpdateVisuals(); } - protected override bool ShowDimContent => !ShowStoryboard.Value || !StoryboardReplacesBackground.Value || !ShowVideo.Value; // The background needs to be hidden in the case of it being replaced by the storyboard + protected override bool ShowDimContent => !ShowStoryboard.Value || !StoryboardReplacesBackground.Value; // The background needs to be hidden in the case of it being replaced by the storyboard protected override void UpdateVisuals() { diff --git a/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs b/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs index 9db3a587fa..bfb77e823f 100644 --- a/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs @@ -15,7 +15,6 @@ namespace osu.Game.Screens.Play.PlayerSettings private readonly PlayerSliderBar dimSliderBar; private readonly PlayerSliderBar blurSliderBar; private readonly PlayerCheckbox showStoryboardToggle; - private readonly PlayerCheckbox showVideoToggle; private readonly PlayerCheckbox beatmapSkinsToggle; private readonly PlayerCheckbox beatmapHitsoundsToggle; @@ -44,7 +43,6 @@ namespace osu.Game.Screens.Play.PlayerSettings Text = "Toggles:" }, showStoryboardToggle = new PlayerCheckbox { LabelText = "Storyboards" }, - showVideoToggle = new PlayerCheckbox { LabelText = "Video" }, beatmapSkinsToggle = new PlayerCheckbox { LabelText = "Beatmap skins" }, beatmapHitsoundsToggle = new PlayerCheckbox { LabelText = "Beatmap hitsounds" } }; @@ -56,7 +54,6 @@ namespace osu.Game.Screens.Play.PlayerSettings dimSliderBar.Bindable = config.GetBindable(OsuSetting.DimLevel); blurSliderBar.Bindable = config.GetBindable(OsuSetting.BlurLevel); showStoryboardToggle.Current = config.GetBindable(OsuSetting.ShowStoryboard); - showVideoToggle.Current = config.GetBindable(OsuSetting.ShowVideoBackground); beatmapSkinsToggle.Current = config.GetBindable(OsuSetting.BeatmapSkins); beatmapHitsoundsToggle.Current = config.GetBindable(OsuSetting.BeatmapHitsounds); } diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs index 2c887a1553..ef14ccd4d7 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs @@ -32,11 +32,13 @@ namespace osu.Game.Storyboards.Drawables RelativeSizeAxes = Axes.Both; } - [BackgroundDependencyLoader] + [BackgroundDependencyLoader(true)] private void load(GameplayClock clock, IBindable beatmap, TextureStore textureStore) { - if (clock != null) - this.clock = clock; + if (clock == null) + return; + + this.clock = clock; var path = beatmap.Value.BeatmapSetInfo?.Files?.Find(f => f.Filename.Equals(Video.Path, StringComparison.OrdinalIgnoreCase))?.FileInfo.StoragePath; @@ -64,7 +66,7 @@ namespace osu.Game.Storyboards.Drawables protected override void Update() { - if (clock.CurrentTime > Video.StartTime) + if (clock != null && clock.CurrentTime > Video.StartTime) { if (!videoStarted) { From 3caffb81e187e2d546fba85640791004d12e3ad5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Mar 2020 16:03:18 +0900 Subject: [PATCH 0243/2376] Add new element colours to TournamentGame --- osu.Game.Tournament/TournamentGame.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tournament/TournamentGame.cs b/osu.Game.Tournament/TournamentGame.cs index 608fc5f04a..6d597d5e7d 100644 --- a/osu.Game.Tournament/TournamentGame.cs +++ b/osu.Game.Tournament/TournamentGame.cs @@ -2,15 +2,25 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Game.Graphics; using osu.Game.Graphics.Cursor; +using osu.Game.Tournament.Models; using osuTK.Graphics; namespace osu.Game.Tournament { public class TournamentGame : TournamentGameBase { - public static readonly Color4 COLOUR_RED = new Color4(144, 0, 0, 255); - public static readonly Color4 COLOUR_BLUE = new Color4(0, 84, 144, 255); + public static ColourInfo GetTeamColour(TeamColour teamColour) => teamColour == TeamColour.Red ? COLOUR_RED : COLOUR_BLUE; + + public static readonly Color4 COLOUR_RED = OsuColour.FromHex("#AA1414"); + public static readonly Color4 COLOUR_BLUE = OsuColour.FromHex("#1462AA"); + + public static readonly Color4 ELEMENT_BACKGROUND_COLOUR = OsuColour.FromHex("#fff"); + public static readonly Color4 ELEMENT_FOREGROUND_COLOUR = OsuColour.FromHex("#000"); + + public static readonly Color4 TEXT_COLOUR = OsuColour.FromHex("#fff"); protected override void LoadComplete() { From 129c8fe24ff86054d5875650d47acf5da098307d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Mar 2020 16:03:34 +0900 Subject: [PATCH 0244/2376] Add helper method to get winning team colour --- osu.Game.Tournament/Models/TournamentMatch.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tournament/Models/TournamentMatch.cs b/osu.Game.Tournament/Models/TournamentMatch.cs index 06cce3d59e..8ebcbf4e15 100644 --- a/osu.Game.Tournament/Models/TournamentMatch.cs +++ b/osu.Game.Tournament/Models/TournamentMatch.cs @@ -90,6 +90,8 @@ namespace osu.Game.Tournament.Models [JsonIgnore] public TournamentTeam Loser => !Completed.Value ? null : Team1Score.Value > Team2Score.Value ? Team2.Value : Team1.Value; + public TeamColour WinnerColour => Winner == Team1.Value ? TeamColour.Red : TeamColour.Blue; + public int PointsToWin => Round.Value?.BestOf.Value / 2 + 1 ?? 0; /// From aeb6bf5b4619e139526600b5f2fd78ca5f72bfd4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Mar 2020 16:36:07 +0900 Subject: [PATCH 0245/2376] Remove unnecessary width specification on editor screens --- osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs index e4256e727d..8e5df72cc8 100644 --- a/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs @@ -40,7 +40,6 @@ namespace osu.Game.Tournament.Screens.Editors new OsuScrollContainer { RelativeSizeAxes = Axes.Both, - Width = 0.9f, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Child = flow = new FillFlowContainer From 01e32896eebdbdaf880610b888993ce9f061e81e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Mar 2020 17:43:47 +0900 Subject: [PATCH 0246/2376] Make save changes button more prominent --- osu.Game.Tournament/TournamentGameBase.cs | 37 +++++++++++++++++++---- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 435f315c8d..41165ca141 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -22,6 +22,7 @@ using osu.Game.Online.API.Requests; using osu.Game.Tournament.IPC; using osu.Game.Tournament.Models; using osu.Game.Users; +using osuTK; using osuTK.Graphics; using osuTK.Input; @@ -74,16 +75,40 @@ namespace osu.Game.Tournament AddRange(new[] { - new TourneyButton + new Container { - Text = "Save Changes", - Width = 140, - Height = 50, + CornerRadius = 10, Depth = float.MinValue, + Position = new Vector2(5), + Masking = true, + AutoSizeAxes = Axes.Both, Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, - Padding = new MarginPadding(10), - Action = SaveChanges, + Children = new Drawable[] + { + new Box + { + Colour = OsuColour.Gray(0.2f), + RelativeSizeAxes = Axes.Both, + }, + new TourneyButton + { + Text = "Save Changes", + Width = 140, + Height = 50, + Padding = new MarginPadding + { + Top = 10, + Left = 10, + }, + Margin = new MarginPadding + { + Right = 10, + Bottom = 10, + }, + Action = SaveChanges, + }, + } }, heightWarning = new Container { From aed52179f0ffaea6be4e15775857370201ee5fd9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 7 Mar 2020 16:29:11 +0900 Subject: [PATCH 0247/2376] Fix weird reverse logic --- osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs | 6 +++--- osu.Game/Online/Chat/StandAloneChatDisplay.cs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs index d632e7c5f3..6ba57c60b8 100644 --- a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs +++ b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs @@ -162,7 +162,7 @@ namespace osu.Game.Tournament.Screens.Gameplay void expand() { - chat?.Expand(); + chat?.Contract(); using (BeginDelayedSequence(300, true)) { @@ -176,7 +176,7 @@ namespace osu.Game.Tournament.Screens.Gameplay SongBar.Expanded = false; scoreDisplay.FadeOut(100); using (chat?.BeginDelayedSequence(500)) - chat?.Contract(); + chat?.Expand(); } switch (state.NewValue) @@ -203,7 +203,7 @@ namespace osu.Game.Tournament.Screens.Gameplay break; default: - chat.Expand(); + chat.Contract(); expand(); break; } diff --git a/osu.Game/Online/Chat/StandAloneChatDisplay.cs b/osu.Game/Online/Chat/StandAloneChatDisplay.cs index 21d0bcc4bf..881dd19d8e 100644 --- a/osu.Game/Online/Chat/StandAloneChatDisplay.cs +++ b/osu.Game/Online/Chat/StandAloneChatDisplay.cs @@ -92,13 +92,13 @@ namespace osu.Game.Online.Chat textbox.Text = string.Empty; } - public void Contract() + public void Expand() { this.FadeIn(300); this.MoveToY(0, 500, Easing.OutQuint); } - public void Expand() + public void Contract() { this.FadeOut(200); this.MoveToY(100, 500, Easing.In); From 1c5d6e0cf453eea4323ac204150b0811cd770057 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Mar 2020 16:07:54 +0900 Subject: [PATCH 0248/2376] Split out nested classes to higher level for better code sharing --- .../TestSceneDrawableTournamentTeam.cs | 124 ++++++++++ .../Components/DrawableTeamFlag.cs | 33 +++ .../Components/DrawableTeamHeader.cs | 20 ++ .../Components/DrawableTeamTitle.cs | 32 +++ .../Components/DrawableTeamTitleWithHeader.cs | 30 +++ .../Components/DrawableTeamWithPlayers.cs | 71 ++++++ .../Components/DrawableTournamentTeam.cs | 6 +- .../Components/DrawableTournamentTitleText.cs | 16 ++ .../Components/RoundDisplay.cs | 36 +++ .../TournamentSpriteTextWithBackground.cs | 37 +++ .../Screens/Drawings/Components/Group.cs | 49 ---- .../Screens/Drawings/Components/GroupTeam.cs | 60 +++++ .../Screens/Editors/TeamEditorScreen.cs | 13 - .../Gameplay/Components/MatchHeader.cs | 228 +++++------------- .../Gameplay/Components/RoundDisplay.cs | 56 +++++ .../Gameplay/Components/TeamDisplay.cs | 50 ++++ .../Screens/Gameplay/Components/TeamScore.cs | 38 +++ 17 files changed, 662 insertions(+), 237 deletions(-) create mode 100644 osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs create mode 100644 osu.Game.Tournament/Components/DrawableTeamFlag.cs create mode 100644 osu.Game.Tournament/Components/DrawableTeamHeader.cs create mode 100644 osu.Game.Tournament/Components/DrawableTeamTitle.cs create mode 100644 osu.Game.Tournament/Components/DrawableTeamTitleWithHeader.cs create mode 100644 osu.Game.Tournament/Components/DrawableTeamWithPlayers.cs create mode 100644 osu.Game.Tournament/Components/DrawableTournamentTitleText.cs create mode 100644 osu.Game.Tournament/Components/RoundDisplay.cs create mode 100644 osu.Game.Tournament/Components/TournamentSpriteTextWithBackground.cs create mode 100644 osu.Game.Tournament/Screens/Drawings/Components/GroupTeam.cs create mode 100644 osu.Game.Tournament/Screens/Gameplay/Components/RoundDisplay.cs create mode 100644 osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs create mode 100644 osu.Game.Tournament/Screens/Gameplay/Components/TeamScore.cs diff --git a/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs b/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs new file mode 100644 index 0000000000..41f7d3d847 --- /dev/null +++ b/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs @@ -0,0 +1,124 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using osu.Framework.Graphics; +using osu.Game.Tests.Visual; +using osu.Game.Tournament.Components; +using osu.Game.Tournament.Models; +using osu.Game.Tournament.Screens.Drawings.Components; +using osu.Game.Tournament.Screens.Gameplay.Components; +using osu.Game.Tournament.Screens.Ladder.Components; +using osu.Game.Users; + +namespace osu.Game.Tournament.Tests.Components +{ + public class TestSceneDrawableTournamentTeam : OsuGridTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(DrawableTeamFlag), + typeof(DrawableTeamTitle), + typeof(DrawableTeamTitleWithHeader), + typeof(DrawableMatchTeam), + typeof(DrawableTeamWithPlayers), + typeof(GroupTeam), + typeof(TeamDisplay), + }; + + public TestSceneDrawableTournamentTeam() + : base(4, 3) + { + var team = new TournamentTeam + { + FlagName = { Value = "AU" }, + FullName = { Value = "Australia" }, + Players = + { + new User { Username = "ASecretBox" }, + new User { Username = "Dereban" }, + new User { Username = "mReKk" }, + new User { Username = "uyghti" }, + new User { Username = "Parkes" }, + new User { Username = "Shiroha" }, + new User { Username = "Jordan The Bear" }, + } + }; + + var match = new TournamentMatch { Team1 = { Value = team } }; + + int i = 0; + + Cell(i++).AddRange(new Drawable[] + { + new TournamentSpriteText { Text = "DrawableTeamFlag" }, + new DrawableTeamFlag(team) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }); + + Cell(i++).AddRange(new Drawable[] + { + new TournamentSpriteText { Text = "DrawableTeamTitle" }, + new DrawableTeamTitle(team) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }); + + Cell(i++).AddRange(new Drawable[] + { + new TournamentSpriteText { Text = "DrawableTeamTitleWithHeader" }, + new DrawableTeamTitleWithHeader(team, TeamColour.Red) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }); + + Cell(i++).AddRange(new Drawable[] + { + new TournamentSpriteText { Text = "DrawableMatchTeam" }, + new DrawableMatchTeam(team, match, false) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }); + + Cell(i++).AddRange(new Drawable[] + { + new TournamentSpriteText { Text = "TeamWithPlayers" }, + new DrawableTeamWithPlayers(team, TeamColour.Blue) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }); + + Cell(i++).AddRange(new Drawable[] + { + new TournamentSpriteText { Text = "GroupTeam" }, + new GroupTeam(team) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }); + + Cell(i).AddRange(new Drawable[] + { + new TournamentSpriteText { Text = "TeamDisplay" }, + new TeamDisplay(team, TournamentGame.COLOUR_RED, false) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }); + } + } +} diff --git a/osu.Game.Tournament/Components/DrawableTeamFlag.cs b/osu.Game.Tournament/Components/DrawableTeamFlag.cs new file mode 100644 index 0000000000..8c85c9a46f --- /dev/null +++ b/osu.Game.Tournament/Components/DrawableTeamFlag.cs @@ -0,0 +1,33 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Game.Tournament.Models; + +namespace osu.Game.Tournament.Components +{ + public class DrawableTeamFlag : Sprite + { + private readonly TournamentTeam team; + + [UsedImplicitly] + private Bindable flag; + + public DrawableTeamFlag(TournamentTeam team) + { + this.team = team; + } + + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + if (team == null) return; + + (flag = team.FlagName.GetBoundCopy()).BindValueChanged(acronym => Texture = textures.Get($@"Flags/{team.FlagName}"), true); + } + } +} diff --git a/osu.Game.Tournament/Components/DrawableTeamHeader.cs b/osu.Game.Tournament/Components/DrawableTeamHeader.cs new file mode 100644 index 0000000000..3d9e8a6e00 --- /dev/null +++ b/osu.Game.Tournament/Components/DrawableTeamHeader.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.Tournament.Models; +using osuTK; + +namespace osu.Game.Tournament.Components +{ + public class DrawableTeamHeader : TournamentSpriteTextWithBackground + { + public DrawableTeamHeader(TeamColour colour) + { + Background.Colour = TournamentGame.GetTeamColour(colour); + + Text.Colour = TournamentGame.TEXT_COLOUR; + Text.Text = $"Team {colour}".ToUpperInvariant(); + Text.Scale = new Vector2(0.6f); + } + } +} diff --git a/osu.Game.Tournament/Components/DrawableTeamTitle.cs b/osu.Game.Tournament/Components/DrawableTeamTitle.cs new file mode 100644 index 0000000000..5aac37259f --- /dev/null +++ b/osu.Game.Tournament/Components/DrawableTeamTitle.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.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics.Textures; +using osu.Game.Tournament.Models; + +namespace osu.Game.Tournament.Components +{ + public class DrawableTeamTitle : TournamentSpriteTextWithBackground + { + private readonly TournamentTeam team; + + [UsedImplicitly] + private Bindable acronym; + + public DrawableTeamTitle(TournamentTeam team) + { + this.team = team; + } + + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + if (team == null) return; + + (acronym = team.Acronym.GetBoundCopy()).BindValueChanged(acronym => Text.Text = team?.FullName.Value ?? string.Empty, true); + } + } +} diff --git a/osu.Game.Tournament/Components/DrawableTeamTitleWithHeader.cs b/osu.Game.Tournament/Components/DrawableTeamTitleWithHeader.cs new file mode 100644 index 0000000000..ceffe3d315 --- /dev/null +++ b/osu.Game.Tournament/Components/DrawableTeamTitleWithHeader.cs @@ -0,0 +1,30 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Tournament.Models; +using osuTK; + +namespace osu.Game.Tournament.Components +{ + public class DrawableTeamTitleWithHeader : CompositeDrawable + { + public DrawableTeamTitleWithHeader(TournamentTeam team, TeamColour colour) + { + AutoSizeAxes = Axes.Both; + + InternalChild = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 10), + Children = new Drawable[] + { + new DrawableTeamHeader(colour), + new DrawableTeamTitle(team), + } + }; + } + } +} diff --git a/osu.Game.Tournament/Components/DrawableTeamWithPlayers.cs b/osu.Game.Tournament/Components/DrawableTeamWithPlayers.cs new file mode 100644 index 0000000000..0b80bef903 --- /dev/null +++ b/osu.Game.Tournament/Components/DrawableTeamWithPlayers.cs @@ -0,0 +1,71 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Tournament.Models; +using osu.Game.Users; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Tournament.Components +{ + public class DrawableTeamWithPlayers : CompositeDrawable + { + public DrawableTeamWithPlayers(TournamentTeam team, TeamColour colour) + { + AutoSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(30), + Children = new Drawable[] + { + new DrawableTeamTitleWithHeader(team, colour), + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Padding = new MarginPadding { Left = 10 }, + Spacing = new Vector2(30), + Children = new Drawable[] + { + new FillFlowContainer + { + Direction = FillDirection.Vertical, + AutoSizeAxes = Axes.Both, + ChildrenEnumerable = team?.Players.Select(createPlayerText).Take(5) ?? Enumerable.Empty() + }, + new FillFlowContainer + { + Direction = FillDirection.Vertical, + AutoSizeAxes = Axes.Both, + ChildrenEnumerable = team?.Players.Select(p => new TournamentSpriteText + { + Text = p.Username, + Font = OsuFont.Torus.With(size: 24, weight: FontWeight.SemiBold), + Colour = Color4.White, + }).Skip(5) ?? Enumerable.Empty() + }, + } + }, + } + }, + }; + + TournamentSpriteText createPlayerText(User p) => + new TournamentSpriteText + { + Text = p.Username, + Font = OsuFont.Torus.With(size: 24, weight: FontWeight.SemiBold), + Colour = Color4.White, + }; + } + } +} diff --git a/osu.Game.Tournament/Components/DrawableTournamentTeam.cs b/osu.Game.Tournament/Components/DrawableTournamentTeam.cs index 99116d4a17..f8aed26ce1 100644 --- a/osu.Game.Tournament/Components/DrawableTournamentTeam.cs +++ b/osu.Game.Tournament/Components/DrawableTournamentTeam.cs @@ -23,14 +23,11 @@ namespace osu.Game.Tournament.Components [UsedImplicitly] private Bindable acronym; - [UsedImplicitly] - private Bindable flag; - protected DrawableTournamentTeam(TournamentTeam team) { Team = team; - Flag = new Sprite + Flag = new DrawableTeamFlag(team) { RelativeSizeAxes = Axes.Both, FillMode = FillMode.Fit @@ -48,7 +45,6 @@ namespace osu.Game.Tournament.Components if (Team == null) return; (acronym = Team.Acronym.GetBoundCopy()).BindValueChanged(acronym => AcronymText.Text = Team?.Acronym.Value?.ToUpperInvariant() ?? string.Empty, true); - (flag = Team.FlagName.GetBoundCopy()).BindValueChanged(acronym => Flag.Texture = textures.Get($@"Flags/{Team.FlagName}"), true); } } } diff --git a/osu.Game.Tournament/Components/DrawableTournamentTitleText.cs b/osu.Game.Tournament/Components/DrawableTournamentTitleText.cs new file mode 100644 index 0000000000..4fbc6cd060 --- /dev/null +++ b/osu.Game.Tournament/Components/DrawableTournamentTitleText.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 osu.Game.Graphics; + +namespace osu.Game.Tournament.Components +{ + public class DrawableTournamentTitleText : TournamentSpriteText + { + public DrawableTournamentTitleText() + { + Text = "osu!taiko world cup 2020"; + Font = OsuFont.Torus.With(size: 26, weight: FontWeight.SemiBold); + } + } +} diff --git a/osu.Game.Tournament/Components/RoundDisplay.cs b/osu.Game.Tournament/Components/RoundDisplay.cs new file mode 100644 index 0000000000..dd56c83c57 --- /dev/null +++ b/osu.Game.Tournament/Components/RoundDisplay.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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Tournament.Models; + +namespace osu.Game.Tournament.Components +{ + public class RoundDisplay : CompositeDrawable + { + public RoundDisplay(TournamentMatch match) + { + AutoSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new DrawableTournamentTitleText(), + new TournamentSpriteText + { + Text = match.Round.Value?.Name.Value ?? "Unknown Round", + Font = OsuFont.Torus.With(size: 26, weight: FontWeight.SemiBold) + }, + } + } + }; + } + } +} diff --git a/osu.Game.Tournament/Components/TournamentSpriteTextWithBackground.cs b/osu.Game.Tournament/Components/TournamentSpriteTextWithBackground.cs new file mode 100644 index 0000000000..d92b9eb605 --- /dev/null +++ b/osu.Game.Tournament/Components/TournamentSpriteTextWithBackground.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 osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; + +namespace osu.Game.Tournament.Components +{ + public class TournamentSpriteTextWithBackground : CompositeDrawable + { + protected readonly TournamentSpriteText Text; + protected readonly Box Background; + + public TournamentSpriteTextWithBackground(string text = "") + { + AutoSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + Background = new Box + { + Colour = TournamentGame.ELEMENT_BACKGROUND_COLOUR, + RelativeSizeAxes = Axes.Both, + }, + Text = new TournamentSpriteText + { + Colour = TournamentGame.ELEMENT_FOREGROUND_COLOUR, + Font = OsuFont.Torus.With(weight: FontWeight.SemiBold, size: 50), + Padding = new MarginPadding { Left = 10, Right = 20 }, + Text = text + } + }; + } + } +} diff --git a/osu.Game.Tournament/Screens/Drawings/Components/Group.cs b/osu.Game.Tournament/Screens/Drawings/Components/Group.cs index 4126f2db65..ece1c431e2 100644 --- a/osu.Game.Tournament/Screens/Drawings/Components/Group.cs +++ b/osu.Game.Tournament/Screens/Drawings/Components/Group.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; -using osu.Game.Tournament.Components; using osu.Game.Tournament.Models; using osuTK; using osuTK.Graphics; @@ -116,53 +115,5 @@ namespace osu.Game.Tournament.Screens.Drawings.Components sb.AppendLine(gt.Team.FullName.Value); return sb.ToString(); } - - private class GroupTeam : DrawableTournamentTeam - { - private readonly FillFlowContainer innerContainer; - - public GroupTeam(TournamentTeam team) - : base(team) - { - Width = 36; - AutoSizeAxes = Axes.Y; - - Flag.Anchor = Anchor.TopCentre; - Flag.Origin = Anchor.TopCentre; - - AcronymText.Anchor = Anchor.TopCentre; - AcronymText.Origin = Anchor.TopCentre; - AcronymText.Text = team.Acronym.Value.ToUpperInvariant(); - AcronymText.Font = OsuFont.Torus.With(weight: FontWeight.Bold, size: 10); - - InternalChildren = new Drawable[] - { - innerContainer = new FillFlowContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 5f), - - Children = new Drawable[] - { - Flag, - AcronymText - } - } - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - innerContainer.ScaleTo(1.5f); - innerContainer.ScaleTo(1f, 200); - } - } } } diff --git a/osu.Game.Tournament/Screens/Drawings/Components/GroupTeam.cs b/osu.Game.Tournament/Screens/Drawings/Components/GroupTeam.cs new file mode 100644 index 0000000000..4f0ce0bbe7 --- /dev/null +++ b/osu.Game.Tournament/Screens/Drawings/Components/GroupTeam.cs @@ -0,0 +1,60 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Tournament.Components; +using osu.Game.Tournament.Models; +using osuTK; + +namespace osu.Game.Tournament.Screens.Drawings.Components +{ + public class GroupTeam : DrawableTournamentTeam + { + private readonly FillFlowContainer innerContainer; + + public GroupTeam(TournamentTeam team) + : base(team) + { + Width = 36; + AutoSizeAxes = Axes.Y; + + Flag.Anchor = Anchor.TopCentre; + Flag.Origin = Anchor.TopCentre; + + AcronymText.Anchor = Anchor.TopCentre; + AcronymText.Origin = Anchor.TopCentre; + AcronymText.Text = team.Acronym.Value.ToUpperInvariant(); + AcronymText.Font = OsuFont.Torus.With(weight: FontWeight.Bold, size: 10); + + InternalChildren = new Drawable[] + { + innerContainer = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 5f), + + Children = new Drawable[] + { + Flag, + AcronymText + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + innerContainer.ScaleTo(1.5f); + innerContainer.ScaleTo(1f, 200); + } + } +} diff --git a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs index 631393c6f4..7468c9484d 100644 --- a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs @@ -172,19 +172,6 @@ namespace osu.Game.Tournament.Screens.Editors drawableContainer.Child = new DrawableTeamFlag(Model); } - private class DrawableTeamFlag : DrawableTournamentTeam - { - public DrawableTeamFlag(TournamentTeam team) - : base(team) - { - InternalChild = Flag; - RelativeSizeAxes = Axes.Both; - - Flag.Anchor = Anchor.Centre; - Flag.Origin = Anchor.Centre; - } - } - public class PlayerEditor : CompositeDrawable { private readonly TournamentTeam team; diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs b/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs index ce17c392d0..c86132a802 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs @@ -5,15 +5,9 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; -using osu.Game.Graphics; -using osu.Game.Graphics.UserInterface; -using osu.Game.Tournament.Components; using osu.Game.Tournament.Models; using osu.Game.Tournament.Screens.Showcase; -using osuTK; -using osuTK.Graphics; using osuTK.Input; namespace osu.Game.Tournament.Screens.Gameplay.Components @@ -46,181 +40,75 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components }, }; } + } - private class TeamScoreDisplay : CompositeDrawable + public class TeamScoreDisplay : CompositeDrawable + { + private readonly TeamColour teamColour; + + private readonly Bindable currentMatch = new Bindable(); + private readonly Bindable currentTeam = new Bindable(); + private readonly Bindable currentTeamScore = new Bindable(); + + public TeamScoreDisplay(TeamColour teamColour) { - private readonly TeamColour teamColour; + this.teamColour = teamColour; - private readonly Bindable currentMatch = new Bindable(); - private readonly Bindable currentTeam = new Bindable(); - private readonly Bindable currentTeamScore = new Bindable(); + RelativeSizeAxes = Axes.Y; + Width = 300; + } - public TeamScoreDisplay(TeamColour teamColour) + [BackgroundDependencyLoader] + private void load(LadderInfo ladder) + { + currentMatch.BindValueChanged(matchChanged); + currentMatch.BindTo(ladder.CurrentMatch); + } + + private void matchChanged(ValueChangedEvent match) + { + currentTeamScore.UnbindBindings(); + currentTeamScore.BindTo(teamColour == TeamColour.Red ? match.NewValue.Team1Score : match.NewValue.Team2Score); + + currentTeam.UnbindBindings(); + currentTeam.BindTo(teamColour == TeamColour.Red ? match.NewValue.Team1 : match.NewValue.Team2); + + // team may change to same team, which means score is not in a good state. + // thus we handle this manually. + teamChanged(currentTeam.Value); + } + + protected override bool OnMouseDown(MouseDownEvent e) + { + switch (e.Button) { - this.teamColour = teamColour; + case MouseButton.Left: + if (currentTeamScore.Value < currentMatch.Value.PointsToWin) + currentTeamScore.Value++; + return true; - RelativeSizeAxes = Axes.Y; - Width = 300; + case MouseButton.Right: + if (currentTeamScore.Value > 0) + currentTeamScore.Value--; + return true; } - [BackgroundDependencyLoader] - private void load(LadderInfo ladder) + return base.OnMouseDown(e); + } + + private void teamChanged(TournamentTeam team) + { + var colour = teamColour == TeamColour.Red ? TournamentGame.COLOUR_RED : TournamentGame.COLOUR_BLUE; + var flip = teamColour == TeamColour.Red; + + InternalChildren = new Drawable[] { - currentMatch.BindValueChanged(matchChanged); - currentMatch.BindTo(ladder.CurrentMatch); - } - - private void matchChanged(ValueChangedEvent match) - { - currentTeamScore.UnbindBindings(); - currentTeamScore.BindTo(teamColour == TeamColour.Red ? match.NewValue.Team1Score : match.NewValue.Team2Score); - - currentTeam.UnbindBindings(); - currentTeam.BindTo(teamColour == TeamColour.Red ? match.NewValue.Team1 : match.NewValue.Team2); - - // team may change to same team, which means score is not in a good state. - // thus we handle this manually. - teamChanged(currentTeam.Value); - } - - protected override bool OnMouseDown(MouseDownEvent e) - { - switch (e.Button) + new TeamDisplay(team, colour, flip), + new TeamScore(currentTeamScore, flip, currentMatch.Value.PointsToWin) { - case MouseButton.Left: - if (currentTeamScore.Value < currentMatch.Value.PointsToWin) - currentTeamScore.Value++; - return true; - - case MouseButton.Right: - if (currentTeamScore.Value > 0) - currentTeamScore.Value--; - return true; + Colour = colour } - - return base.OnMouseDown(e); - } - - private void teamChanged(TournamentTeam team) - { - var colour = teamColour == TeamColour.Red ? TournamentGame.COLOUR_RED : TournamentGame.COLOUR_BLUE; - var flip = teamColour != TeamColour.Red; - - InternalChildren = new Drawable[] - { - new TeamDisplay(team, colour, flip), - new TeamScore(currentTeamScore, flip, currentMatch.Value.PointsToWin) - { - Colour = colour - } - }; - } - } - - private class TeamScore : CompositeDrawable - { - private readonly Bindable currentTeamScore = new Bindable(); - private readonly StarCounter counter; - - public TeamScore(Bindable score, bool flip, int count) - { - var anchor = flip ? Anchor.CentreRight : Anchor.CentreLeft; - - Anchor = anchor; - Origin = anchor; - - InternalChild = counter = new StarCounter(count) - { - Anchor = anchor, - X = (flip ? -1 : 1) * 90, - Y = 5, - Scale = flip ? new Vector2(-1, 1) : Vector2.One, - }; - - currentTeamScore.BindValueChanged(scoreChanged); - currentTeamScore.BindTo(score); - } - - private void scoreChanged(ValueChangedEvent score) => counter.CountStars = score.NewValue ?? 0; - } - - private class TeamDisplay : DrawableTournamentTeam - { - public TeamDisplay(TournamentTeam team, Color4 colour, bool flip) - : base(team) - { - RelativeSizeAxes = Axes.Both; - - var anchor = flip ? Anchor.CentreRight : Anchor.CentreLeft; - - Anchor = Origin = anchor; - - Flag.Anchor = Flag.Origin = anchor; - Flag.RelativeSizeAxes = Axes.None; - Flag.Size = new Vector2(60, 40); - Flag.Margin = new MarginPadding(20); - - InternalChild = new Container - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - Flag, - new TournamentSpriteText - { - Text = team?.FullName.Value.ToUpper() ?? "???", - X = (flip ? -1 : 1) * 90, - Y = -10, - Colour = colour, - Font = OsuFont.Torus.With(weight: FontWeight.Regular, size: 20), - Origin = anchor, - Anchor = anchor, - }, - } - }; - } - } - - private class RoundDisplay : CompositeDrawable - { - private readonly Bindable currentMatch = new Bindable(); - - private readonly TournamentSpriteText text; - - public RoundDisplay() - { - Width = 200; - Height = 20; - - Masking = true; - CornerRadius = 10; - - InternalChildren = new Drawable[] - { - new Box - { - Colour = OsuColour.Gray(0.18f), - RelativeSizeAxes = Axes.Both, - }, - text = new TournamentSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Colour = Color4.White, - Font = OsuFont.Torus.With(weight: FontWeight.Regular, size: 16), - }, - }; - } - - [BackgroundDependencyLoader] - private void load(LadderInfo ladder) - { - currentMatch.BindValueChanged(matchChanged); - currentMatch.BindTo(ladder.CurrentMatch); - } - - private void matchChanged(ValueChangedEvent match) => - text.Text = match.NewValue.Round.Value?.Name.Value ?? "Unknown Round"; + }; } } } diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/RoundDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/RoundDisplay.cs new file mode 100644 index 0000000000..5322cf9a76 --- /dev/null +++ b/osu.Game.Tournament/Screens/Gameplay/Components/RoundDisplay.cs @@ -0,0 +1,56 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Tournament.Models; +using osuTK.Graphics; + +namespace osu.Game.Tournament.Screens.Gameplay.Components +{ + public class RoundDisplay : CompositeDrawable + { + private readonly Bindable currentMatch = new Bindable(); + + private readonly TournamentSpriteText text; + + public RoundDisplay() + { + Width = 200; + Height = 20; + + Masking = true; + CornerRadius = 10; + + InternalChildren = new Drawable[] + { + new Box + { + Colour = OsuColour.Gray(0.18f), + RelativeSizeAxes = Axes.Both, + }, + text = new TournamentSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = Color4.White, + Font = OsuFont.Torus.With(weight: FontWeight.Regular, size: 16), + }, + }; + } + + [BackgroundDependencyLoader] + private void load(LadderInfo ladder) + { + currentMatch.BindValueChanged(matchChanged); + currentMatch.BindTo(ladder.CurrentMatch); + } + + private void matchChanged(ValueChangedEvent match) => + text.Text = match.NewValue.Round.Value?.Name.Value ?? "Unknown Round"; + } +} diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs new file mode 100644 index 0000000000..891435c48e --- /dev/null +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs @@ -0,0 +1,50 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Tournament.Components; +using osu.Game.Tournament.Models; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Tournament.Screens.Gameplay.Components +{ + public class TeamDisplay : DrawableTournamentTeam + { + public TeamDisplay(TournamentTeam team, Color4 colour, bool flip) + : base(team) + { + RelativeSizeAxes = Axes.Both; + + var anchor = flip ? Anchor.CentreRight : Anchor.CentreLeft; + + Anchor = Origin = anchor; + + Flag.Anchor = Flag.Origin = anchor; + Flag.RelativeSizeAxes = Axes.None; + Flag.Size = new Vector2(60, 40); + Flag.Margin = new MarginPadding(20); + + InternalChild = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + Flag, + new TournamentSpriteText + { + Text = team?.FullName.Value.ToUpper() ?? "???", + X = (flip ? -1 : 1) * 90, + Y = -10, + Colour = colour, + Font = OsuFont.Torus.With(weight: FontWeight.Regular, size: 20), + Origin = anchor, + Anchor = anchor, + }, + } + }; + } + } +} diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TeamScore.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScore.cs new file mode 100644 index 0000000000..608d98a16a --- /dev/null +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScore.cs @@ -0,0 +1,38 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osuTK; + +namespace osu.Game.Tournament.Screens.Gameplay.Components +{ + public class TeamScore : CompositeDrawable + { + private readonly Bindable currentTeamScore = new Bindable(); + private readonly StarCounter counter; + + public TeamScore(Bindable score, bool flip, int count) + { + var anchor = flip ? Anchor.CentreRight : Anchor.CentreLeft; + + Anchor = anchor; + Origin = anchor; + + InternalChild = counter = new StarCounter(count) + { + Anchor = anchor, + X = (flip ? -1 : 1) * 90, + Y = 5, + Scale = flip ? new Vector2(-1, 1) : Vector2.One, + }; + + currentTeamScore.BindValueChanged(scoreChanged); + currentTeamScore.BindTo(score); + } + + private void scoreChanged(ValueChangedEvent score) => counter.CountStars = score.NewValue ?? 0; + } +} From 4d74493289a7de37137727545b67629ca3576343 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Mar 2020 16:08:05 +0900 Subject: [PATCH 0249/2376] Initial pass of win screen design update --- .../Screens/TeamWin/TeamWinScreen.cs | 110 ++++-------------- 1 file changed, 23 insertions(+), 87 deletions(-) diff --git a/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs b/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs index 1765ab7ba2..8b3f4488d0 100644 --- a/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs +++ b/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs @@ -10,7 +10,6 @@ using osu.Game.Graphics; using osu.Game.Tournament.Components; using osu.Game.Tournament.Models; using osuTK; -using osuTK.Graphics; namespace osu.Game.Tournament.Screens.TeamWin { @@ -73,105 +72,42 @@ namespace osu.Game.Tournament.Screens.TeamWin return; } - bool redWin = match.Winner == match.Team1.Value; - redWinVideo.Alpha = redWin ? 1 : 0; - blueWinVideo.Alpha = redWin ? 0 : 1; + redWinVideo.Alpha = match.WinnerColour == TeamColour.Red ? 1 : 0; + blueWinVideo.Alpha = match.WinnerColour == TeamColour.Blue ? 1 : 0; mainContainer.Children = new Drawable[] { - new TeamFlagDisplay(match.Winner) + new DrawableTeamFlag(match.Winner) { Size = new Vector2(300, 200), Scale = new Vector2(0.5f), Anchor = Anchor.Centre, Origin = Anchor.Centre, - X = -387, + Position = new Vector2(-300, 10), }, - new TournamentSpriteText + new FillFlowContainer { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, Anchor = Anchor.Centre, - Origin = Anchor.TopLeft, - Position = new Vector2(78, -70), - Colour = OsuColour.Gray(0.33f), - Text = match.Round.Value?.Name.Value ?? "Unknown Round", - Font = OsuFont.Torus.With(size: 30, weight: FontWeight.Regular) - }, - new TeamWithPlayers(match.Winner, redWin) - { - RelativeSizeAxes = Axes.Both, - Width = 0.5f, - Height = 0.6f, - Anchor = Anchor.Centre, - Origin = Anchor.TopLeft, - Position = new Vector2(78, 0), + Origin = Anchor.Centre, + X = 260, + Children = new Drawable[] + { + new RoundDisplay(match) + { + Margin = new MarginPadding { Bottom = 30 }, + }, + new TournamentSpriteText + { + Text = "WINNER", + Font = OsuFont.Torus.With(size: 100, weight: FontWeight.Bold), + Margin = new MarginPadding { Bottom = 50 }, + }, + new DrawableTeamWithPlayers(match.Winner, match.WinnerColour) + } }, }; } - - private class TeamWithPlayers : CompositeDrawable - { - public TeamWithPlayers(TournamentTeam team, bool left = false) - { - FillFlowContainer players; - - var colour = left ? TournamentGame.COLOUR_RED : TournamentGame.COLOUR_BLUE; - InternalChildren = new Drawable[] - { - new FillFlowContainer - { - Direction = FillDirection.Vertical, - AutoSizeAxes = Axes.Both, - Children = new Drawable[] - { - new TournamentSpriteText - { - Text = "WINNER", - Font = OsuFont.Torus.With(size: 24, weight: FontWeight.SemiBold), - Colour = Color4.Black, - }, - new TournamentSpriteText - { - Text = team?.FullName.Value ?? "???", - Font = OsuFont.Torus.With(size: 30, weight: FontWeight.SemiBold), - Colour = Color4.Black, - }, - players = new FillFlowContainer - { - Direction = FillDirection.Vertical, - AutoSizeAxes = Axes.Both, - Padding = new MarginPadding { Top = 10 }, - }, - } - }, - }; - - if (team != null) - { - foreach (var p in team.Players) - { - players.Add(new TournamentSpriteText - { - Text = p.Username, - Font = OsuFont.Torus.With(size: 24), - Colour = colour, - Anchor = left ? Anchor.CentreRight : Anchor.CentreLeft, - Origin = left ? Anchor.CentreRight : Anchor.CentreLeft, - }); - } - } - } - } - - private class TeamFlagDisplay : DrawableTournamentTeam - { - public TeamFlagDisplay(TournamentTeam team) - : base(team) - { - InternalChildren = new Drawable[] - { - Flag - }; - } - } } } From 77c94afcf182ff33a893ed3d3930f9cb04e3b344 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 7 Mar 2020 14:28:10 +0900 Subject: [PATCH 0250/2376] Add better flow logic to map pool layout when few beatmaps are present --- .../Screens/MapPool/MapPoolScreen.cs | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs index c42d0a6da3..d7aeac02cb 100644 --- a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs +++ b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs @@ -47,9 +47,10 @@ namespace osu.Game.Tournament.Screens.MapPool { Y = 100, Spacing = new Vector2(10, 10), - Padding = new MarginPadding(25), + Padding = new MarginPadding(5) { Horizontal = 100 }, Direction = FillDirection.Vertical, - RelativeSizeAxes = Axes.Both, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, }, new ControlPanel { @@ -90,6 +91,7 @@ namespace osu.Game.Tournament.Screens.MapPool Text = "Reset", Action = reset }, + new ControlPanel.Spacer(), } } }; @@ -206,11 +208,15 @@ namespace osu.Game.Tournament.Screens.MapPool { mapFlows.Clear(); + int totalRows = 0; + if (match.NewValue.Round.Value != null) { FillFlowContainer currentFlow = null; string currentMod = null; + int flowCount = 0; + foreach (var b in match.NewValue.Round.Value.Beatmaps) { if (currentFlow == null || currentMod != b.Mods) @@ -224,6 +230,15 @@ namespace osu.Game.Tournament.Screens.MapPool }); currentMod = b.Mods; + + totalRows++; + flowCount = 0; + } + + if (++flowCount > 2) + { + totalRows++; + flowCount = 0; } currentFlow.Add(new TournamentBeatmapPanel(b.BeatmapInfo, b.Mods) @@ -233,6 +248,10 @@ namespace osu.Game.Tournament.Screens.MapPool }); } } + + if (totalRows > 8) + // remove horizontal padding to increase flow width to 3 panels + mapFlows.Padding = new MarginPadding(5); } } } From 29817304423d154a39a0f5212e9ac2aeb3514148 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 7 Mar 2020 13:51:11 +0900 Subject: [PATCH 0251/2376] Initial pass of map pool screen design update --- .../Components/TournamentBeatmapPanel.cs | 22 ++----------------- .../Screens/MapPool/MapPoolScreen.cs | 5 +++++ 2 files changed, 7 insertions(+), 20 deletions(-) diff --git a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs index 394ffe304e..09c4a96807 100644 --- a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs +++ b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs @@ -27,7 +27,7 @@ namespace osu.Game.Tournament.Components private readonly string mods; private const float horizontal_padding = 10; - private const float vertical_padding = 5; + private const float vertical_padding = 10; public const float HEIGHT = 50; @@ -50,8 +50,6 @@ namespace osu.Game.Tournament.Components currentMatch.BindValueChanged(matchChanged); currentMatch.BindTo(ladder.CurrentMatch); - CornerRadius = HEIGHT / 2; - CornerExponent = 2; Masking = true; AddRangeInternal(new Drawable[] @@ -70,16 +68,12 @@ namespace osu.Game.Tournament.Components new FillFlowContainer { AutoSizeAxes = Axes.Both, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, Padding = new MarginPadding(vertical_padding), Direction = FillDirection.Vertical, Children = new Drawable[] { new TournamentSpriteText { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, Text = new LocalisedString(( $"{Beatmap.Metadata.ArtistUnicode ?? Beatmap.Metadata.Artist} - {Beatmap.Metadata.TitleUnicode ?? Beatmap.Metadata.Title}", $"{Beatmap.Metadata.Artist} - {Beatmap.Metadata.Title}")), @@ -88,9 +82,6 @@ namespace osu.Game.Tournament.Components new FillFlowContainer { AutoSizeAxes = Axes.Both, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Padding = new MarginPadding(vertical_padding), Direction = FillDirection.Horizontal, Children = new Drawable[] { @@ -170,16 +161,7 @@ namespace osu.Game.Tournament.Components BorderThickness = 6; - switch (found.Team) - { - case TeamColour.Red: - BorderColour = Color4.Red; - break; - - case TeamColour.Blue: - BorderColour = Color4.Blue; - break; - } + BorderColour = TournamentGame.GetTeamColour(found.Team); switch (found.Type) { diff --git a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs index c42d0a6da3..4f3f7cfdbf 100644 --- a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs +++ b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs @@ -42,6 +42,11 @@ namespace osu.Game.Tournament.Screens.MapPool { InternalChildren = new Drawable[] { + new TourneyVideo("gameplay") + { + Loop = true, + RelativeSizeAxes = Axes.Both, + }, new MatchHeader(), mapFlows = new FillFlowContainer> { From ba6c4abbe6568c3d4acc9701dac310feae4ce1a8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 7 Mar 2020 01:37:15 +0900 Subject: [PATCH 0252/2376] Initial pass of ladder screen design update --- .../Ladder/Components/DrawableMatchTeam.cs | 60 +++++++++---------- .../Components/DrawableTournamentMatch.cs | 11 ++-- .../Components/DrawableTournamentRound.cs | 5 +- .../Screens/Ladder/LadderScreen.cs | 10 +++- 4 files changed, 44 insertions(+), 42 deletions(-) diff --git a/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs b/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs index 88d7b95b0c..4f10a7e7d0 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs @@ -27,12 +27,12 @@ namespace osu.Game.Tournament.Screens.Ladder.Components private readonly bool losers; private TournamentSpriteText scoreText; private Box background; + private Box backgroundRight; private readonly Bindable score = new Bindable(); private readonly BindableBool completed = new BindableBool(); private Color4 colourWinner; - private Color4 colourNormal; private readonly Func isWinner; private LadderEditorScreen ladderEditor; @@ -60,15 +60,12 @@ namespace osu.Game.Tournament.Screens.Ladder.Components this.losers = losers; Size = new Vector2(150, 40); - Masking = true; - CornerRadius = 5; - Flag.Scale = new Vector2(0.9f); Flag.Anchor = Flag.Origin = Anchor.CentreLeft; AcronymText.Anchor = AcronymText.Origin = Anchor.CentreLeft; AcronymText.Padding = new MarginPadding { Left = 50 }; - AcronymText.Font = OsuFont.Torus.With(size: 24); + AcronymText.Font = OsuFont.Torus.With(size: 22, weight: FontWeight.Bold); if (match != null) { @@ -85,8 +82,9 @@ namespace osu.Game.Tournament.Screens.Ladder.Components { this.ladderEditor = ladderEditor; - colourWinner = losers ? colours.YellowDarker : colours.BlueDarker; - colourNormal = OsuColour.Gray(0.2f); + colourWinner = losers + ? OsuColour.FromHex("#8E7F48") + : OsuColour.FromHex("#1462AA"); InternalChildren = new Drawable[] { @@ -102,29 +100,28 @@ namespace osu.Game.Tournament.Screens.Ladder.Components { AcronymText, Flag, - new Container + } + }, + new Container + { + Masking = true, + Width = 0.3f, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + backgroundRight = new Box { - Masking = true, - CornerRadius = 5, - Width = 0.3f, - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, + Colour = OsuColour.Gray(0.1f), + Alpha = 0.8f, RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Box - { - Colour = OsuColour.Gray(0.1f), - Alpha = 0.8f, - RelativeSizeAxes = Axes.Both, - }, - scoreText = new TournamentSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Font = OsuFont.Torus.With(size: 20), - } - } + }, + scoreText = new TournamentSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.Torus.With(size: 22), } } } @@ -181,9 +178,12 @@ namespace osu.Game.Tournament.Screens.Ladder.Components { bool winner = completed.Value && isWinner?.Invoke() == true; - background.FadeColour(winner ? colourWinner : colourNormal, winner ? 500 : 0, Easing.OutQuint); + background.FadeColour(winner ? Color4.White : OsuColour.FromHex("#444"), winner ? 500 : 0, Easing.OutQuint); + backgroundRight.FadeColour(winner ? colourWinner : OsuColour.FromHex("#333"), winner ? 500 : 0, Easing.OutQuint); - scoreText.Font = AcronymText.Font = OsuFont.Torus.With(weight: winner ? FontWeight.Bold : FontWeight.Regular); + AcronymText.Colour = winner ? Color4.Black : Color4.White; + + scoreText.Font = scoreText.Font.With(weight: winner ? FontWeight.Bold : FontWeight.Regular); } public MenuItem[] ContextMenuItems diff --git a/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs b/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs index c4b670f059..82a4c7017f 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; +using osu.Game.Graphics; using osu.Game.Tournament.Models; using osuTK; using osuTK.Graphics; @@ -45,9 +46,7 @@ namespace osu.Game.Tournament.Screens.Ladder.Components { selectionBox = new Container { - CornerRadius = 5, - Masking = true, - Scale = new Vector2(1.05f), + Scale = new Vector2(1.1f), RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -57,14 +56,12 @@ namespace osu.Game.Tournament.Screens.Ladder.Components }, currentMatchSelectionBox = new Container { - CornerRadius = 5, - Masking = true, - Scale = new Vector2(1.05f), + Scale = new Vector2(1.1f), RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, Alpha = 0, - Colour = Color4.OrangeRed, + Colour = OsuColour.FromHex("#D24747"), Child = new Box { RelativeSizeAxes = Axes.Both } }, Flow = new FillFlowContainer diff --git a/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentRound.cs b/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentRound.cs index d14ebb4d03..cad0b827c0 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentRound.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentRound.cs @@ -7,7 +7,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Game.Tournament.Models; -using osuTK.Graphics; namespace osu.Game.Tournament.Screens.Ladder.Components { @@ -33,14 +32,14 @@ namespace osu.Game.Tournament.Screens.Ladder.Components { textDescription = new TournamentSpriteText { - Colour = Color4.Black, + Colour = TournamentGame.TEXT_COLOUR, Origin = Anchor.TopCentre, Anchor = Anchor.TopCentre }, textName = new TournamentSpriteText { Font = OsuFont.Torus.With(weight: FontWeight.Bold), - Colour = Color4.Black, + Colour = TournamentGame.TEXT_COLOUR, Origin = Anchor.TopCentre, Anchor = Anchor.TopCentre }, diff --git a/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs b/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs index 7b265ded32..c7e59cfa7b 100644 --- a/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs +++ b/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs @@ -32,8 +32,8 @@ namespace osu.Game.Tournament.Screens.Ladder [BackgroundDependencyLoader] private void load(OsuColour colours, Storage storage) { - normalPathColour = colours.BlueDarker.Darken(2); - losersPathColour = colours.YellowDarker.Darken(2); + normalPathColour = OsuColour.FromHex("#66D1FF"); + losersPathColour = OsuColour.FromHex("#FFC700"); RelativeSizeAxes = Axes.Both; @@ -47,6 +47,12 @@ namespace osu.Game.Tournament.Screens.Ladder RelativeSizeAxes = Axes.Both, Loop = true, }, + new DrawableTournamentTitleText + { + Y = 100, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, ScrollContent = new LadderDragContainer { RelativeSizeAxes = Axes.Both, From 66e54ba9837f4c8bf5aa7fdc8788c8dc7c6e7eb3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 7 Mar 2020 12:49:09 +0900 Subject: [PATCH 0253/2376] Increase flexibility of StarCounter component --- .../Visual/Gameplay/TestSceneScoreCounter.cs | 12 +-- .../Graphics/UserInterface/StarCounter.cs | 101 ++++++++++-------- .../Carousel/DrawableCarouselBeatmap.cs | 2 +- 3 files changed, 61 insertions(+), 54 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs index ffd6f55b53..88bb83b446 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs @@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.Gameplay Origin = Anchor.BottomLeft, Anchor = Anchor.BottomLeft, Position = new Vector2(20, -160), - CountStars = 5, + Current = 5, }; Add(stars); @@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Gameplay Origin = Anchor.BottomLeft, Anchor = Anchor.BottomLeft, Position = new Vector2(20, -190), - Text = stars.CountStars.ToString("0.00"), + Text = stars.Current.ToString("0.00"), }; Add(starsLabel); @@ -69,8 +69,8 @@ namespace osu.Game.Tests.Visual.Gameplay comboCounter.Current.Value = 0; numerator = denominator = 0; accuracyCounter.SetFraction(0, 0); - stars.CountStars = 0; - starsLabel.Text = stars.CountStars.ToString("0.00"); + stars.Current = 0; + starsLabel.Text = stars.Current.ToString("0.00"); }); AddStep(@"Hit! :D", delegate @@ -91,8 +91,8 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep(@"Alter stars", delegate { - stars.CountStars = RNG.NextSingle() * (stars.StarCount + 1); - starsLabel.Text = stars.CountStars.ToString("0.00"); + stars.Current = RNG.NextSingle() * (stars.StarCount + 1); + starsLabel.Text = stars.Current.ToString("0.00"); }); AddStep(@"Stop counters", delegate diff --git a/osu.Game/Graphics/UserInterface/StarCounter.cs b/osu.Game/Graphics/UserInterface/StarCounter.cs index 586cd2ce84..b13d6485ac 100644 --- a/osu.Game/Graphics/UserInterface/StarCounter.cs +++ b/osu.Game/Graphics/UserInterface/StarCounter.cs @@ -13,7 +13,7 @@ namespace osu.Game.Graphics.UserInterface { public class StarCounter : Container { - private readonly Container stars; + private readonly FillFlowContainer stars; /// /// Maximum amount of stars displayed. @@ -23,34 +23,29 @@ namespace osu.Game.Graphics.UserInterface /// public int StarCount { get; } - private double animationDelay => 80; + /// + /// The added delay for each subsequent star to be animated. + /// + protected virtual double AnimationDelay => 80; - private double scalingDuration => 1000; - private Easing scalingEasing => Easing.OutElasticHalf; - private float minStarScale => 0.4f; - - private double fadingDuration => 100; - private float minStarAlpha => 0.5f; - - private const float star_size = 20; private const float star_spacing = 4; - private float countStars; + private float current; /// /// Amount of stars represented. /// - public float CountStars + public float Current { - get => countStars; + get => current; set { - if (countStars == value) return; + if (current == value) return; if (IsLoaded) - transformCount(value); - countStars = value; + animate(value); + current = value; } } @@ -71,11 +66,13 @@ namespace osu.Game.Graphics.UserInterface AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, Spacing = new Vector2(star_spacing), - ChildrenEnumerable = Enumerable.Range(0, StarCount).Select(i => new Star { Alpha = minStarAlpha }) + ChildrenEnumerable = Enumerable.Range(0, StarCount).Select(i => CreateStar()) } }; } + public virtual Star CreateStar() => new DefaultStar(); + protected override void LoadComplete() { base.LoadComplete(); @@ -86,63 +83,60 @@ namespace osu.Game.Graphics.UserInterface public void ResetCount() { - countStars = 0; + current = 0; StopAnimation(); } public void ReplayAnimation() { - var t = countStars; + var t = current; ResetCount(); - CountStars = t; + Current = t; } public void StopAnimation() { - int i = 0; - + animate(current); foreach (var star in stars.Children) + star.FinishTransforms(true); + } + + private float getStarScale(int i, float value) => i + 1 <= value ? 1.0f : Interpolation.ValueAt(value, 0, 1.0f, i, i + 1); + + private void animate(float newValue) + { + for (var i = 0; i < stars.Children.Count; i++) { + var star = stars.Children[i]; + star.ClearTransforms(true); - star.FadeTo(i < countStars ? 1.0f : minStarAlpha); - star.Icon.ScaleTo(getStarScale(i, countStars)); - i++; + + double delay = (current <= newValue ? Math.Max(i - current, 0) : Math.Max(current - 1 - i, 0)) * AnimationDelay; + + using (star.BeginDelayedSequence(delay, true)) + star.DisplayAt(getStarScale(i, newValue)); } } - private float getStarScale(int i, float value) + public class DefaultStar : Star { - if (value <= i) - return minStarScale; + private const double scaling_duration = 1000; - return i + 1 <= value ? 1.0f : Interpolation.ValueAt(value, minStarScale, 1.0f, i, i + 1); - } + private const double fading_duration = 100; - private void transformCount(float newValue) - { - int i = 0; + private const Easing scaling_easing = Easing.OutElasticHalf; - foreach (var star in stars.Children) - { - star.ClearTransforms(true); + private const float min_star_scale = 0.4f; - var delay = (countStars <= newValue ? Math.Max(i - countStars, 0) : Math.Max(countStars - 1 - i, 0)) * animationDelay; - star.Delay(delay).FadeTo(i < newValue ? 1.0f : minStarAlpha, fadingDuration); - star.Icon.Delay(delay).ScaleTo(getStarScale(i, newValue), scalingDuration, scalingEasing); + private const float star_size = 20; - i++; - } - } - - private class Star : Container - { public readonly SpriteIcon Icon; - public Star() + public DefaultStar() { Size = new Vector2(star_size); - Child = Icon = new SpriteIcon + InternalChild = Icon = new SpriteIcon { Size = new Vector2(star_size), Icon = FontAwesome.Solid.Star, @@ -150,6 +144,19 @@ namespace osu.Game.Graphics.UserInterface Origin = Anchor.Centre, }; } + + public override void DisplayAt(float scale) + { + scale = Math.Clamp(scale, min_star_scale, 1); + + this.FadeTo(scale, fading_duration); + Icon.ScaleTo(scale, scaling_duration, scaling_easing); + } + } + + public abstract class Star : CompositeDrawable + { + public abstract void DisplayAt(float scale); } } } diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index d9eeec9f85..50419a5fb9 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -123,7 +123,7 @@ namespace osu.Game.Screens.Select.Carousel }, starCounter = new StarCounter { - CountStars = (float)beatmap.StarDifficulty, + Current = (float)beatmap.StarDifficulty, Scale = new Vector2(0.8f), } } From 86b12a384b4244bf14ae93ea19f3cd3cf13d6207 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 7 Mar 2020 12:51:11 +0900 Subject: [PATCH 0254/2376] Initial pass of gameplay screen design update --- .../TestSceneDrawableTournamentTeam.cs | 3 +- .../Screens/TestSceneGameplayScreen.cs | 11 +++ .../Gameplay/Components/MatchHeader.cs | 39 +++++---- .../Gameplay/Components/RoundDisplay.cs | 37 +-------- .../Gameplay/Components/TeamDisplay.cs | 73 ++++++++++++----- .../Screens/Gameplay/Components/TeamScore.cs | 81 +++++++++++++++++-- .../Screens/Gameplay/GameplayScreen.cs | 32 +------- 7 files changed, 170 insertions(+), 106 deletions(-) diff --git a/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs b/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs index 41f7d3d847..01edcb66e4 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Tests.Visual; using osu.Game.Tournament.Components; @@ -113,7 +114,7 @@ namespace osu.Game.Tournament.Tests.Components Cell(i).AddRange(new Drawable[] { new TournamentSpriteText { Text = "TeamDisplay" }, - new TeamDisplay(team, TournamentGame.COLOUR_RED, false) + new TeamDisplay(team, TeamColour.Red, new Bindable(2), 6) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs index 9de00818a5..964930a8de 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs @@ -1,9 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Collections.Generic; using osu.Framework.Allocation; using osu.Game.Tournament.Components; using osu.Game.Tournament.Screens.Gameplay; +using osu.Game.Tournament.Screens.Gameplay.Components; namespace osu.Game.Tournament.Tests.Screens { @@ -12,6 +15,14 @@ namespace osu.Game.Tournament.Tests.Screens [Cached] private TournamentMatchChatDisplay chat = new TournamentMatchChatDisplay(); + public override IReadOnlyList RequiredTypes => new[] + { + typeof(TeamScore), + typeof(TeamScoreDisplay), + typeof(TeamDisplay), + typeof(MatchHeader), + }; + [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs b/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs index c86132a802..45f46c462a 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs @@ -6,8 +6,9 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; +using osu.Game.Tournament.Components; using osu.Game.Tournament.Models; -using osu.Game.Tournament.Screens.Showcase; +using osuTK; using osuTK.Input; namespace osu.Game.Tournament.Screens.Gameplay.Components @@ -21,13 +22,28 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components Height = 95; Children = new Drawable[] { - new TournamentLogo(), - new RoundDisplay + new FillFlowContainer { - Y = 5, - Anchor = Anchor.BottomCentre, - Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(5), + Children = new Drawable[] + { + new DrawableTournamentTitleText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(1.2f) + }, + new RoundDisplay + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(0.4f) + }, + } }, + new TeamScoreDisplay(TeamColour.Red) { Anchor = Anchor.TopLeft, @@ -55,7 +71,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components this.teamColour = teamColour; RelativeSizeAxes = Axes.Y; - Width = 300; + AutoSizeAxes = Axes.X; } [BackgroundDependencyLoader] @@ -98,16 +114,9 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components private void teamChanged(TournamentTeam team) { - var colour = teamColour == TeamColour.Red ? TournamentGame.COLOUR_RED : TournamentGame.COLOUR_BLUE; - var flip = teamColour == TeamColour.Red; - InternalChildren = new Drawable[] { - new TeamDisplay(team, colour, flip), - new TeamScore(currentTeamScore, flip, currentMatch.Value.PointsToWin) - { - Colour = colour - } + new TeamDisplay(team, teamColour, currentTeamScore, currentMatch.Value.PointsToWin), }; } } diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/RoundDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/RoundDisplay.cs index 5322cf9a76..c8b0d3bdda 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/RoundDisplay.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/RoundDisplay.cs @@ -3,46 +3,15 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics; +using osu.Game.Tournament.Components; using osu.Game.Tournament.Models; -using osuTK.Graphics; namespace osu.Game.Tournament.Screens.Gameplay.Components { - public class RoundDisplay : CompositeDrawable + public class RoundDisplay : TournamentSpriteTextWithBackground { private readonly Bindable currentMatch = new Bindable(); - private readonly TournamentSpriteText text; - - public RoundDisplay() - { - Width = 200; - Height = 20; - - Masking = true; - CornerRadius = 10; - - InternalChildren = new Drawable[] - { - new Box - { - Colour = OsuColour.Gray(0.18f), - RelativeSizeAxes = Axes.Both, - }, - text = new TournamentSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Colour = Color4.White, - Font = OsuFont.Torus.With(weight: FontWeight.Regular, size: 16), - }, - }; - } - [BackgroundDependencyLoader] private void load(LadderInfo ladder) { @@ -51,6 +20,6 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components } private void matchChanged(ValueChangedEvent match) => - text.Text = match.NewValue.Round.Value?.Name.Value ?? "Unknown Round"; + Text.Text = match.NewValue.Round.Value?.Name.Value ?? "Unknown Round"; } } diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs index 891435c48e..4bfeb2bc81 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs @@ -1,47 +1,84 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Graphics; using osu.Game.Tournament.Components; using osu.Game.Tournament.Models; using osuTK; -using osuTK.Graphics; namespace osu.Game.Tournament.Screens.Gameplay.Components { public class TeamDisplay : DrawableTournamentTeam { - public TeamDisplay(TournamentTeam team, Color4 colour, bool flip) + public TeamDisplay(TournamentTeam team, TeamColour colour, Bindable currentTeamScore, int pointsToWin) : base(team) { - RelativeSizeAxes = Axes.Both; + AutoSizeAxes = Axes.Both; - var anchor = flip ? Anchor.CentreRight : Anchor.CentreLeft; + bool flip = colour == TeamColour.Red; - Anchor = Origin = anchor; + var anchor = flip ? Anchor.TopLeft : Anchor.TopRight; - Flag.Anchor = Flag.Origin = anchor; Flag.RelativeSizeAxes = Axes.None; Flag.Size = new Vector2(60, 40); - Flag.Margin = new MarginPadding(20); + Flag.Origin = anchor; + Flag.Anchor = anchor; + + Margin = new MarginPadding(20); InternalChild = new Container { - RelativeSizeAxes = Axes.Both, + AutoSizeAxes = Axes.Both, Children = new Drawable[] { - Flag, - new TournamentSpriteText + new FillFlowContainer { - Text = team?.FullName.Value.ToUpper() ?? "???", - X = (flip ? -1 : 1) * 90, - Y = -10, - Colour = colour, - Font = OsuFont.Torus.With(weight: FontWeight.Regular, size: 20), - Origin = anchor, - Anchor = anchor, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5), + Children = new Drawable[] + { + Flag, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Origin = anchor, + Anchor = anchor, + Spacing = new Vector2(5), + Children = new Drawable[] + { + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5), + Children = new Drawable[] + { + new DrawableTeamHeader(colour) + { + Scale = new Vector2(0.75f), + Origin = anchor, + Anchor = anchor, + }, + new TeamScore(currentTeamScore, colour, pointsToWin) + { + Origin = anchor, + Anchor = anchor, + } + } + }, + new TournamentSpriteTextWithBackground(team?.FullName.Value ?? "???") + { + Scale = new Vector2(0.5f), + Origin = anchor, + Anchor = anchor, + }, + } + }, + } }, } }; diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TeamScore.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScore.cs index 608d98a16a..056f8387fb 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/TeamScore.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScore.cs @@ -2,10 +2,16 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; +using osu.Game.Tournament.Models; using osuTK; +using osuTK.Graphics; namespace osu.Game.Tournament.Screens.Gameplay.Components { @@ -14,18 +20,16 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components private readonly Bindable currentTeamScore = new Bindable(); private readonly StarCounter counter; - public TeamScore(Bindable score, bool flip, int count) + public TeamScore(Bindable score, TeamColour colour, int count) { - var anchor = flip ? Anchor.CentreRight : Anchor.CentreLeft; + bool flip = colour == TeamColour.Blue; + var anchor = flip ? Anchor.TopRight : Anchor.TopLeft; - Anchor = anchor; - Origin = anchor; + AutoSizeAxes = Axes.Both; - InternalChild = counter = new StarCounter(count) + InternalChild = counter = new TeamScoreStarCounter(count) { Anchor = anchor, - X = (flip ? -1 : 1) * 90, - Y = 5, Scale = flip ? new Vector2(-1, 1) : Vector2.One, }; @@ -33,6 +37,67 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components currentTeamScore.BindTo(score); } - private void scoreChanged(ValueChangedEvent score) => counter.CountStars = score.NewValue ?? 0; + private void scoreChanged(ValueChangedEvent score) => counter.Current = score.NewValue ?? 0; + + public class TeamScoreStarCounter : StarCounter + { + public TeamScoreStarCounter(int count) + : base(count) + { + } + + public override Star CreateStar() => new LightSquare(); + + public class LightSquare : Star + { + private Box box; + + public LightSquare() + { + Size = new Vector2(22.5f); + + InternalChildren = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + BorderColour = OsuColour.Gray(0.5f), + BorderThickness = 3, + Children = new Drawable[] + { + new Box + { + Colour = Color4.Transparent, + RelativeSizeAxes = Axes.Both, + AlwaysPresent = true, + }, + } + }, + box = new Box + { + Colour = OsuColour.FromHex("#FFE8AD"), + RelativeSizeAxes = Axes.Both, + }, + }; + + Masking = true; + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = OsuColour.FromHex("#FFE8AD").Opacity(0.1f), + Hollow = true, + Radius = 20, + Roundness = 10, + }; + } + + public override void DisplayAt(float scale) + { + box.FadeTo(scale, 500, Easing.OutQuint); + FadeEdgeEffectTo(0.2f * scale, 500, Easing.OutQuint); + } + } + } } } diff --git a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs index 6ba57c60b8..df3ca59f1f 100644 --- a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs +++ b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs @@ -30,9 +30,6 @@ namespace osu.Game.Tournament.Screens.Gameplay private OsuButton warmupButton; private MatchIPCInfo ipc; - private readonly Color4 red = new Color4(186, 0, 18, 255); - private readonly Color4 blue = new Color4(17, 136, 170, 255); - [Resolved(canBeNull: true)] private TournamentSceneManager sceneManager { get; set; } @@ -66,36 +63,11 @@ namespace osu.Game.Tournament.Screens.Gameplay // chroma key area for stable gameplay Name = "chroma", RelativeSizeAxes = Axes.X, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, Height = 512, Colour = new Color4(0, 255, 0, 255), }, - new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Y = -4, - Children = new Drawable[] - { - new Circle - { - Name = "top bar red", - RelativeSizeAxes = Axes.X, - Height = 8, - Width = 0.5f, - Colour = red, - }, - new Circle - { - Name = "top bar blue", - RelativeSizeAxes = Axes.X, - Height = 8, - Width = 0.5f, - Colour = blue, - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - }, - } - }, } }, scoreDisplay = new MatchScoreDisplay From e25206728f39761932ecc8379a34e33d495348be Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 7 Mar 2020 14:46:40 +0900 Subject: [PATCH 0255/2376] Hide score displays during warmup --- .../Gameplay/Components/MatchHeader.cs | 23 +++++++++++++++---- .../Gameplay/Components/TeamDisplay.cs | 6 ++++- .../Screens/Gameplay/GameplayScreen.cs | 9 ++++++-- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs b/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs index 45f46c462a..7e9d0178d8 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs @@ -15,6 +15,18 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components { public class MatchHeader : Container { + private TeamScoreDisplay teamDisplay1; + private TeamScoreDisplay teamDisplay2; + + public bool ShowScores + { + set + { + teamDisplay1.ShowScore = value; + teamDisplay2.ShowScore = value; + } + } + [BackgroundDependencyLoader] private void load() { @@ -43,13 +55,12 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components }, } }, - - new TeamScoreDisplay(TeamColour.Red) + teamDisplay1 = new TeamScoreDisplay(TeamColour.Red) { Anchor = Anchor.TopLeft, Origin = Anchor.TopLeft, }, - new TeamScoreDisplay(TeamColour.Blue) + teamDisplay2 = new TeamScoreDisplay(TeamColour.Blue) { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, @@ -66,6 +77,10 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components private readonly Bindable currentTeam = new Bindable(); private readonly Bindable currentTeamScore = new Bindable(); + private TeamDisplay teamDisplay; + + public bool ShowScore { set => teamDisplay.ShowScore = value; } + public TeamScoreDisplay(TeamColour teamColour) { this.teamColour = teamColour; @@ -116,7 +131,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components { InternalChildren = new Drawable[] { - new TeamDisplay(team, teamColour, currentTeamScore, currentMatch.Value.PointsToWin), + teamDisplay = new TeamDisplay(team, teamColour, currentTeamScore, currentMatch.Value.PointsToWin), }; } } diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs index 4bfeb2bc81..29908e8e7c 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs @@ -12,6 +12,10 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components { public class TeamDisplay : DrawableTournamentTeam { + private readonly TeamScore score; + + public bool ShowScore { set => score.FadeTo(value ? 1 : 0, 200); } + public TeamDisplay(TournamentTeam team, TeamColour colour, Bindable currentTeamScore, int pointsToWin) : base(team) { @@ -63,7 +67,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components Origin = anchor, Anchor = anchor, }, - new TeamScore(currentTeamScore, colour, pointsToWin) + score = new TeamScore(currentTeamScore, colour, pointsToWin) { Origin = anchor, Anchor = anchor, diff --git a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs index df3ca59f1f..78d27c87ff 100644 --- a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs +++ b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs @@ -48,7 +48,7 @@ namespace osu.Game.Tournament.Screens.Gameplay Loop = true, RelativeSizeAxes = Axes.Both, }, - new MatchHeader(), + header = new MatchHeader(), new Container { RelativeSizeAxes = Axes.X, @@ -108,13 +108,18 @@ namespace osu.Game.Tournament.Screens.Gameplay currentMatch.BindTo(ladder.CurrentMatch); - warmup.BindValueChanged(w => warmupButton.Alpha = !w.NewValue ? 0.5f : 1, true); + warmup.BindValueChanged(w => + { + warmupButton.Alpha = !w.NewValue ? 0.5f : 1; + header.ShowScores = !w.NewValue; + }, true); } private ScheduledDelegate scheduledOperation; private MatchScoreDisplay scoreDisplay; private TourneyState lastState; + private MatchHeader header; private void stateChanged(ValueChangedEvent state) { From 3807c449bd350c838c9e0749c17631a63724aeb2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 7 Mar 2020 15:28:19 +0900 Subject: [PATCH 0256/2376] Update chat position --- .../Components/TournamentMatchChatDisplay.cs | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs index 48c5b9bd35..6963f0a0cb 100644 --- a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs +++ b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs @@ -10,7 +10,6 @@ using osu.Game.Overlays.Chat; using osu.Game.Tournament.IPC; using osu.Game.Tournament.Models; using osuTK; -using osuTK.Graphics; namespace osu.Game.Tournament.Components { @@ -23,11 +22,11 @@ namespace osu.Game.Tournament.Components public TournamentMatchChatDisplay() { RelativeSizeAxes = Axes.X; - Y = 100; - Size = new Vector2(0.45f, 112); - Margin = new MarginPadding(10); - Anchor = Anchor.BottomCentre; - Origin = Anchor.BottomCentre; + Size = new Vector2(0.5f, 142); + Anchor = Anchor.BottomLeft; + Origin = Anchor.BottomLeft; + + CornerRadius = 0; } [BackgroundDependencyLoader(true)] @@ -75,19 +74,15 @@ namespace osu.Game.Tournament.Components { } - [BackgroundDependencyLoader] private void load(LadderInfo info) { - //if (info.CurrentMatch.Value.Team1.Value.Players.Any(u => u.Id == Message.Sender.Id)) - // ColourBox.Colour = red; - //else if (info.CurrentMatch.Value.Team2.Value.Players.Any(u => u.Id == Message.Sender.Id)) - // ColourBox.Colour = blue; - //else if (Message.Sender.Colour != null) - // SenderText.Colour = ColourBox.Colour = OsuColour.FromHex(Message.Sender.Colour); + // if (info.CurrentMatch.Value.Team1.Value.Players.Any(u => u.Id == Message.Sender.Id)) + // SenderText.Colour = TournamentGame.COLOUR_RED; + // else if (info.CurrentMatch.Value.Team2.Value.Players.Any(u => u.Id == Message.Sender.Id)) + // SenderText.Colour = TournamentGame.COLOUR_BLUE; + // else if (Message.Sender.Colour != null) + // SenderText.Colour = ColourBox.Colour = OsuColour.FromHex(Message.Sender.Colour); } - - private readonly Color4 red = new Color4(186, 0, 18, 255); - private readonly Color4 blue = new Color4(17, 136, 170, 255); } } } From 9bd837da4123cb2781dc25ac89ca4a2755794924 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 7 Mar 2020 16:22:52 +0900 Subject: [PATCH 0257/2376] Update match score display --- .../Screens/TestSceneGameplayScreen.cs | 1 + .../Gameplay/Components/MatchScoreDisplay.cs | 53 ++++++++++++------- .../Screens/Gameplay/Components/TeamScore.cs | 2 +- .../Screens/Gameplay/GameplayScreen.cs | 6 +-- 4 files changed, 37 insertions(+), 25 deletions(-) diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs index 964930a8de..ae7d9d853a 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs @@ -21,6 +21,7 @@ namespace osu.Game.Tournament.Tests.Screens typeof(TeamScoreDisplay), typeof(TeamDisplay), typeof(MatchHeader), + typeof(MatchScoreDisplay), }; [BackgroundDependencyLoader] diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/MatchScoreDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/MatchScoreDisplay.cs index fcf1469278..ed14956793 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/MatchScoreDisplay.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/MatchScoreDisplay.cs @@ -11,16 +11,12 @@ using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Tournament.IPC; using osu.Game.Tournament.Models; -using osuTK.Graphics; namespace osu.Game.Tournament.Screens.Gameplay.Components { public class MatchScoreDisplay : CompositeDrawable { - private readonly Color4 red = new Color4(186, 0, 18, 255); - private readonly Color4 blue = new Color4(17, 136, 170, 255); - - private const float bar_height = 20; + private const float bar_height = 18; private readonly BindableInt score1 = new BindableInt(); private readonly BindableInt score2 = new BindableInt(); @@ -28,45 +24,63 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components private readonly MatchScoreCounter score1Text; private readonly MatchScoreCounter score2Text; - private readonly Circle score1Bar; - private readonly Circle score2Bar; + private readonly Drawable score1Bar; + private readonly Drawable score2Bar; public MatchScoreDisplay() { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - InternalChildren = new Drawable[] + InternalChildren = new[] { - score1Bar = new Circle + new Box + { + Name = "top bar red (static)", + RelativeSizeAxes = Axes.X, + Height = bar_height / 4, + Width = 0.5f, + Colour = TournamentGame.COLOUR_RED, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopRight + }, + new Box + { + Name = "top bar blue (static)", + RelativeSizeAxes = Axes.X, + Height = bar_height / 4, + Width = 0.5f, + Colour = TournamentGame.COLOUR_BLUE, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopLeft + }, + score1Bar = new Box { Name = "top bar red", RelativeSizeAxes = Axes.X, Height = bar_height, Width = 0, - Colour = red, + Colour = TournamentGame.COLOUR_RED, Anchor = Anchor.TopCentre, Origin = Anchor.TopRight }, score1Text = new MatchScoreCounter { - Colour = red, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre }, - score2Bar = new Circle + score2Bar = new Box { Name = "top bar blue", RelativeSizeAxes = Axes.X, Height = bar_height, Width = 0, - Colour = blue, + Colour = TournamentGame.COLOUR_BLUE, Anchor = Anchor.TopCentre, Origin = Anchor.TopLeft }, score2Text = new MatchScoreCounter { - Colour = blue, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre }, @@ -103,10 +117,9 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components winningBar.ResizeWidthTo(Math.Min(0.4f, MathF.Pow(diff / 1500000f, 0.5f) / 2), 400, Easing.OutQuint); } - protected override void Update() + protected override void UpdateAfterChildren() { - base.Update(); - + base.UpdateAfterChildren(); score1Text.X = -Math.Max(5 + score1Text.DrawWidth / 2, score1Bar.DrawWidth); score2Text.X = Math.Max(5 + score2Text.DrawWidth / 2, score2Bar.DrawWidth); } @@ -115,7 +128,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components { public MatchScoreCounter() { - Margin = new MarginPadding { Top = bar_height + 5, Horizontal = 10 }; + Margin = new MarginPadding { Top = bar_height, Horizontal = 10 }; Winning = false; } @@ -123,8 +136,8 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components public bool Winning { set => DisplayedCountSpriteText.Font = value - ? OsuFont.Torus.With(weight: FontWeight.Regular, size: 60) - : OsuFont.Torus.With(weight: FontWeight.Light, size: 40); + ? OsuFont.Torus.With(weight: FontWeight.Bold, size: 50) + : OsuFont.Torus.With(weight: FontWeight.Regular, size: 40); } } } diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TeamScore.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScore.cs index 056f8387fb..c7071484ca 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/TeamScore.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScore.cs @@ -50,7 +50,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components public class LightSquare : Star { - private Box box; + private readonly Box box; public LightSquare() { diff --git a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs index 78d27c87ff..ad00dffac7 100644 --- a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs +++ b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs @@ -15,7 +15,6 @@ using osu.Game.Tournament.Models; using osu.Game.Tournament.Screens.Gameplay.Components; using osu.Game.Tournament.Screens.MapPool; using osu.Game.Tournament.Screens.TeamWin; -using osuTK; using osuTK.Graphics; namespace osu.Game.Tournament.Screens.Gameplay @@ -72,10 +71,9 @@ namespace osu.Game.Tournament.Screens.Gameplay }, scoreDisplay = new MatchScoreDisplay { - Y = -60, - Scale = new Vector2(0.8f), + Y = -147, Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, + Origin = Anchor.TopCentre, }, new ControlPanel { From 979988235dab7aa08e138e9a1e5f20eeadd6ad67 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 7 Mar 2020 12:49:09 +0900 Subject: [PATCH 0258/2376] Increase flexibility of StarCounter component --- .../Visual/Gameplay/TestSceneScoreCounter.cs | 12 +-- .../Graphics/UserInterface/StarCounter.cs | 101 ++++++++++-------- .../Carousel/DrawableCarouselBeatmap.cs | 2 +- 3 files changed, 61 insertions(+), 54 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs index ffd6f55b53..88bb83b446 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs @@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.Gameplay Origin = Anchor.BottomLeft, Anchor = Anchor.BottomLeft, Position = new Vector2(20, -160), - CountStars = 5, + Current = 5, }; Add(stars); @@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Gameplay Origin = Anchor.BottomLeft, Anchor = Anchor.BottomLeft, Position = new Vector2(20, -190), - Text = stars.CountStars.ToString("0.00"), + Text = stars.Current.ToString("0.00"), }; Add(starsLabel); @@ -69,8 +69,8 @@ namespace osu.Game.Tests.Visual.Gameplay comboCounter.Current.Value = 0; numerator = denominator = 0; accuracyCounter.SetFraction(0, 0); - stars.CountStars = 0; - starsLabel.Text = stars.CountStars.ToString("0.00"); + stars.Current = 0; + starsLabel.Text = stars.Current.ToString("0.00"); }); AddStep(@"Hit! :D", delegate @@ -91,8 +91,8 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep(@"Alter stars", delegate { - stars.CountStars = RNG.NextSingle() * (stars.StarCount + 1); - starsLabel.Text = stars.CountStars.ToString("0.00"); + stars.Current = RNG.NextSingle() * (stars.StarCount + 1); + starsLabel.Text = stars.Current.ToString("0.00"); }); AddStep(@"Stop counters", delegate diff --git a/osu.Game/Graphics/UserInterface/StarCounter.cs b/osu.Game/Graphics/UserInterface/StarCounter.cs index 586cd2ce84..b13d6485ac 100644 --- a/osu.Game/Graphics/UserInterface/StarCounter.cs +++ b/osu.Game/Graphics/UserInterface/StarCounter.cs @@ -13,7 +13,7 @@ namespace osu.Game.Graphics.UserInterface { public class StarCounter : Container { - private readonly Container stars; + private readonly FillFlowContainer stars; /// /// Maximum amount of stars displayed. @@ -23,34 +23,29 @@ namespace osu.Game.Graphics.UserInterface /// public int StarCount { get; } - private double animationDelay => 80; + /// + /// The added delay for each subsequent star to be animated. + /// + protected virtual double AnimationDelay => 80; - private double scalingDuration => 1000; - private Easing scalingEasing => Easing.OutElasticHalf; - private float minStarScale => 0.4f; - - private double fadingDuration => 100; - private float minStarAlpha => 0.5f; - - private const float star_size = 20; private const float star_spacing = 4; - private float countStars; + private float current; /// /// Amount of stars represented. /// - public float CountStars + public float Current { - get => countStars; + get => current; set { - if (countStars == value) return; + if (current == value) return; if (IsLoaded) - transformCount(value); - countStars = value; + animate(value); + current = value; } } @@ -71,11 +66,13 @@ namespace osu.Game.Graphics.UserInterface AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, Spacing = new Vector2(star_spacing), - ChildrenEnumerable = Enumerable.Range(0, StarCount).Select(i => new Star { Alpha = minStarAlpha }) + ChildrenEnumerable = Enumerable.Range(0, StarCount).Select(i => CreateStar()) } }; } + public virtual Star CreateStar() => new DefaultStar(); + protected override void LoadComplete() { base.LoadComplete(); @@ -86,63 +83,60 @@ namespace osu.Game.Graphics.UserInterface public void ResetCount() { - countStars = 0; + current = 0; StopAnimation(); } public void ReplayAnimation() { - var t = countStars; + var t = current; ResetCount(); - CountStars = t; + Current = t; } public void StopAnimation() { - int i = 0; - + animate(current); foreach (var star in stars.Children) + star.FinishTransforms(true); + } + + private float getStarScale(int i, float value) => i + 1 <= value ? 1.0f : Interpolation.ValueAt(value, 0, 1.0f, i, i + 1); + + private void animate(float newValue) + { + for (var i = 0; i < stars.Children.Count; i++) { + var star = stars.Children[i]; + star.ClearTransforms(true); - star.FadeTo(i < countStars ? 1.0f : minStarAlpha); - star.Icon.ScaleTo(getStarScale(i, countStars)); - i++; + + double delay = (current <= newValue ? Math.Max(i - current, 0) : Math.Max(current - 1 - i, 0)) * AnimationDelay; + + using (star.BeginDelayedSequence(delay, true)) + star.DisplayAt(getStarScale(i, newValue)); } } - private float getStarScale(int i, float value) + public class DefaultStar : Star { - if (value <= i) - return minStarScale; + private const double scaling_duration = 1000; - return i + 1 <= value ? 1.0f : Interpolation.ValueAt(value, minStarScale, 1.0f, i, i + 1); - } + private const double fading_duration = 100; - private void transformCount(float newValue) - { - int i = 0; + private const Easing scaling_easing = Easing.OutElasticHalf; - foreach (var star in stars.Children) - { - star.ClearTransforms(true); + private const float min_star_scale = 0.4f; - var delay = (countStars <= newValue ? Math.Max(i - countStars, 0) : Math.Max(countStars - 1 - i, 0)) * animationDelay; - star.Delay(delay).FadeTo(i < newValue ? 1.0f : minStarAlpha, fadingDuration); - star.Icon.Delay(delay).ScaleTo(getStarScale(i, newValue), scalingDuration, scalingEasing); + private const float star_size = 20; - i++; - } - } - - private class Star : Container - { public readonly SpriteIcon Icon; - public Star() + public DefaultStar() { Size = new Vector2(star_size); - Child = Icon = new SpriteIcon + InternalChild = Icon = new SpriteIcon { Size = new Vector2(star_size), Icon = FontAwesome.Solid.Star, @@ -150,6 +144,19 @@ namespace osu.Game.Graphics.UserInterface Origin = Anchor.Centre, }; } + + public override void DisplayAt(float scale) + { + scale = Math.Clamp(scale, min_star_scale, 1); + + this.FadeTo(scale, fading_duration); + Icon.ScaleTo(scale, scaling_duration, scaling_easing); + } + } + + public abstract class Star : CompositeDrawable + { + public abstract void DisplayAt(float scale); } } } diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index d9eeec9f85..50419a5fb9 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -123,7 +123,7 @@ namespace osu.Game.Screens.Select.Carousel }, starCounter = new StarCounter { - CountStars = (float)beatmap.StarDifficulty, + Current = (float)beatmap.StarDifficulty, Scale = new Vector2(0.8f), } } From 059aea8ead39601ffe0ff79d21333bb0443045b3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Mar 2020 16:09:07 +0900 Subject: [PATCH 0259/2376] Initial pass of schedule screen design update --- .../Screens/Schedule/ScheduleScreen.cs | 126 +++++++++++++----- 1 file changed, 95 insertions(+), 31 deletions(-) diff --git a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs index 4c93c04fcf..9ca1aa626a 100644 --- a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs +++ b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using Humanizer; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -18,7 +19,7 @@ using osuTK.Graphics; namespace osu.Game.Tournament.Screens.Schedule { - public class ScheduleScreen : TournamentScreen + public class ScheduleScreen : TournamentScreen // IProvidesVideo { private readonly Bindable currentMatch = new Bindable(); private Container mainContainer; @@ -38,10 +39,63 @@ namespace osu.Game.Tournament.Screens.Schedule RelativeSizeAxes = Axes.Both, Loop = true, }, - mainContainer = new Container + new Container { RelativeSizeAxes = Axes.Both, - } + Padding = new MarginPadding(100) { Bottom = 50 }, + Children = new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(), + }, + Content = new[] + { + new Drawable[] + { + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new DrawableTournamentTitleText(), + new Container + { + Margin = new MarginPadding { Top = 40 }, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + Colour = Color4.White, + Size = new Vector2(50, 10), + }, + new TournamentSpriteTextWithBackground("Schedule") + { + X = 60, + Scale = new Vector2(0.8f) + } + } + }, + } + }, + }, + new Drawable[] + { + mainContainer = new Container + { + RelativeSizeAxes = Axes.Both, + } + } + } + } + } + }, }; currentMatch.BindValueChanged(matchChanged); @@ -91,7 +145,7 @@ namespace osu.Game.Tournament.Screens.Schedule .Take(8) .Select(p => new ScheduleMatch(p)) }, - new ScheduleContainer("match overview") + new ScheduleContainer("upcoming matches") { RelativeSizeAxes = Axes.Both, Width = 0.6f, @@ -100,26 +154,45 @@ namespace osu.Game.Tournament.Screens.Schedule } } }, - new ScheduleContainer("current match") + new ScheduleContainer("coming up next") { RelativeSizeAxes = Axes.Both, Height = 0.25f, Children = new Drawable[] { - new TournamentSpriteText + new FillFlowContainer { - Margin = new MarginPadding { Left = -10, Bottom = 10, Top = -5 }, - Spacing = new Vector2(10, 0), - Text = match.NewValue.Round.Value?.Name.Value, - Colour = Color4.Black, - Font = OsuFont.Torus.With(size: 20) - }, - new ScheduleMatch(match.NewValue, false), - new TournamentSpriteText - { - Text = "Start Time " + match.NewValue.Date.Value.ToUniversalTime().ToString("HH:mm UTC"), - Colour = Color4.Black, - Font = OsuFont.Torus.With(size: 20) + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(30), + Children = new Drawable[] + { + new ScheduleMatch(match.NewValue, false) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + new TournamentSpriteTextWithBackground(match.NewValue.Round.Value?.Name.Value) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Scale = new Vector2(0.5f) + }, + new TournamentSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Text = match.NewValue.Team1.Value?.FullName + " vs " + match.NewValue.Team2.Value?.FullName, + Font = OsuFont.Torus.With(size: 24, weight: FontWeight.SemiBold) + }, + new TournamentSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Text = $"Starting {match.NewValue.Date.Value.Humanize()}", + Font = OsuFont.Torus.With(size: 24, weight: FontWeight.Regular) + }, + } }, } } @@ -170,29 +243,20 @@ namespace osu.Game.Tournament.Screens.Schedule public ScheduleContainer(string title) { - Padding = new MarginPadding { Left = 30, Top = 30 }; + Padding = new MarginPadding { Left = 30, Top = 10 }; InternalChildren = new Drawable[] { - new TournamentSpriteText + new TournamentSpriteTextWithBackground(title.ToUpperInvariant()) { X = 30, - Text = title, - Colour = Color4.Black, - Spacing = new Vector2(10, 0), - Font = OsuFont.Torus.With(size: 30) + Scale = new Vector2(0.5f) }, content = new FillFlowContainer { Direction = FillDirection.Vertical, RelativeSizeAxes = Axes.Both, - Margin = new MarginPadding(40) + Margin = new MarginPadding(10) }, - new Circle - { - Colour = new Color4(233, 187, 79, 255), - Width = 5, - RelativeSizeAxes = Axes.Y, - } }; } } From 434feb5ac6b1986d27eb2907ca042be60175759b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 7 Mar 2020 13:26:54 +0900 Subject: [PATCH 0260/2376] Fix alignment on schedule screen --- .../Screens/TestSceneScheduleScreen.cs | 3 ++ .../Components/DrawableTournamentMatch.cs | 12 +++---- .../Screens/Schedule/ScheduleScreen.cs | 36 ++++++++++++------- 3 files changed, 31 insertions(+), 20 deletions(-) diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneScheduleScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneScheduleScreen.cs index 2277302e98..b240ef3ae5 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneScheduleScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneScheduleScreen.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Tournament.Components; using osu.Game.Tournament.Screens.Schedule; namespace osu.Game.Tournament.Tests.Screens @@ -11,6 +13,7 @@ namespace osu.Game.Tournament.Tests.Screens [BackgroundDependencyLoader] private void load() { + Add(new TourneyVideo("main") { RelativeSizeAxes = Axes.Both }); Add(new ScheduleScreen()); } } diff --git a/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs b/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs index c4b670f059..d0ba9a96f4 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs @@ -23,7 +23,7 @@ namespace osu.Game.Tournament.Screens.Ladder.Components private readonly bool editor; protected readonly FillFlowContainer Flow; private readonly Drawable selectionBox; - private readonly Drawable currentMatchSelectionBox; + protected readonly Drawable CurrentMatchSelectionBox; private Bindable globalSelection; [Resolved(CanBeNull = true)] @@ -55,11 +55,9 @@ namespace osu.Game.Tournament.Screens.Ladder.Components Colour = Color4.YellowGreen, Child = new Box { RelativeSizeAxes = Axes.Both } }, - currentMatchSelectionBox = new Container + CurrentMatchSelectionBox = new Container { - CornerRadius = 5, - Masking = true, - Scale = new Vector2(1.05f), + Scale = new Vector2(1.05f, 1.1f), RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -128,9 +126,9 @@ namespace osu.Game.Tournament.Screens.Ladder.Components private void updateCurrentMatch() { if (Match.Current.Value) - currentMatchSelectionBox.Show(); + CurrentMatchSelectionBox.Show(); else - currentMatchSelectionBox.Hide(); + CurrentMatchSelectionBox.Hide(); } private bool selected; diff --git a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs index 9ca1aa626a..634ceb11a5 100644 --- a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs +++ b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs @@ -127,7 +127,7 @@ namespace osu.Game.Tournament.Screens.Schedule new Container { RelativeSizeAxes = Axes.Both, - Height = 0.65f, + Height = 0.74f, Child = new FillFlowContainer { RelativeSizeAxes = Axes.Both, @@ -162,7 +162,7 @@ namespace osu.Game.Tournament.Screens.Schedule { new FillFlowContainer { - RelativeSizeAxes = Axes.Both, + AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, Spacing = new Vector2(30), Children = new Drawable[] @@ -207,6 +207,8 @@ namespace osu.Game.Tournament.Screens.Schedule { Flow.Direction = FillDirection.Horizontal; + Scale = new Vector2(0.8f); + bool conditional = match is ConditionalTournamentMatch; if (conditional) @@ -218,15 +220,16 @@ namespace osu.Game.Tournament.Screens.Schedule { Anchor = Anchor.TopRight, Origin = Anchor.TopLeft, - Colour = Color4.Black, + Colour = OsuColour.Gray(0.7f), Alpha = conditional ? 0.6f : 1, + Font = OsuFont.Torus, Margin = new MarginPadding { Horizontal = 10, Vertical = 5 }, }); AddInternal(new TournamentSpriteText { Anchor = Anchor.BottomRight, Origin = Anchor.BottomLeft, - Colour = Color4.Black, + Colour = OsuColour.Gray(0.7f), Alpha = conditional ? 0.6f : 1, Margin = new MarginPadding { Horizontal = 10, Vertical = 5 }, Text = match.Date.Value.ToUniversalTime().ToString("HH:mm UTC") + (conditional ? " (conditional)" : "") @@ -243,19 +246,26 @@ namespace osu.Game.Tournament.Screens.Schedule public ScheduleContainer(string title) { - Padding = new MarginPadding { Left = 30, Top = 10 }; + Padding = new MarginPadding { Left = 60, Top = 10 }; InternalChildren = new Drawable[] { - new TournamentSpriteTextWithBackground(title.ToUpperInvariant()) + new FillFlowContainer { - X = 30, - Scale = new Vector2(0.5f) - }, - content = new FillFlowContainer - { - Direction = FillDirection.Vertical, RelativeSizeAxes = Axes.Both, - Margin = new MarginPadding(10) + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new TournamentSpriteTextWithBackground(title.ToUpperInvariant()) + { + Scale = new Vector2(0.5f) + }, + content = new FillFlowContainer + { + Direction = FillDirection.Vertical, + RelativeSizeAxes = Axes.Both, + Margin = new MarginPadding(10) + }, + } }, }; } From 9934a97bd0a9dc3a7eb97113b7a74af88fdb004a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 7 Mar 2020 16:31:28 +0900 Subject: [PATCH 0261/2376] Limit upcoming matches displayed to 8 --- osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs index 634ceb11a5..01d6d33fee 100644 --- a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs +++ b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs @@ -116,7 +116,7 @@ namespace osu.Game.Tournament.Screens.Schedule .SelectMany(m => m.ConditionalMatches.Where(cp => m.Acronyms.TrueForAll(a => cp.Acronyms.Contains(a)))); upcoming = upcoming.Concat(conditionals); - upcoming = upcoming.OrderBy(p => p.Date.Value).Take(12); + upcoming = upcoming.OrderBy(p => p.Date.Value).Take(8); mainContainer.Child = new FillFlowContainer { From 6c0a27e0b956e0ff955c77be57981614a680be6e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 7 Mar 2020 16:38:50 +0900 Subject: [PATCH 0262/2376] Improve look of selected match --- .../Screens/Ladder/Components/DrawableTournamentMatch.cs | 2 +- osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs b/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs index d0ba9a96f4..320e76775a 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs @@ -62,7 +62,7 @@ namespace osu.Game.Tournament.Screens.Ladder.Components Anchor = Anchor.Centre, Origin = Anchor.Centre, Alpha = 0, - Colour = Color4.OrangeRed, + Colour = Color4.White, Child = new Box { RelativeSizeAxes = Axes.Both } }, Flow = new FillFlowContainer diff --git a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs index 01d6d33fee..27a1fe98d2 100644 --- a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs +++ b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs @@ -209,6 +209,8 @@ namespace osu.Game.Tournament.Screens.Schedule Scale = new Vector2(0.8f); + CurrentMatchSelectionBox.Scale = new Vector2(1.02f, 1.15f); + bool conditional = match is ConditionalTournamentMatch; if (conditional) From 8ab9ca77d60b4d8c065454b5f495ec87e75bc0cc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 8 Mar 2020 15:06:06 +0900 Subject: [PATCH 0263/2376] Fix next match timer not updating --- .../Screens/Schedule/ScheduleScreen.cs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs index 27a1fe98d2..0fcec645e3 100644 --- a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs +++ b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs @@ -3,7 +3,6 @@ using System; using System.Linq; -using Humanizer; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -185,12 +184,24 @@ namespace osu.Game.Tournament.Screens.Schedule Text = match.NewValue.Team1.Value?.FullName + " vs " + match.NewValue.Team2.Value?.FullName, Font = OsuFont.Torus.With(size: 24, weight: FontWeight.SemiBold) }, - new TournamentSpriteText + new FillFlowContainer { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Text = $"Starting {match.NewValue.Date.Value.Humanize()}", - Font = OsuFont.Torus.With(size: 24, weight: FontWeight.Regular) + Children = new Drawable[] + { + new TournamentSpriteText + { + Text = "Starting ", + Font = OsuFont.Torus.With(size: 24, weight: FontWeight.Regular) + }, + new DrawableDate(match.NewValue.Date.Value) + { + Font = OsuFont.Torus.With(size: 24, weight: FontWeight.Regular) + } + } }, } }, From 3ac599246dc816247c7790aec8a95e0fc50aa62b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Mar 2020 16:08:24 +0900 Subject: [PATCH 0264/2376] Initial pass of seeding screen design update --- .../Screens/TeamIntro/SeedingScreen.cs | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs b/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs index 513d84b594..d48e396b89 100644 --- a/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs +++ b/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs @@ -15,7 +15,6 @@ using osu.Game.Tournament.Components; using osu.Game.Tournament.Models; using osu.Game.Tournament.Screens.Ladder.Components; using osuTK; -using osuTK.Graphics; namespace osu.Game.Tournament.Screens.TeamIntro { @@ -140,9 +139,9 @@ namespace osu.Game.Tournament.Screens.TeamIntro Spacing = new Vector2(5), Children = new Drawable[] { - new TournamentSpriteText { Text = beatmap.BeatmapInfo.Metadata.Title, Colour = Color4.Black, }, - new TournamentSpriteText { Text = "by", Colour = Color4.Black, Font = OsuFont.Torus.With(weight: FontWeight.Regular) }, - new TournamentSpriteText { Text = beatmap.BeatmapInfo.Metadata.Artist, Colour = Color4.Black, Font = OsuFont.Torus.With(weight: FontWeight.Regular) }, + new TournamentSpriteText { Text = beatmap.BeatmapInfo.Metadata.Title, Colour = TournamentGame.TEXT_COLOUR, }, + new TournamentSpriteText { Text = "by", Colour = TournamentGame.TEXT_COLOUR, Font = OsuFont.Torus.With(weight: FontWeight.Regular) }, + new TournamentSpriteText { Text = beatmap.BeatmapInfo.Metadata.Artist, Colour = TournamentGame.TEXT_COLOUR, Font = OsuFont.Torus.With(weight: FontWeight.Regular) }, } }, new FillFlowContainer @@ -154,8 +153,8 @@ namespace osu.Game.Tournament.Screens.TeamIntro Spacing = new Vector2(40), Children = new Drawable[] { - new TournamentSpriteText { Text = beatmap.Score.ToString("#,0"), Colour = Color4.Black, Width = 80 }, - new TournamentSpriteText { Text = "#" + beatmap.Seed.Value.ToString("#,0"), Colour = Color4.Black, Font = OsuFont.Torus.With(weight: FontWeight.Regular) }, + new TournamentSpriteText { Text = beatmap.Score.ToString("#,0"), Colour = TournamentGame.TEXT_COLOUR, Width = 80 }, + new TournamentSpriteText { Text = "#" + beatmap.Seed.Value.ToString("#,0"), Colour = TournamentGame.TEXT_COLOUR, Font = OsuFont.Torus.With(weight: FontWeight.Regular) }, } }, }; @@ -204,7 +203,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro new Box { RelativeSizeAxes = Axes.Both, - Colour = Color4.Black, + Colour = TournamentGame.TEXT_COLOUR, }, new TournamentSpriteText { @@ -260,20 +259,18 @@ namespace osu.Game.Tournament.Screens.TeamIntro AutoSizeAxes = Axes.Y; RelativeSizeAxes = Axes.X; - var colour = OsuColour.Gray(0.3f); - InternalChildren = new Drawable[] { new TournamentSpriteText { Text = left, - Colour = colour, - Font = OsuFont.Torus.With(size: 22), + Colour = TournamentGame.TEXT_COLOUR, + Font = OsuFont.Torus.With(size: 22, weight: FontWeight.SemiBold), }, new TournamentSpriteText { Text = right, - Colour = colour, + Colour = TournamentGame.TEXT_COLOUR, Anchor = Anchor.TopRight, Origin = Anchor.TopLeft, Font = OsuFont.Torus.With(size: 22, weight: FontWeight.Regular), @@ -305,7 +302,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro { Text = team?.FullName.Value ?? "???", Font = OsuFont.Torus.With(size: 32, weight: FontWeight.SemiBold), - Colour = Color4.Black, + Colour = TournamentGame.TEXT_COLOUR, }, } }; From 3a3a2ad2a71126973caac66e7b98ee7acf938caa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 8 Mar 2020 14:47:29 +0900 Subject: [PATCH 0265/2376] Fix video looping not propagating when set too early in initialisation --- osu.Game.Tournament/Components/TourneyVideo.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Components/TourneyVideo.cs b/osu.Game.Tournament/Components/TourneyVideo.cs index 7d2eaff515..786b7b3c67 100644 --- a/osu.Game.Tournament/Components/TourneyVideo.cs +++ b/osu.Game.Tournament/Components/TourneyVideo.cs @@ -38,7 +38,8 @@ namespace osu.Game.Tournament.Components { RelativeSizeAxes = Axes.Both, FillMode = FillMode.Fit, - Clock = new FramedClock(manualClock = new ManualClock()) + Clock = new FramedClock(manualClock = new ManualClock()), + Loop = loop, }; } else if (drawFallbackGradient) @@ -51,10 +52,13 @@ namespace osu.Game.Tournament.Components } } + private bool loop; + public bool Loop { set { + loop = value; if (video != null) video.Loop = value; } From a85cef2f064640f6a5d7029b65aded95e9ed353b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 8 Mar 2020 14:23:13 +0900 Subject: [PATCH 0266/2376] Reset win screen video on display; add fade in transition --- osu.Game.Tournament/Components/TourneyVideo.cs | 2 ++ .../Screens/TeamWin/TeamWinScreen.cs | 17 +++++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tournament/Components/TourneyVideo.cs b/osu.Game.Tournament/Components/TourneyVideo.cs index 7d2eaff515..9b1350ca23 100644 --- a/osu.Game.Tournament/Components/TourneyVideo.cs +++ b/osu.Game.Tournament/Components/TourneyVideo.cs @@ -60,6 +60,8 @@ namespace osu.Game.Tournament.Components } } + public void Reset() => manualClock.CurrentTime = 0; + protected override void Update() { base.Update(); diff --git a/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs b/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs index 8b3f4488d0..3870f486e1 100644 --- a/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs +++ b/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs @@ -62,7 +62,9 @@ namespace osu.Game.Tournament.Screens.TeamWin update(); } - private void update() + private bool firstDisplay = true; + + private void update() => Schedule(() => { var match = currentMatch.Value; @@ -75,6 +77,15 @@ namespace osu.Game.Tournament.Screens.TeamWin redWinVideo.Alpha = match.WinnerColour == TeamColour.Red ? 1 : 0; blueWinVideo.Alpha = match.WinnerColour == TeamColour.Blue ? 1 : 0; + if (firstDisplay) + { + if (match.WinnerColour == TeamColour.Red) + redWinVideo.Reset(); + else + blueWinVideo.Reset(); + firstDisplay = false; + } + mainContainer.Children = new Drawable[] { new DrawableTeamFlag(match.Winner) @@ -108,6 +119,8 @@ namespace osu.Game.Tournament.Screens.TeamWin } }, }; - } + mainContainer.FadeOut(); + mainContainer.Delay(2000).FadeIn(1600, Easing.OutQuint); + }); } } From 5d5910822bc6d45d595f2eb83332b17bdfeb3743 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Mar 2020 16:08:12 +0900 Subject: [PATCH 0267/2376] Initial pass of intro screen design update --- .../Screens/TeamIntro/TeamIntroScreen.cs | 153 +++--------------- 1 file changed, 22 insertions(+), 131 deletions(-) diff --git a/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs b/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs index d584c21058..2daf9d35f3 100644 --- a/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs +++ b/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs @@ -6,7 +6,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Platform; -using osu.Game.Graphics; using osu.Game.Tournament.Components; using osu.Game.Tournament.Models; using osuTK; @@ -49,141 +48,33 @@ namespace osu.Game.Tournament.Screens.TeamIntro return; } + const float y_flag_offset = 288; + + const float y_offset = 460; + mainContainer.Children = new Drawable[] { - new TeamWithPlayers(match.NewValue.Team1.Value, true) - { - RelativeSizeAxes = Axes.Both, - Width = 0.5f, - Height = 0.6f, - Anchor = Anchor.Centre, - Origin = Anchor.CentreRight - }, - new TeamWithPlayers(match.NewValue.Team2.Value) - { - RelativeSizeAxes = Axes.Both, - Width = 0.5f, - Height = 0.6f, - Anchor = Anchor.Centre, - Origin = Anchor.CentreLeft - }, new RoundDisplay(match.NewValue) { - RelativeSizeAxes = Axes.Both, - Height = 0.25f, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Y = 180, - } + Position = new Vector2(100, 100) + }, + new DrawableTeamFlag(match.NewValue.Team1.Value) + { + Position = new Vector2(160, y_flag_offset), + }, + new DrawableTeamWithPlayers(match.NewValue.Team1.Value, TeamColour.Red) + { + Position = new Vector2(160, y_offset), + }, + new DrawableTeamFlag(match.NewValue.Team2.Value) + { + Position = new Vector2(740, y_flag_offset), + }, + new DrawableTeamWithPlayers(match.NewValue.Team2.Value, TeamColour.Blue) + { + Position = new Vector2(740, y_offset), + }, }; } - - private class RoundDisplay : CompositeDrawable - { - public RoundDisplay(TournamentMatch match) - { - InternalChildren = new Drawable[] - { - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 10), - Children = new Drawable[] - { - new TournamentSpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Colour = OsuColour.Gray(0.33f), - Text = match.Round.Value?.Name.Value ?? "Unknown Round", - Font = OsuFont.Torus.With(size: 26, weight: FontWeight.Light) - }, - } - } - }; - } - } - - private class TeamWithPlayers : CompositeDrawable - { - public TeamWithPlayers(TournamentTeam team, bool left = false) - { - FillFlowContainer players; - var colour = left ? TournamentGame.COLOUR_RED : TournamentGame.COLOUR_BLUE; - InternalChildren = new Drawable[] - { - new TeamDisplay(team) - { - Anchor = left ? Anchor.CentreRight : Anchor.CentreLeft, - Origin = Anchor.TopCentre, - RelativePositionAxes = Axes.Both, - X = (left ? -1 : 1) * 0.3145f, - Y = -0.077f, - }, - players = new FillFlowContainer - { - Direction = FillDirection.Vertical, - AutoSizeAxes = Axes.Both, - Spacing = new Vector2(0, 5), - Padding = new MarginPadding(20), - Anchor = left ? Anchor.CentreRight : Anchor.CentreLeft, - Origin = left ? Anchor.CentreRight : Anchor.CentreLeft, - RelativePositionAxes = Axes.Both, - X = (left ? -1 : 1) * 0.58f, - }, - }; - - if (team != null) - { - foreach (var p in team.Players) - { - players.Add(new TournamentSpriteText - { - Text = p.Username, - Font = OsuFont.Torus.With(size: 24), - Colour = colour, - Anchor = left ? Anchor.CentreRight : Anchor.CentreLeft, - Origin = left ? Anchor.CentreRight : Anchor.CentreLeft, - }); - } - } - } - - private class TeamDisplay : DrawableTournamentTeam - { - public TeamDisplay(TournamentTeam team) - : base(team) - { - AutoSizeAxes = Axes.Both; - - Flag.Anchor = Flag.Origin = Anchor.TopCentre; - Flag.RelativeSizeAxes = Axes.None; - Flag.Size = new Vector2(300, 200); - Flag.Scale = new Vector2(0.32f); - - InternalChild = new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Spacing = new Vector2(160), - Children = new Drawable[] - { - Flag, - new TournamentSpriteText - { - Text = team?.FullName.Value ?? "???", - Font = OsuFont.Torus.With(size: 20, weight: FontWeight.Regular), - Colour = OsuColour.Gray(0.2f), - Origin = Anchor.TopCentre, - Anchor = Anchor.TopCentre, - }, - } - }; - } - } - } } } From 8e4b15aaa5f0a4e3aeaf66a85d628ecf9f7df54d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 8 Mar 2020 17:43:55 +0900 Subject: [PATCH 0268/2376] Update test scene --- osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs index ae7d9d853a..34fa7a4997 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using osu.Framework.Allocation; using osu.Game.Tournament.Components; +using osu.Game.Tournament.Screens; using osu.Game.Tournament.Screens.Gameplay; using osu.Game.Tournament.Screens.Gameplay.Components; @@ -13,7 +14,7 @@ namespace osu.Game.Tournament.Tests.Screens public class TestSceneGameplayScreen : TournamentTestScene { [Cached] - private TournamentMatchChatDisplay chat = new TournamentMatchChatDisplay(); + private TournamentMatchChatDisplay chat = new TournamentMatchChatDisplay { Width = 0.5f }; public override IReadOnlyList RequiredTypes => new[] { @@ -22,6 +23,8 @@ namespace osu.Game.Tournament.Tests.Screens typeof(TeamDisplay), typeof(MatchHeader), typeof(MatchScoreDisplay), + typeof(BeatmapInfoScreen), + typeof(SongBar), }; [BackgroundDependencyLoader] From 0102aaf32a53e6c2a0714ca79a85ca7b24fe949d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 8 Mar 2020 18:11:57 +0900 Subject: [PATCH 0269/2376] Move chat expand/contract logic local to tournament --- .../Components/TournamentMatchChatDisplay.cs | 4 ++++ osu.Game/Online/Chat/StandAloneChatDisplay.cs | 12 ------------ 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs index 48c5b9bd35..f9cd18be2c 100644 --- a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs +++ b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs @@ -66,6 +66,10 @@ namespace osu.Game.Tournament.Components } } + public void Expand() => this.FadeIn(300); + + public void Contract() => this.FadeOut(200); + protected override ChatLine CreateMessage(Message message) => new MatchMessage(message); protected class MatchMessage : StandAloneMessage diff --git a/osu.Game/Online/Chat/StandAloneChatDisplay.cs b/osu.Game/Online/Chat/StandAloneChatDisplay.cs index 881dd19d8e..0914f688e9 100644 --- a/osu.Game/Online/Chat/StandAloneChatDisplay.cs +++ b/osu.Game/Online/Chat/StandAloneChatDisplay.cs @@ -92,18 +92,6 @@ namespace osu.Game.Online.Chat textbox.Text = string.Empty; } - public void Expand() - { - this.FadeIn(300); - this.MoveToY(0, 500, Easing.OutQuint); - } - - public void Contract() - { - this.FadeOut(200); - this.MoveToY(100, 500, Easing.In); - } - protected virtual ChatLine CreateMessage(Message message) => new StandAloneMessage(message); private void channelChanged(ValueChangedEvent e) From 3744aaf55f228462c7b1227f1034fb137197eaa1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 8 Mar 2020 18:16:32 +0900 Subject: [PATCH 0270/2376] Update vertical alignment of chroma area --- osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs index ad00dffac7..4d770855cd 100644 --- a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs +++ b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs @@ -52,9 +52,9 @@ namespace osu.Game.Tournament.Screens.Gameplay { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Y = 5, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, + Y = 110, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, Children = new Drawable[] { new Box From 8b0b910196f693343a7feb3a21be0c49e0a6de85 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 8 Mar 2020 14:46:09 +0900 Subject: [PATCH 0271/2376] Update song / chat / beatmap info display to reflect new design --- .../Screens/TestSceneGameplayScreen.cs | 1 + osu.Game.Tournament/Components/SongBar.cs | 204 ++++++++---------- .../Components/TournamentBeatmapPanel.cs | 8 +- .../Components/TournamentMatchChatDisplay.cs | 3 +- .../Screens/BeatmapInfoScreen.cs | 1 + osu.Game.Tournament/TournamentSceneManager.cs | 7 +- 6 files changed, 107 insertions(+), 117 deletions(-) diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs index 34fa7a4997..1e20687a87 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs @@ -35,3 +35,4 @@ namespace osu.Game.Tournament.Tests.Screens } } } + diff --git a/osu.Game.Tournament/Components/SongBar.cs b/osu.Game.Tournament/Components/SongBar.cs index 48ea36a8f3..8d766ec9ba 100644 --- a/osu.Game.Tournament/Components/SongBar.cs +++ b/osu.Game.Tournament/Components/SongBar.cs @@ -4,10 +4,8 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Game.Beatmaps; @@ -24,6 +22,8 @@ namespace osu.Game.Tournament.Components { private BeatmapInfo beatmap; + private const float height = 145; + [Resolved] private IBindable ruleset { get; set; } @@ -52,15 +52,7 @@ namespace osu.Game.Tournament.Components } } - private Container panelContents; - private Container innerPanel; - private Container outerPanel; - private TournamentBeatmapPanel panel; - - private float panelWidth => expanded ? 0.6f : 1; - - private const float main_width = 0.97f; - private const float inner_panel_width = 0.7f; + private FillFlowContainer flow; private bool expanded; @@ -70,86 +62,27 @@ namespace osu.Game.Tournament.Components set { expanded = value; - panel?.ResizeWidthTo(panelWidth, 800, Easing.OutQuint); - - if (expanded) - { - innerPanel.ResizeWidthTo(inner_panel_width, 800, Easing.OutQuint); - outerPanel.ResizeWidthTo(main_width, 800, Easing.OutQuint); - } - else - { - innerPanel.ResizeWidthTo(1, 800, Easing.OutQuint); - outerPanel.ResizeWidthTo(0.25f, 800, Easing.OutQuint); - } + flow.Direction = expanded ? FillDirection.Full : FillDirection.Vertical; } } [BackgroundDependencyLoader] private void load() { - RelativeSizeAxes = Axes.Both; + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; InternalChildren = new Drawable[] { - outerPanel = new Container + flow = new FillFlowContainer { - Masking = true, - EdgeEffect = new EdgeEffectParameters - { - Colour = Color4.Black.Opacity(0.2f), - Type = EdgeEffectType.Shadow, - Radius = 5, - }, RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + LayoutDuration = 500, + LayoutEasing = Easing.OutQuint, + Direction = FillDirection.Full, Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, - RelativePositionAxes = Axes.X, - X = -(1 - main_width) / 2, - Y = -10, - Width = main_width, - Height = TournamentBeatmapPanel.HEIGHT, - CornerRadius = TournamentBeatmapPanel.HEIGHT / 2, - CornerExponent = 2, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = OsuColour.Gray(0.93f), - }, - new OsuLogo - { - Triangles = false, - Colour = OsuColour.Gray(0.33f), - Scale = new Vector2(0.08f), - Margin = new MarginPadding(50), - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - }, - innerPanel = new Container - { - Masking = true, - CornerRadius = TournamentBeatmapPanel.HEIGHT / 2, - CornerExponent = 2, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Width = inner_panel_width, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = OsuColour.Gray(0.86f), - }, - panelContents = new Container - { - RelativeSizeAxes = Axes.Both, - } - } - } - } } }; @@ -160,7 +93,7 @@ namespace osu.Game.Tournament.Components { if (beatmap == null) { - panelContents.Clear(); + flow.Clear(); return; } @@ -219,34 +152,86 @@ namespace osu.Game.Tournament.Components break; } - panelContents.Children = new Drawable[] + flow.Children = new Drawable[] { - new DiffPiece(("Length", TimeSpan.FromMilliseconds(length).ToString(@"mm\:ss"))) + new Container { - Anchor = Anchor.CentreLeft, - Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + Height = height / 2, + Width = 0.5f, + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + + Children = new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.Both, + + Content = new[] + { + new Drawable[] + { + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new DiffPiece(stats), + new DiffPiece(("Star Rating", $"{beatmap.StarDifficulty:0.#}{srExtra}")) + } + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new DiffPiece(("Length", TimeSpan.FromMilliseconds(length).ToString(@"mm\:ss"))), + new DiffPiece(("BPM", $"{bpm:0.#}")) + } + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + Colour = Color4.Black, + RelativeSizeAxes = Axes.Both, + Alpha = 0.1f, + }, + new OsuLogo + { + Triangles = false, + Scale = new Vector2(0.08f), + Margin = new MarginPadding(50), + X = -10, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + }, + } + }, + }, + } + } + } }, - new DiffPiece(("BPM", $"{bpm:0.#}")) + new TournamentBeatmapPanel(beatmap) { - Anchor = Anchor.CentreLeft, - Origin = Anchor.TopLeft - }, - new DiffPiece(stats) - { - Anchor = Anchor.CentreRight, - Origin = Anchor.BottomRight - }, - new DiffPiece(("Star Rating", $"{beatmap.StarDifficulty:0.#}{srExtra}")) - { - Anchor = Anchor.CentreRight, - Origin = Anchor.TopRight - }, - panel = new TournamentBeatmapPanel(beatmap) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Size = new Vector2(panelWidth, 1) + RelativeSizeAxes = Axes.X, + Width = 0.5f, + Height = height / 2, + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, } }; } @@ -258,10 +243,9 @@ namespace osu.Game.Tournament.Components Margin = new MarginPadding { Horizontal = 15, Vertical = 1 }; AutoSizeAxes = Axes.Both; - static void cp(SpriteText s, Color4 colour) + static void cp(SpriteText s, bool bold) { - s.Colour = colour; - s.Font = OsuFont.Torus.With(weight: FontWeight.Bold, size: 15); + s.Font = OsuFont.Torus.With(weight: bold ? FontWeight.Bold : FontWeight.Regular, size: 15); } for (var i = 0; i < tuples.Length; i++) @@ -272,14 +256,14 @@ namespace osu.Game.Tournament.Components { AddText(" / ", s => { - cp(s, OsuColour.Gray(0.33f)); + cp(s, false); s.Spacing = new Vector2(-2, 0); }); } - AddText(new TournamentSpriteText { Text = heading }, s => cp(s, OsuColour.Gray(0.33f))); - AddText(" ", s => cp(s, OsuColour.Gray(0.33f))); - AddText(new TournamentSpriteText { Text = content }, s => cp(s, OsuColour.Gray(0.5f))); + AddText(new TournamentSpriteText { Text = heading }, s => cp(s, false)); + AddText(" ", s => cp(s, false)); + AddText(new TournamentSpriteText { Text = content }, s => cp(s, true)); } } } diff --git a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs index 394ffe304e..e09af06c89 100644 --- a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs +++ b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs @@ -70,8 +70,8 @@ namespace osu.Game.Tournament.Components new FillFlowContainer { AutoSizeAxes = Axes.Both, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, Padding = new MarginPadding(vertical_padding), Direction = FillDirection.Vertical, Children = new Drawable[] @@ -137,8 +137,8 @@ namespace osu.Game.Tournament.Components Texture = textures.Get($"mods/{mods}"), Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, - Margin = new MarginPadding(20), - Scale = new Vector2(0.5f) + Margin = new MarginPadding(10), + Scale = new Vector2(0.8f) }); } } diff --git a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs index f4fd27784c..8eb1c98ba0 100644 --- a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs +++ b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs @@ -9,7 +9,6 @@ using osu.Game.Online.Chat; using osu.Game.Overlays.Chat; using osu.Game.Tournament.IPC; using osu.Game.Tournament.Models; -using osuTK; namespace osu.Game.Tournament.Components { @@ -22,7 +21,7 @@ namespace osu.Game.Tournament.Components public TournamentMatchChatDisplay() { RelativeSizeAxes = Axes.X; - Size = new Vector2(0.5f, 142); + Height = 144; Anchor = Anchor.BottomLeft; Origin = Anchor.BottomLeft; diff --git a/osu.Game.Tournament/Screens/BeatmapInfoScreen.cs b/osu.Game.Tournament/Screens/BeatmapInfoScreen.cs index fccd35ca9e..0a3163ef43 100644 --- a/osu.Game.Tournament/Screens/BeatmapInfoScreen.cs +++ b/osu.Game.Tournament/Screens/BeatmapInfoScreen.cs @@ -21,6 +21,7 @@ namespace osu.Game.Tournament.Screens { Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, + Depth = float.MinValue, }); } diff --git a/osu.Game.Tournament/TournamentSceneManager.cs b/osu.Game.Tournament/TournamentSceneManager.cs index 287e25b1fb..ef8d16011d 100644 --- a/osu.Game.Tournament/TournamentSceneManager.cs +++ b/osu.Game.Tournament/TournamentSceneManager.cs @@ -194,9 +194,14 @@ namespace osu.Game.Tournament switch (currentScreen) { - case GameplayScreen _: case MapPoolScreen _: chatContainer.FadeIn(TournamentScreen.FADE_DELAY); + chatContainer.ResizeWidthTo(1, 500, Easing.OutQuint); + break; + + case GameplayScreen _: + chatContainer.FadeIn(TournamentScreen.FADE_DELAY); + chatContainer.ResizeWidthTo(0.5f, 500, Easing.OutQuint); break; default: From 9138bafbeb5012f6af842731fd4383e5edecadaf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 8 Mar 2020 18:40:13 +0900 Subject: [PATCH 0272/2376] Fix alignment of flags on team intro screen --- osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs b/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs index 2daf9d35f3..6c2848897b 100644 --- a/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs +++ b/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs @@ -48,7 +48,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro return; } - const float y_flag_offset = 288; + const float y_flag_offset = 292; const float y_offset = 460; @@ -60,11 +60,11 @@ namespace osu.Game.Tournament.Screens.TeamIntro }, new DrawableTeamFlag(match.NewValue.Team1.Value) { - Position = new Vector2(160, y_flag_offset), + Position = new Vector2(165, y_flag_offset), }, new DrawableTeamWithPlayers(match.NewValue.Team1.Value, TeamColour.Red) { - Position = new Vector2(160, y_offset), + Position = new Vector2(165, y_offset), }, new DrawableTeamFlag(match.NewValue.Team2.Value) { From 2fe32b7d2b3b584335101c8036a32cbeb108d542 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 8 Mar 2020 19:41:22 +0900 Subject: [PATCH 0273/2376] Remove LadderInfo requirement in DrawableMatchTeam --- .../Screens/Ladder/Components/DrawableMatchTeam.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs b/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs index 88d7b95b0c..38e906e07b 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs @@ -37,11 +37,13 @@ namespace osu.Game.Tournament.Screens.Ladder.Components private readonly Func isWinner; private LadderEditorScreen ladderEditor; - [Resolved] + [Resolved(canBeNull: true)] private LadderInfo ladderInfo { get; set; } private void setCurrent() { + if (ladderInfo == null) return; + //todo: tournamentgamebase? if (ladderInfo.CurrentMatch.Value != null) ladderInfo.CurrentMatch.Value.Current.Value = false; From cc5cae4db995d6b680022e11b1ba1f272983d3bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 8 Mar 2020 14:08:49 +0100 Subject: [PATCH 0274/2376] Do not transition to result screen --- osu.Game/Tests/Visual/ModPerfectTestScene.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Tests/Visual/ModPerfectTestScene.cs b/osu.Game/Tests/Visual/ModPerfectTestScene.cs index 4bc00425bf..8b8070caf1 100644 --- a/osu.Game/Tests/Visual/ModPerfectTestScene.cs +++ b/osu.Game/Tests/Visual/ModPerfectTestScene.cs @@ -36,6 +36,11 @@ namespace osu.Game.Tests.Visual private class PerfectModTestPlayer : TestPlayer { + public PerfectModTestPlayer() + : base(showResults: false) + { + } + protected override bool AllowFail => true; public bool CheckFailed(bool failed) From c803de2b499a44a8dde5eba44961ddcea684fb4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 8 Mar 2020 14:18:45 +0100 Subject: [PATCH 0275/2376] Fix player instantiation Since ModTestScene.CreatePlayer would apply mods in addition to instantiating the player, overriding it could lead to mistakenly also overriding the code that was supposed to set up the test via currentTestData. Make ModTestScene.CreatePlayer sealed, which ensures that mod & autoplay changes are applied, and expose ModTestScene.CreateModPlayer instead which has the expected semantics. --- osu.Game/Tests/Visual/ModPerfectTestScene.cs | 2 +- osu.Game/Tests/Visual/ModTestScene.cs | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Tests/Visual/ModPerfectTestScene.cs b/osu.Game/Tests/Visual/ModPerfectTestScene.cs index 8b8070caf1..d6255d2478 100644 --- a/osu.Game/Tests/Visual/ModPerfectTestScene.cs +++ b/osu.Game/Tests/Visual/ModPerfectTestScene.cs @@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual PassCondition = () => ((PerfectModTestPlayer)Player).CheckFailed(shouldMiss && testCaseData.FailOnMiss) }); - protected override TestPlayer CreatePlayer(Ruleset ruleset) => new PerfectModTestPlayer(); + protected override TestPlayer CreateModPlayer(Ruleset ruleset) => new PerfectModTestPlayer(); private class PerfectModTestPlayer : TestPlayer { diff --git a/osu.Game/Tests/Visual/ModTestScene.cs b/osu.Game/Tests/Visual/ModTestScene.cs index eb418304d9..8b41fb5075 100644 --- a/osu.Game/Tests/Visual/ModTestScene.cs +++ b/osu.Game/Tests/Visual/ModTestScene.cs @@ -46,7 +46,7 @@ namespace osu.Game.Tests.Visual protected sealed override IBeatmap CreateBeatmap(RulesetInfo ruleset) => currentTestData?.Beatmap ?? base.CreateBeatmap(ruleset); - protected override TestPlayer CreatePlayer(Ruleset ruleset) + protected sealed override TestPlayer CreatePlayer(Ruleset ruleset) { var mods = new List(SelectedMods.Value); @@ -57,9 +57,11 @@ namespace osu.Game.Tests.Visual SelectedMods.Value = mods; - return new ModTestPlayer(AllowFail); + return CreateModPlayer(ruleset); } + protected virtual TestPlayer CreateModPlayer(Ruleset ruleset) => new ModTestPlayer(AllowFail); + protected class ModTestPlayer : TestPlayer { protected override bool AllowFail { get; } From 0953751d242d4b6a850c4dbd9066d0de1aad125a Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sun, 8 Mar 2020 15:51:57 +0100 Subject: [PATCH 0276/2376] Clamp relative position of judgement ticks in range [0;1] --- osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs b/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs index 208bdd17ad..c7f763e9ab 100644 --- a/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs +++ b/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; @@ -224,7 +225,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters , arrow_move_duration, Easing.Out); } - private float getRelativeJudgementPosition(double value) => (float)((value / maxHitWindow) + 1) / 2; + private float getRelativeJudgementPosition(double value) => Math.Clamp((float)((value / maxHitWindow) + 1) / 2, 0, 1); private class JudgementLine : CompositeDrawable { From 414e704d37c456ee10ac333f9e574c17607cd502 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 9 Mar 2020 00:18:28 +0900 Subject: [PATCH 0277/2376] Use existing local function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Bartłomiej Dach --- osu.Game.Tournament/Components/DrawableTeamWithPlayers.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/osu.Game.Tournament/Components/DrawableTeamWithPlayers.cs b/osu.Game.Tournament/Components/DrawableTeamWithPlayers.cs index 0b80bef903..e949bf9881 100644 --- a/osu.Game.Tournament/Components/DrawableTeamWithPlayers.cs +++ b/osu.Game.Tournament/Components/DrawableTeamWithPlayers.cs @@ -46,12 +46,7 @@ namespace osu.Game.Tournament.Components { Direction = FillDirection.Vertical, AutoSizeAxes = Axes.Both, - ChildrenEnumerable = team?.Players.Select(p => new TournamentSpriteText - { - Text = p.Username, - Font = OsuFont.Torus.With(size: 24, weight: FontWeight.SemiBold), - Colour = Color4.White, - }).Skip(5) ?? Enumerable.Empty() + ChildrenEnumerable = team?.Players.Select(createPlayerText).Skip(5) ?? Enumerable.Empty() }, } }, From 61297847a74e7412da247a5223e1e01b57eb68c8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 9 Mar 2020 01:21:37 +0900 Subject: [PATCH 0278/2376] Fix compilation failure --- osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs b/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs index ce17c392d0..01be91dfe5 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs @@ -141,7 +141,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components currentTeamScore.BindTo(score); } - private void scoreChanged(ValueChangedEvent score) => counter.CountStars = score.NewValue ?? 0; + private void scoreChanged(ValueChangedEvent score) => counter.Current = score.NewValue ?? 0; } private class TeamDisplay : DrawableTournamentTeam From c2fbc85e7706a90c715096fe9cc722662d97b287 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 9 Mar 2020 01:26:19 +0900 Subject: [PATCH 0279/2376] Split out test scene for StarCounter --- .../Visual/Gameplay/TestSceneScoreCounter.cs | 37 ------------ .../Visual/Gameplay/TestSceneStarCounter.cs | 57 +++++++++++++++++++ 2 files changed, 57 insertions(+), 37 deletions(-) create mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneStarCounter.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs index 88bb83b446..030d420ec0 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs @@ -3,9 +3,6 @@ using NUnit.Framework; using osu.Framework.Graphics; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Utils; -using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Screens.Play.HUD; using osuTK; @@ -45,32 +42,12 @@ namespace osu.Game.Tests.Visual.Gameplay }; Add(accuracyCounter); - StarCounter stars = new StarCounter - { - Origin = Anchor.BottomLeft, - Anchor = Anchor.BottomLeft, - Position = new Vector2(20, -160), - Current = 5, - }; - Add(stars); - - SpriteText starsLabel = new OsuSpriteText - { - Origin = Anchor.BottomLeft, - Anchor = Anchor.BottomLeft, - Position = new Vector2(20, -190), - Text = stars.Current.ToString("0.00"), - }; - Add(starsLabel); - AddStep(@"Reset all", delegate { score.Current.Value = 0; comboCounter.Current.Value = 0; numerator = denominator = 0; accuracyCounter.SetFraction(0, 0); - stars.Current = 0; - starsLabel.Text = stars.Current.ToString("0.00"); }); AddStep(@"Hit! :D", delegate @@ -88,20 +65,6 @@ namespace osu.Game.Tests.Visual.Gameplay denominator++; accuracyCounter.SetFraction(numerator, denominator); }); - - AddStep(@"Alter stars", delegate - { - stars.Current = RNG.NextSingle() * (stars.StarCount + 1); - starsLabel.Text = stars.Current.ToString("0.00"); - }); - - AddStep(@"Stop counters", delegate - { - score.StopRolling(); - comboCounter.StopRolling(); - accuracyCounter.StopRolling(); - stars.StopAnimation(); - }); } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStarCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStarCounter.cs new file mode 100644 index 0000000000..709e71d195 --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStarCounter.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 NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Utils; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osuTK; + +namespace osu.Game.Tests.Visual.Gameplay +{ + [TestFixture] + public class TestSceneStarCounter : OsuTestScene + { + public TestSceneStarCounter() + { + StarCounter stars = new StarCounter + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Current = 5, + }; + + Add(stars); + + SpriteText starsLabel = new OsuSpriteText + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Scale = new Vector2(2), + Y = 50, + Text = stars.Current.ToString("0.00"), + }; + + Add(starsLabel); + + AddRepeatStep(@"random value", delegate + { + stars.Current = RNG.NextSingle() * (stars.StarCount + 1); + starsLabel.Text = stars.Current.ToString("0.00"); + }, 10); + + AddStep(@"Stop animation", delegate + { + stars.StopAnimation(); + }); + + AddStep(@"Reset", delegate + { + stars.Current = 0; + starsLabel.Text = stars.Current.ToString("0.00"); + }); + } + } +} From 22dd93a4f6a0ef4367e0ee0591b672659d35ad13 Mon Sep 17 00:00:00 2001 From: voidedWarranties Date: Sun, 8 Mar 2020 14:02:39 -0700 Subject: [PATCH 0280/2376] Code quality, read position offsets --- osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs | 4 +++- osu.Game/Beatmaps/Legacy/LegacyStoryLayer.cs | 5 +++-- osu.Game/Migrations/OsuDbContextModelSnapshot.cs | 2 -- osu.Game/Screens/Play/GameplayClockContainer.cs | 12 +++++------- .../Storyboards/Drawables/DrawableStoryboardVideo.cs | 4 +++- osu.Game/Storyboards/Storyboard.cs | 7 +++++-- osu.Game/Storyboards/StoryboardVideo.cs | 8 +++++++- 7 files changed, 26 insertions(+), 16 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs index b44d4947d4..0358ec2e42 100644 --- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs @@ -92,8 +92,10 @@ namespace osu.Game.Beatmaps.Formats { var offset = Parsing.ParseInt(split[1]); var filename = CleanFilename(split[2]); + var xOffset = split.Length > 3 ? Parsing.ParseInt(split[3]) : 0; + var yOffset = split.Length > 4 ? Parsing.ParseInt(split[4]) : 0; - storyboard.GetLayer("Video").Add(new StoryboardVideo(filename, offset)); + storyboard.GetLayer(LegacyStoryLayer.Video).Add(new StoryboardVideo(filename, offset, xOffset, yOffset)); break; } diff --git a/osu.Game/Beatmaps/Legacy/LegacyStoryLayer.cs b/osu.Game/Beatmaps/Legacy/LegacyStoryLayer.cs index 5237445640..c1329921ec 100644 --- a/osu.Game/Beatmaps/Legacy/LegacyStoryLayer.cs +++ b/osu.Game/Beatmaps/Legacy/LegacyStoryLayer.cs @@ -3,11 +3,12 @@ namespace osu.Game.Beatmaps.Legacy { - internal enum LegacyStoryLayer + public enum LegacyStoryLayer { Background = 0, Fail = 1, Pass = 2, - Foreground = 3 + Foreground = 3, + Video = 4 } } diff --git a/osu.Game/Migrations/OsuDbContextModelSnapshot.cs b/osu.Game/Migrations/OsuDbContextModelSnapshot.cs index 6f91688ddb..bc4fc3342d 100644 --- a/osu.Game/Migrations/OsuDbContextModelSnapshot.cs +++ b/osu.Game/Migrations/OsuDbContextModelSnapshot.cs @@ -141,8 +141,6 @@ namespace osu.Game.Migrations b.Property("VideoFile"); - b.Property("VideoOffset"); - b.HasKey("ID"); b.ToTable("BeatmapMetadata"); diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index ac0a4bcadc..87b0c196d3 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -14,6 +14,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Timing; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Legacy; using osu.Game.Configuration; using osu.Game.Rulesets.Mods; @@ -117,15 +118,12 @@ namespace osu.Game.Screens.Play startTime = Math.Min(startTime, firstHitObjectTime - beatmap.BeatmapInfo.AudioLeadIn); // some beatmaps have no AudioLeadIn but the video starts before the first object - var videoLayer = beatmap.Storyboard.GetLayer("Video"); + var videoLayer = beatmap.Storyboard.GetLayer(LegacyStoryLayer.Video); - if (videoLayer.Elements.Any()) - { - var videoOffset = videoLayer.Elements.First().StartTime; + var videoOffset = videoLayer.Elements.SingleOrDefault()?.StartTime; - if (videoOffset != 0) - startTime = Math.Min(startTime, videoOffset); - } + if (videoOffset != null) + startTime = Math.Min(startTime, videoOffset.GetValueOrDefault()); Seek(startTime); diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs index ef14ccd4d7..b46150785b 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Video; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Screens.Play; +using osuTK; namespace osu.Game.Storyboards.Drawables { @@ -57,7 +58,8 @@ namespace osu.Game.Storyboards.Drawables Anchor = Anchor.Centre, Origin = Anchor.Centre, AlwaysPresent = true, - Alpha = 0 + Alpha = 0, + Position = new Vector2(Video.XOffset, Video.YOffset) }); videoClock = new ManualClock(); diff --git a/osu.Game/Storyboards/Storyboard.cs b/osu.Game/Storyboards/Storyboard.cs index e58c422c6d..fac489838e 100644 --- a/osu.Game/Storyboards/Storyboard.cs +++ b/osu.Game/Storyboards/Storyboard.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Legacy; using osu.Game.Storyboards.Drawables; using System.Collections.Generic; using System.Linq; @@ -28,6 +29,8 @@ namespace osu.Game.Storyboards layers.Add("Foreground", new StoryboardLayer("Foreground", 0)); } + public StoryboardLayer GetLayer(LegacyStoryLayer layer) => GetLayer(layer.ToString()); + public StoryboardLayer GetLayer(string name) { if (!layers.TryGetValue(name, out var layer)) @@ -47,14 +50,14 @@ namespace osu.Game.Storyboards if (backgroundPath == null) return false; - return GetLayer("Background").Elements.Any(e => e.Path.ToLowerInvariant() == backgroundPath); + return GetLayer(LegacyStoryLayer.Background).Elements.Any(e => e.Path.ToLowerInvariant() == backgroundPath); } } public DrawableStoryboard CreateDrawable(WorkingBeatmap working = null) { var drawable = new DrawableStoryboard(this); - drawable.Width = drawable.Height * (BeatmapInfo.WidescreenStoryboard || GetLayer("Video").Elements.Any() ? 16 / 9f : 4 / 3f); + drawable.Width = drawable.Height * (BeatmapInfo.WidescreenStoryboard || GetLayer(LegacyStoryLayer.Video).Elements.Any() ? 16 / 9f : 4 / 3f); return drawable; } } diff --git a/osu.Game/Storyboards/StoryboardVideo.cs b/osu.Game/Storyboards/StoryboardVideo.cs index 4652e45852..7d22b8f7c9 100644 --- a/osu.Game/Storyboards/StoryboardVideo.cs +++ b/osu.Game/Storyboards/StoryboardVideo.cs @@ -14,10 +14,16 @@ namespace osu.Game.Storyboards public double StartTime { get; } - public StoryboardVideo(string path, int offset) + public int XOffset { get; } + + public int YOffset { get; } + + public StoryboardVideo(string path, int offset, int xOffset, int yOffset) { Path = path; StartTime = offset; + XOffset = xOffset; + YOffset = yOffset; } public Drawable CreateDrawable() => new DrawableStoryboardVideo(this); From 4624582703724ef12fabd3410d5fcaf6abcd63bb Mon Sep 17 00:00:00 2001 From: voidedWarranties Date: Sun, 8 Mar 2020 14:40:36 -0700 Subject: [PATCH 0281/2376] Revert position offset change for separate pull --- osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs | 4 +--- osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs | 4 +--- osu.Game/Storyboards/StoryboardVideo.cs | 8 +------- 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs index 0358ec2e42..be82721a76 100644 --- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs @@ -92,10 +92,8 @@ namespace osu.Game.Beatmaps.Formats { var offset = Parsing.ParseInt(split[1]); var filename = CleanFilename(split[2]); - var xOffset = split.Length > 3 ? Parsing.ParseInt(split[3]) : 0; - var yOffset = split.Length > 4 ? Parsing.ParseInt(split[4]) : 0; - storyboard.GetLayer(LegacyStoryLayer.Video).Add(new StoryboardVideo(filename, offset, xOffset, yOffset)); + storyboard.GetLayer(LegacyStoryLayer.Video).Add(new StoryboardVideo(filename, offset)); break; } diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs index b46150785b..ef14ccd4d7 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs @@ -11,7 +11,6 @@ using osu.Framework.Graphics.Video; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Screens.Play; -using osuTK; namespace osu.Game.Storyboards.Drawables { @@ -58,8 +57,7 @@ namespace osu.Game.Storyboards.Drawables Anchor = Anchor.Centre, Origin = Anchor.Centre, AlwaysPresent = true, - Alpha = 0, - Position = new Vector2(Video.XOffset, Video.YOffset) + Alpha = 0 }); videoClock = new ManualClock(); diff --git a/osu.Game/Storyboards/StoryboardVideo.cs b/osu.Game/Storyboards/StoryboardVideo.cs index 7d22b8f7c9..4652e45852 100644 --- a/osu.Game/Storyboards/StoryboardVideo.cs +++ b/osu.Game/Storyboards/StoryboardVideo.cs @@ -14,16 +14,10 @@ namespace osu.Game.Storyboards public double StartTime { get; } - public int XOffset { get; } - - public int YOffset { get; } - - public StoryboardVideo(string path, int offset, int xOffset, int yOffset) + public StoryboardVideo(string path, int offset) { Path = path; StartTime = offset; - XOffset = xOffset; - YOffset = yOffset; } public Drawable CreateDrawable() => new DrawableStoryboardVideo(this); From 3903423a37d53b60cd690c1583c968842f2fcdc4 Mon Sep 17 00:00:00 2001 From: Joehu Date: Sun, 8 Mar 2020 19:43:53 -0700 Subject: [PATCH 0282/2376] Fix textbox characters not animating when typing/backspacing --- osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs | 6 +++++- osu.Game/Graphics/UserInterface/OsuTextBox.cs | 6 +++++- osu.Game/Overlays/Comments/CommentEditor.cs | 6 +++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs b/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs index e7699e5255..0c82a869f8 100644 --- a/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs @@ -18,7 +18,11 @@ namespace osu.Game.Graphics.UserInterface { public class OsuPasswordTextBox : OsuTextBox, ISuppressKeyEventLogging { - protected override Drawable GetDrawableCharacter(char c) => new PasswordMaskChar(CalculatedTextSize); + protected override Drawable GetDrawableCharacter(char c) => new FallingDownContainer + { + AutoSizeAxes = Axes.Both, + Child = new PasswordMaskChar(CalculatedTextSize), + }; protected override bool AllowClipboardExport => false; diff --git a/osu.Game/Graphics/UserInterface/OsuTextBox.cs b/osu.Game/Graphics/UserInterface/OsuTextBox.cs index 4abbf8db57..6f440d8138 100644 --- a/osu.Game/Graphics/UserInterface/OsuTextBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuTextBox.cs @@ -63,7 +63,11 @@ namespace osu.Game.Graphics.UserInterface base.OnFocusLost(e); } - protected override Drawable GetDrawableCharacter(char c) => new OsuSpriteText { Text = c.ToString(), Font = OsuFont.GetFont(size: CalculatedTextSize) }; + protected override Drawable GetDrawableCharacter(char c) => new FallingDownContainer + { + AutoSizeAxes = Axes.Both, + Child = new OsuSpriteText { Text = c.ToString(), Font = OsuFont.GetFont(size: CalculatedTextSize) }, + }; protected override Caret CreateCaret() => new OsuCaret { diff --git a/osu.Game/Overlays/Comments/CommentEditor.cs b/osu.Game/Overlays/Comments/CommentEditor.cs index 2fa4cb68f3..7b4bf882dc 100644 --- a/osu.Game/Overlays/Comments/CommentEditor.cs +++ b/osu.Game/Overlays/Comments/CommentEditor.cs @@ -158,7 +158,11 @@ namespace osu.Game.Overlays.Comments Font = OsuFont.GetFont(weight: FontWeight.Regular), }; - protected override Drawable GetDrawableCharacter(char c) => new OsuSpriteText { Text = c.ToString(), Font = OsuFont.GetFont(size: CalculatedTextSize) }; + protected override Drawable GetDrawableCharacter(char c) => new FallingDownContainer + { + AutoSizeAxes = Axes.Both, + Child = new OsuSpriteText { Text = c.ToString(), Font = OsuFont.GetFont(size: CalculatedTextSize) }, + }; } private class CommitButton : LoadingButton From b61e56cda519ffa4cb1c284e3134e36077be8a52 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 9 Mar 2020 13:29:17 +0900 Subject: [PATCH 0283/2376] Resolve post-merge issue --- osu.Game.Tournament/Screens/Gameplay/Components/TeamScore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TeamScore.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScore.cs index 608d98a16a..04fee8cd7d 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/TeamScore.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScore.cs @@ -33,6 +33,6 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components currentTeamScore.BindTo(score); } - private void scoreChanged(ValueChangedEvent score) => counter.CountStars = score.NewValue ?? 0; + private void scoreChanged(ValueChangedEvent score) => counter.Current = score.NewValue ?? 0; } } From 832e64cc958b7c427bc1cf0d58d0291d1990168a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 9 Mar 2020 14:57:19 +0900 Subject: [PATCH 0284/2376] Fix test failures due to null current match --- .../Screens/Gameplay/Components/MatchHeader.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs b/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs index 7e9d0178d8..69a68c946b 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs @@ -92,17 +92,20 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components [BackgroundDependencyLoader] private void load(LadderInfo ladder) { - currentMatch.BindValueChanged(matchChanged); currentMatch.BindTo(ladder.CurrentMatch); + currentMatch.BindValueChanged(matchChanged, true); } private void matchChanged(ValueChangedEvent match) { currentTeamScore.UnbindBindings(); - currentTeamScore.BindTo(teamColour == TeamColour.Red ? match.NewValue.Team1Score : match.NewValue.Team2Score); - currentTeam.UnbindBindings(); - currentTeam.BindTo(teamColour == TeamColour.Red ? match.NewValue.Team1 : match.NewValue.Team2); + + if (match.NewValue != null) + { + currentTeamScore.BindTo(teamColour == TeamColour.Red ? match.NewValue.Team1Score : match.NewValue.Team2Score); + currentTeam.BindTo(teamColour == TeamColour.Red ? match.NewValue.Team1 : match.NewValue.Team2); + } // team may change to same team, which means score is not in a good state. // thus we handle this manually. @@ -131,7 +134,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components { InternalChildren = new Drawable[] { - teamDisplay = new TeamDisplay(team, teamColour, currentTeamScore, currentMatch.Value.PointsToWin), + teamDisplay = new TeamDisplay(team, teamColour, currentTeamScore, currentMatch.Value?.PointsToWin ?? 0), }; } } From 33f457d663bf6e433664655935dadf764691ea65 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 9 Mar 2020 15:08:24 +0900 Subject: [PATCH 0285/2376] Fix layout issues with TournamentBeatmapPanel --- osu.Game.Tournament/Components/TournamentBeatmapPanel.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs index e09af06c89..8fa35003e7 100644 --- a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs +++ b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs @@ -50,8 +50,6 @@ namespace osu.Game.Tournament.Components currentMatch.BindValueChanged(matchChanged); currentMatch.BindTo(ladder.CurrentMatch); - CornerRadius = HEIGHT / 2; - CornerExponent = 2; Masking = true; AddRangeInternal(new Drawable[] @@ -72,14 +70,12 @@ namespace osu.Game.Tournament.Components AutoSizeAxes = Axes.Both, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Padding = new MarginPadding(vertical_padding), + Padding = new MarginPadding(15), Direction = FillDirection.Vertical, Children = new Drawable[] { new TournamentSpriteText { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, Text = new LocalisedString(( $"{Beatmap.Metadata.ArtistUnicode ?? Beatmap.Metadata.Artist} - {Beatmap.Metadata.TitleUnicode ?? Beatmap.Metadata.Title}", $"{Beatmap.Metadata.Artist} - {Beatmap.Metadata.Title}")), @@ -88,9 +84,6 @@ namespace osu.Game.Tournament.Components new FillFlowContainer { AutoSizeAxes = Axes.Both, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Padding = new MarginPadding(vertical_padding), Direction = FillDirection.Horizontal, Children = new Drawable[] { From 48c46efdd72ea4a8a34243ebbe3f3f56d37d3b05 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 9 Mar 2020 15:09:34 +0900 Subject: [PATCH 0286/2376] Remove rogue newline --- osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs index 1e20687a87..34fa7a4997 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs @@ -35,4 +35,3 @@ namespace osu.Game.Tournament.Tests.Screens } } } - From 6421f28ac70964d3438aae62cf0df9888c4ea4f0 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 9 Mar 2020 19:07:44 +0900 Subject: [PATCH 0287/2376] Fix nullref --- osu.Game.Tournament/Components/TourneyVideo.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Components/TourneyVideo.cs b/osu.Game.Tournament/Components/TourneyVideo.cs index 687acaa6d5..43088d6b92 100644 --- a/osu.Game.Tournament/Components/TourneyVideo.cs +++ b/osu.Game.Tournament/Components/TourneyVideo.cs @@ -64,7 +64,11 @@ namespace osu.Game.Tournament.Components } } - public void Reset() => manualClock.CurrentTime = 0; + public void Reset() + { + if (manualClock != null) + manualClock.CurrentTime = 0; + } protected override void Update() { From 5aa99d8b34aff0c44911f852e2a673135a0fd416 Mon Sep 17 00:00:00 2001 From: voidedWarranties Date: Mon, 9 Mar 2020 16:04:23 -0700 Subject: [PATCH 0288/2376] Hide background image when video is present --- osu.Game/Storyboards/Storyboard.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Storyboards/Storyboard.cs b/osu.Game/Storyboards/Storyboard.cs index fac489838e..64ad0208c9 100644 --- a/osu.Game/Storyboards/Storyboard.cs +++ b/osu.Game/Storyboards/Storyboard.cs @@ -50,6 +50,9 @@ namespace osu.Game.Storyboards if (backgroundPath == null) return false; + if (GetLayer(LegacyStoryLayer.Video).Elements.Any()) + return true; + return GetLayer(LegacyStoryLayer.Background).Elements.Any(e => e.Path.ToLowerInvariant() == backgroundPath); } } From 19ce2d643e39726119d570d008e2af277deebd58 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 10 Mar 2020 09:51:30 +0900 Subject: [PATCH 0289/2376] Remove unused using --- .../Screens/Ladder/Components/DrawableTournamentMatch.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs b/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs index 18ff8abf61..655beb4bdd 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; -using osu.Game.Graphics; using osu.Game.Tournament.Models; using osuTK; using osuTK.Graphics; From e7f1f0f38b50946a147828699265e35c6ad9ccaa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Mar 2020 10:07:47 +0900 Subject: [PATCH 0290/2376] Fix hyperdash not initiating correctly when juice streams are present --- .../TestSceneHyperDash.cs | 52 ++++++++++++++++--- osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 4 ++ 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs index da36673930..7a7c3f4103 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs @@ -1,9 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Linq; using NUnit.Framework; +using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Catch.UI; using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Catch.Tests @@ -22,8 +26,17 @@ namespace osu.Game.Rulesets.Catch.Tests public void TestHyperDash() { AddAssert("First note is hyperdash", () => Beatmap.Value.Beatmap.HitObjects[0] is Fruit f && f.HyperDash); + AddUntilStep("wait for left hyperdash", () => getCatcher().Scale.X < 0 && getCatcher().HyperDashing); + + for (int i = 0; i < 2; i++) + { + AddUntilStep("wait for right hyperdash", () => getCatcher().Scale.X > 0 && getCatcher().HyperDashing); + AddUntilStep("wait for left hyperdash", () => getCatcher().Scale.X < 0 && getCatcher().HyperDashing); + } } + private CatcherArea.Catcher getCatcher() => Player.ChildrenOfType().First().MovableCatcher; + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) { var beatmap = new Beatmap @@ -35,17 +48,40 @@ namespace osu.Game.Rulesets.Catch.Tests } }; - // Should produce a hyper-dash - beatmap.HitObjects.Add(new Fruit { StartTime = 816, X = 308 / 512f, NewCombo = true }); - beatmap.HitObjects.Add(new Fruit { StartTime = 1008, X = 56 / 512f, }); + // Should produce a hyper-dash (edge case test) + beatmap.HitObjects.Add(new Fruit { StartTime = 1816, X = 308 / 512f, NewCombo = true }); + beatmap.HitObjects.Add(new JuiceStream { StartTime = 2008, X = 56 / 512f, }); - for (int i = 0; i < 512; i++) - { - if (i % 5 < 3) - beatmap.HitObjects.Add(new Fruit { X = i % 10 < 5 ? 0.02f : 0.98f, StartTime = 2000 + i * 100, NewCombo = i % 8 == 0 }); - } + double startTime = 3000; + + const float left_x = 0.02f; + const float right_x = 0.98f; + + createObjects(() => new Fruit(), left_x); + createObjects(() => new JuiceStream(), right_x); + createObjects(() => new JuiceStream(), left_x); + createObjects(() => new Fruit(), right_x); + createObjects(() => new Fruit(), left_x); + createObjects(() => new Fruit(), right_x); + createObjects(() => new JuiceStream(), left_x); return beatmap; + + void createObjects(Func createObject, float x) + { + const float spacing = 140; + + for (int i = 0; i < 3; i++) + { + var hitObject = createObject(); + hitObject.X = x; + hitObject.StartTime = startTime + i * spacing; + + beatmap.HitObjects.Add(hitObject); + } + + startTime += 700; + } } } } diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index b977d46611..71228f1c07 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -270,6 +270,10 @@ namespace osu.Game.Rulesets.Catch.UI catchObjectPosition >= catcherPosition - halfCatchWidth && catchObjectPosition <= catcherPosition + halfCatchWidth; + // only update hyperdash state if we are catching a fruit. + // exceptions are Droplets and JuiceStreams. + if (!(fruit is Fruit)) return validCatch; + if (validCatch && fruit.HyperDash) { var target = fruit.HyperDashTarget; From 059af2a9866ff15d6c1200b1eecdc6b597267b5c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Mar 2020 10:43:06 +0900 Subject: [PATCH 0291/2376] 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 97f7a7edb1..6a8e66ee6a 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 855bda3679..cc1ab654ab 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -23,7 +23,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index e2c4c09047..04b688cfa3 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + @@ -79,7 +79,7 @@ - + From e6858bf1304f0a96e1f3ead46640b0b83fc72d4e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 10 Mar 2020 11:58:33 +0900 Subject: [PATCH 0292/2376] Fix crashes on some storyboards --- .../Formats/LegacyStoryboardDecoder.cs | 81 +++++++++---------- 1 file changed, 40 insertions(+), 41 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs index 6569f76b2d..c81f933bca 100644 --- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.IO; using osuTK; using osuTK.Graphics; @@ -93,8 +92,8 @@ namespace osu.Game.Beatmaps.Formats var layer = parseLayer(split[1]); var origin = parseOrigin(split[2]); var path = CleanFilename(split[3]); - var x = float.Parse(split[4], NumberFormatInfo.InvariantInfo); - var y = float.Parse(split[5], NumberFormatInfo.InvariantInfo); + var x = Parsing.ParseFloat(split[4], Parsing.MAX_COORDINATE_VALUE); + var y = Parsing.ParseFloat(split[5], Parsing.MAX_COORDINATE_VALUE); storyboardSprite = new StoryboardSprite(path, origin, new Vector2(x, y)); storyboard.GetLayer(layer).Add(storyboardSprite); break; @@ -105,10 +104,10 @@ namespace osu.Game.Beatmaps.Formats var layer = parseLayer(split[1]); var origin = parseOrigin(split[2]); var path = CleanFilename(split[3]); - var x = float.Parse(split[4], NumberFormatInfo.InvariantInfo); - var y = float.Parse(split[5], NumberFormatInfo.InvariantInfo); - var frameCount = int.Parse(split[6]); - var frameDelay = double.Parse(split[7], NumberFormatInfo.InvariantInfo); + var x = Parsing.ParseFloat(split[4], Parsing.MAX_COORDINATE_VALUE); + var y = Parsing.ParseFloat(split[5], Parsing.MAX_COORDINATE_VALUE); + var frameCount = Parsing.ParseInt(split[6]); + var frameDelay = Parsing.ParseDouble(split[7]); var loopType = split.Length > 8 ? (AnimationLoopType)Enum.Parse(typeof(AnimationLoopType), split[8]) : AnimationLoopType.LoopForever; storyboardSprite = new StoryboardAnimation(path, origin, new Vector2(x, y), frameCount, frameDelay, loopType); storyboard.GetLayer(layer).Add(storyboardSprite); @@ -117,10 +116,10 @@ namespace osu.Game.Beatmaps.Formats case LegacyEventType.Sample: { - var time = double.Parse(split[1], CultureInfo.InvariantCulture); + var time = Parsing.ParseDouble(split[1]); var layer = parseLayer(split[2]); var path = CleanFilename(split[3]); - var volume = split.Length > 4 ? float.Parse(split[4], CultureInfo.InvariantCulture) : 100; + var volume = split.Length > 4 ? Parsing.ParseFloat(split[4]) : 100; storyboard.GetLayer(layer).Add(new StoryboardSampleInfo(path, time, (int)volume)); break; } @@ -138,17 +137,17 @@ namespace osu.Game.Beatmaps.Formats case "T": { var triggerName = split[1]; - var startTime = split.Length > 2 ? double.Parse(split[2], CultureInfo.InvariantCulture) : double.MinValue; - var endTime = split.Length > 3 ? double.Parse(split[3], CultureInfo.InvariantCulture) : double.MaxValue; - var groupNumber = split.Length > 4 ? int.Parse(split[4]) : 0; + var startTime = split.Length > 2 ? Parsing.ParseDouble(split[2]) : double.MinValue; + var endTime = split.Length > 3 ? Parsing.ParseDouble(split[3]) : double.MaxValue; + var groupNumber = split.Length > 4 ? Parsing.ParseInt(split[4]) : 0; timelineGroup = storyboardSprite?.AddTrigger(triggerName, startTime, endTime, groupNumber); break; } case "L": { - var startTime = double.Parse(split[1], CultureInfo.InvariantCulture); - var loopCount = int.Parse(split[2]); + var startTime = Parsing.ParseDouble(split[1]); + var loopCount = Parsing.ParseInt(split[2]); timelineGroup = storyboardSprite?.AddLoop(startTime, loopCount); break; } @@ -158,52 +157,52 @@ namespace osu.Game.Beatmaps.Formats if (string.IsNullOrEmpty(split[3])) split[3] = split[2]; - var easing = (Easing)int.Parse(split[1]); - var startTime = double.Parse(split[2], CultureInfo.InvariantCulture); - var endTime = double.Parse(split[3], CultureInfo.InvariantCulture); + var easing = (Easing)Parsing.ParseInt(split[1]); + var startTime = Parsing.ParseDouble(split[2]); + var endTime = Parsing.ParseDouble(split[3]); switch (commandType) { case "F": { - var startValue = float.Parse(split[4], CultureInfo.InvariantCulture); - var endValue = split.Length > 5 ? float.Parse(split[5], CultureInfo.InvariantCulture) : startValue; + var startValue = Parsing.ParseFloat(split[4]); + var endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue; timelineGroup?.Alpha.Add(easing, startTime, endTime, startValue, endValue); break; } case "S": { - var startValue = float.Parse(split[4], CultureInfo.InvariantCulture); - var endValue = split.Length > 5 ? float.Parse(split[5], CultureInfo.InvariantCulture) : startValue; + var startValue = Parsing.ParseFloat(split[4]); + var endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue; timelineGroup?.Scale.Add(easing, startTime, endTime, startValue, endValue); break; } case "V": { - var startX = float.Parse(split[4], CultureInfo.InvariantCulture); - var startY = float.Parse(split[5], CultureInfo.InvariantCulture); - var endX = split.Length > 6 ? float.Parse(split[6], CultureInfo.InvariantCulture) : startX; - var endY = split.Length > 7 ? float.Parse(split[7], CultureInfo.InvariantCulture) : startY; + var startX = Parsing.ParseFloat(split[4]); + var startY = Parsing.ParseFloat(split[5]); + var endX = split.Length > 6 ? Parsing.ParseFloat(split[6]) : startX; + var endY = split.Length > 7 ? Parsing.ParseFloat(split[7]) : startY; timelineGroup?.VectorScale.Add(easing, startTime, endTime, new Vector2(startX, startY), new Vector2(endX, endY)); break; } case "R": { - var startValue = float.Parse(split[4], CultureInfo.InvariantCulture); - var endValue = split.Length > 5 ? float.Parse(split[5], CultureInfo.InvariantCulture) : startValue; + var startValue = Parsing.ParseFloat(split[4]); + var endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue; timelineGroup?.Rotation.Add(easing, startTime, endTime, MathUtils.RadiansToDegrees(startValue), MathUtils.RadiansToDegrees(endValue)); break; } case "M": { - var startX = float.Parse(split[4], CultureInfo.InvariantCulture); - var startY = float.Parse(split[5], CultureInfo.InvariantCulture); - var endX = split.Length > 6 ? float.Parse(split[6], CultureInfo.InvariantCulture) : startX; - var endY = split.Length > 7 ? float.Parse(split[7], CultureInfo.InvariantCulture) : startY; + var startX = Parsing.ParseFloat(split[4]); + var startY = Parsing.ParseFloat(split[5]); + var endX = split.Length > 6 ? Parsing.ParseFloat(split[6]) : startX; + var endY = split.Length > 7 ? Parsing.ParseFloat(split[7]) : startY; timelineGroup?.X.Add(easing, startTime, endTime, startX, endX); timelineGroup?.Y.Add(easing, startTime, endTime, startY, endY); break; @@ -211,28 +210,28 @@ namespace osu.Game.Beatmaps.Formats case "MX": { - var startValue = float.Parse(split[4], CultureInfo.InvariantCulture); - var endValue = split.Length > 5 ? float.Parse(split[5], CultureInfo.InvariantCulture) : startValue; + var startValue = Parsing.ParseFloat(split[4]); + var endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue; timelineGroup?.X.Add(easing, startTime, endTime, startValue, endValue); break; } case "MY": { - var startValue = float.Parse(split[4], CultureInfo.InvariantCulture); - var endValue = split.Length > 5 ? float.Parse(split[5], CultureInfo.InvariantCulture) : startValue; + var startValue = Parsing.ParseFloat(split[4]); + var endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue; timelineGroup?.Y.Add(easing, startTime, endTime, startValue, endValue); break; } case "C": { - var startRed = float.Parse(split[4], CultureInfo.InvariantCulture); - var startGreen = float.Parse(split[5], CultureInfo.InvariantCulture); - var startBlue = float.Parse(split[6], CultureInfo.InvariantCulture); - var endRed = split.Length > 7 ? float.Parse(split[7], CultureInfo.InvariantCulture) : startRed; - var endGreen = split.Length > 8 ? float.Parse(split[8], CultureInfo.InvariantCulture) : startGreen; - var endBlue = split.Length > 9 ? float.Parse(split[9], CultureInfo.InvariantCulture) : startBlue; + var startRed = Parsing.ParseFloat(split[4]); + var startGreen = Parsing.ParseFloat(split[5]); + var startBlue = Parsing.ParseFloat(split[6]); + var endRed = split.Length > 7 ? Parsing.ParseFloat(split[7]) : startRed; + var endGreen = split.Length > 8 ? Parsing.ParseFloat(split[8]) : startGreen; + var endBlue = split.Length > 9 ? Parsing.ParseFloat(split[9]) : startBlue; timelineGroup?.Colour.Add(easing, startTime, endTime, new Color4(startRed / 255f, startGreen / 255f, startBlue / 255f, 1), new Color4(endRed / 255f, endGreen / 255f, endBlue / 255f, 1)); From 7a9c85d69da0a826ac03fd78fb615e146a0eef96 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 10 Mar 2020 12:21:40 +0900 Subject: [PATCH 0293/2376] Fix now failing test due to parsing ranges --- osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs | 2 +- osu.Game.Tests/Resources/variable-with-suffix.osb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs index 96ff6b81e3..76b76aa357 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs @@ -99,7 +99,7 @@ namespace osu.Game.Tests.Beatmaps.Formats var storyboard = decoder.Decode(stream); StoryboardLayer background = storyboard.Layers.Single(l => l.Depth == 3); - Assert.AreEqual(123456, ((StoryboardSprite)background.Elements.Single()).InitialPosition.X); + Assert.AreEqual(3456, ((StoryboardSprite)background.Elements.Single()).InitialPosition.X); } } } diff --git a/osu.Game.Tests/Resources/variable-with-suffix.osb b/osu.Game.Tests/Resources/variable-with-suffix.osb index 5c9b46ca98..fd284eb055 100644 --- a/osu.Game.Tests/Resources/variable-with-suffix.osb +++ b/osu.Game.Tests/Resources/variable-with-suffix.osb @@ -1,5 +1,5 @@ [Variables] -$var=1234 +$var=34 [Events] Sprite,Background,TopCentre,"img.jpg",$var56,240 From 0d18ea1d2955dd76832d6d75381bfc9ba0994105 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Mar 2020 11:42:08 +0900 Subject: [PATCH 0294/2376] Add animation and fallback catcher support --- .../special-skin/fruit-catcher-idle-0@2x.png | Bin 0 -> 57593 bytes .../special-skin/fruit-catcher-idle-1@2x.png | Bin 0 -> 57011 bytes .../special-skin/fruit-catcher-idle-2@2x.png | Bin 0 -> 57264 bytes .../special-skin/fruit-catcher-idle-3@2x.png | Bin 0 -> 57077 bytes .../special-skin/fruit-catcher-idle-4@2x.png | Bin 0 -> 56155 bytes .../special-skin/fruit-catcher-idle-5@2x.png | Bin 0 -> 57143 bytes .../special-skin/fruit-catcher-idle.png | Bin 133664 -> 0 bytes .../TestSceneCatcher.cs | 1 + .../CatchSkinComponents.cs | 3 ++- .../Skinning/CatchLegacySkinTransformer.cs | 4 +++ osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 10 +++++--- osu.Game.Rulesets.Catch/UI/CatcherSprite.cs | 24 ++++++++++-------- 12 files changed, 27 insertions(+), 15 deletions(-) create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-0@2x.png create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-1@2x.png create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-2@2x.png create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-3@2x.png create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-4@2x.png create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-5@2x.png delete mode 100755 osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle.png diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-0@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-0@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..786e5cc25ad2e175251acdee06825f936c8c9ea7 GIT binary patch literal 57593 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 /// Add a caught fruit to the catcher's stack. @@ -290,9 +300,25 @@ namespace osu.Game.Rulesets.Catch.UI SetHyperDashState(); } + if (validCatch) + updateState(fruit.Kiai ? CatcherAnimationState.Kiai : CatcherAnimationState.Idle); + else + updateState(CatcherAnimationState.Fail); + return validCatch; } + private void updateState(CatcherAnimationState state) + { + if (currentState == state) + return; + + currentState = state; + updateCatcher(); + } + + private CatcherAnimationState currentState; + private double hyperDashModifier = 1; private int hyperDashDirection; private float hyperDashTargetPosition; diff --git a/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs b/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs index 78020114cd..52eb8d597e 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs @@ -14,9 +14,9 @@ namespace osu.Game.Rulesets.Catch.UI { protected override bool ApplySizeRestrictionsToDefault => true; - public CatcherSprite() - : base(new CatchSkinComponent(CatchSkinComponents.CatcherIdle), _ => - new DefaultCatcherSprite(), confineMode: ConfineMode.ScaleDownToFit) + public CatcherSprite(CatcherAnimationState state) + : base(new CatchSkinComponent(componentFromState(state)), _ => + new DefaultCatcherSprite(state), confineMode: ConfineMode.ScaleDownToFit) { RelativeSizeAxes = Axes.None; Size = new Vector2(CatcherArea.CATCHER_SIZE); @@ -25,12 +25,34 @@ namespace osu.Game.Rulesets.Catch.UI OriginPosition = new Vector2(0.5f, 0.06f) * CatcherArea.CATCHER_SIZE; } + private static CatchSkinComponents componentFromState(CatcherAnimationState state) + { + switch (state) + { + case CatcherAnimationState.Fail: + return CatchSkinComponents.CatcherFail; + + case CatcherAnimationState.Kiai: + return CatchSkinComponents.CatcherKiai; + + default: + return CatchSkinComponents.CatcherIdle; + } + } + private class DefaultCatcherSprite : Sprite { + private readonly CatcherAnimationState state; + + public DefaultCatcherSprite(CatcherAnimationState state) + { + this.state = state; + } + [BackgroundDependencyLoader] private void load(TextureStore textures) { - Texture = textures.Get("Gameplay/catch/fruit-catcher-idle"); + Texture = textures.Get($"Gameplay/catch/fruit-catcher-{state.ToString().ToLower()}"); } } } From 742698acab9ba2034ebae716bcd8cc1634d5a13e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 10 Mar 2020 15:30:24 +0900 Subject: [PATCH 0297/2376] Add notelock implementation --- .../Objects/Drawables/DrawableHitCircle.cs | 2 +- .../Objects/Drawables/DrawableOsuHitObject.cs | 7 +++++ osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 28 +++++++++++++++++-- 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index da1e666aba..3ca2714511 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -118,7 +118,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables var result = HitObject.HitWindows.ResultFor(timeOffset); - if (result == HitResult.None) + if (result == HitResult.None || CheckHittable?.Invoke(this) == false) { Shake(Math.Abs(timeOffset) - HitObject.HitWindows.WindowFor(HitResult.Miss)); return; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index a677cb6a72..82a81040e4 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.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.Rulesets.Objects.Drawables; using osu.Framework.Graphics; using osu.Game.Rulesets.Judgements; @@ -16,6 +17,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables // Must be set to update IsHovered as it's used in relax mdo to detect osu hit objects. public override bool HandlePositionalInput => true; + /// + /// Whether this can be hit. + /// If not-null, this will not receive a judgement until this function returns true. + /// + public Func CheckHittable; + protected DrawableOsuHitObject(OsuHitObject hitObject) : base(hitObject) { diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 6d1ea4bbfc..9eb2786951 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.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.Extensions.IEnumerableExtensions; using osuTK; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -64,7 +65,10 @@ namespace osu.Game.Rulesets.Osu.UI base.Add(h); - followPoints.AddFollowPoints((DrawableOsuHitObject)h); + DrawableOsuHitObject osuHitObject = (DrawableOsuHitObject)h; + osuHitObject.CheckHittable = checkHittable; + + followPoints.AddFollowPoints(osuHitObject); } public override bool Remove(DrawableHitObject h) @@ -72,11 +76,31 @@ namespace osu.Game.Rulesets.Osu.UI bool result = base.Remove(h); if (result) - followPoints.RemoveFollowPoints((DrawableOsuHitObject)h); + { + DrawableOsuHitObject osuHitObject = (DrawableOsuHitObject)h; + osuHitObject.CheckHittable = null; + + followPoints.RemoveFollowPoints(osuHitObject); + } return result; } + private bool checkHittable(DrawableOsuHitObject osuHitObject) + { + var lastObject = HitObjectContainer.AliveObjects.GetPrevious(osuHitObject); + + // Ensure the last object is not alive anymore, in which case always allow the hit. + if (lastObject == null) + return true; + + // Ensure that either the last object has received a judgement or the hit time occurs after the last object's start time. + if (lastObject.Judged || Time.Current > lastObject.HitObject.StartTime) + return true; + + return false; + } + private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) { if (!judgedObject.DisplayResult || !DisplayJudgements.Value) From 678f33eea36615c5a2ac2463ff13fc3c6f50a704 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 10 Mar 2020 15:45:21 +0900 Subject: [PATCH 0298/2376] Add late miss judgements --- ...cs => TestSceneMissHitWindowJudgements.cs} | 49 +++++++++++++++---- 1 file changed, 40 insertions(+), 9 deletions(-) rename osu.Game.Rulesets.Osu.Tests/{TestSceneEarlyMissJudgement.cs => TestSceneMissHitWindowJudgements.cs} (58%) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneEarlyMissJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs similarity index 58% rename from osu.Game.Rulesets.Osu.Tests/TestSceneEarlyMissJudgement.cs rename to osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs index 27a32aa96e..5f3596976d 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneEarlyMissJudgement.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs @@ -8,6 +8,7 @@ using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Replays; +using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Tests.Visual; @@ -16,24 +17,54 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Tests { - public class TestSceneEarlyMissJudgement : ModTestScene + public class TestSceneMissHitWindowJudgements : ModTestScene { - public TestSceneEarlyMissJudgement() + public TestSceneMissHitWindowJudgements() : base(new OsuRuleset()) { } [Test] - public void TestHitCircleEarly() => CreateModTest(new ModTestData + public void TestMissViaEarlyHit() { - Autoplay = false, - Mod = new TestAutoMod(), - Beatmap = new Beatmap + var beatmap = new Beatmap { HitObjects = { new HitCircle { Position = new Vector2(256, 192) } } - }, - PassCondition = () => Player.Results.Count > 0 && Player.Results[0].TimeOffset < 0 && Player.Results[0].Type == HitResult.Miss - }); + }; + + var hitWindows = new OsuHitWindows(); + hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty); + + CreateModTest(new ModTestData + { + Autoplay = false, + Mod = new TestAutoMod(), + Beatmap = new Beatmap + { + HitObjects = { new HitCircle { Position = new Vector2(256, 192) } } + }, + PassCondition = () => Player.Results.Count > 0 && Player.Results[0].TimeOffset < -hitWindows.WindowFor(HitResult.Meh) && Player.Results[0].Type == HitResult.Miss + }); + } + + [Test] + public void TestMissViaNotHitting() + { + var beatmap = new Beatmap + { + HitObjects = { new HitCircle { Position = new Vector2(256, 192) } } + }; + + var hitWindows = new OsuHitWindows(); + hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty); + + CreateModTest(new ModTestData + { + Autoplay = false, + Beatmap = beatmap, + PassCondition = () => Player.Results.Count > 0 && Player.Results[0].TimeOffset >= hitWindows.WindowFor(HitResult.Meh) && Player.Results[0].Type == HitResult.Miss + }); + } private class TestAutoMod : OsuModAutoplay { From 2b33594400dae72c9dc05d428f927353b53fe407 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Mar 2020 15:59:13 +0900 Subject: [PATCH 0299/2376] Add random rotation and scale factors to osu!catch bananas --- .../TestSceneBananaShower.cs | 2 ++ .../Objects/Drawables/DrawableBanana.cs | 17 +++++++++++++++++ osu.Game/Tests/Visual/ScreenTestScene.cs | 5 ++--- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.cs index 20911b8d06..024c4cefb0 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.cs @@ -18,7 +18,9 @@ namespace osu.Game.Rulesets.Catch.Tests public override IReadOnlyList RequiredTypes => new[] { typeof(BananaShower), + typeof(Banana), typeof(DrawableBananaShower), + typeof(DrawableBanana), typeof(CatchRuleset), typeof(DrawableCatchRuleset), diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs index cf7231ebb2..2e7618b8df 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using osu.Framework.Graphics; using osu.Framework.Utils; using osuTK.Graphics; @@ -22,6 +23,22 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables return colour ??= getBananaColour(); } + protected override void UpdateInitialTransforms() + { + base.UpdateInitialTransforms(); + + const float end_scale = 0.6f; + const float random_scale_range = 1.6f; + + ScaleContainer.ScaleTo(HitObject.Scale * (end_scale + random_scale_range * RNG.NextSingle())) + .Then().ScaleTo(HitObject.Scale * end_scale, HitObject.TimePreempt); + + const float random_angle_range = 180; + + ScaleContainer.RotateTo(random_angle_range * (RNG.NextSingle() * 2 - 1)) + .Then().RotateTo(random_angle_range * (RNG.NextSingle() * 2 - 1), HitObject.TimePreempt); + } + private Color4 getBananaColour() { switch (RNG.Next(0, 3)) diff --git a/osu.Game/Tests/Visual/ScreenTestScene.cs b/osu.Game/Tests/Visual/ScreenTestScene.cs index d26aacf2bc..1a6ebed425 100644 --- a/osu.Game/Tests/Visual/ScreenTestScene.cs +++ b/osu.Game/Tests/Visual/ScreenTestScene.cs @@ -38,12 +38,11 @@ namespace osu.Game.Tests.Visual private void addExitAllScreensStep() { - AddUntilStep("exit all screens", () => + AddStep("exit all screens", () => { - if (Stack.CurrentScreen == null) return true; + if (Stack.CurrentScreen == null) return; Stack.Exit(); - return false; }); } } From eab544b49f47d2543ff25d80a281a3531f636f4e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Mar 2020 16:41:08 +0900 Subject: [PATCH 0300/2376] Add afterimage glow when entering hyperdash --- .../TestSceneCatcherArea.cs | 5 ---- .../TestSceneHyperDash.cs | 6 ++++ osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 30 ++++++++++++++----- 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs index df1ac4c725..0eea352603 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs @@ -22,11 +22,6 @@ namespace osu.Game.Rulesets.Catch.Tests { private RulesetInfo catchRuleset; - public override IReadOnlyList RequiredTypes => new[] - { - typeof(CatcherArea), - }; - public TestSceneCatcherArea() { AddSliderStep("CircleSize", 0, 8, 5, createCatcher); diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs index 7a7c3f4103..30df2202f5 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.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; @@ -15,6 +16,11 @@ namespace osu.Game.Rulesets.Catch.Tests [TestFixture] public class TestSceneHyperDash : PlayerTestScene { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(CatcherArea), + }; + public TestSceneHyperDash() : base(new CatchRuleset()) { diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index dfeaf6e89f..cc0f41a14f 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -205,21 +205,28 @@ namespace osu.Game.Rulesets.Catch.UI if (!Trail) return; + var additive = createAdditiveSprite(HyperDashing); + + additive.FadeTo(0.4f).FadeOut(800, Easing.OutQuint); + additive.Expire(true); + + Scheduler.AddDelayed(beginTrail, HyperDashing ? 25 : 50); + } + + private Drawable createAdditiveSprite(bool hyperDash) + { var additive = createCatcherSprite(); additive.Anchor = Anchor; additive.Scale = Scale; - additive.Colour = HyperDashing ? Color4.Red : Color4.White; + additive.Colour = hyperDash ? Color4.Red : Color4.White; additive.Blending = BlendingParameters.Additive; additive.RelativePositionAxes = RelativePositionAxes; additive.Position = Position; AdditiveTarget.Add(additive); - additive.FadeTo(0.4f).FadeOut(800, Easing.OutQuint); - additive.Expire(true); - - Scheduler.AddDelayed(beginTrail, HyperDashing ? 25 : 50); + return additive; } private Drawable createCatcherSprite() => new CatcherSprite(); @@ -311,14 +318,14 @@ namespace osu.Game.Rulesets.Catch.UI { const float hyper_dash_transition_length = 180; - bool previouslyHyperDashing = HyperDashing; + bool wasHyperDashing = HyperDashing; if (modifier <= 1 || X == targetPosition) { hyperDashModifier = 1; hyperDashDirection = 0; - if (previouslyHyperDashing) + if (wasHyperDashing) { this.FadeColour(Color4.White, hyper_dash_transition_length, Easing.OutQuint); this.FadeTo(1, hyper_dash_transition_length, Easing.OutQuint); @@ -331,11 +338,18 @@ namespace osu.Game.Rulesets.Catch.UI hyperDashDirection = Math.Sign(targetPosition - X); hyperDashTargetPosition = targetPosition; - if (!previouslyHyperDashing) + if (!wasHyperDashing) { this.FadeColour(Color4.OrangeRed, hyper_dash_transition_length, Easing.OutQuint); this.FadeTo(0.2f, hyper_dash_transition_length, Easing.OutQuint); Trail = true; + + var hyperDashEndGlow = createAdditiveSprite(true); + + hyperDashEndGlow.MoveToOffset(new Vector2(0, -20), 1200, Easing.In); + hyperDashEndGlow.ScaleTo(hyperDashEndGlow.Scale * 0.9f).ScaleTo(hyperDashEndGlow.Scale * 1.2f, 1200, Easing.In); + hyperDashEndGlow.FadeOut(1200); + hyperDashEndGlow.Expire(true); } } } From 8ad44952f8da454f5d8d77172b6615099f86ebe0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Mar 2020 16:47:53 +0900 Subject: [PATCH 0301/2376] Remove unused usings --- osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs index 0eea352603..cf4843c200 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; From 5329b222f6de157989225761aeaf5dd89c60d72f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Mar 2020 17:49:51 +0900 Subject: [PATCH 0302/2376] Fix hyperdash test having a zero-length juice stream --- .../TestSceneHyperDash.cs | 45 ++++++++++++------- .../Objects/JuiceStream.cs | 4 +- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs index 7a7c3f4103..5b3a114506 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs @@ -8,7 +8,9 @@ using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.UI; +using osu.Game.Rulesets.Objects; using osu.Game.Tests.Visual; +using osuTK; namespace osu.Game.Rulesets.Catch.Tests { @@ -26,9 +28,11 @@ namespace osu.Game.Rulesets.Catch.Tests public void TestHyperDash() { AddAssert("First note is hyperdash", () => Beatmap.Value.Beatmap.HitObjects[0] is Fruit f && f.HyperDash); - AddUntilStep("wait for left hyperdash", () => getCatcher().Scale.X < 0 && getCatcher().HyperDashing); + AddUntilStep("wait for right movement", () => getCatcher().Scale.X > 0); // don't check hyperdashing as it happens too fast. - for (int i = 0; i < 2; i++) + AddUntilStep("wait for left movement", () => getCatcher().Scale.X < 0); + + for (int i = 0; i < 3; i++) { AddUntilStep("wait for right hyperdash", () => getCatcher().Scale.X > 0 && getCatcher().HyperDashing); AddUntilStep("wait for left hyperdash", () => getCatcher().Scale.X < 0 && getCatcher().HyperDashing); @@ -49,39 +53,50 @@ namespace osu.Game.Rulesets.Catch.Tests }; // Should produce a hyper-dash (edge case test) - beatmap.HitObjects.Add(new Fruit { StartTime = 1816, X = 308 / 512f, NewCombo = true }); - beatmap.HitObjects.Add(new JuiceStream { StartTime = 2008, X = 56 / 512f, }); + beatmap.HitObjects.Add(new Fruit { StartTime = 1816, X = 56 / 512f, NewCombo = true }); + beatmap.HitObjects.Add(new Fruit { StartTime = 2008, X = 308 / 512f, NewCombo = true }); double startTime = 3000; const float left_x = 0.02f; const float right_x = 0.98f; - createObjects(() => new Fruit(), left_x); - createObjects(() => new JuiceStream(), right_x); - createObjects(() => new JuiceStream(), left_x); - createObjects(() => new Fruit(), right_x); - createObjects(() => new Fruit(), left_x); - createObjects(() => new Fruit(), right_x); - createObjects(() => new JuiceStream(), left_x); + createObjects(() => new Fruit { X = left_x }); + createObjects(() => new TestJuiceStream(right_x), 1); + createObjects(() => new TestJuiceStream(left_x), 1); + createObjects(() => new Fruit { X = right_x }); + createObjects(() => new Fruit { X = left_x }); + createObjects(() => new Fruit { X = right_x }); + createObjects(() => new TestJuiceStream(left_x), 1); return beatmap; - void createObjects(Func createObject, float x) + void createObjects(Func createObject, int count = 3) { const float spacing = 140; - for (int i = 0; i < 3; i++) + for (int i = 0; i < count; i++) { var hitObject = createObject(); - hitObject.X = x; hitObject.StartTime = startTime + i * spacing; - beatmap.HitObjects.Add(hitObject); } startTime += 700; } } + + private class TestJuiceStream : JuiceStream + { + public TestJuiceStream(float x) + { + X = x; + Path = new SliderPath(new[] + { + new PathControlPoint(new Vector2(x, 0)), + new PathControlPoint(new Vector2(x + 30, 0)), + }); + } + } } } diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index 642ff0246e..bcc2d9d9bb 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -24,8 +24,8 @@ namespace osu.Game.Rulesets.Catch.Objects public int RepeatCount { get; set; } - public double Velocity; - public double TickDistance; + public double Velocity { get; private set; } + public double TickDistance { get; private set; } /// /// The length of one span of this . From 14192c069f8a694dd7fd7b28a6d6785af44ceb6b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Mar 2020 18:05:44 +0900 Subject: [PATCH 0303/2376] Don't play samples on catching a tiny droplet --- osu.Game.Rulesets.Catch/Objects/JuiceStream.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index 642ff0246e..1e4d8e15db 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Catch.Objects { base.CreateNestedHitObjects(); - var tickSamples = Samples.Select(s => new HitSampleInfo + var dropletSamples = Samples.Select(s => new HitSampleInfo { Bank = s.Bank, Name = @"slidertick", @@ -75,7 +75,6 @@ namespace osu.Game.Rulesets.Catch.Objects { AddNested(new TinyDroplet { - Samples = tickSamples, StartTime = t + lastEvent.Value.Time, X = X + Path.PositionAt( lastEvent.Value.PathProgress + (t / sinceLastTick) * (e.PathProgress - lastEvent.Value.PathProgress)).X / CatchPlayfield.BASE_WIDTH, @@ -93,7 +92,7 @@ namespace osu.Game.Rulesets.Catch.Objects case SliderEventType.Tick: AddNested(new Droplet { - Samples = tickSamples, + Samples = dropletSamples, StartTime = e.Time, X = X + Path.PositionAt(e.PathProgress).X / CatchPlayfield.BASE_WIDTH, }); From 4daba48a1df153bd543d9251a9060aa163cbcd36 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Mar 2020 19:30:31 +0900 Subject: [PATCH 0304/2376] Stop rotating DrawableCatchHitObjects at the top level --- osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs | 2 +- osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs index 0a8e830af9..cad8892283 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs @@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables float startRotation = RNG.NextSingle() * 20; double duration = HitObject.TimePreempt + 2000; - this.RotateTo(startRotation).RotateTo(startRotation + 720, duration); + ScaleContainer.RotateTo(startRotation).RotateTo(startRotation + 720, duration); } } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs index 197ad41247..fae5a10d04 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs @@ -13,7 +13,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables public DrawableFruit(Fruit h) : base(h) { - Rotation = (float)(RNG.NextDouble() - 0.5f) * 40; } [BackgroundDependencyLoader] @@ -21,6 +20,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables { ScaleContainer.Child = new SkinnableDrawable( new CatchSkinComponent(getComponent(HitObject.VisualRepresentation)), _ => new FruitPiece()); + + ScaleContainer.Rotation = (float)(RNG.NextDouble() - 0.5f) * 40; } private CatchSkinComponents getComponent(FruitVisualRepresentation hitObjectVisualRepresentation) From 9ad519e5a5dd1207f4f6cca8b3cfbae6bb9906fe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Mar 2020 19:35:08 +0900 Subject: [PATCH 0305/2376] Remove fade and custom InitialLifetimeOffset --- .../Objects/Drawables/DrawableCatchHitObject.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs index 5bfe0515a1..6844be5941 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs @@ -91,10 +91,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables ApplyResult(r => r.Type = CheckPosition.Invoke(HitObject) ? HitResult.Perfect : HitResult.Miss); } - protected sealed override double InitialLifetimeOffset => HitObject.TimePreempt; - - protected override void UpdateInitialTransforms() => this.FadeInFromZero(200); - protected override void UpdateStateTransforms(ArmedState state) { var endTime = HitObject.GetEndTime(); From 8ec2c35c4f6833a745eecc5ee98fb7c1eca7e422 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Mar 2020 19:34:52 +0900 Subject: [PATCH 0306/2376] Change origin of nested objects inside JuiceStream to fix visibility issues --- .../Objects/Drawables/DrawableJuiceStream.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs index 932464cfd1..7bc016d94f 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; +using osuTK; namespace osu.Game.Rulesets.Catch.Objects.Drawables { @@ -14,11 +15,13 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables private readonly Func> createDrawableRepresentation; private readonly Container dropletContainer; + public override Vector2 OriginPosition => base.OriginPosition - new Vector2(0, CatchHitObject.OBJECT_RADIUS); + public DrawableJuiceStream(JuiceStream s, Func> createDrawableRepresentation = null) : base(s) { this.createDrawableRepresentation = createDrawableRepresentation; - RelativeSizeAxes = Axes.Both; + RelativeSizeAxes = Axes.X; Origin = Anchor.BottomLeft; X = 0; @@ -27,6 +30,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables protected override void AddNestedHitObject(DrawableHitObject hitObject) { + hitObject.Origin = Anchor.BottomCentre; + base.AddNestedHitObject(hitObject); dropletContainer.Add(hitObject); } From 8294dd0b717a2f6f0071eee12078d899eca10dc5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Mar 2020 19:59:49 +0900 Subject: [PATCH 0307/2376] Fix changing ruleset at song selectnot scrolling the current selection back into view --- osu.Game/Screens/Select/BeatmapCarousel.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 1db97af2f0..71744d8b80 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -189,7 +189,7 @@ namespace osu.Game.Screens.Select root.AddChild(newSet); - applyActiveCriteria(false, false); + applyActiveCriteria(false); //check if we can/need to maintain our current selection. if (previouslySelectedID != null) @@ -239,7 +239,7 @@ namespace osu.Game.Screens.Select { Debug.Assert(bypassFilters); - applyActiveCriteria(false, true); + applyActiveCriteria(false); } return true; @@ -396,7 +396,7 @@ namespace osu.Game.Screens.Select { if (PendingFilter?.Completed == false) { - applyActiveCriteria(false, false); + applyActiveCriteria(false); Update(); } } @@ -406,10 +406,10 @@ namespace osu.Game.Screens.Select if (newCriteria != null) activeCriteria = newCriteria; - applyActiveCriteria(debounce, true); + applyActiveCriteria(debounce); } - private void applyActiveCriteria(bool debounce, bool scroll) + private void applyActiveCriteria(bool debounce) { if (root.Children.Any() != true) return; @@ -419,7 +419,7 @@ namespace osu.Game.Screens.Select root.Filter(activeCriteria); itemsCache.Invalidate(); - if (scroll) scrollPositionCache.Invalidate(); + scrollPositionCache.Invalidate(); } PendingFilter?.Cancel(); From ad7cda87357d32be258e39104a5350ee9f6798a1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Mar 2020 20:11:06 +0900 Subject: [PATCH 0308/2376] Fix download failures causing a non-safe drawable change --- osu.Game/Database/IModelDownloader.cs | 2 ++ osu.Game/Online/DownloadTrackingComposite.cs | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game/Database/IModelDownloader.cs b/osu.Game/Database/IModelDownloader.cs index 17f1ccab06..99aeb4eacf 100644 --- a/osu.Game/Database/IModelDownloader.cs +++ b/osu.Game/Database/IModelDownloader.cs @@ -15,11 +15,13 @@ namespace osu.Game.Database { /// /// Fired when a download begins. + /// This is NOT run on the update thread and should be scheduled. /// event Action> DownloadBegan; /// /// Fired when a download is interrupted, either due to user cancellation or failure. + /// This is NOT run on the update thread and should be scheduled. /// event Action> DownloadFailed; diff --git a/osu.Game/Online/DownloadTrackingComposite.cs b/osu.Game/Online/DownloadTrackingComposite.cs index 6e7ef99c6d..0769be2998 100644 --- a/osu.Game/Online/DownloadTrackingComposite.cs +++ b/osu.Game/Online/DownloadTrackingComposite.cs @@ -53,17 +53,17 @@ namespace osu.Game.Online manager.ItemRemoved += itemRemoved; } - private void downloadBegan(ArchiveDownloadRequest request) + private void downloadBegan(ArchiveDownloadRequest request) => Schedule(() => { if (request.Model.Equals(Model.Value)) attachDownload(request); - } + }); - private void downloadFailed(ArchiveDownloadRequest request) + private void downloadFailed(ArchiveDownloadRequest request) => Schedule(() => { if (request.Model.Equals(Model.Value)) attachDownload(null); - } + }); private ArchiveDownloadRequest attachedRequest; From 0be423183daad7a77e186e5834bffdb623c4d08d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Mar 2020 00:36:56 +0900 Subject: [PATCH 0309/2376] Rename data class --- .../Mods/TestSceneCatchModPerfect.cs | 10 +++++----- .../Mods/TestSceneManiaModPerfect.cs | 4 ++-- .../Mods/TestSceneOsuModPerfect.cs | 6 +++--- .../Mods/TestSceneTaikoModPerfect.cs | 6 +++--- osu.Game/Tests/Visual/ModPerfectTestScene.cs | 10 +++++----- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs index 56d2fe1ee0..47e91e50d4 100644 --- a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs +++ b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs @@ -20,11 +20,11 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods [TestCase(false)] [TestCase(true)] - public void TestBananaShower(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestCase(new BananaShower { StartTime = 1000, EndTime = 3000 }, false), shouldMiss); + public void TestBananaShower(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new BananaShower { StartTime = 1000, EndTime = 3000 }, false), shouldMiss); [TestCase(false)] [TestCase(true)] - public void TestFruit(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestCase(new Fruit { StartTime = 1000 }), shouldMiss); + public void TestFruit(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new Fruit { StartTime = 1000 }), shouldMiss); [TestCase(false)] [TestCase(true)] @@ -40,15 +40,15 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods }) }; - CreateHitObjectTest(new HitObjectTestCase(stream), shouldMiss); + CreateHitObjectTest(new HitObjectTestData(stream), shouldMiss); } // We only care about testing misses, hits are tested via JuiceStream [TestCase(true)] - public void TestDroplet(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestCase(new Droplet { StartTime = 1000 }), shouldMiss); + public void TestDroplet(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new Droplet { StartTime = 1000 }), shouldMiss); // We only care about testing misses, hits are tested via JuiceStream [TestCase(true)] - public void TestTinyDroplet(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestCase(new TinyDroplet { StartTime = 1000 }), shouldMiss); + public void TestTinyDroplet(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new TinyDroplet { StartTime = 1000 }), shouldMiss); } } diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModPerfect.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModPerfect.cs index 4e11a302c9..607d42a1bb 100644 --- a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModPerfect.cs +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModPerfect.cs @@ -17,10 +17,10 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods [TestCase(false)] [TestCase(true)] - public void TestNote(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestCase(new Note { StartTime = 1000 }), shouldMiss); + public void TestNote(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new Note { StartTime = 1000 }), shouldMiss); [TestCase(false)] [TestCase(true)] - public void TestHoldNote(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestCase(new HoldNote { StartTime = 1000, EndTime = 3000 }), shouldMiss); + public void TestHoldNote(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new HoldNote { StartTime = 1000, EndTime = 3000 }), shouldMiss); } } diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModPerfect.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModPerfect.cs index fc2dfa16ec..b03a894085 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModPerfect.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModPerfect.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods [TestCase(false)] [TestCase(true)] - public void TestHitCircle(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestCase(new HitCircle { StartTime = 1000 }), shouldMiss); + public void TestHitCircle(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new HitCircle { StartTime = 1000 }), shouldMiss); [TestCase(false)] [TestCase(true)] @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(100, 0), }) }; - CreateHitObjectTest(new HitObjectTestCase(slider), shouldMiss); + CreateHitObjectTest(new HitObjectTestData(slider), shouldMiss); } [TestCase(false)] @@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods Position = new Vector2(256, 192) }; - CreateHitObjectTest(new HitObjectTestCase(spinner), shouldMiss); + CreateHitObjectTest(new HitObjectTestData(spinner), shouldMiss); } } } diff --git a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModPerfect.cs b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModPerfect.cs index fd9d01a3db..d3be2cdf0d 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModPerfect.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModPerfect.cs @@ -19,15 +19,15 @@ namespace osu.Game.Rulesets.Taiko.Tests.Mods [TestCase(false)] [TestCase(true)] - public void TestHit(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestCase(new CentreHit { StartTime = 1000 }), shouldMiss); + public void TestHit(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new CentreHit { StartTime = 1000 }), shouldMiss); [TestCase(false)] [TestCase(true)] - public void TestDrumRoll(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestCase(new DrumRoll { StartTime = 1000, EndTime = 3000 }), shouldMiss); + public void TestDrumRoll(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new DrumRoll { StartTime = 1000, EndTime = 3000 }), shouldMiss); [TestCase(false)] [TestCase(true)] - public void TestSwell(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestCase(new Swell { StartTime = 1000, EndTime = 3000 }), shouldMiss); + public void TestSwell(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new Swell { StartTime = 1000, EndTime = 3000 }), shouldMiss); private class TestTaikoRuleset : TaikoRuleset { diff --git a/osu.Game/Tests/Visual/ModPerfectTestScene.cs b/osu.Game/Tests/Visual/ModPerfectTestScene.cs index d6255d2478..798947eb40 100644 --- a/osu.Game/Tests/Visual/ModPerfectTestScene.cs +++ b/osu.Game/Tests/Visual/ModPerfectTestScene.cs @@ -20,16 +20,16 @@ namespace osu.Game.Tests.Visual this.mod = mod; } - protected void CreateHitObjectTest(HitObjectTestCase testCaseData, bool shouldMiss) => CreateModTest(new ModTestData + protected void CreateHitObjectTest(HitObjectTestData testData, bool shouldMiss) => CreateModTest(new ModTestData { Mod = mod, Beatmap = new Beatmap { BeatmapInfo = { Ruleset = ruleset.RulesetInfo }, - HitObjects = { testCaseData.HitObject } + HitObjects = { testData.HitObject } }, Autoplay = !shouldMiss, - PassCondition = () => ((PerfectModTestPlayer)Player).CheckFailed(shouldMiss && testCaseData.FailOnMiss) + PassCondition = () => ((PerfectModTestPlayer)Player).CheckFailed(shouldMiss && testData.FailOnMiss) }); protected override TestPlayer CreateModPlayer(Ruleset ruleset) => new PerfectModTestPlayer(); @@ -52,12 +52,12 @@ namespace osu.Game.Tests.Visual } } - protected class HitObjectTestCase + protected class HitObjectTestData { public readonly HitObject HitObject; public readonly bool FailOnMiss; - public HitObjectTestCase(HitObject hitObject, bool failOnMiss = true) + public HitObjectTestData(HitObject hitObject, bool failOnMiss = true) { HitObject = hitObject; FailOnMiss = failOnMiss; From 998ca05a0cf144bb30a8b7f7605369e35cac45e5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Mar 2020 02:35:36 +0900 Subject: [PATCH 0310/2376] Fix disclaimer test scene supporter toggle --- osu.Game.Tests/Visual/Menus/TestSceneDisclaimer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneDisclaimer.cs b/osu.Game.Tests/Visual/Menus/TestSceneDisclaimer.cs index 681bf1b40b..49fab08ded 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneDisclaimer.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneDisclaimer.cs @@ -19,7 +19,7 @@ namespace osu.Game.Tests.Visual.Menus API.LocalUser.Value = new User { Username = API.LocalUser.Value.Username, - Id = API.LocalUser.Value.Id, + Id = API.LocalUser.Value.Id + 1, IsSupporter = !API.LocalUser.Value.IsSupporter, }; }); From 7b368dca359f9ec9a766877cc77d7edbfe821fdb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Mar 2020 02:59:24 +0900 Subject: [PATCH 0311/2376] Add test coverage --- .../SongSelect/TestScenePlaySongSelect.cs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 51d302123b..105d96cdfe 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -465,6 +465,43 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert("carousel still correct", () => songSelect.Carousel.SelectedBeatmap.OnlineBeatmapID == target.OnlineBeatmapID); } + [Test] + public void TestExternalBeatmapChangeWhileFilteredThenRefilter() + { + createSongSelect(); + addManyTestMaps(); + + changeRuleset(0); + + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap != 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); + + BeatmapInfo target = null; + + AddStep("select beatmap externally", () => + { + target = manager.GetAllUsableBeatmapSets().Where(b => b.Beatmaps.Any(bi => bi.RulesetID == 1)) + .ElementAt(5).Beatmaps.First(); + + Beatmap.Value = manager.GetWorkingBeatmap(target); + }); + + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap != null); + + AddUntilStep("carousel has correct", () => songSelect.Carousel.SelectedBeatmap?.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); + } + [Test] public void TestAutoplayViaCtrlEnter() { From ed837d311529bfe180871ac4600824daff890a21 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Mar 2020 10:18:41 +0900 Subject: [PATCH 0312/2376] Use framework extension method for FromHex --- .../Objects/Drawables/DrawableSpinner.cs | 4 +- .../Visual/Online/TestSceneChatLink.cs | 3 +- .../Components/TournamentMatchChatDisplay.cs | 2 +- .../Screens/Gameplay/Components/TeamScore.cs | 4 +- .../Ladder/Components/DrawableMatchTeam.cs | 9 +- .../Screens/Ladder/LadderScreen.cs | 4 +- osu.Game.Tournament/TournamentGame.cs | 12 +- osu.Game/Graphics/OsuColour.cs | 214 +++++++----------- .../UserInterface/DrawableOsuMenuItem.cs | 5 +- .../UserInterfaceV2/LabelledDrawable.cs | 3 +- osu.Game/Online/Leaderboards/DrawableRank.cs | 24 +- .../Online/Leaderboards/LeaderboardScore.cs | 8 +- osu.Game/Overlays/BeatmapSet/BasicStats.cs | 5 +- .../BeatmapSet/Buttons/HeaderButton.cs | 8 +- osu.Game/Overlays/Chat/ChatLine.cs | 68 +++--- .../Chat/Selection/ChannelSelectionOverlay.cs | 10 +- .../Chat/Tabs/PrivateChannelTabItem.cs | 2 +- osu.Game/Overlays/Dialog/PopupDialog.cs | 7 +- osu.Game/Overlays/Dialog/PopupDialogButton.cs | 4 +- osu.Game/Overlays/Direct/FilterControl.cs | 3 +- osu.Game/Overlays/Direct/Header.cs | 3 +- osu.Game/Overlays/DirectOverlay.cs | 7 +- osu.Game/Overlays/MedalOverlay.cs | 6 +- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 8 +- .../Profile/Header/TopHeaderContainer.cs | 3 +- osu.Game/Overlays/Profile/ProfileHeader.cs | 3 +- osu.Game/Overlays/Social/FilterControl.cs | 4 +- osu.Game/Overlays/Social/Header.cs | 3 +- osu.Game/Overlays/SocialOverlay.cs | 8 +- .../Edit/Components/Menus/EditorMenuBar.cs | 3 +- .../Components/Timeline/TimelineArea.cs | 8 +- osu.Game/Screens/Menu/IntroSequence.cs | 12 +- osu.Game/Screens/Menu/OsuLogo.cs | 8 +- .../Multi/Components/DrawableGameType.cs | 3 +- .../Multi/Components/ParticipantsList.cs | 4 +- osu.Game/Screens/Multi/Header.cs | 3 +- .../Multi/Lounge/Components/DrawableRoom.cs | 2 +- .../Screens/Multi/Match/Components/Footer.cs | 3 +- .../Match/Components/MatchSettingsOverlay.cs | 4 +- .../Match/Components/PurpleTriangleButton.cs | 8 +- .../Components/RoomAvailabilityPicker.cs | 3 +- osu.Game/Screens/Multi/Multiplayer.cs | 11 +- osu.Game/Screens/Select/BeatmapInfoWedge.cs | 4 +- .../Carousel/DrawableCarouselBeatmap.cs | 5 +- 44 files changed, 249 insertions(+), 276 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index de11ab6419..0ec7f2ebfe 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -36,8 +36,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private readonly SpriteIcon symbol; - private readonly Color4 baseColour = OsuColour.FromHex(@"002c3c"); - private readonly Color4 fillColour = OsuColour.FromHex(@"005b7c"); + private readonly Color4 baseColour = Color4Extensions.FromHex(@"002c3c"); + private readonly Color4 fillColour = Color4Extensions.FromHex(@"005b7c"); private readonly IBindable positionBindable = new Bindable(); diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs index a1c77e2db0..c76d4fd5b8 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs @@ -7,6 +7,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; @@ -102,7 +103,7 @@ namespace osu.Game.Tests.Visual.Online { bool hasBackground = !string.IsNullOrEmpty(newLine.Message.Sender.Colour); - Color4 textColour = isAction && hasBackground ? OsuColour.FromHex(newLine.Message.Sender.Colour) : Color4.White; + Color4 textColour = isAction && hasBackground ? Color4Extensions.FromHex(newLine.Message.Sender.Colour) : Color4.White; var linkCompilers = newLine.ContentFlow.Where(d => d is DrawableLinkCompiler).ToList(); var linkSprites = linkCompilers.SelectMany(comp => ((DrawableLinkCompiler)comp).Parts); diff --git a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs index 8eb1c98ba0..2a183d0d45 100644 --- a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs +++ b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs @@ -84,7 +84,7 @@ namespace osu.Game.Tournament.Components // else if (info.CurrentMatch.Value.Team2.Value.Players.Any(u => u.Id == Message.Sender.Id)) // SenderText.Colour = TournamentGame.COLOUR_BLUE; // else if (Message.Sender.Colour != null) - // SenderText.Colour = ColourBox.Colour = OsuColour.FromHex(Message.Sender.Colour); + // SenderText.Colour = ColourBox.Colour = Color4Extensions.FromHex(Message.Sender.Colour); } } } diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TeamScore.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScore.cs index c7071484ca..36c78c5ac1 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/TeamScore.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScore.cs @@ -76,7 +76,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components }, box = new Box { - Colour = OsuColour.FromHex("#FFE8AD"), + Colour = Color4Extensions.FromHex("#FFE8AD"), RelativeSizeAxes = Axes.Both, }, }; @@ -85,7 +85,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow, - Colour = OsuColour.FromHex("#FFE8AD").Opacity(0.1f), + Colour = Color4Extensions.FromHex("#FFE8AD").Opacity(0.1f), Hollow = true, Radius = 20, Roundness = 10, diff --git a/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs b/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs index fe7e80873c..15cb7e44cb 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs @@ -4,6 +4,7 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; @@ -85,8 +86,8 @@ namespace osu.Game.Tournament.Screens.Ladder.Components this.ladderEditor = ladderEditor; colourWinner = losers - ? OsuColour.FromHex("#8E7F48") - : OsuColour.FromHex("#1462AA"); + ? Color4Extensions.FromHex("#8E7F48") + : Color4Extensions.FromHex("#1462AA"); InternalChildren = new Drawable[] { @@ -180,8 +181,8 @@ namespace osu.Game.Tournament.Screens.Ladder.Components { bool winner = completed.Value && isWinner?.Invoke() == true; - background.FadeColour(winner ? Color4.White : OsuColour.FromHex("#444"), winner ? 500 : 0, Easing.OutQuint); - backgroundRight.FadeColour(winner ? colourWinner : OsuColour.FromHex("#333"), winner ? 500 : 0, Easing.OutQuint); + background.FadeColour(winner ? Color4.White : Color4Extensions.FromHex("#444"), winner ? 500 : 0, Easing.OutQuint); + backgroundRight.FadeColour(winner ? colourWinner : Color4Extensions.FromHex("#333"), winner ? 500 : 0, Easing.OutQuint); AcronymText.Colour = winner ? Color4.Black : Color4.White; diff --git a/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs b/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs index c7e59cfa7b..6f62b3ddba 100644 --- a/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs +++ b/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs @@ -32,8 +32,8 @@ namespace osu.Game.Tournament.Screens.Ladder [BackgroundDependencyLoader] private void load(OsuColour colours, Storage storage) { - normalPathColour = OsuColour.FromHex("#66D1FF"); - losersPathColour = OsuColour.FromHex("#FFC700"); + normalPathColour = Color4Extensions.FromHex("#66D1FF"); + losersPathColour = Color4Extensions.FromHex("#FFC700"); RelativeSizeAxes = Axes.Both; diff --git a/osu.Game.Tournament/TournamentGame.cs b/osu.Game.Tournament/TournamentGame.cs index 6d597d5e7d..78bb66d553 100644 --- a/osu.Game.Tournament/TournamentGame.cs +++ b/osu.Game.Tournament/TournamentGame.cs @@ -1,9 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; -using osu.Game.Graphics; using osu.Game.Graphics.Cursor; using osu.Game.Tournament.Models; using osuTK.Graphics; @@ -14,13 +14,13 @@ namespace osu.Game.Tournament { public static ColourInfo GetTeamColour(TeamColour teamColour) => teamColour == TeamColour.Red ? COLOUR_RED : COLOUR_BLUE; - public static readonly Color4 COLOUR_RED = OsuColour.FromHex("#AA1414"); - public static readonly Color4 COLOUR_BLUE = OsuColour.FromHex("#1462AA"); + public static readonly Color4 COLOUR_RED = Color4Extensions.FromHex("#AA1414"); + public static readonly Color4 COLOUR_BLUE = Color4Extensions.FromHex("#1462AA"); - public static readonly Color4 ELEMENT_BACKGROUND_COLOUR = OsuColour.FromHex("#fff"); - public static readonly Color4 ELEMENT_FOREGROUND_COLOUR = OsuColour.FromHex("#000"); + public static readonly Color4 ELEMENT_BACKGROUND_COLOUR = Color4Extensions.FromHex("#fff"); + public static readonly Color4 ELEMENT_FOREGROUND_COLOUR = Color4Extensions.FromHex("#000"); - public static readonly Color4 TEXT_COLOUR = OsuColour.FromHex("#fff"); + public static readonly Color4 TEXT_COLOUR = Color4Extensions.FromHex("#fff"); protected override void LoadComplete() { diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs index 59dd823266..984f5e52d1 100644 --- a/osu.Game/Graphics/OsuColour.cs +++ b/osu.Game/Graphics/OsuColour.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; -using System.Globalization; +using osu.Framework.Extensions.Color4Extensions; using osu.Game.Beatmaps; using osuTK.Graphics; @@ -13,45 +12,6 @@ namespace osu.Game.Graphics public static Color4 Gray(float amt) => new Color4(amt, amt, amt, 1f); public static Color4 Gray(byte amt) => new Color4(amt, amt, amt, 255); - public static Color4 FromHex(string hex) - { - var hexSpan = hex[0] == '#' ? hex.AsSpan().Slice(1) : hex.AsSpan(); - - switch (hexSpan.Length) - { - default: - throw new ArgumentException(@"Invalid hex string length!"); - - case 3: - return new Color4( - (byte)(byte.Parse(hexSpan.Slice(0, 1), NumberStyles.HexNumber) * 17), - (byte)(byte.Parse(hexSpan.Slice(1, 1), NumberStyles.HexNumber) * 17), - (byte)(byte.Parse(hexSpan.Slice(2, 1), NumberStyles.HexNumber) * 17), - 255); - - case 6: - return new Color4( - byte.Parse(hexSpan.Slice(0, 2), NumberStyles.HexNumber), - byte.Parse(hexSpan.Slice(2, 2), NumberStyles.HexNumber), - byte.Parse(hexSpan.Slice(4, 2), NumberStyles.HexNumber), - 255); - - case 4: - return new Color4( - (byte)(byte.Parse(hexSpan.Slice(0, 1), NumberStyles.HexNumber) * 17), - (byte)(byte.Parse(hexSpan.Slice(1, 1), NumberStyles.HexNumber) * 17), - (byte)(byte.Parse(hexSpan.Slice(0, 1), NumberStyles.HexNumber) * 17), - (byte)(byte.Parse(hexSpan.Slice(0, 1), NumberStyles.HexNumber) * 17)); - - case 8: - return new Color4( - byte.Parse(hexSpan.Slice(0, 2), NumberStyles.HexNumber), - byte.Parse(hexSpan.Slice(2, 2), NumberStyles.HexNumber), - byte.Parse(hexSpan.Slice(4, 2), NumberStyles.HexNumber), - byte.Parse(hexSpan.Slice(6, 2), NumberStyles.HexNumber)); - } - } - public Color4 ForDifficultyRating(DifficultyRating difficulty, bool useLighterColour = false) { switch (difficulty) @@ -78,105 +38,105 @@ namespace osu.Game.Graphics } // See https://github.com/ppy/osu-web/blob/master/resources/assets/less/colors.less - public readonly Color4 PurpleLighter = FromHex(@"eeeeff"); - public readonly Color4 PurpleLight = FromHex(@"aa88ff"); - public readonly Color4 PurpleLightAlternative = FromHex(@"cba4da"); - public readonly Color4 Purple = FromHex(@"8866ee"); - public readonly Color4 PurpleDark = FromHex(@"6644cc"); - public readonly Color4 PurpleDarkAlternative = FromHex(@"312436"); - public readonly Color4 PurpleDarker = FromHex(@"441188"); + public readonly Color4 PurpleLighter = Color4Extensions.FromHex(@"eeeeff"); + public readonly Color4 PurpleLight = Color4Extensions.FromHex(@"aa88ff"); + public readonly Color4 PurpleLightAlternative = Color4Extensions.FromHex(@"cba4da"); + public readonly Color4 Purple = Color4Extensions.FromHex(@"8866ee"); + public readonly Color4 PurpleDark = Color4Extensions.FromHex(@"6644cc"); + public readonly Color4 PurpleDarkAlternative = Color4Extensions.FromHex(@"312436"); + public readonly Color4 PurpleDarker = Color4Extensions.FromHex(@"441188"); - public readonly Color4 PinkLighter = FromHex(@"ffddee"); - public readonly Color4 PinkLight = FromHex(@"ff99cc"); - public readonly Color4 Pink = FromHex(@"ff66aa"); - public readonly Color4 PinkDark = FromHex(@"cc5288"); - public readonly Color4 PinkDarker = FromHex(@"bb1177"); + public readonly Color4 PinkLighter = Color4Extensions.FromHex(@"ffddee"); + public readonly Color4 PinkLight = Color4Extensions.FromHex(@"ff99cc"); + public readonly Color4 Pink = Color4Extensions.FromHex(@"ff66aa"); + public readonly Color4 PinkDark = Color4Extensions.FromHex(@"cc5288"); + public readonly Color4 PinkDarker = Color4Extensions.FromHex(@"bb1177"); - public readonly Color4 BlueLighter = FromHex(@"ddffff"); - public readonly Color4 BlueLight = FromHex(@"99eeff"); - public readonly Color4 Blue = FromHex(@"66ccff"); - public readonly Color4 BlueDark = FromHex(@"44aadd"); - public readonly Color4 BlueDarker = FromHex(@"2299bb"); + public readonly Color4 BlueLighter = Color4Extensions.FromHex(@"ddffff"); + public readonly Color4 BlueLight = Color4Extensions.FromHex(@"99eeff"); + public readonly Color4 Blue = Color4Extensions.FromHex(@"66ccff"); + public readonly Color4 BlueDark = Color4Extensions.FromHex(@"44aadd"); + public readonly Color4 BlueDarker = Color4Extensions.FromHex(@"2299bb"); - public readonly Color4 YellowLighter = FromHex(@"ffffdd"); - public readonly Color4 YellowLight = FromHex(@"ffdd55"); - public readonly Color4 Yellow = FromHex(@"ffcc22"); - public readonly Color4 YellowDark = FromHex(@"eeaa00"); - public readonly Color4 YellowDarker = FromHex(@"cc6600"); + public readonly Color4 YellowLighter = Color4Extensions.FromHex(@"ffffdd"); + public readonly Color4 YellowLight = Color4Extensions.FromHex(@"ffdd55"); + public readonly Color4 Yellow = Color4Extensions.FromHex(@"ffcc22"); + public readonly Color4 YellowDark = Color4Extensions.FromHex(@"eeaa00"); + public readonly Color4 YellowDarker = Color4Extensions.FromHex(@"cc6600"); - public readonly Color4 GreenLighter = FromHex(@"eeffcc"); - public readonly Color4 GreenLight = FromHex(@"b3d944"); - public readonly Color4 Green = FromHex(@"88b300"); - public readonly Color4 GreenDark = FromHex(@"668800"); - public readonly Color4 GreenDarker = FromHex(@"445500"); + public readonly Color4 GreenLighter = Color4Extensions.FromHex(@"eeffcc"); + public readonly Color4 GreenLight = Color4Extensions.FromHex(@"b3d944"); + public readonly Color4 Green = Color4Extensions.FromHex(@"88b300"); + public readonly Color4 GreenDark = Color4Extensions.FromHex(@"668800"); + public readonly Color4 GreenDarker = Color4Extensions.FromHex(@"445500"); - public readonly Color4 Sky = FromHex(@"6bb5ff"); - public readonly Color4 GreySkyLighter = FromHex(@"c6e3f4"); - public readonly Color4 GreySkyLight = FromHex(@"8ab3cc"); - public readonly Color4 GreySky = FromHex(@"405461"); - public readonly Color4 GreySkyDark = FromHex(@"303d47"); - public readonly Color4 GreySkyDarker = FromHex(@"21272c"); + public readonly Color4 Sky = Color4Extensions.FromHex(@"6bb5ff"); + public readonly Color4 GreySkyLighter = Color4Extensions.FromHex(@"c6e3f4"); + public readonly Color4 GreySkyLight = Color4Extensions.FromHex(@"8ab3cc"); + public readonly Color4 GreySky = Color4Extensions.FromHex(@"405461"); + public readonly Color4 GreySkyDark = Color4Extensions.FromHex(@"303d47"); + public readonly Color4 GreySkyDarker = Color4Extensions.FromHex(@"21272c"); - public readonly Color4 Seafoam = FromHex(@"05ffa2"); - public readonly Color4 GreySeafoamLighter = FromHex(@"9ebab1"); - public readonly Color4 GreySeafoamLight = FromHex(@"4d7365"); - public readonly Color4 GreySeafoam = FromHex(@"33413c"); - public readonly Color4 GreySeafoamDark = FromHex(@"2c3532"); - public readonly Color4 GreySeafoamDarker = FromHex(@"1e2422"); + public readonly Color4 Seafoam = Color4Extensions.FromHex(@"05ffa2"); + public readonly Color4 GreySeafoamLighter = Color4Extensions.FromHex(@"9ebab1"); + public readonly Color4 GreySeafoamLight = Color4Extensions.FromHex(@"4d7365"); + public readonly Color4 GreySeafoam = Color4Extensions.FromHex(@"33413c"); + public readonly Color4 GreySeafoamDark = Color4Extensions.FromHex(@"2c3532"); + public readonly Color4 GreySeafoamDarker = Color4Extensions.FromHex(@"1e2422"); - public readonly Color4 Cyan = FromHex(@"05f4fd"); - public readonly Color4 GreyCyanLighter = FromHex(@"77b1b3"); - public readonly Color4 GreyCyanLight = FromHex(@"436d6f"); - public readonly Color4 GreyCyan = FromHex(@"293d3e"); - public readonly Color4 GreyCyanDark = FromHex(@"243536"); - public readonly Color4 GreyCyanDarker = FromHex(@"1e2929"); + public readonly Color4 Cyan = Color4Extensions.FromHex(@"05f4fd"); + public readonly Color4 GreyCyanLighter = Color4Extensions.FromHex(@"77b1b3"); + public readonly Color4 GreyCyanLight = Color4Extensions.FromHex(@"436d6f"); + public readonly Color4 GreyCyan = Color4Extensions.FromHex(@"293d3e"); + public readonly Color4 GreyCyanDark = Color4Extensions.FromHex(@"243536"); + public readonly Color4 GreyCyanDarker = Color4Extensions.FromHex(@"1e2929"); - public readonly Color4 Lime = FromHex(@"82ff05"); - public readonly Color4 GreyLimeLighter = FromHex(@"deff87"); - public readonly Color4 GreyLimeLight = FromHex(@"657259"); - public readonly Color4 GreyLime = FromHex(@"3f443a"); - public readonly Color4 GreyLimeDark = FromHex(@"32352e"); - public readonly Color4 GreyLimeDarker = FromHex(@"2e302b"); + public readonly Color4 Lime = Color4Extensions.FromHex(@"82ff05"); + public readonly Color4 GreyLimeLighter = Color4Extensions.FromHex(@"deff87"); + public readonly Color4 GreyLimeLight = Color4Extensions.FromHex(@"657259"); + public readonly Color4 GreyLime = Color4Extensions.FromHex(@"3f443a"); + public readonly Color4 GreyLimeDark = Color4Extensions.FromHex(@"32352e"); + public readonly Color4 GreyLimeDarker = Color4Extensions.FromHex(@"2e302b"); - public readonly Color4 Violet = FromHex(@"bf04ff"); - public readonly Color4 GreyVioletLighter = FromHex(@"ebb8fe"); - public readonly Color4 GreyVioletLight = FromHex(@"685370"); - public readonly Color4 GreyViolet = FromHex(@"46334d"); - public readonly Color4 GreyVioletDark = FromHex(@"2c2230"); - public readonly Color4 GreyVioletDarker = FromHex(@"201823"); + public readonly Color4 Violet = Color4Extensions.FromHex(@"bf04ff"); + public readonly Color4 GreyVioletLighter = Color4Extensions.FromHex(@"ebb8fe"); + public readonly Color4 GreyVioletLight = Color4Extensions.FromHex(@"685370"); + public readonly Color4 GreyViolet = Color4Extensions.FromHex(@"46334d"); + public readonly Color4 GreyVioletDark = Color4Extensions.FromHex(@"2c2230"); + public readonly Color4 GreyVioletDarker = Color4Extensions.FromHex(@"201823"); - public readonly Color4 Carmine = FromHex(@"ff0542"); - public readonly Color4 GreyCarmineLighter = FromHex(@"deaab4"); - public readonly Color4 GreyCarmineLight = FromHex(@"644f53"); - public readonly Color4 GreyCarmine = FromHex(@"342b2d"); - public readonly Color4 GreyCarmineDark = FromHex(@"302a2b"); - public readonly Color4 GreyCarmineDarker = FromHex(@"241d1e"); + public readonly Color4 Carmine = Color4Extensions.FromHex(@"ff0542"); + public readonly Color4 GreyCarmineLighter = Color4Extensions.FromHex(@"deaab4"); + public readonly Color4 GreyCarmineLight = Color4Extensions.FromHex(@"644f53"); + public readonly Color4 GreyCarmine = Color4Extensions.FromHex(@"342b2d"); + public readonly Color4 GreyCarmineDark = Color4Extensions.FromHex(@"302a2b"); + public readonly Color4 GreyCarmineDarker = Color4Extensions.FromHex(@"241d1e"); - public readonly Color4 Gray0 = FromHex(@"000"); - public readonly Color4 Gray1 = FromHex(@"111"); - public readonly Color4 Gray2 = FromHex(@"222"); - public readonly Color4 Gray3 = FromHex(@"333"); - public readonly Color4 Gray4 = FromHex(@"444"); - public readonly Color4 Gray5 = FromHex(@"555"); - public readonly Color4 Gray6 = FromHex(@"666"); - public readonly Color4 Gray7 = FromHex(@"777"); - public readonly Color4 Gray8 = FromHex(@"888"); - public readonly Color4 Gray9 = FromHex(@"999"); - public readonly Color4 GrayA = FromHex(@"aaa"); - public readonly Color4 GrayB = FromHex(@"bbb"); - public readonly Color4 GrayC = FromHex(@"ccc"); - public readonly Color4 GrayD = FromHex(@"ddd"); - public readonly Color4 GrayE = FromHex(@"eee"); - public readonly Color4 GrayF = FromHex(@"fff"); + public readonly Color4 Gray0 = Color4Extensions.FromHex(@"000"); + public readonly Color4 Gray1 = Color4Extensions.FromHex(@"111"); + public readonly Color4 Gray2 = Color4Extensions.FromHex(@"222"); + public readonly Color4 Gray3 = Color4Extensions.FromHex(@"333"); + public readonly Color4 Gray4 = Color4Extensions.FromHex(@"444"); + public readonly Color4 Gray5 = Color4Extensions.FromHex(@"555"); + public readonly Color4 Gray6 = Color4Extensions.FromHex(@"666"); + public readonly Color4 Gray7 = Color4Extensions.FromHex(@"777"); + public readonly Color4 Gray8 = Color4Extensions.FromHex(@"888"); + public readonly Color4 Gray9 = Color4Extensions.FromHex(@"999"); + public readonly Color4 GrayA = Color4Extensions.FromHex(@"aaa"); + public readonly Color4 GrayB = Color4Extensions.FromHex(@"bbb"); + public readonly Color4 GrayC = Color4Extensions.FromHex(@"ccc"); + public readonly Color4 GrayD = Color4Extensions.FromHex(@"ddd"); + public readonly Color4 GrayE = Color4Extensions.FromHex(@"eee"); + public readonly Color4 GrayF = Color4Extensions.FromHex(@"fff"); - public readonly Color4 RedLighter = FromHex(@"ffeded"); - public readonly Color4 RedLight = FromHex(@"ed7787"); - public readonly Color4 Red = FromHex(@"ed1121"); - public readonly Color4 RedDark = FromHex(@"ba0011"); - public readonly Color4 RedDarker = FromHex(@"870000"); + public readonly Color4 RedLighter = Color4Extensions.FromHex(@"ffeded"); + public readonly Color4 RedLight = Color4Extensions.FromHex(@"ed7787"); + public readonly Color4 Red = Color4Extensions.FromHex(@"ed1121"); + public readonly Color4 RedDark = Color4Extensions.FromHex(@"ba0011"); + public readonly Color4 RedDarker = Color4Extensions.FromHex(@"870000"); - public readonly Color4 ChatBlue = FromHex(@"17292e"); + public readonly Color4 ChatBlue = Color4Extensions.FromHex(@"17292e"); - public readonly Color4 ContextMenuGray = FromHex(@"223034"); + public readonly Color4 ContextMenuGray = Color4Extensions.FromHex(@"223034"); } } diff --git a/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs b/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs index 591ed3df83..a3ca851341 100644 --- a/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs +++ b/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; @@ -38,7 +39,7 @@ namespace osu.Game.Graphics.UserInterface sampleClick = audio.Samples.Get(@"UI/generic-select"); BackgroundColour = Color4.Transparent; - BackgroundColourHover = OsuColour.FromHex(@"172023"); + BackgroundColourHover = Color4Extensions.FromHex(@"172023"); updateTextColour(); } @@ -57,7 +58,7 @@ namespace osu.Game.Graphics.UserInterface break; case MenuItemType.Highlighted: - text.Colour = OsuColour.FromHex(@"ffcc22"); + text.Colour = Color4Extensions.FromHex(@"ffcc22"); break; } } diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs index f44bd72aee..0e995ca73d 100644 --- a/osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -42,7 +43,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 new Box { RelativeSizeAxes = Axes.Both, - Colour = OsuColour.FromHex("1c2125"), + Colour = Color4Extensions.FromHex("1c2125"), }, new FillFlowContainer { diff --git a/osu.Game/Online/Leaderboards/DrawableRank.cs b/osu.Game/Online/Leaderboards/DrawableRank.cs index 20bda4601f..45b91bbf81 100644 --- a/osu.Game/Online/Leaderboards/DrawableRank.cs +++ b/osu.Game/Online/Leaderboards/DrawableRank.cs @@ -80,23 +80,23 @@ namespace osu.Game.Online.Leaderboards { case ScoreRank.XH: case ScoreRank.X: - return OsuColour.FromHex(@"ce1c9d"); + return Color4Extensions.FromHex(@"ce1c9d"); case ScoreRank.SH: case ScoreRank.S: - return OsuColour.FromHex(@"00a8b5"); + return Color4Extensions.FromHex(@"00a8b5"); case ScoreRank.A: - return OsuColour.FromHex(@"7cce14"); + return Color4Extensions.FromHex(@"7cce14"); case ScoreRank.B: - return OsuColour.FromHex(@"e3b130"); + return Color4Extensions.FromHex(@"e3b130"); case ScoreRank.C: - return OsuColour.FromHex(@"f18252"); + return Color4Extensions.FromHex(@"f18252"); default: - return OsuColour.FromHex(@"e95353"); + return Color4Extensions.FromHex(@"e95353"); } } @@ -109,23 +109,23 @@ namespace osu.Game.Online.Leaderboards { case ScoreRank.XH: case ScoreRank.SH: - return ColourInfo.GradientVertical(Color4.White, OsuColour.FromHex("afdff0")); + return ColourInfo.GradientVertical(Color4.White, Color4Extensions.FromHex("afdff0")); case ScoreRank.X: case ScoreRank.S: - return ColourInfo.GradientVertical(OsuColour.FromHex(@"ffe7a8"), OsuColour.FromHex(@"ffb800")); + return ColourInfo.GradientVertical(Color4Extensions.FromHex(@"ffe7a8"), Color4Extensions.FromHex(@"ffb800")); case ScoreRank.A: - return OsuColour.FromHex(@"275227"); + return Color4Extensions.FromHex(@"275227"); case ScoreRank.B: - return OsuColour.FromHex(@"553a2b"); + return Color4Extensions.FromHex(@"553a2b"); case ScoreRank.C: - return OsuColour.FromHex(@"473625"); + return Color4Extensions.FromHex(@"473625"); default: - return OsuColour.FromHex(@"512525"); + return Color4Extensions.FromHex(@"512525"); } } } diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index ba92b993a2..1469f29874 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -200,7 +200,7 @@ namespace osu.Game.Online.Leaderboards scoreLabel = new GlowingSpriteText { TextColour = Color4.White, - GlowColour = OsuColour.FromHex(@"83ccfa"), + GlowColour = Color4Extensions.FromHex(@"83ccfa"), Text = score.TotalScore.ToString(@"N0"), Font = OsuFont.Numeric.With(size: 23), }, @@ -325,7 +325,7 @@ namespace osu.Game.Online.Leaderboards Origin = Anchor.Centre, Size = new Vector2(icon_size), Rotation = 45, - Colour = OsuColour.FromHex(@"3087ac"), + Colour = Color4Extensions.FromHex(@"3087ac"), Icon = FontAwesome.Solid.Square, Shadow = true, }, @@ -334,7 +334,7 @@ namespace osu.Game.Online.Leaderboards Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(icon_size - 6), - Colour = OsuColour.FromHex(@"a4edff"), + Colour = Color4Extensions.FromHex(@"a4edff"), Icon = statistic.Icon, }, }, @@ -344,7 +344,7 @@ namespace osu.Game.Online.Leaderboards Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, TextColour = Color4.White, - GlowColour = OsuColour.FromHex(@"83ccfa"), + GlowColour = Color4Extensions.FromHex(@"83ccfa"), Text = statistic.Value, Font = OsuFont.GetFont(size: 17, weight: FontWeight.Bold), }, diff --git a/osu.Game/Overlays/BeatmapSet/BasicStats.cs b/osu.Game/Overlays/BeatmapSet/BasicStats.cs index ba0a62ec2f..a2464bef09 100644 --- a/osu.Game/Overlays/BeatmapSet/BasicStats.cs +++ b/osu.Game/Overlays/BeatmapSet/BasicStats.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; @@ -124,7 +125,7 @@ namespace osu.Game.Overlays.BeatmapSet Icon = FontAwesome.Solid.Square, Size = new Vector2(12), Rotation = 45, - Colour = OsuColour.FromHex(@"441288"), + Colour = Color4Extensions.FromHex(@"441288"), }, new SpriteIcon { @@ -132,7 +133,7 @@ namespace osu.Game.Overlays.BeatmapSet Origin = Anchor.Centre, Icon = icon, Size = new Vector2(12), - Colour = OsuColour.FromHex(@"f7dd55"), + Colour = Color4Extensions.FromHex(@"f7dd55"), Scale = new Vector2(0.8f), }, value = new OsuSpriteText diff --git a/osu.Game/Overlays/BeatmapSet/Buttons/HeaderButton.cs b/osu.Game/Overlays/BeatmapSet/Buttons/HeaderButton.cs index 6de1d3fca7..99b0b2ed3b 100644 --- a/osu.Game/Overlays/BeatmapSet/Buttons/HeaderButton.cs +++ b/osu.Game/Overlays/BeatmapSet/Buttons/HeaderButton.cs @@ -2,8 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; -using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays.BeatmapSet.Buttons @@ -19,9 +19,9 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons [BackgroundDependencyLoader] private void load() { - BackgroundColour = OsuColour.FromHex(@"094c5f"); - Triangles.ColourLight = OsuColour.FromHex(@"0f7c9b"); - Triangles.ColourDark = OsuColour.FromHex(@"094c5f"); + BackgroundColour = Color4Extensions.FromHex(@"094c5f"); + Triangles.ColourLight = Color4Extensions.FromHex(@"0f7c9b"); + Triangles.ColourDark = Color4Extensions.FromHex(@"094c5f"); Triangles.TriangleScale = 1.5f; } } diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs index 8abde8a24f..496986dc56 100644 --- a/osu.Game/Overlays/Chat/ChatLine.cs +++ b/osu.Game/Overlays/Chat/ChatLine.cs @@ -123,7 +123,7 @@ namespace osu.Game.Overlays.Chat EdgeEffect = new EdgeEffectParameters { Radius = 1, - Colour = OsuColour.FromHex(message.Sender.Colour), + Colour = Color4Extensions.FromHex(message.Sender.Colour), Type = EdgeEffectType.Shadow, }, Padding = new MarginPadding { Left = 3, Right = 3, Bottom = 1, Top = -3 }, @@ -172,7 +172,7 @@ namespace osu.Game.Overlays.Chat t.Font = OsuFont.GetFont(italics: true); if (senderHasBackground) - t.Colour = OsuColour.FromHex(message.Sender.Colour); + t.Colour = Color4Extensions.FromHex(message.Sender.Colour); } t.Font = t.Font.With(size: TextSize); @@ -249,41 +249,41 @@ namespace osu.Game.Overlays.Chat private static readonly Color4[] username_colours = { - OsuColour.FromHex("588c7e"), - OsuColour.FromHex("b2a367"), - OsuColour.FromHex("c98f65"), - OsuColour.FromHex("bc5151"), - OsuColour.FromHex("5c8bd6"), - OsuColour.FromHex("7f6ab7"), - OsuColour.FromHex("a368ad"), - OsuColour.FromHex("aa6880"), + Color4Extensions.FromHex("588c7e"), + Color4Extensions.FromHex("b2a367"), + Color4Extensions.FromHex("c98f65"), + Color4Extensions.FromHex("bc5151"), + Color4Extensions.FromHex("5c8bd6"), + Color4Extensions.FromHex("7f6ab7"), + Color4Extensions.FromHex("a368ad"), + Color4Extensions.FromHex("aa6880"), - OsuColour.FromHex("6fad9b"), - OsuColour.FromHex("f2e394"), - OsuColour.FromHex("f2ae72"), - OsuColour.FromHex("f98f8a"), - OsuColour.FromHex("7daef4"), - OsuColour.FromHex("a691f2"), - OsuColour.FromHex("c894d3"), - OsuColour.FromHex("d895b0"), + Color4Extensions.FromHex("6fad9b"), + Color4Extensions.FromHex("f2e394"), + Color4Extensions.FromHex("f2ae72"), + Color4Extensions.FromHex("f98f8a"), + Color4Extensions.FromHex("7daef4"), + Color4Extensions.FromHex("a691f2"), + Color4Extensions.FromHex("c894d3"), + Color4Extensions.FromHex("d895b0"), - OsuColour.FromHex("53c4a1"), - OsuColour.FromHex("eace5c"), - OsuColour.FromHex("ea8c47"), - OsuColour.FromHex("fc4f4f"), - OsuColour.FromHex("3d94ea"), - OsuColour.FromHex("7760ea"), - OsuColour.FromHex("af52c6"), - OsuColour.FromHex("e25696"), + Color4Extensions.FromHex("53c4a1"), + Color4Extensions.FromHex("eace5c"), + Color4Extensions.FromHex("ea8c47"), + Color4Extensions.FromHex("fc4f4f"), + Color4Extensions.FromHex("3d94ea"), + Color4Extensions.FromHex("7760ea"), + Color4Extensions.FromHex("af52c6"), + Color4Extensions.FromHex("e25696"), - OsuColour.FromHex("677c66"), - OsuColour.FromHex("9b8732"), - OsuColour.FromHex("8c5129"), - OsuColour.FromHex("8c3030"), - OsuColour.FromHex("1f5d91"), - OsuColour.FromHex("4335a5"), - OsuColour.FromHex("812a96"), - OsuColour.FromHex("992861"), + Color4Extensions.FromHex("677c66"), + Color4Extensions.FromHex("9b8732"), + Color4Extensions.FromHex("8c5129"), + Color4Extensions.FromHex("8c3030"), + Color4Extensions.FromHex("1f5d91"), + Color4Extensions.FromHex("4335a5"), + Color4Extensions.FromHex("812a96"), + Color4Extensions.FromHex("992861"), }; } } diff --git a/osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs b/osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs index 25a9a51638..b46ca6b040 100644 --- a/osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs +++ b/osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs @@ -41,10 +41,10 @@ namespace osu.Game.Overlays.Chat.Selection { RelativeSizeAxes = Axes.X; - Waves.FirstWaveColour = OsuColour.FromHex("353535"); - Waves.SecondWaveColour = OsuColour.FromHex("434343"); - Waves.ThirdWaveColour = OsuColour.FromHex("515151"); - Waves.FourthWaveColour = OsuColour.FromHex("595959"); + Waves.FirstWaveColour = Color4Extensions.FromHex("353535"); + Waves.SecondWaveColour = Color4Extensions.FromHex("434343"); + Waves.ThirdWaveColour = Color4Extensions.FromHex("515151"); + Waves.FourthWaveColour = Color4Extensions.FromHex("595959"); Children = new Drawable[] { @@ -154,7 +154,7 @@ namespace osu.Game.Overlays.Chat.Selection { bg.Colour = colours.Gray3; triangles.ColourDark = colours.Gray3; - triangles.ColourLight = OsuColour.FromHex(@"353535"); + triangles.ColourLight = Color4Extensions.FromHex(@"353535"); headerBg.Colour = colours.Gray2.Opacity(0.75f); } diff --git a/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs b/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs index 1413b8fe78..5b428a3825 100644 --- a/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs +++ b/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs @@ -89,7 +89,7 @@ namespace osu.Game.Overlays.Chat.Tabs { var user = Value.Users.First(); - BackgroundActive = user.Colour != null ? OsuColour.FromHex(user.Colour) : colours.BlueDark; + BackgroundActive = user.Colour != null ? Color4Extensions.FromHex(user.Colour) : colours.BlueDark; BackgroundInactive = BackgroundActive.Darken(0.5f); } } diff --git a/osu.Game/Overlays/Dialog/PopupDialog.cs b/osu.Game/Overlays/Dialog/PopupDialog.cs index 37db78faa1..02ef900dc5 100644 --- a/osu.Game/Overlays/Dialog/PopupDialog.cs +++ b/osu.Game/Overlays/Dialog/PopupDialog.cs @@ -10,7 +10,6 @@ using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; -using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Containers; using osuTK; @@ -114,13 +113,13 @@ namespace osu.Game.Overlays.Dialog new Box { RelativeSizeAxes = Axes.Both, - Colour = OsuColour.FromHex(@"221a21"), + Colour = Color4Extensions.FromHex(@"221a21"), }, new Triangles { RelativeSizeAxes = Axes.Both, - ColourLight = OsuColour.FromHex(@"271e26"), - ColourDark = OsuColour.FromHex(@"1e171e"), + ColourLight = Color4Extensions.FromHex(@"271e26"), + ColourDark = Color4Extensions.FromHex(@"1e171e"), TriangleScale = 4, }, }, diff --git a/osu.Game/Overlays/Dialog/PopupDialogButton.cs b/osu.Game/Overlays/Dialog/PopupDialogButton.cs index 75bae25b73..76ee438d6d 100644 --- a/osu.Game/Overlays/Dialog/PopupDialogButton.cs +++ b/osu.Game/Overlays/Dialog/PopupDialogButton.cs @@ -1,7 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Game.Graphics; +using osu.Framework.Extensions.Color4Extensions; using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays.Dialog @@ -11,7 +11,7 @@ namespace osu.Game.Overlays.Dialog public PopupDialogButton() { Height = 50; - BackgroundColour = OsuColour.FromHex(@"150e14"); + BackgroundColour = Color4Extensions.FromHex(@"150e14"); TextSize = 18; } } diff --git a/osu.Game/Overlays/Direct/FilterControl.cs b/osu.Game/Overlays/Direct/FilterControl.cs index 70a3ab54fb..e5b2b5cc34 100644 --- a/osu.Game/Overlays/Direct/FilterControl.cs +++ b/osu.Game/Overlays/Direct/FilterControl.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Game.Graphics; using osu.Game.Online.API.Requests; @@ -16,7 +17,7 @@ namespace osu.Game.Overlays.Direct { private DirectRulesetSelector rulesetSelector; - protected override Color4 BackgroundColour => OsuColour.FromHex(@"384552"); + protected override Color4 BackgroundColour => Color4Extensions.FromHex(@"384552"); protected override DirectSortCriteria DefaultTab => DirectSortCriteria.Ranked; protected override BeatmapSearchCategory DefaultCategory => BeatmapSearchCategory.Leaderboard; diff --git a/osu.Game/Overlays/Direct/Header.cs b/osu.Game/Overlays/Direct/Header.cs index 80870dcb68..5b3e394a18 100644 --- a/osu.Game/Overlays/Direct/Header.cs +++ b/osu.Game/Overlays/Direct/Header.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.ComponentModel; +using osu.Framework.Extensions.Color4Extensions; using osuTK.Graphics; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; @@ -13,7 +14,7 @@ namespace osu.Game.Overlays.Direct { public class Header : SearchableListHeader { - protected override Color4 BackgroundColour => OsuColour.FromHex(@"252f3a"); + protected override Color4 BackgroundColour => Color4Extensions.FromHex(@"252f3a"); protected override DirectTab DefaultTab => DirectTab.Search; protected override Drawable CreateHeaderText() => new OsuSpriteText { Text = @"osu!direct", Font = OsuFont.GetFont(size: 25) }; diff --git a/osu.Game/Overlays/DirectOverlay.cs b/osu.Game/Overlays/DirectOverlay.cs index a6f8b65a0d..61986d1cf0 100644 --- a/osu.Game/Overlays/DirectOverlay.cs +++ b/osu.Game/Overlays/DirectOverlay.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Humanizer; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Threading; @@ -34,9 +35,9 @@ namespace osu.Game.Overlays private readonly OsuSpriteText resultCountsText; private FillFlowContainer panels; - protected override Color4 BackgroundColour => OsuColour.FromHex(@"485e74"); - protected override Color4 TrianglesColourLight => OsuColour.FromHex(@"465b71"); - protected override Color4 TrianglesColourDark => OsuColour.FromHex(@"3f5265"); + protected override Color4 BackgroundColour => Color4Extensions.FromHex(@"485e74"); + protected override Color4 TrianglesColourLight => Color4Extensions.FromHex(@"465b71"); + protected override Color4 TrianglesColourDark => Color4Extensions.FromHex(@"3f5265"); protected override SearchableListHeader CreateHeader() => new Header(); protected override SearchableListFilterControl CreateFilterControl() => new FilterControl(); diff --git a/osu.Game/Overlays/MedalOverlay.cs b/osu.Game/Overlays/MedalOverlay.cs index aa28b0659d..4425c2f168 100644 --- a/osu.Game/Overlays/MedalOverlay.cs +++ b/osu.Game/Overlays/MedalOverlay.cs @@ -126,14 +126,14 @@ namespace osu.Game.Overlays new Box { RelativeSizeAxes = Axes.Both, - Colour = OsuColour.FromHex(@"05262f"), + Colour = Color4Extensions.FromHex(@"05262f"), }, new Triangles { RelativeSizeAxes = Axes.Both, TriangleScale = 2, - ColourDark = OsuColour.FromHex(@"04222b"), - ColourLight = OsuColour.FromHex(@"052933"), + ColourDark = Color4Extensions.FromHex(@"04222b"), + ColourLight = Color4Extensions.FromHex(@"052933"), }, innerSpin = new Sprite { diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 466c953151..e9b3598625 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -63,10 +63,10 @@ namespace osu.Game.Overlays.Mods public ModSelectOverlay() { - Waves.FirstWaveColour = OsuColour.FromHex(@"19b0e2"); - Waves.SecondWaveColour = OsuColour.FromHex(@"2280a2"); - Waves.ThirdWaveColour = OsuColour.FromHex(@"005774"); - Waves.FourthWaveColour = OsuColour.FromHex(@"003a4e"); + Waves.FirstWaveColour = Color4Extensions.FromHex(@"19b0e2"); + Waves.SecondWaveColour = Color4Extensions.FromHex(@"2280a2"); + Waves.ThirdWaveColour = Color4Extensions.FromHex(@"005774"); + Waves.FourthWaveColour = Color4Extensions.FromHex(@"003a4e"); RelativeSizeAxes = Axes.Both; diff --git a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs index 6ed4fc3187..2cc1f6533f 100644 --- a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -170,7 +171,7 @@ namespace osu.Game.Overlays.Profile.Header userCountryText.Text = user?.Country?.FullName ?? "Alien"; supporterTag.SupportLevel = user?.SupportLevel ?? 0; titleText.Text = user?.Title ?? string.Empty; - titleText.Colour = OsuColour.FromHex(user?.Colour ?? "fff"); + titleText.Colour = Color4Extensions.FromHex(user?.Colour ?? "fff"); userStats.Clear(); diff --git a/osu.Game/Overlays/Profile/ProfileHeader.cs b/osu.Game/Overlays/Profile/ProfileHeader.cs index 3e78423a5a..f7c09e33c1 100644 --- a/osu.Game/Overlays/Profile/ProfileHeader.cs +++ b/osu.Game/Overlays/Profile/ProfileHeader.cs @@ -7,7 +7,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Profile.Header; using osu.Game.Users; @@ -48,7 +47,7 @@ namespace osu.Game.Overlays.Profile new Box { RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientVertical(OsuColour.FromHex("222").Opacity(0.8f), OsuColour.FromHex("222").Opacity(0.2f)) + Colour = ColourInfo.GradientVertical(Color4Extensions.FromHex("222").Opacity(0.8f), Color4Extensions.FromHex("222").Opacity(0.2f)) }, } }; diff --git a/osu.Game/Overlays/Social/FilterControl.cs b/osu.Game/Overlays/Social/FilterControl.cs index 1c2cb95dfe..93fcc3c401 100644 --- a/osu.Game/Overlays/Social/FilterControl.cs +++ b/osu.Game/Overlays/Social/FilterControl.cs @@ -1,16 +1,16 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Extensions.Color4Extensions; using osuTK.Graphics; using osu.Framework.Graphics; -using osu.Game.Graphics; using osu.Game.Overlays.SearchableList; namespace osu.Game.Overlays.Social { public class FilterControl : SearchableListFilterControl { - protected override Color4 BackgroundColour => OsuColour.FromHex(@"47253a"); + protected override Color4 BackgroundColour => Color4Extensions.FromHex(@"47253a"); protected override SocialSortCriteria DefaultTab => SocialSortCriteria.Rank; protected override SortDirection DefaultCategory => SortDirection.Ascending; diff --git a/osu.Game/Overlays/Social/Header.cs b/osu.Game/Overlays/Social/Header.cs index 22bca9b421..22e0fdcd56 100644 --- a/osu.Game/Overlays/Social/Header.cs +++ b/osu.Game/Overlays/Social/Header.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Framework.Allocation; using System.ComponentModel; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics.Sprites; namespace osu.Game.Overlays.Social @@ -17,7 +18,7 @@ namespace osu.Game.Overlays.Social { private OsuSpriteText browser; - protected override Color4 BackgroundColour => OsuColour.FromHex(@"38202e"); + protected override Color4 BackgroundColour => Color4Extensions.FromHex(@"38202e"); protected override SocialTab DefaultTab => SocialTab.AllPlayers; protected override IconUsage Icon => FontAwesome.Solid.Users; diff --git a/osu.Game/Overlays/SocialOverlay.cs b/osu.Game/Overlays/SocialOverlay.cs index 54c978738d..50c05e1b54 100644 --- a/osu.Game/Overlays/SocialOverlay.cs +++ b/osu.Game/Overlays/SocialOverlay.cs @@ -9,7 +9,6 @@ using osuTK; using osuTK.Graphics; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.API.Requests; @@ -18,6 +17,7 @@ using osu.Game.Overlays.Social; using osu.Game.Users; using System.Threading; using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Threading; namespace osu.Game.Overlays @@ -27,9 +27,9 @@ namespace osu.Game.Overlays private readonly LoadingSpinner loading; private FillFlowContainer panels; - protected override Color4 BackgroundColour => OsuColour.FromHex(@"60284b"); - protected override Color4 TrianglesColourLight => OsuColour.FromHex(@"672b51"); - protected override Color4 TrianglesColourDark => OsuColour.FromHex(@"5c2648"); + protected override Color4 BackgroundColour => Color4Extensions.FromHex(@"60284b"); + protected override Color4 TrianglesColourLight => Color4Extensions.FromHex(@"672b51"); + protected override Color4 TrianglesColourDark => Color4Extensions.FromHex(@"5c2648"); protected override SearchableListHeader CreateHeader() => new Header(); protected override SearchableListFilterControl CreateFilterControl() => new FilterControl(); diff --git a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs index 752615245e..afd9e3d760 100644 --- a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs +++ b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -26,7 +27,7 @@ namespace osu.Game.Screens.Edit.Components.Menus MaskingContainer.CornerRadius = 0; ItemsContainer.Padding = new MarginPadding { Left = 100 }; - BackgroundColour = OsuColour.FromHex("111"); + BackgroundColour = Color4Extensions.FromHex("111"); ScreenSelectionTabControl tabControl; AddRangeInternal(new Drawable[] diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs index 02e5db306d..b99a053859 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs @@ -2,11 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; -using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osuTK; @@ -31,7 +31,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline new Box { RelativeSizeAxes = Axes.Both, - Colour = OsuColour.FromHex("111") + Colour = Color4Extensions.FromHex("111") }, new GridContainer { @@ -49,7 +49,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline new Box { RelativeSizeAxes = Axes.Both, - Colour = OsuColour.FromHex("222") + Colour = Color4Extensions.FromHex("222") }, new FillFlowContainer { @@ -76,7 +76,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline new Box { RelativeSizeAxes = Axes.Both, - Colour = OsuColour.FromHex("333") + Colour = Color4Extensions.FromHex("333") }, new Container { diff --git a/osu.Game/Screens/Menu/IntroSequence.cs b/osu.Game/Screens/Menu/IntroSequence.cs index e2dd14b18c..6731fef6f7 100644 --- a/osu.Game/Screens/Menu/IntroSequence.cs +++ b/osu.Game/Screens/Menu/IntroSequence.cs @@ -94,7 +94,7 @@ namespace osu.Game.Screens.Menu }, } }, - bigRing = new Ring(OsuColour.FromHex(@"B6C5E9"), 0.85f), + bigRing = new Ring(Color4Extensions.FromHex(@"B6C5E9"), 0.85f), mediumRing = new Ring(Color4.White.Opacity(130), 0.7f), smallRing = new Ring(Color4.White, 0.6f), welcomeText = new OsuSpriteText @@ -121,7 +121,7 @@ namespace osu.Game.Screens.Menu Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, Height = 0, - Colour = OsuColour.FromHex(@"C6D8FF").Opacity(160), + Colour = Color4Extensions.FromHex(@"C6D8FF").Opacity(160), }, foregroundFill = new Box { @@ -139,28 +139,28 @@ namespace osu.Game.Screens.Menu Anchor = Anchor.Centre, Origin = Anchor.TopCentre, Position = new Vector2(0, circle_offset), - Colour = OsuColour.FromHex(@"AA92FF"), + Colour = Color4Extensions.FromHex(@"AA92FF"), }, blueCircle = new Circle { Anchor = Anchor.Centre, Origin = Anchor.CentreRight, Position = new Vector2(-circle_offset, 0), - Colour = OsuColour.FromHex(@"8FE5FE"), + Colour = Color4Extensions.FromHex(@"8FE5FE"), }, yellowCircle = new Circle { Anchor = Anchor.Centre, Origin = Anchor.BottomCentre, Position = new Vector2(0, -circle_offset), - Colour = OsuColour.FromHex(@"FFD64C"), + Colour = Color4Extensions.FromHex(@"FFD64C"), }, pinkCircle = new Circle { Anchor = Anchor.Centre, Origin = Anchor.CentreLeft, Position = new Vector2(circle_offset, 0), - Colour = OsuColour.FromHex(@"e967a1"), + Colour = Color4Extensions.FromHex(@"e967a1"), }, }; diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index be2f29cbe9..800520100e 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -6,6 +6,7 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Audio.Track; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -14,7 +15,6 @@ using osu.Framework.Graphics.Textures; using osu.Framework.Input.Events; using osu.Framework.Utils; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Containers; using osuTK; @@ -28,7 +28,7 @@ namespace osu.Game.Screens.Menu /// public class OsuLogo : BeatSyncedContainer { - public readonly Color4 OsuPink = OsuColour.FromHex(@"e967a1"); + public readonly Color4 OsuPink = Color4Extensions.FromHex(@"e967a1"); private const double transition_length = 300; @@ -176,8 +176,8 @@ namespace osu.Game.Screens.Menu triangles = new Triangles { TriangleScale = 4, - ColourLight = OsuColour.FromHex(@"ff7db7"), - ColourDark = OsuColour.FromHex(@"de5b95"), + ColourLight = Color4Extensions.FromHex(@"ff7db7"), + ColourDark = Color4Extensions.FromHex(@"de5b95"), RelativeSizeAxes = Axes.Both, }, } diff --git a/osu.Game/Screens/Multi/Components/DrawableGameType.cs b/osu.Game/Screens/Multi/Components/DrawableGameType.cs index f4941dd73a..28240f0796 100644 --- a/osu.Game/Screens/Multi/Components/DrawableGameType.cs +++ b/osu.Game/Screens/Multi/Components/DrawableGameType.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; @@ -27,7 +28,7 @@ namespace osu.Game.Screens.Multi.Components new Box { RelativeSizeAxes = Axes.Both, - Colour = OsuColour.FromHex(@"545454"), + Colour = Color4Extensions.FromHex(@"545454"), }, }; } diff --git a/osu.Game/Screens/Multi/Components/ParticipantsList.cs b/osu.Game/Screens/Multi/Components/ParticipantsList.cs index 5a2dc19b66..79d130adf5 100644 --- a/osu.Game/Screens/Multi/Components/ParticipantsList.cs +++ b/osu.Game/Screens/Multi/Components/ParticipantsList.cs @@ -2,12 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Threading; -using osu.Game.Graphics; using osu.Game.Users; using osu.Game.Users.Drawables; using osuTK; @@ -114,7 +114,7 @@ namespace osu.Game.Screens.Multi.Components new Box { RelativeSizeAxes = Axes.Both, - Colour = OsuColour.FromHex(@"27252d"), + Colour = Color4Extensions.FromHex(@"27252d"), }, avatar = new UpdateableAvatar { RelativeSizeAxes = Axes.Both }, }; diff --git a/osu.Game/Screens/Multi/Header.cs b/osu.Game/Screens/Multi/Header.cs index 1cbf2a45e7..0a05472ba3 100644 --- a/osu.Game/Screens/Multi/Header.cs +++ b/osu.Game/Screens/Multi/Header.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -30,7 +31,7 @@ namespace osu.Game.Screens.Multi new Box { RelativeSizeAxes = Axes.Both, - Colour = OsuColour.FromHex(@"2f2043"), + Colour = Color4Extensions.FromHex(@"2f2043"), }, new Container { diff --git a/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs index d45dac1ae6..de02d779e1 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs @@ -134,7 +134,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components new Box { RelativeSizeAxes = Axes.Both, - Colour = OsuColour.FromHex(@"212121"), + Colour = Color4Extensions.FromHex(@"212121"), }, new StatusColouredContainer(transition_duration) { diff --git a/osu.Game/Screens/Multi/Match/Components/Footer.cs b/osu.Game/Screens/Multi/Match/Components/Footer.cs index c0c866d815..94d7df6194 100644 --- a/osu.Game/Screens/Multi/Match/Components/Footer.cs +++ b/osu.Game/Screens/Multi/Match/Components/Footer.cs @@ -4,6 +4,7 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -44,7 +45,7 @@ namespace osu.Game.Screens.Multi.Match.Components [BackgroundDependencyLoader] private void load(OsuColour colours) { - background.Colour = OsuColour.FromHex(@"28242d"); + background.Colour = Color4Extensions.FromHex(@"28242d"); } } } diff --git a/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs b/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs index 115ac5037a..5d68de9ce6 100644 --- a/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs +++ b/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs @@ -91,7 +91,7 @@ namespace osu.Game.Screens.Multi.Match.Components new Box { RelativeSizeAxes = Axes.Both, - Colour = OsuColour.FromHex(@"28242d"), + Colour = Color4Extensions.FromHex(@"28242d"), }, new GridContainer { @@ -270,7 +270,7 @@ namespace osu.Game.Screens.Multi.Match.Components new Box { RelativeSizeAxes = Axes.Both, - Colour = OsuColour.FromHex(@"28242d").Darken(0.5f).Opacity(1f), + Colour = Color4Extensions.FromHex(@"28242d").Darken(0.5f).Opacity(1f), }, new FillFlowContainer { diff --git a/osu.Game/Screens/Multi/Match/Components/PurpleTriangleButton.cs b/osu.Game/Screens/Multi/Match/Components/PurpleTriangleButton.cs index 8a0369ceba..1d93116d07 100644 --- a/osu.Game/Screens/Multi/Match/Components/PurpleTriangleButton.cs +++ b/osu.Game/Screens/Multi/Match/Components/PurpleTriangleButton.cs @@ -2,7 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Game.Graphics; +using osu.Framework.Extensions.Color4Extensions; using osu.Game.Graphics.UserInterface; namespace osu.Game.Screens.Multi.Match.Components @@ -12,9 +12,9 @@ namespace osu.Game.Screens.Multi.Match.Components [BackgroundDependencyLoader] private void load() { - BackgroundColour = OsuColour.FromHex(@"593790"); - Triangles.ColourLight = OsuColour.FromHex(@"7247b6"); - Triangles.ColourDark = OsuColour.FromHex(@"593790"); + BackgroundColour = Color4Extensions.FromHex(@"593790"); + Triangles.ColourLight = Color4Extensions.FromHex(@"7247b6"); + Triangles.ColourDark = Color4Extensions.FromHex(@"593790"); } } } diff --git a/osu.Game/Screens/Multi/Match/Components/RoomAvailabilityPicker.cs b/osu.Game/Screens/Multi/Match/Components/RoomAvailabilityPicker.cs index 9de4a61cde..7ef39c2a74 100644 --- a/osu.Game/Screens/Multi/Match/Components/RoomAvailabilityPicker.cs +++ b/osu.Game/Screens/Multi/Match/Components/RoomAvailabilityPicker.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Extensions; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; @@ -52,7 +53,7 @@ namespace osu.Game.Screens.Multi.Match.Components new Box { RelativeSizeAxes = Axes.Both, - Colour = OsuColour.FromHex(@"3d3943"), + Colour = Color4Extensions.FromHex(@"3d3943"), }, selection = new Box { diff --git a/osu.Game/Screens/Multi/Multiplayer.cs b/osu.Game/Screens/Multi/Multiplayer.cs index b0d773869a..863a28609b 100644 --- a/osu.Game/Screens/Multi/Multiplayer.cs +++ b/osu.Game/Screens/Multi/Multiplayer.cs @@ -13,7 +13,6 @@ using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; -using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Input; @@ -75,7 +74,7 @@ namespace osu.Game.Screens.Multi RelativeSizeAxes = Axes.Both; Padding = new MarginPadding { Horizontal = -HORIZONTAL_OVERFLOW_PADDING }; - var backgroundColour = OsuColour.FromHex(@"3e3a44"); + var backgroundColour = Color4Extensions.FromHex(@"3e3a44"); InternalChild = waves = new MultiplayerWaveContainer { @@ -386,10 +385,10 @@ namespace osu.Game.Screens.Multi public MultiplayerWaveContainer() { - FirstWaveColour = OsuColour.FromHex(@"654d8c"); - SecondWaveColour = OsuColour.FromHex(@"554075"); - ThirdWaveColour = OsuColour.FromHex(@"44325e"); - FourthWaveColour = OsuColour.FromHex(@"392850"); + FirstWaveColour = Color4Extensions.FromHex(@"654d8c"); + SecondWaveColour = Color4Extensions.FromHex(@"554075"); + ThirdWaveColour = Color4Extensions.FromHex(@"44325e"); + FourthWaveColour = Color4Extensions.FromHex(@"392850"); } } diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index cf49cf0228..f84aac3081 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -384,7 +384,7 @@ namespace osu.Game.Screens.Select Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - Colour = OsuColour.FromHex(@"441288"), + Colour = Color4Extensions.FromHex(@"441288"), Icon = FontAwesome.Solid.Square, Rotation = 45, }, @@ -394,7 +394,7 @@ namespace osu.Game.Screens.Select Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, Scale = new Vector2(0.8f), - Colour = OsuColour.FromHex(@"f7dd55"), + Colour = Color4Extensions.FromHex(@"f7dd55"), Icon = statistic.Icon, }, } diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index 50419a5fb9..841bbf415c 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; @@ -69,8 +70,8 @@ namespace osu.Game.Screens.Select.Carousel { TriangleScale = 2, RelativeSizeAxes = Axes.Both, - ColourLight = OsuColour.FromHex(@"3a7285"), - ColourDark = OsuColour.FromHex(@"123744") + ColourLight = Color4Extensions.FromHex(@"3a7285"), + ColourDark = Color4Extensions.FromHex(@"123744") }, new FillFlowContainer { From 9683c0756d391930d6514e4d3a93ba9021ad623a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 11 Mar 2020 12:56:12 +0900 Subject: [PATCH 0313/2376] Start path at (0,0) --- osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs index 5b3a114506..f39395ba44 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs @@ -91,10 +91,11 @@ namespace osu.Game.Rulesets.Catch.Tests public TestJuiceStream(float x) { X = x; + Path = new SliderPath(new[] { - new PathControlPoint(new Vector2(x, 0)), - new PathControlPoint(new Vector2(x + 30, 0)), + new PathControlPoint(Vector2.Zero), + new PathControlPoint(new Vector2(30, 0)), }); } } From 638a9a24aa20d9c028dd750ae59b6075f781e93c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Mar 2020 02:35:49 +0900 Subject: [PATCH 0314/2376] Initial disclaimer updates --- osu.Game/Screens/Menu/Disclaimer.cs | 89 ++++++++++++++++++++--------- 1 file changed, 62 insertions(+), 27 deletions(-) diff --git a/osu.Game/Screens/Menu/Disclaimer.cs b/osu.Game/Screens/Menu/Disclaimer.cs index bcab73715b..2c1b0c3166 100644 --- a/osu.Game/Screens/Menu/Disclaimer.cs +++ b/osu.Game/Screens/Menu/Disclaimer.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Screens; +using osu.Framework.Utils; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Online.API; @@ -33,6 +34,7 @@ namespace osu.Game.Screens.Menu private readonly OsuScreen nextScreen; private readonly Bindable currentUser = new Bindable(); + private FillFlowContainer fill; public Disclaimer(OsuScreen nextScreen = null) { @@ -49,16 +51,16 @@ namespace osu.Game.Screens.Menu { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Icon = FontAwesome.Solid.ExclamationTriangle, + Icon = FontAwesome.Solid.Poo, Size = new Vector2(icon_size), Y = icon_y, }, - new FillFlowContainer + fill = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, - Y = icon_y + icon_size, + Y = icon_y, Anchor = Anchor.Centre, Origin = Anchor.TopCentre, Children = new Drawable[] @@ -71,6 +73,8 @@ namespace osu.Game.Screens.Menu Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Spacing = new Vector2(0, 2), + LayoutDuration = 2000, + LayoutEasing = Easing.OutQuint }, supportFlow = new LinkFlowContainer { @@ -86,23 +90,16 @@ namespace osu.Game.Screens.Menu } }; - textFlow.AddText("This is an ", t => t.Font = t.Font.With(Typeface.Exo, 30, FontWeight.Light)); - textFlow.AddText("early development build", t => t.Font = t.Font.With(Typeface.Exo, 30, FontWeight.SemiBold)); + textFlow.AddText("This project is an ongoing ", t => t.Font = t.Font.With(Typeface.Exo, 30, FontWeight.Light)); + textFlow.AddText("work in progress", t => t.Font = t.Font.With(Typeface.Exo, 30, FontWeight.SemiBold)); - textFlow.AddParagraph("Things may not work as expected", t => t.Font = t.Font.With(size: 20)); textFlow.NewParagraph(); static void format(SpriteText t) => t.Font = OsuFont.GetFont(size: 15, weight: FontWeight.SemiBold); - textFlow.AddParagraph("Detailed bug reports are welcomed via github issues.", format); + textFlow.AddParagraph(getRandomTip(), t => t.Font = t.Font.With(Typeface.Exo, 20, FontWeight.SemiBold)); textFlow.NewParagraph(); - textFlow.AddText("Visit ", format); - textFlow.AddLink("discord.gg/ppy", "https://discord.gg/ppy", creationParameters: format); - textFlow.AddText(" to help out or follow progress!", format); - - textFlow.NewParagraph(); - textFlow.NewParagraph(); textFlow.NewParagraph(); iconColour = colours.Yellow; @@ -114,7 +111,7 @@ namespace osu.Game.Screens.Menu if (e.NewValue.IsSupporter) { - supportFlow.AddText("Thank you for supporting osu!", format); + supportFlow.AddText("Eternal thanks to you for supporting osu!", format); } else { @@ -125,7 +122,7 @@ namespace osu.Game.Screens.Menu heart = supportFlow.AddIcon(FontAwesome.Solid.Heart, t => { - t.Padding = new MarginPadding { Left = 5 }; + t.Padding = new MarginPadding { Left = 5, Top = 3 }; t.Font = t.Font.With(size: 12); t.Origin = Anchor.Centre; t.Colour = colours.Pink; @@ -139,11 +136,6 @@ namespace osu.Game.Screens.Menu }, true); } - private void animateHeart() - { - heart.FlashColour(Color4.White, 750, Easing.OutQuint).Loop(); - } - protected override void LoadComplete() { base.LoadComplete(); @@ -155,15 +147,28 @@ namespace osu.Game.Screens.Menu { base.OnEntering(last); - icon.Delay(1000).FadeColour(iconColour, 200, Easing.OutQuint); - icon.Delay(1000) - .MoveToY(icon_y * 1.1f, 160, Easing.OutCirc) - .RotateTo(-10, 160, Easing.OutCirc) - .Then() - .MoveToY(icon_y, 160, Easing.InCirc) - .RotateTo(0, 160, Easing.InCirc); + icon.RotateTo(10); + icon.FadeOut(); + icon.ScaleTo(0.5f); + + icon.Delay(500).FadeIn(500).ScaleTo(1, 500, Easing.OutQuint); + + using (BeginDelayedSequence(3000, true)) + { + icon.FadeColour(iconColour, 200, Easing.OutQuint); + icon.MoveToY(icon_y * 1.3f, 500, Easing.OutCirc) + .RotateTo(-360, 520, Easing.OutQuint) + .Then() + .MoveToY(icon_y, 160, Easing.InQuart) + .FadeColour(Color4.White, 160); + + fill.Delay(520 + 160).MoveToOffset(new Vector2(0, 15), 160, Easing.OutQuart); + } supportFlow.FadeOut().Delay(2000).FadeIn(500); + double delay = 500; + foreach (var c in textFlow.Children) + c.FadeTo(0.001f).Delay(delay += 20).FadeIn(500); animateHeart(); @@ -178,5 +183,35 @@ namespace osu.Game.Screens.Menu this.Push(nextScreen); }); } + + private string getRandomTip() + { + string[] tips = + { + "You can press Ctrl-T anywhere in the game to toggle the toolbar!", + "You can press Ctrl-O anywhere in the game to access options!", + "All settings are dynamic and take effect in real-time. Try changing the skin while playing!", + "New features are coming online every update. Make sure to stay up-to-date!", + "If you find the UI too large or small, try adjusting UI scale in settings!", + "Try adjusting the \"Screen Scaling\" mode to change your gameplay or UI area, even in fullscreen!", + "For now, osu!direct is available to all users on lazer. You can access it anywhere using Ctrl-D!", + "Seeking in replays is available by dragging on the difficulty bar at the bottom of the screen!", + "Multithreading support means that even with low \"FPS\" your input and judgements will be accurate!", + "Try scrolling down in the mod select panel to find a bunch of new fun mods!", + "Most of the web content (profiles, rankings, etc.) are available natively in-game from the icons on the toolbar!", + "Get more details, hide or delete a beatmap by right-clicking on its panel at song select!", + "All delete operations are temoprary until quit. Restore accidentally deleted content from the maintenance settings!", + "Check out the \"timeshift\" multiplayer system, which has local permanent leaderboards and playlist support!", + "Toggle advanced frame / thread statistics with Ctrl-F11!", + "Take a look under the hood at performance counters and enable verbose performance logging with Ctrl-F2!", + }; + + return tips[RNG.Next(0, tips.Length)]; + } + + private void animateHeart() + { + heart.FlashColour(Color4.White, 750, Easing.OutQuint).Loop(); + } } } From b8d3e644166381e5f3d3b3e52a4c213dd939253c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Mar 2020 02:49:20 +0900 Subject: [PATCH 0315/2376] Rename loader test scene --- .../Menus/{TestSceneLoaderAnimation.cs => TestSceneLoader.cs} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename osu.Game.Tests/Visual/Menus/{TestSceneLoaderAnimation.cs => TestSceneLoader.cs} (96%) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneLoaderAnimation.cs b/osu.Game.Tests/Visual/Menus/TestSceneLoader.cs similarity index 96% rename from osu.Game.Tests/Visual/Menus/TestSceneLoaderAnimation.cs rename to osu.Game.Tests/Visual/Menus/TestSceneLoader.cs index 61fed3013e..82b0139155 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneLoaderAnimation.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneLoader.cs @@ -14,14 +14,14 @@ using osuTK.Graphics; namespace osu.Game.Tests.Visual.Menus { [TestFixture] - public class TestSceneLoaderAnimation : ScreenTestScene + public class TestSceneLoader : ScreenTestScene { private TestLoader loader; [Cached] private OsuLogo logo; - public TestSceneLoaderAnimation() + public TestSceneLoader() { Child = logo = new OsuLogo { From 4012e878b06dedfe739a6725cb30b7b515bad78c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Mar 2020 02:49:34 +0900 Subject: [PATCH 0316/2376] Update loader look --- .../Graphics/UserInterface/LoadingSpinner.cs | 6 ++- osu.Game/Screens/Loader.cs | 39 ++++++++----------- 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/LoadingSpinner.cs b/osu.Game/Graphics/UserInterface/LoadingSpinner.cs index 36d429b8c1..fed3dda579 100644 --- a/osu.Game/Graphics/UserInterface/LoadingSpinner.cs +++ b/osu.Game/Graphics/UserInterface/LoadingSpinner.cs @@ -27,7 +27,8 @@ namespace osu.Game.Graphics.UserInterface /// Constuct a new loading spinner. /// /// Whether the spinner should have a surrounding black box for visibility. - public LoadingSpinner(bool withBox = false) + /// Whether colours should be inverted (black spinner instead of white). + public LoadingSpinner(bool withBox = false, bool inverted = false) { Size = new Vector2(60); @@ -45,7 +46,7 @@ namespace osu.Game.Graphics.UserInterface { new Box { - Colour = Color4.Black, + Colour = inverted ? Color4.White : Color4.Black, RelativeSizeAxes = Axes.Both, Alpha = withBox ? 0.7f : 0 }, @@ -53,6 +54,7 @@ namespace osu.Game.Graphics.UserInterface { Anchor = Anchor.Centre, Origin = Anchor.Centre, + Colour = inverted ? Color4.Black : Color4.White, Scale = new Vector2(withBox ? 0.6f : 1), RelativeSizeAxes = Axes.Both, Icon = FontAwesome.Solid.CircleNotch diff --git a/osu.Game/Screens/Loader.cs b/osu.Game/Screens/Loader.cs index 289413c65a..d26dc0d660 100644 --- a/osu.Game/Screens/Loader.cs +++ b/osu.Game/Screens/Loader.cs @@ -8,9 +8,10 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Shaders; using osu.Framework.Utils; using osu.Game.Screens.Menu; -using osuTK; using osu.Framework.Screens; +using osu.Framework.Threading; using osu.Game.Configuration; +using osu.Game.Graphics.UserInterface; using IntroSequence = osu.Game.Configuration.IntroSequence; namespace osu.Game.Screens @@ -24,31 +25,12 @@ namespace osu.Game.Screens ValidForResume = false; } - protected override void LogoArriving(OsuLogo logo, bool resuming) - { - base.LogoArriving(logo, resuming); - - logo.BeatMatching = false; - logo.Triangles = false; - logo.RelativePositionAxes = Axes.None; - logo.Origin = Anchor.BottomRight; - logo.Anchor = Anchor.BottomRight; - logo.Position = new Vector2(-40); - logo.Scale = new Vector2(0.2f); - - logo.Delay(500).FadeInFromZero(1000, Easing.OutQuint); - } - - protected override void LogoSuspending(OsuLogo logo) - { - base.LogoSuspending(logo); - logo.FadeOut(logo.Alpha * 400); - } - private OsuScreen loadableScreen; private ShaderPrecompiler precompiler; private IntroSequence introSequence; + private LoadingSpinner spinner; + private ScheduledDelegate spinnerShow; protected virtual OsuScreen CreateLoadableScreen() { @@ -82,6 +64,17 @@ namespace osu.Game.Screens LoadComponentAsync(precompiler = CreateShaderPrecompiler(), AddInternal); LoadComponentAsync(loadableScreen = CreateLoadableScreen()); + LoadComponentAsync(spinner = new LoadingSpinner(true, true) + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Margin = new MarginPadding(40), + }, _ => + { + AddInternal(spinner); + spinnerShow = Scheduler.AddDelayed(spinner.Show, 200); + }); + checkIfLoaded(); } @@ -93,6 +86,8 @@ namespace osu.Game.Screens return; } + spinnerShow?.Cancel(); + spinner.Hide(); this.Push(loadableScreen); } From ec88f7a71250bb5acf50273fadcdbb3da1bd3ee7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Mar 2020 13:20:31 +0900 Subject: [PATCH 0317/2376] Update tests and delay push animation until loader is done disappearing --- .../Visual/Menus/TestSceneLoader.cs | 23 +++++++++++-------- .../Graphics/UserInterface/LoadingSpinner.cs | 2 +- osu.Game/Screens/Loader.cs | 11 +++++++-- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneLoader.cs b/osu.Game.Tests/Visual/Menus/TestSceneLoader.cs index 82b0139155..6003d05ecd 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneLoader.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneLoader.cs @@ -1,12 +1,15 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using System.Threading; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Screens; +using osu.Framework.Testing; +using osu.Game.Graphics.UserInterface; using osu.Game.Screens; using osu.Game.Screens.Menu; using osuTK.Graphics; @@ -42,33 +45,33 @@ namespace osu.Game.Tests.Visual.Menus LoadScreen(loader); }); + + AddAssert("spinner did not display", () => loader.LoadingSpinner.Alpha == 0); + + AddUntilStep("loaded", () => loader.ScreenLoaded); + AddUntilStep("not current", () => !loader.IsCurrentScreen()); } [Test] public void TestDelayedLoad() { AddStep("begin loading", () => LoadScreen(loader = new TestLoader())); - AddUntilStep("wait for logo visible", () => loader.Logo?.Alpha > 0); + AddUntilStep("wait for spinner visible", () => loader.LoadingSpinner?.Alpha > 0); AddStep("finish loading", () => loader.AllowLoad.Set()); - AddUntilStep("loaded", () => loader.Logo != null && loader.ScreenLoaded); - AddUntilStep("logo gone", () => loader.Logo?.Alpha == 0); + AddUntilStep("spinner gone", () => loader.LoadingSpinner?.Alpha == 0); + AddUntilStep("loaded", () => loader.ScreenLoaded); + AddUntilStep("not current", () => !loader.IsCurrentScreen()); } private class TestLoader : Loader { public readonly ManualResetEventSlim AllowLoad = new ManualResetEventSlim(); - public OsuLogo Logo; + public LoadingSpinner LoadingSpinner => this.ChildrenOfType().Single(); private TestScreen screen; public bool ScreenLoaded => screen.IsCurrentScreen(); - protected override void LogoArriving(OsuLogo logo, bool resuming) - { - Logo = logo; - base.LogoArriving(logo, resuming); - } - protected override OsuScreen CreateLoadableScreen() => screen = new TestScreen(); protected override ShaderPrecompiler CreateShaderPrecompiler() => new TestShaderPrecompiler(AllowLoad); diff --git a/osu.Game/Graphics/UserInterface/LoadingSpinner.cs b/osu.Game/Graphics/UserInterface/LoadingSpinner.cs index fed3dda579..4f4607c114 100644 --- a/osu.Game/Graphics/UserInterface/LoadingSpinner.cs +++ b/osu.Game/Graphics/UserInterface/LoadingSpinner.cs @@ -19,7 +19,7 @@ namespace osu.Game.Graphics.UserInterface protected Container MainContents; - protected const float TRANSITION_DURATION = 500; + public const float TRANSITION_DURATION = 500; private const float spin_duration = 900; diff --git a/osu.Game/Screens/Loader.cs b/osu.Game/Screens/Loader.cs index d26dc0d660..a5b55a24e5 100644 --- a/osu.Game/Screens/Loader.cs +++ b/osu.Game/Screens/Loader.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shaders; using osu.Framework.Utils; using osu.Game.Screens.Menu; @@ -87,8 +88,14 @@ namespace osu.Game.Screens } spinnerShow?.Cancel(); - spinner.Hide(); - this.Push(loadableScreen); + + if (spinner.State.Value == Visibility.Visible) + { + spinner.Hide(); + Scheduler.AddDelayed(() => this.Push(loadableScreen), LoadingSpinner.TRANSITION_DURATION); + } + else + this.Push(loadableScreen); } [BackgroundDependencyLoader] From 93aec4e6920759e76c00f10ad322b3fd6ff8cf27 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Mar 2020 13:23:26 +0900 Subject: [PATCH 0318/2376] Improve english Co-Authored-By: Dan Balasescu --- osu.Game/Screens/Menu/Disclaimer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/Disclaimer.cs b/osu.Game/Screens/Menu/Disclaimer.cs index 2c1b0c3166..ee8200321b 100644 --- a/osu.Game/Screens/Menu/Disclaimer.cs +++ b/osu.Game/Screens/Menu/Disclaimer.cs @@ -200,7 +200,7 @@ namespace osu.Game.Screens.Menu "Try scrolling down in the mod select panel to find a bunch of new fun mods!", "Most of the web content (profiles, rankings, etc.) are available natively in-game from the icons on the toolbar!", "Get more details, hide or delete a beatmap by right-clicking on its panel at song select!", - "All delete operations are temoprary until quit. Restore accidentally deleted content from the maintenance settings!", + "All delete operations are temporary until exiting. Restore accidentally deleted content from the maintenance settings!", "Check out the \"timeshift\" multiplayer system, which has local permanent leaderboards and playlist support!", "Toggle advanced frame / thread statistics with Ctrl-F11!", "Take a look under the hood at performance counters and enable verbose performance logging with Ctrl-F2!", From 1bad2ff879ca53ecf35bbd5c42faa19c13450454 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Mar 2020 13:45:55 +0900 Subject: [PATCH 0319/2376] Load all catcher states ahead-of-time to avoid blocking loads --- osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 47 ++++++++++++++++++++--- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 2beda02398..dca3fea0d1 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -6,6 +6,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Input.Bindings; @@ -148,28 +149,62 @@ namespace osu.Game.Rulesets.Catch.UI [BackgroundDependencyLoader] private void load() { - Children = new[] + Children = new Drawable[] { caughtFruit = new Container { Anchor = Anchor.TopCentre, Origin = Anchor.BottomCentre, }, + catcherIdle = new CatcherSprite(CatcherAnimationState.Idle) + { + Anchor = Anchor.TopCentre, + Alpha = 0, + }, + catcherKiai = new CatcherSprite(CatcherAnimationState.Kiai) + { + Anchor = Anchor.TopCentre, + Alpha = 0, + }, + catcherFail = new CatcherSprite(CatcherAnimationState.Fail) + { + Anchor = Anchor.TopCentre, + Alpha = 0, + } }; updateCatcher(); } - private Drawable catcherSprite; + private CatcherSprite catcherIdle; + private CatcherSprite catcherKiai; + private CatcherSprite catcherFail; private void updateCatcher() { - catcherSprite?.Expire(); + catcherIdle.Hide(); + catcherKiai.Hide(); + catcherFail.Hide(); - Add(catcherSprite = createCatcherSprite().With(c => + CatcherSprite current; + + switch (currentState) { - c.Anchor = Anchor.TopCentre; - })); + default: + current = catcherIdle; + break; + + case CatcherAnimationState.Fail: + current = catcherFail; + break; + + case CatcherAnimationState.Kiai: + current = catcherKiai; + break; + } + + current.Show(); + (current.Drawable as IAnimation)?.GotoFrame(0); } private int currentDirection; From 73b225ad62148567cf8d6148b39706ab7abe0475 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Mar 2020 14:28:13 +0900 Subject: [PATCH 0320/2376] Make catcher's trail reflect the current animation frame rather than play the full animation --- osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 52 ++++++++++--------- .../UI/CatcherTrailSprite.cs | 22 ++++++++ 2 files changed, 50 insertions(+), 24 deletions(-) create mode 100644 osu.Game.Rulesets.Catch/UI/CatcherTrailSprite.cs diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index dca3fea0d1..43d98dc617 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -9,6 +9,8 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; using osu.Framework.Input.Bindings; using osu.Framework.Utils; using osu.Game.Beatmaps; @@ -180,31 +182,29 @@ namespace osu.Game.Rulesets.Catch.UI private CatcherSprite catcherKiai; private CatcherSprite catcherFail; + private CatcherSprite currentCatcher; + private void updateCatcher() { - catcherIdle.Hide(); - catcherKiai.Hide(); - catcherFail.Hide(); - - CatcherSprite current; + currentCatcher?.Hide(); switch (currentState) { default: - current = catcherIdle; + currentCatcher = catcherIdle; break; case CatcherAnimationState.Fail: - current = catcherFail; + currentCatcher = catcherFail; break; case CatcherAnimationState.Kiai: - current = catcherKiai; + currentCatcher = catcherKiai; break; } - current.Show(); - (current.Drawable as IAnimation)?.GotoFrame(0); + currentCatcher.Show(); + (currentCatcher.Drawable as IAnimation)?.GotoFrame(0); } private int currentDirection; @@ -227,14 +227,14 @@ namespace osu.Game.Rulesets.Catch.UI private bool trail; /// - /// Activate or deactive the trail. Will be automatically deactivated when conditions to keep the trail displayed are no longer met. + /// Activate or deactivate the trail. Will be automatically deactivated when conditions to keep the trail displayed are no longer met. /// protected bool Trail { get => trail; set { - if (value == trail) return; + if (value == trail || AdditiveTarget == null) return; trail = value; @@ -245,21 +245,25 @@ namespace osu.Game.Rulesets.Catch.UI private void beginTrail() { - Trail &= dashing || HyperDashing; - Trail &= AdditiveTarget != null; + if (!dashing && !HyperDashing) + { + Trail = false; + return; + } - if (!Trail) return; + Texture tex = (currentCatcher.Drawable as TextureAnimation)?.CurrentFrame ?? ((Sprite)currentCatcher.Drawable).Texture; - var additive = createCatcherSprite(); + var additive = new CatcherTrailSprite(tex) + { + Anchor = Anchor, + Scale = Scale, + Colour = HyperDashing ? Color4.Red : Color4.White, + Blending = BlendingParameters.Additive, + RelativePositionAxes = RelativePositionAxes, + Position = Position + }; - additive.Anchor = Anchor; - additive.Scale = Scale; - additive.Colour = HyperDashing ? Color4.Red : Color4.White; - additive.Blending = BlendingParameters.Additive; - additive.RelativePositionAxes = RelativePositionAxes; - additive.Position = Position; - - AdditiveTarget.Add(additive); + AdditiveTarget?.Add(additive); additive.FadeTo(0.4f).FadeOut(800, Easing.OutQuint); additive.Expire(true); diff --git a/osu.Game.Rulesets.Catch/UI/CatcherTrailSprite.cs b/osu.Game.Rulesets.Catch/UI/CatcherTrailSprite.cs new file mode 100644 index 0000000000..56cb7dbfda --- /dev/null +++ b/osu.Game.Rulesets.Catch/UI/CatcherTrailSprite.cs @@ -0,0 +1,22 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osuTK; + +namespace osu.Game.Rulesets.Catch.UI +{ + public class CatcherTrailSprite : Sprite + { + public CatcherTrailSprite(Texture texture) + { + Texture = texture; + + Size = new Vector2(CatcherArea.CATCHER_SIZE); + + // Sets the origin roughly to the centre of the catcher's plate to allow for correct scaling. + OriginPosition = new Vector2(0.5f, 0.06f) * CatcherArea.CATCHER_SIZE; + } + } +} From 6de244389bdff557869a0491ceeb0193458e2cdb Mon Sep 17 00:00:00 2001 From: voidedWarranties Date: Tue, 10 Mar 2020 22:50:20 -0700 Subject: [PATCH 0321/2376] Use OffsetClock instead of ManualClock --- .../Visual/Gameplay/TestSceneStoryboard.cs | 5 --- .../Screens/Play/GameplayClockContainer.cs | 9 ------ .../Drawables/DrawableStoryboardVideo.cs | 32 +++++-------------- 3 files changed, 8 insertions(+), 38 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs index 16a985c796..ff8437311e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs @@ -10,7 +10,6 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Overlays; -using osu.Game.Screens.Play; using osu.Game.Storyboards.Drawables; using osuTK.Graphics; @@ -25,13 +24,9 @@ namespace osu.Game.Tests.Visual.Gameplay [Cached] private MusicController musicController = new MusicController(); - [Cached] - private GameplayClock gameplayClock; - public TestSceneStoryboard() { Clock = new FramedClock(); - gameplayClock = new GameplayClock(Clock); AddRange(new Drawable[] { diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 87b0c196d3..591e969ad8 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -14,7 +14,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Timing; using osu.Game.Beatmaps; -using osu.Game.Beatmaps.Legacy; using osu.Game.Configuration; using osu.Game.Rulesets.Mods; @@ -117,14 +116,6 @@ namespace osu.Game.Screens.Play if (beatmap.BeatmapInfo.AudioLeadIn > 0) startTime = Math.Min(startTime, firstHitObjectTime - beatmap.BeatmapInfo.AudioLeadIn); - // some beatmaps have no AudioLeadIn but the video starts before the first object - var videoLayer = beatmap.Storyboard.GetLayer(LegacyStoryLayer.Video); - - var videoOffset = videoLayer.Elements.SingleOrDefault()?.StartTime; - - if (videoOffset != null) - startTime = Math.Min(startTime, videoOffset.GetValueOrDefault()); - Seek(startTime); adjustableClock.ProcessFrame(); diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs index ef14ccd4d7..d3f77e03ae 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs @@ -10,7 +10,6 @@ using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Video; using osu.Framework.Timing; using osu.Game.Beatmaps; -using osu.Game.Screens.Play; namespace osu.Game.Storyboards.Drawables { @@ -18,10 +17,6 @@ namespace osu.Game.Storyboards.Drawables { public readonly StoryboardVideo Video; private VideoSprite videoSprite; - private ManualClock videoClock; - private GameplayClock clock; - - private bool videoStarted; public override bool RemoveWhenNotAlive => false; @@ -33,13 +28,8 @@ namespace osu.Game.Storyboards.Drawables } [BackgroundDependencyLoader(true)] - private void load(GameplayClock clock, IBindable beatmap, TextureStore textureStore) + private void load(IBindable beatmap, TextureStore textureStore) { - if (clock == null) - return; - - this.clock = clock; - var path = beatmap.Value.BeatmapSetInfo?.Files?.Find(f => f.Filename.Equals(Video.Path, StringComparison.OrdinalIgnoreCase))?.FileInfo.StoragePath; if (path == null) @@ -56,29 +46,23 @@ namespace osu.Game.Storyboards.Drawables FillMode = FillMode.Fill, Anchor = Anchor.Centre, Origin = Anchor.Centre, - AlwaysPresent = true, Alpha = 0 }); - - videoClock = new ManualClock(); - videoSprite.Clock = new FramedClock(videoClock); } - protected override void Update() + protected override void LoadComplete() { - if (clock != null && clock.CurrentTime > Video.StartTime) + using (videoSprite.BeginAbsoluteSequence(Video.StartTime)) { - if (!videoStarted) + videoSprite.Clock = new FramedOffsetClock(Clock) { - videoSprite.FadeIn(500); - videoStarted = true; - } + Offset = -Video.StartTime + }; - // handle seeking before the video starts (break skipping, replay seek) - videoClock.CurrentTime = clock.CurrentTime - Video.StartTime; + videoSprite.FadeIn(500); } - base.Update(); + base.LoadComplete(); } } } From ae7245a51bea2b812a8ce814db2afa325a27a163 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 11 Mar 2020 15:11:54 +0900 Subject: [PATCH 0322/2376] Refactor + fix fade in too late --- .../Drawables/DrawableStoryboardVideo.cs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs index d3f77e03ae..75135495cc 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs @@ -46,23 +46,17 @@ namespace osu.Game.Storyboards.Drawables FillMode = FillMode.Fill, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Alpha = 0 + Alpha = 0, + Clock = new FramedOffsetClock(Clock) { Offset = -Video.StartTime } }); } protected override void LoadComplete() { - using (videoSprite.BeginAbsoluteSequence(Video.StartTime)) - { - videoSprite.Clock = new FramedOffsetClock(Clock) - { - Offset = -Video.StartTime - }; - - videoSprite.FadeIn(500); - } - base.LoadComplete(); + + using (videoSprite.BeginAbsoluteSequence(0)) + videoSprite.FadeIn(500); } } } From 401429feeccbcb92818cfd31f5020ffdf3070abe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Mar 2020 15:52:18 +0900 Subject: [PATCH 0323/2376] Revert changes to ScrenTestScene --- osu.Game/Tests/Visual/ScreenTestScene.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Tests/Visual/ScreenTestScene.cs b/osu.Game/Tests/Visual/ScreenTestScene.cs index 1a6ebed425..d26aacf2bc 100644 --- a/osu.Game/Tests/Visual/ScreenTestScene.cs +++ b/osu.Game/Tests/Visual/ScreenTestScene.cs @@ -38,11 +38,12 @@ namespace osu.Game.Tests.Visual private void addExitAllScreensStep() { - AddStep("exit all screens", () => + AddUntilStep("exit all screens", () => { - if (Stack.CurrentScreen == null) return; + if (Stack.CurrentScreen == null) return true; Stack.Exit(); + return false; }); } } From 966e5bbc8aa441cecde5fa54fce6f659015b6392 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Mar 2020 15:54:03 +0900 Subject: [PATCH 0324/2376] User helper function to reduce copy paste --- .../Objects/Drawables/DrawableBanana.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs index 2e7618b8df..01b76ceed9 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs @@ -33,10 +33,11 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables ScaleContainer.ScaleTo(HitObject.Scale * (end_scale + random_scale_range * RNG.NextSingle())) .Then().ScaleTo(HitObject.Scale * end_scale, HitObject.TimePreempt); - const float random_angle_range = 180; + ScaleContainer.RotateTo(getRandomAngle()) + .Then() + .RotateTo(getRandomAngle(), HitObject.TimePreempt); - ScaleContainer.RotateTo(random_angle_range * (RNG.NextSingle() * 2 - 1)) - .Then().RotateTo(random_angle_range * (RNG.NextSingle() * 2 - 1), HitObject.TimePreempt); + float getRandomAngle() => 180 * (RNG.NextSingle() * 2 - 1); } private Color4 getBananaColour() From 6546fd3f812d99141813914df402b6d0ee72ff46 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Mar 2020 16:07:44 +0900 Subject: [PATCH 0325/2376] Fix potential null due to async load --- osu.Game.Tests/Visual/Menus/TestSceneLoader.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneLoader.cs b/osu.Game.Tests/Visual/Menus/TestSceneLoader.cs index 6003d05ecd..b3064ba9be 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneLoader.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneLoader.cs @@ -46,7 +46,7 @@ namespace osu.Game.Tests.Visual.Menus LoadScreen(loader); }); - AddAssert("spinner did not display", () => loader.LoadingSpinner.Alpha == 0); + AddAssert("spinner did not display", () => loader.LoadingSpinner?.Alpha == 0); AddUntilStep("loaded", () => loader.ScreenLoaded); AddUntilStep("not current", () => !loader.IsCurrentScreen()); @@ -67,7 +67,7 @@ namespace osu.Game.Tests.Visual.Menus { public readonly ManualResetEventSlim AllowLoad = new ManualResetEventSlim(); - public LoadingSpinner LoadingSpinner => this.ChildrenOfType().Single(); + public LoadingSpinner LoadingSpinner => this.ChildrenOfType().FirstOrDefault(); private TestScreen screen; public bool ScreenLoaded => screen.IsCurrentScreen(); From 424f9afbf42203d0f32c97209c72531e4a8b16ec Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 11 Mar 2020 17:55:20 +0900 Subject: [PATCH 0326/2376] Fix incorrect offset with custom clock --- osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs index 75135495cc..00df388d09 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs @@ -13,7 +13,7 @@ using osu.Game.Beatmaps; namespace osu.Game.Storyboards.Drawables { - public class DrawableStoryboardVideo : Container + public class DrawableStoryboardVideo : CompositeDrawable { public readonly StoryboardVideo Video; private VideoSprite videoSprite; @@ -40,7 +40,7 @@ namespace osu.Game.Storyboards.Drawables if (stream == null) return; - AddInternal(videoSprite = new VideoSprite(stream) + InternalChild = videoSprite = new VideoSprite(stream, false) { RelativeSizeAxes = Axes.Both, FillMode = FillMode.Fill, @@ -48,7 +48,7 @@ namespace osu.Game.Storyboards.Drawables Origin = Anchor.Centre, Alpha = 0, Clock = new FramedOffsetClock(Clock) { Offset = -Video.StartTime } - }); + }; } protected override void LoadComplete() From 8eb8572c738b549be8915ea809a7ad20d600e307 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 11 Mar 2020 18:00:39 +0900 Subject: [PATCH 0327/2376] Apply osu!-side video sprite changes --- osu.Game.Tournament/Components/TourneyVideo.cs | 2 +- osu.Game/Screens/Menu/IntroTriangles.cs | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tournament/Components/TourneyVideo.cs b/osu.Game.Tournament/Components/TourneyVideo.cs index 43088d6b92..81620b017c 100644 --- a/osu.Game.Tournament/Components/TourneyVideo.cs +++ b/osu.Game.Tournament/Components/TourneyVideo.cs @@ -34,7 +34,7 @@ namespace osu.Game.Tournament.Components if (stream != null) { - InternalChild = video = new VideoSprite(stream) + InternalChild = video = new VideoSprite(stream, false) { RelativeSizeAxes = Axes.Both, FillMode = FillMode.Fit, diff --git a/osu.Game/Screens/Menu/IntroTriangles.cs b/osu.Game/Screens/Menu/IntroTriangles.cs index 4e51ff939a..be5762e68d 100644 --- a/osu.Game/Screens/Menu/IntroTriangles.cs +++ b/osu.Game/Screens/Menu/IntroTriangles.cs @@ -259,11 +259,18 @@ namespace osu.Game.Screens.Menu private class LazerLogo : CompositeDrawable { + private readonly Stream videoStream; + public LazerLogo(Stream videoStream) { + this.videoStream = videoStream; Size = new Vector2(960); + } - InternalChild = new VideoSprite(videoStream) + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new VideoSprite(videoStream, false) { RelativeSizeAxes = Axes.Both, Clock = new FramedOffsetClock(Clock) { Offset = -logo_1 } From 758bb3711f6f7105296a1922b0108a4129743013 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 11 Mar 2020 18:07:11 +0900 Subject: [PATCH 0328/2376] Add more sane limit for maximum slider length --- osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 1fc51d2ce8..8d3ad5984f 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Objects.Legacy if (split.Length > 7) { - length = Math.Max(0, Parsing.ParseDouble(split[7])); + length = Math.Max(0, Parsing.ParseDouble(split[7], Parsing.MAX_COORDINATE_VALUE)); if (length == 0) length = null; } From 9667934ed9d0a7a44f55af4c7515fdb0df3bded8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 11 Mar 2020 18:17:32 +0900 Subject: [PATCH 0329/2376] Remove unlimited timing points in difficulty calculation --- .../LegacyDifficultyCalculatorBeatmapDecoder.cs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs index 527f520172..bf52a87865 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs @@ -26,17 +26,5 @@ namespace osu.Game.Beatmaps.Formats AddDecoder(@"osu file format v", m => new LegacyDifficultyCalculatorBeatmapDecoder(int.Parse(m.Split('v').Last()))); SetFallbackDecoder(() => new LegacyDifficultyCalculatorBeatmapDecoder()); } - - protected override TimingControlPoint CreateTimingControlPoint() - => new LegacyDifficultyCalculatorTimingControlPoint(); - - private class LegacyDifficultyCalculatorTimingControlPoint : TimingControlPoint - { - public LegacyDifficultyCalculatorTimingControlPoint() - { - BeatLengthBindable.MinValue = double.MinValue; - BeatLengthBindable.MaxValue = double.MaxValue; - } - } } } From 40ab860ab56f7b89264c0c7c3475d680a09c0951 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 11 Mar 2020 18:23:30 +0900 Subject: [PATCH 0330/2376] Remove unused using --- .../Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs index bf52a87865..3420fcf260 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System.Linq; -using osu.Game.Beatmaps.ControlPoints; namespace osu.Game.Beatmaps.Formats { From 5b03b3e36304b1546c671aa875ce3d97bec79ced Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 11 Mar 2020 18:36:37 +0900 Subject: [PATCH 0331/2376] Fix hyperdashes not recalculated with HR application --- .../Beatmaps/CatchBeatmapProcessor.cs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs index 1a5d0f983b..e0c65a8317 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs @@ -28,8 +28,6 @@ namespace osu.Game.Rulesets.Catch.Beatmaps ApplyPositionOffsets(Beatmap); - initialiseHyperDash((List)Beatmap.HitObjects); - int index = 0; foreach (var obj in Beatmap.HitObjects.OfType()) @@ -90,6 +88,8 @@ namespace osu.Game.Rulesets.Catch.Beatmaps break; } } + + initialiseHyperDash(beatmap); } private static void applyHardRockOffset(CatchHitObject hitObject, ref float? lastPosition, ref double lastStartTime, FastRandom rng) @@ -191,14 +191,14 @@ namespace osu.Game.Rulesets.Catch.Beatmaps } } - private void initialiseHyperDash(List objects) + private static void initialiseHyperDash(IBeatmap beatmap) { List objectWithDroplets = new List(); - foreach (var currentObject in objects) + foreach (var currentObject in beatmap.HitObjects) { - if (currentObject is Fruit) - objectWithDroplets.Add(currentObject); + if (currentObject is Fruit fruitObject) + objectWithDroplets.Add(fruitObject); if (currentObject is JuiceStream) { @@ -212,7 +212,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps objectWithDroplets.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime)); - double halfCatcherWidth = CatcherArea.GetCatcherSize(Beatmap.BeatmapInfo.BaseDifficulty) / 2; + double halfCatcherWidth = CatcherArea.GetCatcherSize(beatmap.BeatmapInfo.BaseDifficulty) / 2; int lastDirection = 0; double lastExcess = halfCatcherWidth; @@ -221,6 +221,10 @@ namespace osu.Game.Rulesets.Catch.Beatmaps CatchHitObject currentObject = objectWithDroplets[i]; CatchHitObject nextObject = objectWithDroplets[i + 1]; + // Reset variables in-case values have changed (e.g. after applying HR) + currentObject.HyperDashTarget = null; + currentObject.DistanceToHyperDash = 0; + int thisDirection = nextObject.X > currentObject.X ? 1 : -1; double timeToNext = nextObject.StartTime - currentObject.StartTime - 1000f / 60f / 4; // 1/4th of a frame of grace time, taken from osu-stable double distanceToNext = Math.Abs(nextObject.X - currentObject.X) - (lastDirection == thisDirection ? lastExcess : halfCatcherWidth); From f8e7579f4574aee46ae278fe853e43864d047710 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 11 Mar 2020 18:37:58 +0900 Subject: [PATCH 0332/2376] Fix juice stream position reset not ever being applied --- .../Beatmaps/CatchBeatmapProcessor.cs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs index e0c65a8317..e76e95e9aa 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs @@ -74,6 +74,12 @@ namespace osu.Game.Rulesets.Catch.Beatmaps break; case JuiceStream juiceStream: + // Todo: BUG!! Stable used the last control point as the final position of the path, but it should use the computed path instead. + lastPosition = juiceStream.X + juiceStream.Path.ControlPoints[^1].Position.Value.X / CatchPlayfield.BASE_WIDTH; + + // Todo: BUG!! Stable attempted to use the end time of the stream, but referenced it too early in execution and used the start time instead. + lastStartTime = juiceStream.StartTime; + foreach (var nested in juiceStream.NestedHitObjects) { var catchObject = (CatchHitObject)nested; @@ -94,13 +100,6 @@ namespace osu.Game.Rulesets.Catch.Beatmaps private static void applyHardRockOffset(CatchHitObject hitObject, ref float? lastPosition, ref double lastStartTime, FastRandom rng) { - if (hitObject is JuiceStream stream) - { - lastPosition = stream.EndX; - lastStartTime = stream.EndTime; - return; - } - if (!(hitObject is Fruit)) return; From 919410c6277644329b96a6724a5045b6a94072ed Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 11 Mar 2020 18:39:47 +0900 Subject: [PATCH 0333/2376] Remove always-false condition --- osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs index e76e95e9aa..5f23bf1428 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs @@ -100,9 +100,6 @@ namespace osu.Game.Rulesets.Catch.Beatmaps private static void applyHardRockOffset(CatchHitObject hitObject, ref float? lastPosition, ref double lastStartTime, FastRandom rng) { - if (!(hitObject is Fruit)) - return; - float offsetPosition = hitObject.X; double startTime = hitObject.StartTime; From 5c051027e707db7fea7bf6a11b121249d44afd70 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 11 Mar 2020 18:43:08 +0900 Subject: [PATCH 0334/2376] Fix different offset being applied from stable --- osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs index 5f23bf1428..986dc9dbb9 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs @@ -112,7 +112,9 @@ namespace osu.Game.Rulesets.Catch.Beatmaps } float positionDiff = offsetPosition - lastPosition.Value; - double timeDiff = startTime - lastStartTime; + + // Todo: BUG!! Stable calculated time deltas as ints, which affects randomisation. This should be changed to a double. + int timeDiff = (int)(startTime - lastStartTime); if (timeDiff > 1000) { @@ -128,7 +130,8 @@ namespace osu.Game.Rulesets.Catch.Beatmaps return; } - if (Math.Abs(positionDiff * CatchPlayfield.BASE_WIDTH) < timeDiff / 3d) + // ReSharper disable once PossibleLossOfFraction + if (Math.Abs(positionDiff * CatchPlayfield.BASE_WIDTH) < timeDiff / 3) applyOffset(ref offsetPosition, positionDiff); hitObject.XOffset = offsetPosition - hitObject.X; From 2866d626531f678e07c81b44ab27d7fecae95d87 Mon Sep 17 00:00:00 2001 From: Olle Kelderman Date: Wed, 11 Mar 2020 16:17:28 +0100 Subject: [PATCH 0335/2376] Use environment variable for initializing osuInstallPath --- osu.Game.Tournament/IPC/FileBasedIPC.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/osu.Game.Tournament/IPC/FileBasedIPC.cs b/osu.Game.Tournament/IPC/FileBasedIPC.cs index b19f2bedf0..eefa9fcfe6 100644 --- a/osu.Game.Tournament/IPC/FileBasedIPC.cs +++ b/osu.Game.Tournament/IPC/FileBasedIPC.cs @@ -163,12 +163,7 @@ namespace osu.Game.Tournament.IPC { try { - stableInstallPath = "G:\\My Drive\\Main\\osu!tourney"; - - if (checkExists(stableInstallPath)) - return stableInstallPath; - - stableInstallPath = "G:\\My Drive\\Main\\osu!mappool"; + stableInstallPath = Environment.GetEnvironmentVariable("OSU_STABLE_PATH"); if (checkExists(stableInstallPath)) return stableInstallPath; From efceeba0769999db723bb74d0f0a2b5c143270c2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Mar 2020 02:22:02 +0900 Subject: [PATCH 0336/2376] Use fixed width for tournament score displays --- .../Screens/Gameplay/Components/MatchScoreDisplay.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/MatchScoreDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/MatchScoreDisplay.cs index ed14956793..2e7484542a 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/MatchScoreDisplay.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/MatchScoreDisplay.cs @@ -11,6 +11,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Tournament.IPC; using osu.Game.Tournament.Models; +using osuTK; namespace osu.Game.Tournament.Screens.Gameplay.Components { @@ -131,13 +132,15 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components Margin = new MarginPadding { Top = bar_height, Horizontal = 10 }; Winning = false; + + DisplayedCountSpriteText.Spacing = new Vector2(-6); } public bool Winning { set => DisplayedCountSpriteText.Font = value - ? OsuFont.Torus.With(weight: FontWeight.Bold, size: 50) - : OsuFont.Torus.With(weight: FontWeight.Regular, size: 40); + ? OsuFont.Torus.With(weight: FontWeight.Bold, size: 50, fixedWidth: true) + : OsuFont.Torus.With(weight: FontWeight.Regular, size: 40, fixedWidth: true); } } } From 09b9983286d5a1a7f960ed968f33a2b3bc139c67 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 11 Mar 2020 21:14:07 +0300 Subject: [PATCH 0337/2376] Fix CatcherAnimationState is Fail if missing banana shower --- osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 9ee94636f1..d18f5e165f 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -345,7 +345,10 @@ namespace osu.Game.Rulesets.Catch.UI if (validCatch) updateState(fruit.Kiai ? CatcherAnimationState.Kiai : CatcherAnimationState.Idle); else - updateState(CatcherAnimationState.Fail); + { + if (!(fruit is Banana)) + updateState(CatcherAnimationState.Fail); + } return validCatch; } From e46c070d951341370f16733da1d63c33c0f116c6 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 11 Mar 2020 23:09:29 +0300 Subject: [PATCH 0338/2376] Add test scene --- .../TestSceneDrawableHitObjects.cs | 53 +++++++++++++++---- osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 10 ++-- 2 files changed, 48 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs index 070847c0c1..304c7e3854 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs @@ -4,7 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; -using osu.Framework.Allocation; +using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; @@ -34,8 +34,8 @@ namespace osu.Game.Rulesets.Catch.Tests private DrawableCatchRuleset drawableRuleset; private double playfieldTime => drawableRuleset.Playfield.Time.Current; - [BackgroundDependencyLoader] - private void load() + [SetUp] + public void Setup() => Schedule(() => { var controlPointInfo = new ControlPointInfo(); controlPointInfo.Add(0, new TimingControlPoint()); @@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Catch.Tests ControlPointInfo = controlPointInfo }); - Add(new Container + Child = new Container { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -66,16 +66,49 @@ namespace osu.Game.Rulesets.Catch.Tests { drawableRuleset = new DrawableCatchRuleset(new CatchRuleset(), beatmap.GetPlayableBeatmap(new CatchRuleset().RulesetInfo)) } - }); + }; + }); + + [Test] + public void TestFruits() + { + AddStep("hit fruits", () => spawnFruits(true)); + AddUntilStep("wait for completion", () => playfieldIsEmpty); + AddAssert("catcher state is idle", () => catcherState == CatcherAnimationState.Idle); AddStep("miss fruits", () => spawnFruits()); - AddStep("hit fruits", () => spawnFruits(true)); - AddStep("miss juicestream", () => spawnJuiceStream()); - AddStep("hit juicestream", () => spawnJuiceStream(true)); - AddStep("miss bananas", () => spawnBananas()); - AddStep("hit bananas", () => spawnBananas(true)); + AddUntilStep("wait for completion", () => playfieldIsEmpty); + AddAssert("catcher state is failed", () => catcherState == CatcherAnimationState.Fail); } + [Test] + public void TestJuicestream() + { + AddStep("hit juicestream", () => spawnJuiceStream(true)); + AddUntilStep("wait for completion", () => playfieldIsEmpty); + AddAssert("catcher state is idle", () => catcherState == CatcherAnimationState.Idle); + + AddStep("miss juicestream", () => spawnJuiceStream()); + AddUntilStep("wait for completion", () => playfieldIsEmpty); + AddAssert("catcher state is failed", () => catcherState == CatcherAnimationState.Fail); + } + + [Test] + public void TestBananas() + { + AddStep("hit bananas", () => spawnBananas(true)); + AddUntilStep("wait for completion", () => playfieldIsEmpty); + AddAssert("catcher state is idle", () => catcherState == CatcherAnimationState.Idle); + + AddStep("miss bananas", () => spawnBananas()); + AddUntilStep("wait for completion", () => playfieldIsEmpty); + AddAssert("catcher state is idle", () => catcherState == CatcherAnimationState.Idle); + } + + private bool playfieldIsEmpty => !((CatchPlayfield)drawableRuleset.Playfield).AllHitObjects.Any(h => h.IsAlive); + + private CatcherAnimationState catcherState => ((CatchPlayfield)drawableRuleset.Playfield).CatcherArea.MovableCatcher.CurrentState; + private void spawnFruits(bool hit = false) { for (int i = 1; i <= 4; i++) diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index d18f5e165f..441f9126f6 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -188,7 +188,7 @@ namespace osu.Game.Rulesets.Catch.UI CatcherSprite current; - switch (currentState) + switch (CurrentState) { default: current = catcherIdle; @@ -274,7 +274,7 @@ namespace osu.Game.Rulesets.Catch.UI return additive; } - private Drawable createCatcherSprite() => new CatcherSprite(currentState); + private Drawable createCatcherSprite() => new CatcherSprite(CurrentState); /// /// Add a caught fruit to the catcher's stack. @@ -355,14 +355,14 @@ namespace osu.Game.Rulesets.Catch.UI private void updateState(CatcherAnimationState state) { - if (currentState == state) + if (CurrentState == state) return; - currentState = state; + CurrentState = state; updateCatcher(); } - private CatcherAnimationState currentState; + public CatcherAnimationState CurrentState; private double hyperDashModifier = 1; private int hyperDashDirection; From fd21e87670ebde3018e43a550aff6ef9b30383f9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Mar 2020 11:28:45 +0900 Subject: [PATCH 0339/2376] Disable adjusting volume via "select next" and "select previous" as fallbacks --- .../Overlays/Volume/VolumeControlReceptor.cs | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/osu.Game/Overlays/Volume/VolumeControlReceptor.cs b/osu.Game/Overlays/Volume/VolumeControlReceptor.cs index 4bff8146b4..3478f18a40 100644 --- a/osu.Game/Overlays/Volume/VolumeControlReceptor.cs +++ b/osu.Game/Overlays/Volume/VolumeControlReceptor.cs @@ -14,22 +14,8 @@ namespace osu.Game.Overlays.Volume public Func ActionRequested; public Func ScrollActionRequested; - public bool OnPressed(GlobalAction action) - { - // if nothing else handles selection actions in the game, it's safe to let volume be adjusted. - switch (action) - { - case GlobalAction.SelectPrevious: - action = GlobalAction.IncreaseVolume; - break; - - case GlobalAction.SelectNext: - action = GlobalAction.DecreaseVolume; - break; - } - - return ActionRequested?.Invoke(action) ?? false; - } + public bool OnPressed(GlobalAction action) => + ActionRequested?.Invoke(action) ?? false; public bool OnScroll(GlobalAction action, float amount, bool isPrecise) => ScrollActionRequested?.Invoke(action, amount, isPrecise) ?? false; From 39bb98bfb219a2c174c1193850ce500eb0b5aeba Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Mar 2020 13:26:58 +0900 Subject: [PATCH 0340/2376] Allow videos to be loaded with any extension Also moves all tournament user resources to a "tournament" subfolder. --- .../Components/TourneyVideo.cs | 5 ++--- osu.Game.Tournament/TournamentGameBase.cs | 6 +++++- osu.Game.Tournament/TournamentStorage.cs | 19 +++++++++++++++++++ 3 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 osu.Game.Tournament/TournamentStorage.cs diff --git a/osu.Game.Tournament/Components/TourneyVideo.cs b/osu.Game.Tournament/Components/TourneyVideo.cs index 43088d6b92..d8488ce4f6 100644 --- a/osu.Game.Tournament/Components/TourneyVideo.cs +++ b/osu.Game.Tournament/Components/TourneyVideo.cs @@ -7,7 +7,6 @@ using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Video; -using osu.Framework.Platform; using osu.Framework.Timing; using osu.Game.Graphics; @@ -28,9 +27,9 @@ namespace osu.Game.Tournament.Components } [BackgroundDependencyLoader] - private void load(Storage storage) + private void load(TournamentStorage storage) { - var stream = storage.GetStream($@"videos/{filename}.m4v"); + var stream = storage.GetStream($@"videos/{filename}"); if (stream != null) { diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 41165ca141..41822ae2c3 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -37,6 +37,8 @@ namespace osu.Game.Tournament private Storage storage; + private TournamentStorage tournamentStorage; + private DependencyContainer dependencies; private Bindable windowSize; @@ -54,7 +56,9 @@ namespace osu.Game.Tournament { Resources.AddStore(new DllResourceStore(typeof(TournamentGameBase).Assembly)); - Textures.AddStore(new TextureLoaderStore(new ResourceStore(new StorageBackedResourceStore(storage)))); + dependencies.CacheAs(tournamentStorage = new TournamentStorage(storage)); + + Textures.AddStore(new TextureLoaderStore(tournamentStorage)); this.storage = storage; diff --git a/osu.Game.Tournament/TournamentStorage.cs b/osu.Game.Tournament/TournamentStorage.cs new file mode 100644 index 0000000000..139ad3857b --- /dev/null +++ b/osu.Game.Tournament/TournamentStorage.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.IO.Stores; +using osu.Framework.Platform; + +namespace osu.Game.Tournament +{ + internal class TournamentStorage : NamespacedResourceStore + { + public TournamentStorage(Storage storage) + : base(new StorageBackedResourceStore(storage), "tournament") + { + AddExtension("m4v"); + AddExtension("avi"); + AddExtension("mp4"); + } + } +} From 190ff974862e9e379917eeccd40034c546448980 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Mar 2020 13:29:09 +0900 Subject: [PATCH 0341/2376] Rename classes to better suit purpose --- .../Components/DrawableTournamentHeaderLogo.cs | 18 ++++++++++++++++++ .../Components/DrawableTournamentHeaderText.cs | 18 ++++++++++++++++++ .../Components/DrawableTournamentTitleText.cs | 16 ---------------- osu.Game.Tournament/Components/RoundDisplay.cs | 2 +- .../Screens/Gameplay/Components/MatchHeader.cs | 4 ++-- .../{RoundDisplay.cs => MatchRoundDisplay.cs} | 2 +- .../Screens/Ladder/LadderScreen.cs | 2 +- .../Screens/Schedule/ScheduleScreen.cs | 2 +- 8 files changed, 42 insertions(+), 22 deletions(-) create mode 100644 osu.Game.Tournament/Components/DrawableTournamentHeaderLogo.cs create mode 100644 osu.Game.Tournament/Components/DrawableTournamentHeaderText.cs delete mode 100644 osu.Game.Tournament/Components/DrawableTournamentTitleText.cs rename osu.Game.Tournament/Screens/Gameplay/Components/{RoundDisplay.cs => MatchRoundDisplay.cs} (92%) diff --git a/osu.Game.Tournament/Components/DrawableTournamentHeaderLogo.cs b/osu.Game.Tournament/Components/DrawableTournamentHeaderLogo.cs new file mode 100644 index 0000000000..b6f74a75e7 --- /dev/null +++ b/osu.Game.Tournament/Components/DrawableTournamentHeaderLogo.cs @@ -0,0 +1,18 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; + +namespace osu.Game.Tournament.Components +{ + public class DrawableTournamentHeaderLogo : Sprite + { + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + Texture = textures.Get("header-text"); + } + } +} diff --git a/osu.Game.Tournament/Components/DrawableTournamentHeaderText.cs b/osu.Game.Tournament/Components/DrawableTournamentHeaderText.cs new file mode 100644 index 0000000000..38500fa857 --- /dev/null +++ b/osu.Game.Tournament/Components/DrawableTournamentHeaderText.cs @@ -0,0 +1,18 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; + +namespace osu.Game.Tournament.Components +{ + public class DrawableTournamentHeaderText : Sprite + { + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + Texture = textures.Get("header-text"); + } + } +} diff --git a/osu.Game.Tournament/Components/DrawableTournamentTitleText.cs b/osu.Game.Tournament/Components/DrawableTournamentTitleText.cs deleted file mode 100644 index 4fbc6cd060..0000000000 --- a/osu.Game.Tournament/Components/DrawableTournamentTitleText.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Graphics; - -namespace osu.Game.Tournament.Components -{ - public class DrawableTournamentTitleText : TournamentSpriteText - { - public DrawableTournamentTitleText() - { - Text = "osu!taiko world cup 2020"; - Font = OsuFont.Torus.With(size: 26, weight: FontWeight.SemiBold); - } - } -} diff --git a/osu.Game.Tournament/Components/RoundDisplay.cs b/osu.Game.Tournament/Components/RoundDisplay.cs index dd56c83c57..bebede6782 100644 --- a/osu.Game.Tournament/Components/RoundDisplay.cs +++ b/osu.Game.Tournament/Components/RoundDisplay.cs @@ -22,7 +22,7 @@ namespace osu.Game.Tournament.Components Direction = FillDirection.Vertical, Children = new Drawable[] { - new DrawableTournamentTitleText(), + new DrawableTournamentHeaderText(), new TournamentSpriteText { Text = match.Round.Value?.Name.Value ?? "Unknown Round", diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs b/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs index 69a68c946b..aa4bd4a701 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs @@ -41,13 +41,13 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components Spacing = new Vector2(5), Children = new Drawable[] { - new DrawableTournamentTitleText + new DrawableTournamentHeaderText { Anchor = Anchor.Centre, Origin = Anchor.Centre, Scale = new Vector2(1.2f) }, - new RoundDisplay + new MatchRoundDisplay { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/RoundDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/MatchRoundDisplay.cs similarity index 92% rename from osu.Game.Tournament/Screens/Gameplay/Components/RoundDisplay.cs rename to osu.Game.Tournament/Screens/Gameplay/Components/MatchRoundDisplay.cs index c8b0d3bdda..87793f7e1b 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/RoundDisplay.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/MatchRoundDisplay.cs @@ -8,7 +8,7 @@ using osu.Game.Tournament.Models; namespace osu.Game.Tournament.Screens.Gameplay.Components { - public class RoundDisplay : TournamentSpriteTextWithBackground + public class MatchRoundDisplay : TournamentSpriteTextWithBackground { private readonly Bindable currentMatch = new Bindable(); diff --git a/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs b/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs index 6f62b3ddba..534c402f6c 100644 --- a/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs +++ b/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs @@ -47,7 +47,7 @@ namespace osu.Game.Tournament.Screens.Ladder RelativeSizeAxes = Axes.Both, Loop = true, }, - new DrawableTournamentTitleText + new DrawableTournamentHeaderText { Y = 100, Anchor = Anchor.TopCentre, diff --git a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs index 0fcec645e3..88289ad6bd 100644 --- a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs +++ b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs @@ -62,7 +62,7 @@ namespace osu.Game.Tournament.Screens.Schedule Direction = FillDirection.Vertical, Children = new Drawable[] { - new DrawableTournamentTitleText(), + new DrawableTournamentHeaderText(), new Container { Margin = new MarginPadding { Top = 40 }, From b6b802e8212f44a669675f6705fd16db667fd758 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Mar 2020 14:06:40 +0900 Subject: [PATCH 0342/2376] Add back customisable header logo/text Also adds test scene for MatchHeader component. --- .../Components/TestSceneMatchHeader.cs | 33 +++++ .../DrawableTournamentHeaderLogo.cs | 27 +++- .../DrawableTournamentHeaderText.cs | 27 +++- .../Gameplay/Components/MatchHeader.cs | 119 +++++++----------- .../Gameplay/Components/TeamScoreDisplay.cs | 83 ++++++++++++ 5 files changed, 206 insertions(+), 83 deletions(-) create mode 100644 osu.Game.Tournament.Tests/Components/TestSceneMatchHeader.cs create mode 100644 osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs diff --git a/osu.Game.Tournament.Tests/Components/TestSceneMatchHeader.cs b/osu.Game.Tournament.Tests/Components/TestSceneMatchHeader.cs new file mode 100644 index 0000000000..b29e4964b6 --- /dev/null +++ b/osu.Game.Tournament.Tests/Components/TestSceneMatchHeader.cs @@ -0,0 +1,33 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Tournament.Screens.Gameplay.Components; +using osuTK; + +namespace osu.Game.Tournament.Tests.Components +{ + public class TestSceneMatchHeader : TournamentTestScene + { + public TestSceneMatchHeader() + { + Child = new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(50), + Children = new Drawable[] + { + new TournamentSpriteText { Text = "with logo", Font = OsuFont.Torus.With(size: 30) }, + new MatchHeader(), + new TournamentSpriteText { Text = "without logo", Font = OsuFont.Torus.With(size: 30) }, + new MatchHeader { ShowLogo = false }, + new TournamentSpriteText { Text = "without scores", Font = OsuFont.Torus.With(size: 30) }, + new MatchHeader { ShowScores = false }, + } + }; + } + } +} diff --git a/osu.Game.Tournament/Components/DrawableTournamentHeaderLogo.cs b/osu.Game.Tournament/Components/DrawableTournamentHeaderLogo.cs index b6f74a75e7..a61cb59fed 100644 --- a/osu.Game.Tournament/Components/DrawableTournamentHeaderLogo.cs +++ b/osu.Game.Tournament/Components/DrawableTournamentHeaderLogo.cs @@ -2,17 +2,36 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; namespace osu.Game.Tournament.Components { - public class DrawableTournamentHeaderLogo : Sprite + public class DrawableTournamentHeaderLogo : CompositeDrawable { - [BackgroundDependencyLoader] - private void load(TextureStore textures) + public DrawableTournamentHeaderLogo() { - Texture = textures.Get("header-text"); + InternalChild = new LogoSprite(); + + Height = 50; + RelativeSizeAxes = Axes.X; + } + + private class LogoSprite : Sprite + { + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + RelativeSizeAxes = Axes.Both; + FillMode = FillMode.Fit; + + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + Texture = textures.Get("header-logo"); + } } } } diff --git a/osu.Game.Tournament/Components/DrawableTournamentHeaderText.cs b/osu.Game.Tournament/Components/DrawableTournamentHeaderText.cs index 38500fa857..2539075c0f 100644 --- a/osu.Game.Tournament/Components/DrawableTournamentHeaderText.cs +++ b/osu.Game.Tournament/Components/DrawableTournamentHeaderText.cs @@ -2,17 +2,36 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; namespace osu.Game.Tournament.Components { - public class DrawableTournamentHeaderText : Sprite + public class DrawableTournamentHeaderText : CompositeDrawable { - [BackgroundDependencyLoader] - private void load(TextureStore textures) + public DrawableTournamentHeaderText() { - Texture = textures.Get("header-text"); + InternalChild = new TextSprite(); + + Height = 25; + RelativeSizeAxes = Axes.X; + } + + private class TextSprite : Sprite + { + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + RelativeSizeAxes = Axes.Both; + FillMode = FillMode.Fit; + + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + Texture = textures.Get("header-text"); + } } } } diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs b/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs index aa4bd4a701..751a763333 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs @@ -2,14 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Input.Events; using osu.Game.Tournament.Components; using osu.Game.Tournament.Models; using osuTK; -using osuTK.Input; namespace osu.Game.Tournament.Screens.Gameplay.Components { @@ -17,13 +14,39 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components { private TeamScoreDisplay teamDisplay1; private TeamScoreDisplay teamDisplay2; + private DrawableTournamentHeaderLogo logo; + + private bool showScores = true; public bool ShowScores { + get => showScores; set { - teamDisplay1.ShowScore = value; - teamDisplay2.ShowScore = value; + if (value == showScores) + return; + + showScores = value; + + if (IsLoaded) + updateDisplay(); + } + } + + private bool showLogo = true; + + public bool ShowLogo + { + get => showLogo; + set + { + if (value == showLogo) + return; + + showLogo = value; + + if (IsLoaded) + updateDisplay(); } } @@ -38,19 +61,25 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components { RelativeSizeAxes = Axes.Both, Direction = FillDirection.Vertical, + Padding = new MarginPadding(5), Spacing = new Vector2(5), Children = new Drawable[] { + logo = new DrawableTournamentHeaderLogo + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Alpha = showLogo ? 1 : 0 + }, new DrawableTournamentHeaderText { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(1.2f) + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, }, new MatchRoundDisplay { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, Scale = new Vector2(0.4f) }, } @@ -66,76 +95,16 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components Origin = Anchor.TopRight, }, }; - } - } - public class TeamScoreDisplay : CompositeDrawable - { - private readonly TeamColour teamColour; - - private readonly Bindable currentMatch = new Bindable(); - private readonly Bindable currentTeam = new Bindable(); - private readonly Bindable currentTeamScore = new Bindable(); - - private TeamDisplay teamDisplay; - - public bool ShowScore { set => teamDisplay.ShowScore = value; } - - public TeamScoreDisplay(TeamColour teamColour) - { - this.teamColour = teamColour; - - RelativeSizeAxes = Axes.Y; - AutoSizeAxes = Axes.X; + updateDisplay(); } - [BackgroundDependencyLoader] - private void load(LadderInfo ladder) + private void updateDisplay() { - currentMatch.BindTo(ladder.CurrentMatch); - currentMatch.BindValueChanged(matchChanged, true); - } + teamDisplay1.ShowScore = showScores; + teamDisplay2.ShowScore = showScores; - private void matchChanged(ValueChangedEvent match) - { - currentTeamScore.UnbindBindings(); - currentTeam.UnbindBindings(); - - if (match.NewValue != null) - { - currentTeamScore.BindTo(teamColour == TeamColour.Red ? match.NewValue.Team1Score : match.NewValue.Team2Score); - currentTeam.BindTo(teamColour == TeamColour.Red ? match.NewValue.Team1 : match.NewValue.Team2); - } - - // team may change to same team, which means score is not in a good state. - // thus we handle this manually. - teamChanged(currentTeam.Value); - } - - protected override bool OnMouseDown(MouseDownEvent e) - { - switch (e.Button) - { - case MouseButton.Left: - if (currentTeamScore.Value < currentMatch.Value.PointsToWin) - currentTeamScore.Value++; - return true; - - case MouseButton.Right: - if (currentTeamScore.Value > 0) - currentTeamScore.Value--; - return true; - } - - return base.OnMouseDown(e); - } - - private void teamChanged(TournamentTeam team) - { - InternalChildren = new Drawable[] - { - teamDisplay = new TeamDisplay(team, teamColour, currentTeamScore, currentMatch.Value?.PointsToWin ?? 0), - }; + logo.Alpha = showLogo ? 1 : 0; } } } diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs new file mode 100644 index 0000000000..462015f004 --- /dev/null +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.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 osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Events; +using osu.Game.Tournament.Models; +using osuTK.Input; + +namespace osu.Game.Tournament.Screens.Gameplay.Components +{ + public class TeamScoreDisplay : CompositeDrawable + { + private readonly TeamColour teamColour; + + private readonly Bindable currentMatch = new Bindable(); + private readonly Bindable currentTeam = new Bindable(); + private readonly Bindable currentTeamScore = new Bindable(); + + private TeamDisplay teamDisplay; + + public bool ShowScore { set => teamDisplay.ShowScore = value; } + + public TeamScoreDisplay(TeamColour teamColour) + { + this.teamColour = teamColour; + + RelativeSizeAxes = Axes.Y; + AutoSizeAxes = Axes.X; + } + + [BackgroundDependencyLoader] + private void load(LadderInfo ladder) + { + currentMatch.BindTo(ladder.CurrentMatch); + currentMatch.BindValueChanged(matchChanged, true); + } + + private void matchChanged(ValueChangedEvent match) + { + currentTeamScore.UnbindBindings(); + currentTeam.UnbindBindings(); + + if (match.NewValue != null) + { + currentTeamScore.BindTo(teamColour == TeamColour.Red ? match.NewValue.Team1Score : match.NewValue.Team2Score); + currentTeam.BindTo(teamColour == TeamColour.Red ? match.NewValue.Team1 : match.NewValue.Team2); + } + + // team may change to same team, which means score is not in a good state. + // thus we handle this manually. + teamChanged(currentTeam.Value); + } + + protected override bool OnMouseDown(MouseDownEvent e) + { + switch (e.Button) + { + case MouseButton.Left: + if (currentTeamScore.Value < currentMatch.Value.PointsToWin) + currentTeamScore.Value++; + return true; + + case MouseButton.Right: + if (currentTeamScore.Value > 0) + currentTeamScore.Value--; + return true; + } + + return base.OnMouseDown(e); + } + + private void teamChanged(TournamentTeam team) + { + InternalChildren = new Drawable[] + { + teamDisplay = new TeamDisplay(team, teamColour, currentTeamScore, currentMatch.Value?.PointsToWin ?? 0), + }; + } + } +} From 7b1ac03b18dae6767ee4c13be887b33913570f31 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Mar 2020 14:26:22 +0900 Subject: [PATCH 0343/2376] Hide logo on gameplay screen --- osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs index 4d770855cd..8920990d1b 100644 --- a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs +++ b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs @@ -47,7 +47,10 @@ namespace osu.Game.Tournament.Screens.Gameplay Loop = true, RelativeSizeAxes = Axes.Both, }, - header = new MatchHeader(), + header = new MatchHeader + { + ShowLogo = false + }, new Container { RelativeSizeAxes = Axes.X, From ec1c6f88ee3889cbe9657e5745a4753255b7cbf2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Mar 2020 14:26:39 +0900 Subject: [PATCH 0344/2376] Adjust metrics to align logo pieces correctly on gameplay / map pool --- .../Components/TestSceneMatchHeader.cs | 9 +++++++++ .../Components/DrawableTournamentHeaderLogo.cs | 2 +- .../Components/DrawableTournamentHeaderText.cs | 2 +- .../Screens/Gameplay/Components/MatchHeader.cs | 2 +- osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs | 3 ++- 5 files changed, 14 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tournament.Tests/Components/TestSceneMatchHeader.cs b/osu.Game.Tournament.Tests/Components/TestSceneMatchHeader.cs index b29e4964b6..9f885ed827 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneMatchHeader.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneMatchHeader.cs @@ -1,9 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Collections.Generic; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; +using osu.Game.Tournament.Components; using osu.Game.Tournament.Screens.Gameplay.Components; using osuTK; @@ -11,6 +14,12 @@ namespace osu.Game.Tournament.Tests.Components { public class TestSceneMatchHeader : TournamentTestScene { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(DrawableTournamentHeaderText), + typeof(DrawableTournamentHeaderLogo), + }; + public TestSceneMatchHeader() { Child = new FillFlowContainer diff --git a/osu.Game.Tournament/Components/DrawableTournamentHeaderLogo.cs b/osu.Game.Tournament/Components/DrawableTournamentHeaderLogo.cs index a61cb59fed..3f5ab42fd7 100644 --- a/osu.Game.Tournament/Components/DrawableTournamentHeaderLogo.cs +++ b/osu.Game.Tournament/Components/DrawableTournamentHeaderLogo.cs @@ -15,7 +15,7 @@ namespace osu.Game.Tournament.Components { InternalChild = new LogoSprite(); - Height = 50; + Height = 82; RelativeSizeAxes = Axes.X; } diff --git a/osu.Game.Tournament/Components/DrawableTournamentHeaderText.cs b/osu.Game.Tournament/Components/DrawableTournamentHeaderText.cs index 2539075c0f..bda696ba00 100644 --- a/osu.Game.Tournament/Components/DrawableTournamentHeaderText.cs +++ b/osu.Game.Tournament/Components/DrawableTournamentHeaderText.cs @@ -15,7 +15,7 @@ namespace osu.Game.Tournament.Components { InternalChild = new TextSprite(); - Height = 25; + Height = 22; RelativeSizeAxes = Axes.X; } diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs b/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs index 751a763333..d790f4b754 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs @@ -61,7 +61,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components { RelativeSizeAxes = Axes.Both, Direction = FillDirection.Vertical, - Padding = new MarginPadding(5), + Padding = new MarginPadding(20), Spacing = new Vector2(5), Children = new Drawable[] { diff --git a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs index 4f3f7cfdbf..2b0bfe0b74 100644 --- a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs +++ b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs @@ -50,7 +50,7 @@ namespace osu.Game.Tournament.Screens.MapPool new MatchHeader(), mapFlows = new FillFlowContainer> { - Y = 100, + Y = 140, Spacing = new Vector2(10, 10), Padding = new MarginPadding(25), Direction = FillDirection.Vertical, @@ -235,6 +235,7 @@ namespace osu.Game.Tournament.Screens.MapPool { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, + Height = 42, }); } } From 63edcddaf13453d66e86d4368e63ed90d2636962 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Mar 2020 15:01:43 +0900 Subject: [PATCH 0345/2376] Apply ruleset filter in all cases (even when bypassing filter for selection purposes) --- .../Screens/Select/Carousel/CarouselBeatmap.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index 8c264ce974..e0d59e3b18 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -25,18 +25,18 @@ namespace osu.Game.Screens.Select.Carousel { base.Filter(criteria); - if (Beatmap.BeatmapSet?.Equals(criteria.SelectedBeatmapSet) == true) - { - // bypass filtering for selected beatmap - Filtered.Value = false; - return; - } - bool match = criteria.Ruleset == null || Beatmap.RulesetID == criteria.Ruleset.ID || (Beatmap.RulesetID == 0 && criteria.Ruleset.ID > 0 && criteria.AllowConvertedBeatmaps); + if (Beatmap.BeatmapSet?.Equals(criteria.SelectedBeatmapSet) == true) + { + // bypass filtering 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); From 28ac5af91c9bc751f66eb634c0fb3b98e76ba5ba Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Mar 2020 15:26:22 +0900 Subject: [PATCH 0346/2376] Fix beatmap carousel tests loading beatmap manager beatmaps in test browser --- osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs | 4 +++- osu.Game/Screens/Select/BeatmapCarousel.cs | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 71ae47dc66..80e03d82e2 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -497,7 +497,7 @@ namespace osu.Game.Tests.Visual.SongSelect } bool changed = false; - AddStep($"Load {beatmapSets.Count} Beatmaps", () => + AddStep($"Load {(beatmapSets.Count > 0 ? beatmapSets.Count.ToString() : "some")} beatmaps", () => { carousel.Filter(new FilterCriteria()); carousel.BeatmapSetsChanged = () => changed = true; @@ -697,6 +697,8 @@ namespace osu.Game.Tests.Visual.SongSelect public new List Items => base.Items; public bool PendingFilterTask => PendingFilter != null; + + protected override IEnumerable GetLoadableBeatmaps() => Enumerable.Empty(); } } } diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 71744d8b80..04c08cdbd2 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -153,9 +153,11 @@ namespace osu.Game.Screens.Select beatmaps.BeatmapHidden += beatmapHidden; beatmaps.BeatmapRestored += beatmapRestored; - loadBeatmapSets(beatmaps.GetAllUsableBeatmapSetsEnumerable()); + loadBeatmapSets(GetLoadableBeatmaps()); } + protected virtual IEnumerable GetLoadableBeatmaps() => beatmaps.GetAllUsableBeatmapSetsEnumerable(); + public void RemoveBeatmapSet(BeatmapSetInfo beatmapSet) => Schedule(() => { var existingSet = beatmapSets.FirstOrDefault(b => b.BeatmapSet.ID == beatmapSet.ID); From 933a8ffc8a8152e6e5680d39557bd3e2958f407b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Mar 2020 17:10:51 +0900 Subject: [PATCH 0347/2376] Add test coverage --- .../Visual/SongSelect/TestScenePlaySongSelect.cs | 16 +++++++++++++++- .../Carousel/DrawableCarouselBeatmapSet.cs | 8 +++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 105d96cdfe..fb287a4f70 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -436,6 +436,9 @@ namespace osu.Game.Tests.Visual.SongSelect changeRuleset(0); + // used for filter check below + AddStep("allow convert display", () => config.Set(OsuSetting.ShowConvertedBeatmaps, true)); + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap != null); AddStep("set filter text", () => songSelect.FilterControl.ChildrenOfType().First().Text = "nonono"); @@ -446,9 +449,11 @@ namespace osu.Game.Tests.Visual.SongSelect BeatmapInfo target = null; + int targetRuleset = differentRuleset ? 1 : 0; + AddStep("select beatmap externally", () => { - target = manager.GetAllUsableBeatmapSets().Where(b => b.Beatmaps.Any(bi => bi.RulesetID == (differentRuleset ? 1 : 0))) + target = manager.GetAllUsableBeatmapSets().Where(b => b.Beatmaps.Any(bi => bi.RulesetID == targetRuleset)) .ElementAt(5).Beatmaps.First(); Beatmap.Value = manager.GetWorkingBeatmap(target); @@ -456,6 +461,15 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap != null); + AddAssert("selected only shows expected ruleset (plus converts)", () => + { + var selectedPanel = songSelect.Carousel.ChildrenOfType().First(s => s.Item.State.Value == CarouselItemState.Selected); + + // 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); + }); + AddUntilStep("carousel has correct", () => songSelect.Carousel.SelectedBeatmap?.OnlineBeatmapID == target.OnlineBeatmapID); AddUntilStep("game has correct", () => Beatmap.Value.BeatmapInfo.OnlineBeatmapID == target.OnlineBeatmapID); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 6cd145cfef..1454784f03 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -205,7 +205,9 @@ namespace osu.Game.Screens.Select.Carousel { private readonly BindableBool filtered = new BindableBool(); - private readonly CarouselBeatmap item; + public bool IsFiltered => filtered.Value; + + public readonly CarouselBeatmap Item; public FilterableDifficultyIcon(CarouselBeatmap item) : base(item.Beatmap) @@ -214,13 +216,13 @@ namespace osu.Game.Screens.Select.Carousel filtered.ValueChanged += isFiltered => Schedule(() => this.FadeTo(isFiltered.NewValue ? 0.1f : 1, 100)); filtered.TriggerChange(); - this.item = item; + this.Item = item; } protected override bool OnClick(ClickEvent e) { if (!filtered.Value) - item.State.Value = CarouselItemState.Selected; + Item.State.Value = CarouselItemState.Selected; return true; } From fc058f8896eb8f023b6e7702d21355167f1a78b4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Mar 2020 18:03:18 +0900 Subject: [PATCH 0348/2376] Remove unnecessary this. prefix --- osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 1454784f03..547aeaddc6 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -216,7 +216,7 @@ namespace osu.Game.Screens.Select.Carousel filtered.ValueChanged += isFiltered => Schedule(() => this.FadeTo(isFiltered.NewValue ? 0.1f : 1, 100)); filtered.TriggerChange(); - this.Item = item; + Item = item; } protected override bool OnClick(ClickEvent e) From bc2a1cdb623547e9a05740685eba26f75bd00c76 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 12 Mar 2020 12:04:36 +0300 Subject: [PATCH 0349/2376] Apply suggestions --- osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 441f9126f6..2394110165 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -344,11 +344,8 @@ namespace osu.Game.Rulesets.Catch.UI if (validCatch) updateState(fruit.Kiai ? CatcherAnimationState.Kiai : CatcherAnimationState.Idle); - else - { - if (!(fruit is Banana)) - updateState(CatcherAnimationState.Fail); - } + else if (!(fruit is Banana)) + updateState(CatcherAnimationState.Fail); return validCatch; } @@ -362,7 +359,7 @@ namespace osu.Game.Rulesets.Catch.UI updateCatcher(); } - public CatcherAnimationState CurrentState; + public CatcherAnimationState CurrentState { get; private set; } private double hyperDashModifier = 1; private int hyperDashDirection; From 5537b279de1b3707496f4bf8aac49aa359f13cbc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Mar 2020 18:39:43 +0900 Subject: [PATCH 0350/2376] Fix failing test occasionally getting wrong ruleset beatmap --- 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 fb287a4f70..55c1d8451f 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -453,8 +453,9 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("select beatmap externally", () => { - target = manager.GetAllUsableBeatmapSets().Where(b => b.Beatmaps.Any(bi => bi.RulesetID == targetRuleset)) - .ElementAt(5).Beatmaps.First(); + target = manager.GetAllUsableBeatmapSets() + .Where(b => b.Beatmaps.Any(bi => bi.RulesetID == targetRuleset)) + .ElementAt(5).Beatmaps.First(bi => bi.RulesetID == targetRuleset); Beatmap.Value = manager.GetWorkingBeatmap(target); }); From ce5d01ed191ea896b5b643352b791d27615045b8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Mar 2020 17:55:31 +0900 Subject: [PATCH 0351/2376] Allow filtered difficulty icons to be clicked --- .../Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 547aeaddc6..d3a7b4d3d9 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -221,9 +221,7 @@ namespace osu.Game.Screens.Select.Carousel protected override bool OnClick(ClickEvent e) { - if (!filtered.Value) - Item.State.Value = CarouselItemState.Selected; - + Item.State.Value = CarouselItemState.Selected; return true; } } From 2bcf07938676b39a0db435939547500c9a562aca Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Mar 2020 15:34:58 +0900 Subject: [PATCH 0352/2376] Update carousel test logic to match new carousel selection behaviour --- .../Visual/SongSelect/TestSceneBeatmapCarousel.cs | 2 +- .../Visual/SongSelect/TestScenePlaySongSelect.cs | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 71ae47dc66..d80add3015 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -399,7 +399,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 selected", () => carousel.SelectedBeatmap.Equals(testMixed.Beatmaps[0])); + AddAssert("unfiltered beatmap not selected", () => carousel.SelectedBeatmap == null); AddStep("remove mixed set", () => { diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 55c1d8451f..d16fd0bceb 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -591,16 +591,16 @@ namespace osu.Game.Tests.Visual.SongSelect } })); + BeatmapInfo filteredBeatmap = null; DrawableCarouselBeatmapSet.FilterableDifficultyIcon filteredIcon = null; + AddStep("Get filtered icon", () => { - var filteredBeatmap = songSelect.Carousel.SelectedBeatmapSet.Beatmaps.Find(b => b.BPM < maxBPM); + filteredBeatmap = songSelect.Carousel.SelectedBeatmapSet.Beatmaps.Find(b => b.BPM < maxBPM); int filteredBeatmapIndex = getBeatmapIndex(filteredBeatmap.BeatmapSet, filteredBeatmap); filteredIcon = set.ChildrenOfType().ElementAt(filteredBeatmapIndex); }); - int? previousID = null; - AddStep("Store current ID", () => previousID = songSelect.Carousel.SelectedBeatmap.ID); AddStep("Click on a filtered difficulty", () => { InputManager.MoveMouseTo(filteredIcon); @@ -608,7 +608,8 @@ namespace osu.Game.Tests.Visual.SongSelect InputManager.PressButton(MouseButton.Left); InputManager.ReleaseButton(MouseButton.Left); }); - AddAssert("Selected beatmap has not changed", () => songSelect.Carousel.SelectedBeatmap.ID == previousID); + + AddAssert("Selected beatmap correct", () => songSelect.Carousel.SelectedBeatmap == filteredBeatmap); } private int getBeatmapIndex(BeatmapSetInfo set, BeatmapInfo info) => set.Beatmaps.FindIndex(b => b == info); From 6e11c3014ce0830390957797a024d1978d45122e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Mar 2020 16:30:21 +0900 Subject: [PATCH 0353/2376] Allow grouped difficulty icons to be clicked --- .../SongSelect/TestScenePlaySongSelect.cs | 48 +++++++++++++++++-- .../Carousel/DrawableCarouselBeatmapSet.cs | 12 +++-- 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 8295f0aa66..34f442e6ee 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -654,6 +654,48 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("Check ruleset changed to mania", () => Ruleset.Value.ID == 3); } + [Test] + public void TestGroupedDifficultyIconSelecting() + { + changeRuleset(0); + + createSongSelect(); + + AddStep("import huge difficulty count map", () => + { + var usableRulesets = rulesets.AvailableRulesets.Where(r => r.ID != 2).ToArray(); + manager.Import(createTestBeatmapSet(0, usableRulesets, 50)).Wait(); + }); + + DrawableCarouselBeatmapSet set = null; + AddUntilStep("Find the DrawableCarouselBeatmapSet", () => + { + set = songSelect.Carousel.ChildrenOfType().FirstOrDefault(); + return set != null; + }); + + DrawableCarouselBeatmapSet.FilterableGroupedDifficultyIcon groupIcon = null; + AddStep("Find group icon for different ruleset", () => + { + groupIcon = set.ChildrenOfType() + .First(icon => icon.Items.First().Beatmap.Ruleset.ID == 3); + }); + + AddAssert("Check ruleset is osu!", () => Ruleset.Value.ID == 0); + + AddStep("Click on group", () => + { + InputManager.MoveMouseTo(groupIcon); + + InputManager.PressButton(MouseButton.Left); + InputManager.ReleaseButton(MouseButton.Left); + }); + + AddUntilStep("Check ruleset changed to mania", () => Ruleset.Value.ID == 3); + + AddAssert("Check first item in group selected", () => Beatmap.Value.BeatmapInfo == groupIcon.Items.First().Beatmap); + } + private int getBeatmapIndex(BeatmapSetInfo set, BeatmapInfo info) => set.Beatmaps.FindIndex(b => b == info); private int getCurrentBeatmapIndex() => getBeatmapIndex(songSelect.Carousel.SelectedBeatmapSet, songSelect.Carousel.SelectedBeatmap); @@ -695,16 +737,16 @@ namespace osu.Game.Tests.Visual.SongSelect }); } - private BeatmapSetInfo createTestBeatmapSet(int setId, RulesetInfo[] rulesets) + private BeatmapSetInfo createTestBeatmapSet(int setId, RulesetInfo[] rulesets, int countPerRuleset = 6) { int j = 0; RulesetInfo getRuleset() => rulesets[j++ % rulesets.Length]; var beatmaps = new List(); - for (int i = 0; i < 6; i++) + for (int i = 0; i < countPerRuleset; i++) { - int beatmapId = setId * 10 + i; + int beatmapId = setId * 100 + i; int length = RNG.Next(30000, 200000); double bpm = RNG.NextSingle(80, 200); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index d3a7b4d3d9..a53b74c1b8 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -228,12 +228,12 @@ namespace osu.Game.Screens.Select.Carousel public class FilterableGroupedDifficultyIcon : GroupedDifficultyIcon { - private readonly List items; + public readonly List Items; public FilterableGroupedDifficultyIcon(List items, RulesetInfo ruleset) : base(items.Select(i => i.Beatmap).ToList(), ruleset, Color4.White) { - this.items = items; + Items = items; foreach (var item in items) item.Filtered.BindValueChanged(_ => Scheduler.AddOnce(updateFilteredDisplay)); @@ -241,10 +241,16 @@ namespace osu.Game.Screens.Select.Carousel updateFilteredDisplay(); } + protected override bool OnClick(ClickEvent e) + { + Items.First().State.Value = CarouselItemState.Selected; + return true; + } + private void updateFilteredDisplay() { // for now, fade the whole group based on the ratio of hidden items. - this.FadeTo(1 - 0.9f * ((float)items.Count(i => i.Filtered.Value) / items.Count), 100); + this.FadeTo(1 - 0.9f * ((float)Items.Count(i => i.Filtered.Value) / Items.Count), 100); } } } From ca9cfbe51d50a7d9b95096efee9b519d8a607ae3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Mar 2020 15:52:03 +0900 Subject: [PATCH 0354/2376] Move selection fallback logic out of BeatmapCarousel to SongSelect --- osu.Game/Screens/Select/BeatmapCarousel.cs | 26 +++++++++----------- osu.Game/Screens/Select/SongSelect.cs | 28 +++++++++++++++------- 2 files changed, 30 insertions(+), 24 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 71744d8b80..ca20b02bce 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -225,25 +225,21 @@ namespace osu.Game.Screens.Select continue; if (!bypassFilters && item.Filtered.Value) - // The beatmap exists in this set but is filtered, so look for the first unfiltered map in the set - item = set.Beatmaps.FirstOrDefault(b => !b.Filtered.Value); + return false; - if (item != null) + select(item); + + // if we got here and the set is filtered, it means we were bypassing filters. + // in this case, reapplying the filter is necessary to ensure the panel is in the correct place + // (since it is forcefully being included in the carousel). + if (set.Filtered.Value) { - select(item); + Debug.Assert(bypassFilters); - // if we got here and the set is filtered, it means we were bypassing filters. - // in this case, reapplying the filter is necessary to ensure the panel is in the correct place - // (since it is forcefully being included in the carousel). - if (set.Filtered.Value) - { - Debug.Assert(bypassFilters); - - applyActiveCriteria(false); - } - - return true; + applyActiveCriteria(false); } + + return true; } return false; diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 528222a89c..11c680bdb0 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -380,6 +380,8 @@ namespace osu.Game.Screens.Select { if (e.NewValue is DummyWorkingBeatmap || !this.IsCurrentScreen()) return; + Logger.Log($"working beatmap updated to {e.NewValue}"); + if (!Carousel.SelectBeatmap(e.NewValue.BeatmapInfo, false)) { // A selection may not have been possible with filters applied. @@ -446,8 +448,10 @@ namespace osu.Game.Screens.Select if (transferRulesetValue()) { - // if the ruleset changed, the rest of the selection update will happen via updateSelectedRuleset. Mods.Value = Array.Empty(); + + // required to return once in order to have the carousel in a good state. + // if the ruleset changed, the rest of the selection update will happen via updateSelectedRuleset. return; } @@ -472,7 +476,7 @@ namespace osu.Game.Screens.Select if (this.IsCurrentScreen()) ensurePlayingSelected(); - UpdateBeatmap(Beatmap.Value); + updateComponentFromBeatmap(Beatmap.Value); } } @@ -547,7 +551,7 @@ namespace osu.Game.Screens.Select if (Beatmap != null && !Beatmap.Value.BeatmapSetInfo.DeletePending) { - UpdateBeatmap(Beatmap.Value); + updateComponentFromBeatmap(Beatmap.Value); // restart playback on returning to song select, regardless. music?.Play(); @@ -610,10 +614,8 @@ namespace osu.Game.Screens.Select /// This is a debounced call (unlike directly binding to WorkingBeatmap.ValueChanged). /// /// The working beatmap. - protected virtual void UpdateBeatmap(WorkingBeatmap beatmap) + private void updateComponentFromBeatmap(WorkingBeatmap beatmap) { - Logger.Log($"working beatmap updated to {beatmap}"); - if (Background is BackgroundScreenBeatmap backgroundModeBeatmap) { backgroundModeBeatmap.Beatmap = beatmap; @@ -658,9 +660,17 @@ namespace osu.Game.Screens.Select return; // Attempt to select the current beatmap on the carousel, if it is valid to be selected. - if (!Beatmap.IsDefault && Beatmap.Value.BeatmapSetInfo?.DeletePending == false && Beatmap.Value.BeatmapSetInfo?.Protected == false - && Carousel.SelectBeatmap(Beatmap.Value.BeatmapInfo, false)) - return; + if (!Beatmap.IsDefault && Beatmap.Value.BeatmapSetInfo?.DeletePending == false && Beatmap.Value.BeatmapSetInfo?.Protected == false) + { + if (Carousel.SelectBeatmap(Beatmap.Value.BeatmapInfo, false)) + return; + + // prefer not changing ruleset at this point, so look for another difficulty in the currently playing beatmap + var found = Beatmap.Value.BeatmapSetInfo.Beatmaps.FirstOrDefault(b => b.Ruleset.Equals(decoupledRuleset.Value)); + + if (found != null && Carousel.SelectBeatmap(found, false)) + return; + } // If the current active beatmap could not be selected, select a new random beatmap. if (!Carousel.SelectNextRandom()) From db5c8043db509c2b94d46160a14616c359f52f35 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Mar 2020 16:42:26 +0900 Subject: [PATCH 0355/2376] Add test covering ruleset change on difficulty icon selection --- .../SongSelect/TestScenePlaySongSelect.cs | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index d16fd0bceb..8295f0aa66 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -572,6 +572,7 @@ namespace osu.Game.Tests.Visual.SongSelect difficultyIcon = set.ChildrenOfType() .First(icon => getDifficultyIconIndex(set, icon) != getCurrentBeatmapIndex()); }); + AddStep("Click on a difficulty", () => { InputManager.MoveMouseTo(difficultyIcon); @@ -579,6 +580,7 @@ namespace osu.Game.Tests.Visual.SongSelect InputManager.PressButton(MouseButton.Left); InputManager.ReleaseButton(MouseButton.Left); }); + AddAssert("Selected beatmap correct", () => getCurrentBeatmapIndex() == getDifficultyIconIndex(set, difficultyIcon)); double? maxBPM = null; @@ -596,7 +598,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("Get filtered icon", () => { - filteredBeatmap = songSelect.Carousel.SelectedBeatmapSet.Beatmaps.Find(b => b.BPM < maxBPM); + filteredBeatmap = songSelect.Carousel.SelectedBeatmapSet.Beatmaps.First(b => b.BPM < maxBPM); int filteredBeatmapIndex = getBeatmapIndex(filteredBeatmap.BeatmapSet, filteredBeatmap); filteredIcon = set.ChildrenOfType().ElementAt(filteredBeatmapIndex); }); @@ -612,6 +614,46 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert("Selected beatmap correct", () => songSelect.Carousel.SelectedBeatmap == filteredBeatmap); } + [Test] + public void TestDifficultyIconSelectingForDifferentRuleset() + { + changeRuleset(0); + + createSongSelect(); + + AddStep("import multi-ruleset map", () => + { + var usableRulesets = rulesets.AvailableRulesets.Where(r => r.ID != 2).ToArray(); + manager.Import(createTestBeatmapSet(0, usableRulesets)).Wait(); + }); + + DrawableCarouselBeatmapSet set = null; + AddUntilStep("Find the DrawableCarouselBeatmapSet", () => + { + set = songSelect.Carousel.ChildrenOfType().FirstOrDefault(); + return set != null; + }); + + DrawableCarouselBeatmapSet.FilterableDifficultyIcon difficultyIcon = null; + AddStep("Find an icon for different ruleset", () => + { + difficultyIcon = set.ChildrenOfType() + .First(icon => icon.Item.Beatmap.ID == 3); + }); + + AddAssert("Check ruleset is osu!", () => Ruleset.Value.ID == 0); + + AddStep("Click on a difficulty", () => + { + InputManager.MoveMouseTo(difficultyIcon); + + InputManager.PressButton(MouseButton.Left); + InputManager.ReleaseButton(MouseButton.Left); + }); + + AddUntilStep("Check ruleset changed to mania", () => Ruleset.Value.ID == 3); + } + private int getBeatmapIndex(BeatmapSetInfo set, BeatmapInfo info) => set.Beatmaps.FindIndex(b => b == info); private int getCurrentBeatmapIndex() => getBeatmapIndex(songSelect.Carousel.SelectedBeatmapSet, songSelect.Carousel.SelectedBeatmap); From a69fabbd1ff4255c7cad25881b31daa5a48f365e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Mar 2020 18:56:48 +0900 Subject: [PATCH 0356/2376] 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 6a8e66ee6a..f623a92ade 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index cc1ab654ab..ba6f0e2251 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -23,7 +23,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 04b688cfa3..54cd400d51 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + @@ -79,7 +79,7 @@ - + From 1819a15509c8854a1307e9d86a8beb79fdefe59d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Mar 2020 18:56:31 +0900 Subject: [PATCH 0357/2376] Make test ID assigning simpler --- .../SongSelect/TestScenePlaySongSelect.cs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 34f442e6ee..62eb1340fc 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -283,7 +283,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("import multi-ruleset map", () => { var usableRulesets = rulesets.AvailableRulesets.Where(r => r.ID != 2).ToArray(); - manager.Import(createTestBeatmapSet(0, usableRulesets)).Wait(); + manager.Import(createTestBeatmapSet(usableRulesets)).Wait(); }); } else @@ -624,7 +624,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("import multi-ruleset map", () => { var usableRulesets = rulesets.AvailableRulesets.Where(r => r.ID != 2).ToArray(); - manager.Import(createTestBeatmapSet(0, usableRulesets)).Wait(); + manager.Import(createTestBeatmapSet(usableRulesets)).Wait(); }); DrawableCarouselBeatmapSet set = null; @@ -638,7 +638,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("Find an icon for different ruleset", () => { difficultyIcon = set.ChildrenOfType() - .First(icon => icon.Item.Beatmap.ID == 3); + .First(icon => icon.Item.Beatmap.Ruleset.ID == 3); }); AddAssert("Check ruleset is osu!", () => Ruleset.Value.ID == 0); @@ -664,7 +664,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("import huge difficulty count map", () => { var usableRulesets = rulesets.AvailableRulesets.Where(r => r.ID != 2).ToArray(); - manager.Import(createTestBeatmapSet(0, usableRulesets, 50)).Wait(); + manager.Import(createTestBeatmapSet(usableRulesets, 50)).Wait(); }); DrawableCarouselBeatmapSet set = null; @@ -707,7 +707,7 @@ namespace osu.Game.Tests.Visual.SongSelect private void addRulesetImportStep(int id) => AddStep($"import test map for ruleset {id}", () => importForRuleset(id)); - private void importForRuleset(int id) => manager.Import(createTestBeatmapSet(getImportId(), rulesets.AvailableRulesets.Where(r => r.ID == id).ToArray())).Wait(); + private void importForRuleset(int id) => manager.Import(createTestBeatmapSet(rulesets.AvailableRulesets.Where(r => r.ID == id).ToArray())).Wait(); private static int importId; @@ -733,20 +733,22 @@ namespace osu.Game.Tests.Visual.SongSelect var usableRulesets = rulesets.AvailableRulesets.Where(r => r.ID != 2).ToArray(); for (int i = 0; i < 100; i += 10) - manager.Import(createTestBeatmapSet(i, usableRulesets)).Wait(); + manager.Import(createTestBeatmapSet(usableRulesets)).Wait(); }); } - private BeatmapSetInfo createTestBeatmapSet(int setId, RulesetInfo[] rulesets, int countPerRuleset = 6) + private BeatmapSetInfo createTestBeatmapSet(RulesetInfo[] rulesets, int countPerRuleset = 6) { int j = 0; RulesetInfo getRuleset() => rulesets[j++ % rulesets.Length]; + int setId = getImportId(); + var beatmaps = new List(); for (int i = 0; i < countPerRuleset; i++) { - int beatmapId = setId * 100 + i; + int beatmapId = setId * 1000 + i; int length = RNG.Next(30000, 200000); double bpm = RNG.NextSingle(80, 200); From 250061ddf50d1a06541bbcaaf78fc6989b70ebec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Mar 2020 19:46:21 +0900 Subject: [PATCH 0358/2376] Fix test failure due to off-screen panel --- osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 62eb1340fc..9f33d03ac4 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -661,12 +661,16 @@ namespace osu.Game.Tests.Visual.SongSelect createSongSelect(); + BeatmapSetInfo imported = null; + AddStep("import huge difficulty count map", () => { var usableRulesets = rulesets.AvailableRulesets.Where(r => r.ID != 2).ToArray(); - manager.Import(createTestBeatmapSet(usableRulesets, 50)).Wait(); + imported = manager.Import(createTestBeatmapSet(usableRulesets, 50)).Result; }); + AddStep("select the first beatmap of import", () => Beatmap.Value = manager.GetWorkingBeatmap(imported.Beatmaps.First())); + DrawableCarouselBeatmapSet set = null; AddUntilStep("Find the DrawableCarouselBeatmapSet", () => { From bab197553e3968263cf0ee93776707d8da7c53c9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Mar 2020 15:34:58 +0900 Subject: [PATCH 0359/2376] Update carousel test logic to match new carousel selection behaviour --- .../Visual/SongSelect/TestSceneBeatmapCarousel.cs | 2 +- .../Visual/SongSelect/TestScenePlaySongSelect.cs | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 71ae47dc66..d80add3015 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -399,7 +399,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 selected", () => carousel.SelectedBeatmap.Equals(testMixed.Beatmaps[0])); + AddAssert("unfiltered beatmap not selected", () => carousel.SelectedBeatmap == null); AddStep("remove mixed set", () => { diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 55c1d8451f..d16fd0bceb 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -591,16 +591,16 @@ namespace osu.Game.Tests.Visual.SongSelect } })); + BeatmapInfo filteredBeatmap = null; DrawableCarouselBeatmapSet.FilterableDifficultyIcon filteredIcon = null; + AddStep("Get filtered icon", () => { - var filteredBeatmap = songSelect.Carousel.SelectedBeatmapSet.Beatmaps.Find(b => b.BPM < maxBPM); + filteredBeatmap = songSelect.Carousel.SelectedBeatmapSet.Beatmaps.Find(b => b.BPM < maxBPM); int filteredBeatmapIndex = getBeatmapIndex(filteredBeatmap.BeatmapSet, filteredBeatmap); filteredIcon = set.ChildrenOfType().ElementAt(filteredBeatmapIndex); }); - int? previousID = null; - AddStep("Store current ID", () => previousID = songSelect.Carousel.SelectedBeatmap.ID); AddStep("Click on a filtered difficulty", () => { InputManager.MoveMouseTo(filteredIcon); @@ -608,7 +608,8 @@ namespace osu.Game.Tests.Visual.SongSelect InputManager.PressButton(MouseButton.Left); InputManager.ReleaseButton(MouseButton.Left); }); - AddAssert("Selected beatmap has not changed", () => songSelect.Carousel.SelectedBeatmap.ID == previousID); + + AddAssert("Selected beatmap correct", () => songSelect.Carousel.SelectedBeatmap == filteredBeatmap); } private int getBeatmapIndex(BeatmapSetInfo set, BeatmapInfo info) => set.Beatmaps.FindIndex(b => b == info); From 317bb5d0a436e3f6af94c141beeeec314b8f10cc Mon Sep 17 00:00:00 2001 From: Kelvin <2yangk23@gmail.com> Date: Thu, 12 Mar 2020 03:55:45 -0700 Subject: [PATCH 0360/2376] Fallback on invalid AnimationFramerate for legacy skins --- osu.Game/Skinning/LegacySkinExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/LegacySkinExtensions.cs b/osu.Game/Skinning/LegacySkinExtensions.cs index fa4de21eec..9cc58f4490 100644 --- a/osu.Game/Skinning/LegacySkinExtensions.cs +++ b/osu.Game/Skinning/LegacySkinExtensions.cs @@ -61,7 +61,7 @@ namespace osu.Game.Skinning { var iniRate = source.GetConfig(GlobalSkinConfiguration.AnimationFramerate); - if (iniRate != null) + if (iniRate != null && iniRate.Value > 0) return 1000f / iniRate.Value; return 1000f / textures.Length; From c8ea92257765d05bba2ae3d7a0f0966625d2aacf Mon Sep 17 00:00:00 2001 From: Kelvin <2yangk23@users.noreply.github.com> Date: Thu, 12 Mar 2020 04:18:57 -0700 Subject: [PATCH 0361/2376] Update osu.Game/Skinning/LegacySkinExtensions.cs Co-Authored-By: Dean Herbert --- osu.Game/Skinning/LegacySkinExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/LegacySkinExtensions.cs b/osu.Game/Skinning/LegacySkinExtensions.cs index 9cc58f4490..52328d43b2 100644 --- a/osu.Game/Skinning/LegacySkinExtensions.cs +++ b/osu.Game/Skinning/LegacySkinExtensions.cs @@ -61,7 +61,7 @@ namespace osu.Game.Skinning { var iniRate = source.GetConfig(GlobalSkinConfiguration.AnimationFramerate); - if (iniRate != null && iniRate.Value > 0) + if (iniRate?.Value > 0) return 1000f / iniRate.Value; return 1000f / textures.Length; From 3f8b454ff4783b896516506b4ef192cda78134f2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Mar 2020 10:01:28 +0900 Subject: [PATCH 0362/2376] Reword comment to match new filtering behaviour MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Bartłomiej Dach --- osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index e0d59e3b18..6d760df065 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -32,7 +32,7 @@ namespace osu.Game.Screens.Select.Carousel if (Beatmap.BeatmapSet?.Equals(criteria.SelectedBeatmapSet) == true) { - // bypass filtering for selected beatmap + // only check ruleset equality or convertability for selected beatmap Filtered.Value = !match; return; } From 04f1da04db8c685305b8688dd29d7d4af0985c6b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Mar 2020 10:52:08 +0900 Subject: [PATCH 0363/2376] Remove incorrect xmldoc from SelectBeatmap function --- osu.Game/Screens/Select/BeatmapCarousel.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index ca20b02bce..34d659cc90 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -201,9 +201,6 @@ namespace osu.Game.Screens.Select /// /// Selects a given beatmap on the carousel. - /// - /// If bypassFilters is false, we will try to select another unfiltered beatmap in the same set. If the - /// entire set is filtered, no selection is made. /// /// The beatmap to select. /// Whether to select the beatmap even if it is filtered (i.e., not visible on carousel). From ba0dec891d4a576f6158bff9235bc37cbb224f14 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Mar 2020 10:58:36 +0900 Subject: [PATCH 0364/2376] Update test temporarily --- osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index d16fd0bceb..f1ff08b92c 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -609,7 +609,8 @@ namespace osu.Game.Tests.Visual.SongSelect InputManager.ReleaseButton(MouseButton.Left); }); - AddAssert("Selected beatmap correct", () => songSelect.Carousel.SelectedBeatmap == filteredBeatmap); + // todo: this logic is changed in follow up PR. + AddAssert("Selected beatmap not changed", () => songSelect.Carousel.SelectedBeatmap != filteredBeatmap); } private int getBeatmapIndex(BeatmapSetInfo set, BeatmapInfo info) => set.Beatmaps.FindIndex(b => b == info); From de9857ccdc1e5808c23ee0dc99c2c357c2f0984d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Mar 2020 11:00:09 +0900 Subject: [PATCH 0365/2376] Fix incorrect id reference in test --- osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 8295f0aa66..31c6e35492 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -638,7 +638,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("Find an icon for different ruleset", () => { difficultyIcon = set.ChildrenOfType() - .First(icon => icon.Item.Beatmap.ID == 3); + .First(icon => icon.Item.Beatmap.Ruleset.ID == 3); }); AddAssert("Check ruleset is osu!", () => Ruleset.Value.ID == 0); From 5f8d180b5ec5acdff86933d7deeb5e0c3bcff6ff Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Mar 2020 11:51:26 +0900 Subject: [PATCH 0366/2376] Fix carousel scrolling being inoperable during beatmap import --- osu.Game/Screens/Select/BeatmapCarousel.cs | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 04c08cdbd2..2dc063012f 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -191,7 +191,9 @@ namespace osu.Game.Screens.Select root.AddChild(newSet); - applyActiveCriteria(false); + // only reset scroll position if already near the scroll target. + // without this, during a large beatmap import it is impossible to navigate the carousel. + applyActiveCriteria(false, alwaysResetScrollPosition: false); //check if we can/need to maintain our current selection. if (previouslySelectedID != null) @@ -411,7 +413,7 @@ namespace osu.Game.Screens.Select applyActiveCriteria(debounce); } - private void applyActiveCriteria(bool debounce) + private void applyActiveCriteria(bool debounce, bool alwaysResetScrollPosition = true) { if (root.Children.Any() != true) return; @@ -421,7 +423,9 @@ namespace osu.Game.Screens.Select root.Filter(activeCriteria); itemsCache.Invalidate(); - scrollPositionCache.Invalidate(); + + if (alwaysResetScrollPosition || isAtScrollTarget) + ScrollToSelected(); } PendingFilter?.Cancel(); @@ -435,6 +439,9 @@ namespace osu.Game.Screens.Select private float? scrollTarget; + /// + /// Scroll to the current . + /// public void ScrollToSelected() => scrollPositionCache.Invalidate(); protected override bool OnKeyDown(KeyDownEvent e) @@ -601,7 +608,7 @@ namespace osu.Game.Screens.Select SelectionChanged?.Invoke(c.Beatmap); itemsCache.Invalidate(); - scrollPositionCache.Invalidate(); + ScrollToSelected(); } }; } @@ -688,6 +695,11 @@ namespace osu.Game.Screens.Select itemsCache.Validate(); } + /// + /// Denotes whether the current scroll position is roughly at the scroll target (the current selection). + /// + private bool isAtScrollTarget => scrollTarget != null && Precision.AlmostEquals(scrollTarget.Value, scroll.Current, 150); + private bool firstScroll = true; private void updateScrollPosition() From ac70fcc54469fe5edd2d6574f609169ac5dbdb0b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Mar 2020 12:30:27 +0900 Subject: [PATCH 0367/2376] Change logic to be more resilient by identifying user scroll events --- osu.Game/Screens/Select/BeatmapCarousel.cs | 26 ++++++++++++++++------ 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 2dc063012f..20f9bb1be8 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -66,7 +66,7 @@ namespace osu.Game.Screens.Select ///

~&^CXCgh=e9m>w~R$x}C+NMpVTm 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 literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-3@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-3@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..953a04d4e4f9cdde644d6ac6e9715b6256631438 GIT binary patch literal 57077 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 literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle.png deleted file mode 100755 index 17177f3246cafff0cf81669bca7efd0a22599f44..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 133664 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 O0000 RequiredTypes => new[] { typeof(CatcherArea), + typeof(CatcherSprite) }; [BackgroundDependencyLoader] diff --git a/osu.Game.Rulesets.Catch/CatchSkinComponents.cs b/osu.Game.Rulesets.Catch/CatchSkinComponents.cs index 02c045f363..08bff36401 100644 --- a/osu.Game.Rulesets.Catch/CatchSkinComponents.cs +++ b/osu.Game.Rulesets.Catch/CatchSkinComponents.cs @@ -10,6 +10,7 @@ namespace osu.Game.Rulesets.Catch FruitGrapes, FruitOrange, FruitPear, - Droplet + Droplet, + CatcherIdle } } diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs index 36164c5543..af7c60b929 100644 --- a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs @@ -44,6 +44,10 @@ namespace osu.Game.Rulesets.Catch.Skinning return new LegacyFruitPiece("fruit-drop") { Scale = new Vector2(0.8f) }; break; + + case CatchSkinComponents.CatcherIdle: + return this.GetAnimation("fruit-catcher-idle", true, true, true) ?? + this.GetAnimation("fruit-ryuuta", true, true, true); } return null; diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index b977d46611..2015937f2a 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -155,7 +155,10 @@ namespace osu.Game.Rulesets.Catch.UI Anchor = Anchor.TopCentre, Origin = Anchor.BottomCentre, }, - createCatcherSprite(), + createCatcherSprite().With(c => + { + c.Anchor = Anchor.TopCentre; + }) }; } @@ -205,12 +208,11 @@ namespace osu.Game.Rulesets.Catch.UI var additive = createCatcherSprite(); additive.Anchor = Anchor; - additive.OriginPosition += new Vector2(DrawWidth / 2, 0); // also temporary to align sprite correctly. - additive.Position = Position; additive.Scale = Scale; additive.Colour = HyperDashing ? Color4.Red : Color4.White; - additive.RelativePositionAxes = RelativePositionAxes; additive.Blending = BlendingParameters.Additive; + additive.RelativePositionAxes = RelativePositionAxes; + additive.Position = Position; AdditiveTarget.Add(additive); diff --git a/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs b/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs index 025fa9c56e..78020114cd 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs @@ -3,31 +3,35 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; using osu.Game.Skinning; using osuTK; namespace osu.Game.Rulesets.Catch.UI { - public class CatcherSprite : CompositeDrawable + public class CatcherSprite : SkinnableDrawable { + protected override bool ApplySizeRestrictionsToDefault => true; + public CatcherSprite() + : base(new CatchSkinComponent(CatchSkinComponents.CatcherIdle), _ => + new DefaultCatcherSprite(), confineMode: ConfineMode.ScaleDownToFit) { + RelativeSizeAxes = Axes.None; Size = new Vector2(CatcherArea.CATCHER_SIZE); // Sets the origin roughly to the centre of the catcher's plate to allow for correct scaling. - OriginPosition = new Vector2(-0.02f, 0.06f) * CatcherArea.CATCHER_SIZE; + OriginPosition = new Vector2(0.5f, 0.06f) * CatcherArea.CATCHER_SIZE; } - [BackgroundDependencyLoader] - private void load() + private class DefaultCatcherSprite : Sprite { - InternalChild = new SkinnableSprite("Gameplay/catch/fruit-catcher-idle", confineMode: ConfineMode.ScaleDownToFit) + [BackgroundDependencyLoader] + private void load(TextureStore textures) { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - }; + Texture = textures.Get("Gameplay/catch/fruit-catcher-idle"); + } } } } From 9d5327b1accc07234637f51f7903cacb2b9966ea Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 10 Mar 2020 15:00:23 +0900 Subject: [PATCH 0295/2376] Fix osu! shaking instead of missing for early hits --- .../TestSceneEarlyMissJudgement.cs | 70 +++++++++++++++++++ .../Scoring/OsuHitWindows.cs | 2 +- 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Rulesets.Osu.Tests/TestSceneEarlyMissJudgement.cs diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneEarlyMissJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneEarlyMissJudgement.cs new file mode 100644 index 0000000000..27a32aa96e --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneEarlyMissJudgement.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 NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Replays; +using osu.Game.Rulesets.Osu.Beatmaps; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Replays; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Tests.Visual; +using osu.Game.Users; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests +{ + public class TestSceneEarlyMissJudgement : ModTestScene + { + public TestSceneEarlyMissJudgement() + : base(new OsuRuleset()) + { + } + + [Test] + public void TestHitCircleEarly() => CreateModTest(new ModTestData + { + Autoplay = false, + Mod = new TestAutoMod(), + Beatmap = new Beatmap + { + HitObjects = { new HitCircle { Position = new Vector2(256, 192) } } + }, + PassCondition = () => Player.Results.Count > 0 && Player.Results[0].TimeOffset < 0 && Player.Results[0].Type == HitResult.Miss + }); + + private class TestAutoMod : OsuModAutoplay + { + public override Score CreateReplayScore(IBeatmap beatmap) => new Score + { + ScoreInfo = new ScoreInfo { User = new User { Username = "Autoplay" } }, + Replay = new MissingAutoGenerator(beatmap).Generate() + }; + } + + private class MissingAutoGenerator : OsuAutoGeneratorBase + { + public new OsuBeatmap Beatmap => (OsuBeatmap)base.Beatmap; + + public MissingAutoGenerator(IBeatmap beatmap) + : base(beatmap) + { + } + + public override Replay Generate() + { + AddFrameToReplay(new OsuReplayFrame(-100000, new Vector2(256, 500))); + AddFrameToReplay(new OsuReplayFrame(Beatmap.HitObjects[0].StartTime - 1500, new Vector2(256, 500))); + AddFrameToReplay(new OsuReplayFrame(Beatmap.HitObjects[0].StartTime - 1500, new Vector2(256, 500))); + + AddFrameToReplay(new OsuReplayFrame(Beatmap.HitObjects[0].StartTime - 450, Beatmap.HitObjects[0].StackedPosition)); + AddFrameToReplay(new OsuReplayFrame(Beatmap.HitObjects[0].StartTime - 350, Beatmap.HitObjects[0].StackedPosition, OsuAction.LeftButton)); + AddFrameToReplay(new OsuReplayFrame(Beatmap.HitObjects[0].StartTime - 325, Beatmap.HitObjects[0].StackedPosition)); + + return Replay; + } + } + } +} diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs b/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs index a6491bb3f3..6f2998006f 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs @@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Scoring new DifficultyRange(HitResult.Great, 80, 50, 20), new DifficultyRange(HitResult.Good, 140, 100, 60), new DifficultyRange(HitResult.Meh, 200, 150, 100), - new DifficultyRange(HitResult.Miss, 200, 200, 200), + new DifficultyRange(HitResult.Miss, 400, 400, 400), }; public override bool IsHitResultAllowed(HitResult result) From 7069cef9ce6a92cfddf6cb8a25d2cb2dbf48e9b8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Mar 2020 15:26:39 +0900 Subject: [PATCH 0296/2376] Add catcher kiai/fail animation states --- .../TestSceneCatcherArea.cs | 58 +++++++++++++++++-- .../CatchSkinComponents.cs | 4 +- .../Skinning/CatchLegacySkinTransformer.cs | 8 +++ .../UI/CatcherAnimationState.cs | 12 ++++ osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 36 ++++++++++-- osu.Game.Rulesets.Catch/UI/CatcherSprite.cs | 30 ++++++++-- 6 files changed, 134 insertions(+), 14 deletions(-) create mode 100644 osu.Game.Rulesets.Catch/UI/CatcherAnimationState.cs diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs index df1ac4c725..caaad2f704 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs @@ -10,9 +10,15 @@ using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Catch.Beatmaps; +using osu.Game.Rulesets.Catch.Judgements; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.UI; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI; using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Catch.Tests @@ -34,9 +40,41 @@ namespace osu.Game.Rulesets.Catch.Tests CreatedDrawables.OfType().Select(i => i.Child) .OfType().ForEach(c => c.ToggleHyperDash(t))); - AddRepeatStep("catch fruit", () => - this.ChildrenOfType().ForEach(area => - area.MovableCatcher.PlaceOnPlate(new DrawableFruit(new TestSceneFruitObjects.TestCatchFruit(FruitVisualRepresentation.Grape)))), 20); + AddRepeatStep("catch fruit", () => catchFruit(new TestFruit(false) + { + X = this.ChildrenOfType().First().MovableCatcher.X + }), 20); + AddRepeatStep("catch fruit last in combo", () => catchFruit(new TestFruit(false) + { + X = this.ChildrenOfType().First().MovableCatcher.X, + LastInCombo = true, + }), 20); + AddRepeatStep("catch kiai fruit", () => catchFruit(new TestFruit(true) + { + X = this.ChildrenOfType().First().MovableCatcher.X, + }), 20); + AddRepeatStep("miss fruit", () => catchFruit(new Fruit + { + X = this.ChildrenOfType().First().MovableCatcher.X + 100, + LastInCombo = true, + }, true), 20); + } + + private void catchFruit(Fruit fruit, bool miss = false) + { + this.ChildrenOfType().ForEach(area => + { + DrawableFruit drawable = new DrawableFruit(fruit); + area.Add(drawable); + + Schedule(() => + { + area.AttemptCatch(fruit); + area.OnResult(drawable, new JudgementResult(fruit, new CatchJudgement()) { Type = miss ? HitResult.Miss : HitResult.Great }); + + drawable.Expire(); + }); + }); } private void createCatcher(float size) @@ -47,7 +85,8 @@ namespace osu.Game.Rulesets.Catch.Tests Child = new TestCatcherArea(new BeatmapDifficulty { CircleSize = size }) { Anchor = Anchor.CentreLeft, - Origin = Anchor.TopLeft + Origin = Anchor.TopLeft, + CreateDrawableRepresentation = ((DrawableRuleset)catchRuleset.CreateInstance().CreateDrawableRulesetWith(new CatchBeatmap())).CreateDrawableRepresentation }, }); } @@ -58,6 +97,17 @@ namespace osu.Game.Rulesets.Catch.Tests catchRuleset = rulesets.GetRuleset(2); } + public class TestFruit : Fruit + { + public TestFruit(bool kiai) + { + var kiaiCpi = new ControlPointInfo(); + kiaiCpi.Add(0, new EffectControlPoint { KiaiMode = kiai }); + + ApplyDefaultsToSelf(kiaiCpi, new BeatmapDifficulty()); + } + } + private class TestCatcherArea : CatcherArea { public TestCatcherArea(BeatmapDifficulty beatmapDifficulty) diff --git a/osu.Game.Rulesets.Catch/CatchSkinComponents.cs b/osu.Game.Rulesets.Catch/CatchSkinComponents.cs index 08bff36401..80390705fe 100644 --- a/osu.Game.Rulesets.Catch/CatchSkinComponents.cs +++ b/osu.Game.Rulesets.Catch/CatchSkinComponents.cs @@ -11,6 +11,8 @@ namespace osu.Game.Rulesets.Catch FruitOrange, FruitPear, Droplet, - CatcherIdle + CatcherIdle, + CatcherFail, + CatcherKiai } } diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs index af7c60b929..65e6e6f209 100644 --- a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs @@ -48,6 +48,14 @@ namespace osu.Game.Rulesets.Catch.Skinning case CatchSkinComponents.CatcherIdle: return this.GetAnimation("fruit-catcher-idle", true, true, true) ?? this.GetAnimation("fruit-ryuuta", true, true, true); + + case CatchSkinComponents.CatcherFail: + return this.GetAnimation("fruit-catcher-fail", true, true, true) ?? + this.GetAnimation("fruit-ryuuta", true, true, true); + + case CatchSkinComponents.CatcherKiai: + return this.GetAnimation("fruit-catcher-kiai", true, true, true) ?? + this.GetAnimation("fruit-ryuuta", true, true, true); } return null; diff --git a/osu.Game.Rulesets.Catch/UI/CatcherAnimationState.cs b/osu.Game.Rulesets.Catch/UI/CatcherAnimationState.cs new file mode 100644 index 0000000000..566e9d1911 --- /dev/null +++ b/osu.Game.Rulesets.Catch/UI/CatcherAnimationState.cs @@ -0,0 +1,12 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Rulesets.Catch.UI +{ + public enum CatcherAnimationState + { + Idle, + Fail, + Kiai + } +} diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index dfeaf6e89f..2beda02398 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -155,11 +155,21 @@ namespace osu.Game.Rulesets.Catch.UI Anchor = Anchor.TopCentre, Origin = Anchor.BottomCentre, }, - createCatcherSprite().With(c => - { - c.Anchor = Anchor.TopCentre; - }) }; + + updateCatcher(); + } + + private Drawable catcherSprite; + + private void updateCatcher() + { + catcherSprite?.Expire(); + + Add(catcherSprite = createCatcherSprite().With(c => + { + c.Anchor = Anchor.TopCentre; + })); } private int currentDirection; @@ -222,7 +232,7 @@ namespace osu.Game.Rulesets.Catch.UI Scheduler.AddDelayed(beginTrail, HyperDashing ? 25 : 50); } - private Drawable createCatcherSprite() => new CatcherSprite(); + private Drawable createCatcherSprite() => new CatcherSprite(currentState); ///

public bool BeatmapSetsLoaded { get; private set; } - private readonly OsuScrollContainer scroll; + private readonly CarouselScrollContainer scroll; private IEnumerable beatmapSets => root.Children.OfType(); @@ -424,7 +424,7 @@ namespace osu.Game.Screens.Select root.Filter(activeCriteria); itemsCache.Invalidate(); - if (alwaysResetScrollPosition || isAtScrollTarget) + if (alwaysResetScrollPosition || !scroll.UserScrolling) ScrollToSelected(); } @@ -695,11 +695,6 @@ namespace osu.Game.Screens.Select itemsCache.Validate(); } - /// - /// Denotes whether the current scroll position is roughly at the scroll target (the current selection). - /// - private bool isAtScrollTarget => scrollTarget != null && Precision.AlmostEquals(scrollTarget.Value, scroll.Current, 150); - private bool firstScroll = true; private void updateScrollPosition() @@ -779,6 +774,23 @@ namespace osu.Game.Screens.Select { private bool rightMouseScrollBlocked; + /// + /// Whether the last scroll event was user triggered, directly on the scroll container. + /// + public bool UserScrolling { get; private set; } + + protected override void OnUserScroll(float value, bool animated = true, double? distanceDecay = null) + { + UserScrolling = true; + base.OnUserScroll(value, animated, distanceDecay); + } + + public new void ScrollTo(float value, bool animated = true, double? distanceDecay = null) + { + UserScrolling = false; + base.ScrollTo(value, animated, distanceDecay); + } + protected override bool OnMouseDown(MouseDownEvent e) { if (e.Button == MouseButton.Right) From c8cdc5fda5fe6050d0512a56d686e8ec9f28f0d5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 13 Mar 2020 12:43:01 +0900 Subject: [PATCH 0368/2376] Expose half catcher width to movement skill --- .../Difficulty/CatchDifficultyCalculator.cs | 27 ++++++++++--------- .../Difficulty/Skills/Movement.cs | 7 +++++ 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 44e1a8e5cc..a3315d36e8 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -23,6 +23,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty protected override int SectionLength => 750; + private float halfCatcherWidth; + public CatchDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap) : base(ruleset, beatmap) { @@ -48,14 +50,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) { - float halfCatchWidth; - - using (var catcher = new CatcherArea.Catcher(beatmap.BeatmapInfo.BaseDifficulty)) - { - halfCatchWidth = catcher.CatchWidth * 0.5f; - halfCatchWidth *= 0.8f; // We're only using 80% of the catcher's width to simulate imperfect gameplay. - } - CatchHitObject lastObject = null; // In 2B beatmaps, it is possible that a normal Fruit is placed in the middle of a JuiceStream. @@ -69,16 +63,25 @@ namespace osu.Game.Rulesets.Catch.Difficulty continue; if (lastObject != null) - yield return new CatchDifficultyHitObject(hitObject, lastObject, clockRate, halfCatchWidth); + yield return new CatchDifficultyHitObject(hitObject, lastObject, clockRate, halfCatcherWidth); lastObject = hitObject; } } - protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[] + protected override Skill[] CreateSkills(IBeatmap beatmap) { - new Movement(), - }; + using (var catcher = new CatcherArea.Catcher(beatmap.BeatmapInfo.BaseDifficulty)) + { + halfCatcherWidth = catcher.CatchWidth * 0.5f; + halfCatcherWidth *= 0.8f; // We're only using 80% of the catcher's width to simulate imperfect gameplay. + } + + return new Skill[] + { + new Movement(halfCatcherWidth), + }; + } protected override Mod[] DifficultyAdjustmentMods => new Mod[] { diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs index 7cd569035b..fd164907e0 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs @@ -20,9 +20,16 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills protected override double DecayWeight => 0.94; + protected readonly float HalfCatcherWidth; + private float? lastPlayerPosition; private float lastDistanceMoved; + public Movement(float halfCatcherWidth) + { + HalfCatcherWidth = halfCatcherWidth; + } + protected override double StrainValueOf(DifficultyHitObject current) { var catchCurrent = (CatchDifficultyHitObject)current; From 1733519c3a3f7e88cb6c96ecbe2041f4bf0dbb0e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Mar 2020 12:59:30 +0900 Subject: [PATCH 0369/2376] Split out CatcherArea nested classes and reorder methods --- .../TestSceneCatcher.cs | 2 +- .../TestSceneDrawableHitObjects.cs | 2 +- .../TestSceneHyperDash.cs | 2 +- .../Beatmaps/CatchBeatmapProcessor.cs | 2 +- .../Difficulty/CatchDifficultyCalculator.cs | 2 +- osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs | 2 +- .../Replays/CatchAutoGenerator.cs | 2 +- osu.Game.Rulesets.Catch/UI/Catcher.cs | 460 ++++++++++++++ osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 581 +----------------- osu.Game.Rulesets.Catch/UI/HitExplosion.cs | 122 ++++ 10 files changed, 605 insertions(+), 572 deletions(-) create mode 100644 osu.Game.Rulesets.Catch/UI/Catcher.cs create mode 100644 osu.Game.Rulesets.Catch/UI/HitExplosion.cs diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs index fbbe00bb6c..fe0d512166 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Catch.Tests [BackgroundDependencyLoader] private void load() { - SetContents(() => new CatcherArea.Catcher + SetContents(() => new Catcher { RelativePositionAxes = Axes.None, Anchor = Anchor.Centre, diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs index 304c7e3854..df5494aab0 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.Tests { public override IReadOnlyList RequiredTypes => new[] { - typeof(CatcherArea.Catcher), + typeof(Catcher), typeof(DrawableCatchRuleset), typeof(DrawableFruit), typeof(DrawableJuiceStream), diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs index 6f0d8f0a3a..49ff9df4d7 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs @@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Catch.Tests } } - private CatcherArea.Catcher getCatcher() => Player.ChildrenOfType().First().MovableCatcher; + private Catcher getCatcher() => Player.ChildrenOfType().First().MovableCatcher; protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) { diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs index 986dc9dbb9..7c81bcdf0c 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs @@ -227,7 +227,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps int thisDirection = nextObject.X > currentObject.X ? 1 : -1; double timeToNext = nextObject.StartTime - currentObject.StartTime - 1000f / 60f / 4; // 1/4th of a frame of grace time, taken from osu-stable double distanceToNext = Math.Abs(nextObject.X - currentObject.X) - (lastDirection == thisDirection ? lastExcess : halfCatcherWidth); - float distanceToHyper = (float)(timeToNext * CatcherArea.Catcher.BASE_SPEED - distanceToNext); + float distanceToHyper = (float)(timeToNext * Catcher.BASE_SPEED - distanceToNext); if (distanceToHyper < 0) { diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 44e1a8e5cc..8b7080b5bf 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty { float halfCatchWidth; - using (var catcher = new CatcherArea.Catcher(beatmap.BeatmapInfo.BaseDifficulty)) + using (var catcher = new Catcher(beatmap.BeatmapInfo.BaseDifficulty)) { halfCatchWidth = catcher.CatchWidth * 0.5f; halfCatchWidth *= 0.8f; // We're only using 80% of the catcher's width to simulate imperfect gameplay. diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs b/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs index 4c72b9fd3e..1ef235f764 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.Mods private class MouseInputHelper : Drawable, IKeyBindingHandler, IRequireHighFrequencyMousePosition { - private readonly CatcherArea.Catcher catcher; + private readonly Catcher catcher; public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; diff --git a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs index 4649dcae90..b90b5812a6 100644 --- a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs +++ b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Catch.Replays public override Replay Generate() { // todo: add support for HT DT - const double dash_speed = CatcherArea.Catcher.BASE_SPEED; + const double dash_speed = Catcher.BASE_SPEED; const double movement_speed = dash_speed / 2; float lastPosition = 0.5f; double lastTime = 0; diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs new file mode 100644 index 0000000000..2c8b080ee3 --- /dev/null +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -0,0 +1,460 @@ +// 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.Animations; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Bindings; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Catch.Objects.Drawables; +using osu.Game.Rulesets.Objects.Drawables; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Catch.UI +{ + public class Catcher : Container, IKeyBindingHandler + { + /// + /// Whether we are hyper-dashing or not. + /// + public bool HyperDashing => hyperDashModifier != 1; + + /// + /// The relative space to cover in 1 millisecond. based on 1 game pixel per millisecond as in osu-stable. + /// + public const double BASE_SPEED = 1.0 / 512; + + public Container ExplodingFruitTarget; + + public Container AdditiveTarget; + + public CatcherAnimationState CurrentState { get; private set; } + + /// + /// Width of the area that can be used to attempt catches during gameplay. + /// + internal float CatchWidth => CatcherArea.CATCHER_SIZE * Math.Abs(Scale.X); + + protected bool Dashing + { + get => dashing; + set + { + if (value == dashing) return; + + dashing = value; + + Trail |= dashing; + } + } + + /// + /// Activate or deactive the trail. Will be automatically deactivated when conditions to keep the trail displayed are no longer met. + /// + protected bool Trail + { + get => trail; + set + { + if (value == trail) return; + + trail = value; + + if (Trail) + beginTrail(); + } + } + + private Container caughtFruit; + + private CatcherSprite catcherIdle; + private CatcherSprite catcherKiai; + private CatcherSprite catcherFail; + + private int currentDirection; + + private bool dashing; + + private bool trail; + + private double hyperDashModifier = 1; + private int hyperDashDirection; + private float hyperDashTargetPosition; + + public Catcher(BeatmapDifficulty difficulty = null) + { + RelativePositionAxes = Axes.X; + X = 0.5f; + + Origin = Anchor.TopCentre; + + Size = new Vector2(CatcherArea.CATCHER_SIZE); + if (difficulty != null) + Scale = new Vector2(1.0f - 0.7f * (difficulty.CircleSize - 5) / 5); + } + + [BackgroundDependencyLoader] + private void load() + { + Children = new Drawable[] + { + caughtFruit = new Container + { + Anchor = Anchor.TopCentre, + Origin = Anchor.BottomCentre, + }, + catcherIdle = new CatcherSprite(CatcherAnimationState.Idle) + { + Anchor = Anchor.TopCentre, + Alpha = 0, + }, + catcherKiai = new CatcherSprite(CatcherAnimationState.Kiai) + { + Anchor = Anchor.TopCentre, + Alpha = 0, + }, + catcherFail = new CatcherSprite(CatcherAnimationState.Fail) + { + Anchor = Anchor.TopCentre, + Alpha = 0, + } + }; + + updateCatcher(); + } + + /// + /// Add a caught fruit to the catcher's stack. + /// + /// The fruit that was caught. + public void PlaceOnPlate(DrawableCatchHitObject fruit) + { + var ourRadius = fruit.DisplayRadius; + float theirRadius = 0; + + const float allowance = 6; + + while (caughtFruit.Any(f => + f.LifetimeEnd == double.MaxValue && + Vector2Extensions.Distance(f.Position, fruit.Position) < (ourRadius + (theirRadius = f.DrawSize.X / 2 * f.Scale.X)) / (allowance / 2))) + { + var diff = (ourRadius + theirRadius) / allowance; + fruit.X += (RNG.NextSingle() - 0.5f) * 2 * diff; + fruit.Y -= RNG.NextSingle() * diff; + } + + fruit.X = Math.Clamp(fruit.X, -CatcherArea.CATCHER_SIZE / 2, CatcherArea.CATCHER_SIZE / 2); + + caughtFruit.Add(fruit); + + Add(new HitExplosion(fruit) + { + X = fruit.X, + Scale = new Vector2(fruit.HitObject.Scale) + }); + } + + /// + /// Let the catcher attempt to catch a fruit. + /// + /// The fruit to catch. + /// Whether the catch is possible. + public bool AttemptCatch(CatchHitObject fruit) + { + var halfCatchWidth = CatchWidth * 0.5f; + + // this stuff wil disappear once we move fruit to non-relative coordinate space in the future. + var catchObjectPosition = fruit.X * CatchPlayfield.BASE_WIDTH; + var catcherPosition = Position.X * CatchPlayfield.BASE_WIDTH; + + var validCatch = + catchObjectPosition >= catcherPosition - halfCatchWidth && + catchObjectPosition <= catcherPosition + halfCatchWidth; + + // only update hyperdash state if we are catching a fruit. + // exceptions are Droplets and JuiceStreams. + if (!(fruit is Fruit)) return validCatch; + + if (validCatch && fruit.HyperDash) + { + var target = fruit.HyperDashTarget; + var timeDifference = target.StartTime - fruit.StartTime; + double positionDifference = target.X * CatchPlayfield.BASE_WIDTH - catcherPosition; + var velocity = positionDifference / Math.Max(1.0, timeDifference - 1000.0 / 60.0); + + SetHyperDashState(Math.Abs(velocity), target.X); + } + else + SetHyperDashState(); + + if (validCatch) + updateState(fruit.Kiai ? CatcherAnimationState.Kiai : CatcherAnimationState.Idle); + else if (!(fruit is Banana)) + updateState(CatcherAnimationState.Fail); + + return validCatch; + } + + /// + /// Set hyper-dash state. + /// + /// The speed multiplier. If this is less or equals to 1, this catcher will be non-hyper-dashing state. + /// When this catcher crosses this position, this catcher ends hyper-dashing. + public void SetHyperDashState(double modifier = 1, float targetPosition = -1) + { + const float hyper_dash_transition_length = 180; + + var wasHyperDashing = HyperDashing; + + if (modifier <= 1 || X == targetPosition) + { + hyperDashModifier = 1; + hyperDashDirection = 0; + + if (wasHyperDashing) + { + this.FadeColour(Color4.White, hyper_dash_transition_length, Easing.OutQuint); + this.FadeTo(1, hyper_dash_transition_length, Easing.OutQuint); + Trail &= Dashing; + } + } + else + { + hyperDashModifier = modifier; + hyperDashDirection = Math.Sign(targetPosition - X); + hyperDashTargetPosition = targetPosition; + + if (!wasHyperDashing) + { + this.FadeColour(Color4.OrangeRed, hyper_dash_transition_length, Easing.OutQuint); + this.FadeTo(0.2f, hyper_dash_transition_length, Easing.OutQuint); + Trail = true; + + var hyperDashEndGlow = createAdditiveSprite(true); + + hyperDashEndGlow.MoveToOffset(new Vector2(0, -20), 1200, Easing.In); + hyperDashEndGlow.ScaleTo(hyperDashEndGlow.Scale * 0.9f).ScaleTo(hyperDashEndGlow.Scale * 1.2f, 1200, Easing.In); + hyperDashEndGlow.FadeOut(1200); + hyperDashEndGlow.Expire(true); + } + } + } + + public bool OnPressed(CatchAction action) + { + switch (action) + { + case CatchAction.MoveLeft: + currentDirection--; + return true; + + case CatchAction.MoveRight: + currentDirection++; + return true; + + case CatchAction.Dash: + Dashing = true; + return true; + } + + return false; + } + + public void OnReleased(CatchAction action) + { + switch (action) + { + case CatchAction.MoveLeft: + currentDirection++; + break; + + case CatchAction.MoveRight: + currentDirection--; + break; + + case CatchAction.Dash: + Dashing = false; + break; + } + } + + public void UpdatePosition(float position) + { + position = Math.Clamp(position, 0, 1); + + if (position == X) + return; + + Scale = new Vector2(Math.Abs(Scale.X) * (position > X ? 1 : -1), Scale.Y); + X = position; + } + + /// + /// Drop any fruit off the plate. + /// + public void Drop() + { + foreach (var f in caughtFruit.ToArray()) + Drop(f); + } + + /// + /// Explode any fruit off the plate. + /// + public void Explode() + { + foreach (var f in caughtFruit.ToArray()) + Explode(f); + } + + public void Drop(DrawableHitObject fruit) + { + removeFromPlateWithTransform(fruit, f => + { + f.MoveToY(f.Y + 75, 750, Easing.InSine); + f.FadeOut(750); + }); + } + + public void Explode(DrawableHitObject fruit) + { + var originalX = fruit.X * Scale.X; + + removeFromPlateWithTransform(fruit, f => + { + f.MoveToY(f.Y - 50, 250, Easing.OutSine).Then().MoveToY(f.Y + 50, 500, Easing.InSine); + f.MoveToX(f.X + originalX * 6, 1000); + f.FadeOut(750); + }); + } + + protected override void Update() + { + base.Update(); + + if (currentDirection == 0) return; + + var direction = Math.Sign(currentDirection); + + var dashModifier = Dashing ? 1 : 0.5; + var speed = BASE_SPEED * dashModifier * hyperDashModifier; + + UpdatePosition((float)(X + direction * Clock.ElapsedFrameTime * speed)); + + // Correct overshooting. + if (hyperDashDirection > 0 && hyperDashTargetPosition < X || + hyperDashDirection < 0 && hyperDashTargetPosition > X) + { + X = hyperDashTargetPosition; + SetHyperDashState(); + } + } + + private void updateCatcher() + { + catcherIdle.Hide(); + catcherKiai.Hide(); + catcherFail.Hide(); + + CatcherSprite current; + + switch (CurrentState) + { + default: + current = catcherIdle; + break; + + case CatcherAnimationState.Fail: + current = catcherFail; + break; + + case CatcherAnimationState.Kiai: + current = catcherKiai; + break; + } + + current.Show(); + (current.Drawable as IAnimation)?.GotoFrame(0); + } + + private void beginTrail() + { + Trail &= dashing || HyperDashing; + Trail &= AdditiveTarget != null; + + if (!Trail) return; + + var additive = createAdditiveSprite(HyperDashing); + + additive.FadeTo(0.4f).FadeOut(800, Easing.OutQuint); + additive.Expire(true); + + Scheduler.AddDelayed(beginTrail, HyperDashing ? 25 : 50); + } + + private Drawable createAdditiveSprite(bool hyperDash) + { + var additive = createCatcherSprite(); + + additive.Anchor = Anchor; + additive.Scale = Scale; + additive.Colour = hyperDash ? Color4.Red : Color4.White; + additive.Blending = BlendingParameters.Additive; + additive.RelativePositionAxes = RelativePositionAxes; + additive.Position = Position; + + AdditiveTarget.Add(additive); + + return additive; + } + + private Drawable createCatcherSprite() + { + return new CatcherSprite(CurrentState); + } + + private void updateState(CatcherAnimationState state) + { + if (CurrentState == state) + return; + + CurrentState = state; + updateCatcher(); + } + + private void removeFromPlateWithTransform(DrawableHitObject fruit, Action action) + { + if (ExplodingFruitTarget != null) + { + fruit.Anchor = Anchor.TopLeft; + fruit.Position = caughtFruit.ToSpaceOfOtherDrawable(fruit.DrawPosition, ExplodingFruitTarget); + + if (!caughtFruit.Remove(fruit)) + // we may have already been removed by a previous operation (due to the weird OnLoadComplete scheduling). + // this avoids a crash on potentially attempting to Add a fruit to ExplodingFruitTarget twice. + return; + + ExplodingFruitTarget.Add(fruit); + } + + var actionTime = Clock.CurrentTime; + + fruit.ApplyCustomUpdateState += onFruitOnApplyCustomUpdateState; + onFruitOnApplyCustomUpdateState(fruit, fruit.State.Value); + + void onFruitOnApplyCustomUpdateState(DrawableHitObject o, ArmedState state) + { + using (fruit.BeginAbsoluteSequence(actionTime)) + action(fruit); + + fruit.Expire(); + } + } + } +} diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 2394110165..e0d9ff759d 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -2,15 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Linq; -using osu.Framework.Allocation; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Effects; -using osu.Framework.Input.Bindings; -using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Judgements; using osu.Game.Rulesets.Catch.Objects; @@ -20,7 +13,6 @@ using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI; using osuTK; -using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.UI { @@ -28,8 +20,6 @@ namespace osu.Game.Rulesets.Catch.UI { public const float CATCHER_SIZE = 106.75f; - protected internal readonly Catcher MovableCatcher; - public Func> CreateDrawableRepresentation; public Container ExplodingFruitTarget @@ -37,6 +27,8 @@ namespace osu.Game.Rulesets.Catch.UI set => MovableCatcher.ExplodingFruitTarget = value; } + private DrawableCatchHitObject lastPlateableFruit; + public CatcherArea(BeatmapDifficulty difficulty = null) { RelativeSizeAxes = Axes.X; @@ -47,7 +39,10 @@ namespace osu.Game.Rulesets.Catch.UI }; } - private DrawableCatchHitObject lastPlateableFruit; + public static float GetCatcherSize(BeatmapDifficulty difficulty) + { + return CATCHER_SIZE / CatchPlayfield.BASE_WIDTH * (1.0f - 0.7f * (difficulty.CircleSize - 5) / 5); + } public void OnResult(DrawableCatchHitObject fruit, JudgementResult result) { @@ -100,6 +95,15 @@ namespace osu.Game.Rulesets.Catch.UI } } + public void OnReleased(CatchAction action) + { + } + + public bool AttemptCatch(CatchHitObject obj) + { + return MovableCatcher.AttemptCatch(obj); + } + protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); @@ -110,559 +114,6 @@ namespace osu.Game.Rulesets.Catch.UI MovableCatcher.X = state.CatcherX.Value; } - public void OnReleased(CatchAction action) - { - } - - public bool AttemptCatch(CatchHitObject obj) => MovableCatcher.AttemptCatch(obj); - - public static float GetCatcherSize(BeatmapDifficulty difficulty) - { - return CATCHER_SIZE / CatchPlayfield.BASE_WIDTH * (1.0f - 0.7f * (difficulty.CircleSize - 5) / 5); - } - - public class Catcher : Container, IKeyBindingHandler - { - /// - /// Width of the area that can be used to attempt catches during gameplay. - /// - internal float CatchWidth => CATCHER_SIZE * Math.Abs(Scale.X); - - private Container caughtFruit; - - public Container ExplodingFruitTarget; - - public Container AdditiveTarget; - - public Catcher(BeatmapDifficulty difficulty = null) - { - RelativePositionAxes = Axes.X; - X = 0.5f; - - Origin = Anchor.TopCentre; - - Size = new Vector2(CATCHER_SIZE); - if (difficulty != null) - Scale = new Vector2(1.0f - 0.7f * (difficulty.CircleSize - 5) / 5); - } - - [BackgroundDependencyLoader] - private void load() - { - Children = new Drawable[] - { - caughtFruit = new Container - { - Anchor = Anchor.TopCentre, - Origin = Anchor.BottomCentre, - }, - catcherIdle = new CatcherSprite(CatcherAnimationState.Idle) - { - Anchor = Anchor.TopCentre, - Alpha = 0, - }, - catcherKiai = new CatcherSprite(CatcherAnimationState.Kiai) - { - Anchor = Anchor.TopCentre, - Alpha = 0, - }, - catcherFail = new CatcherSprite(CatcherAnimationState.Fail) - { - Anchor = Anchor.TopCentre, - Alpha = 0, - } - }; - - updateCatcher(); - } - - private CatcherSprite catcherIdle; - private CatcherSprite catcherKiai; - private CatcherSprite catcherFail; - - private void updateCatcher() - { - catcherIdle.Hide(); - catcherKiai.Hide(); - catcherFail.Hide(); - - CatcherSprite current; - - switch (CurrentState) - { - default: - current = catcherIdle; - break; - - case CatcherAnimationState.Fail: - current = catcherFail; - break; - - case CatcherAnimationState.Kiai: - current = catcherKiai; - break; - } - - current.Show(); - (current.Drawable as IAnimation)?.GotoFrame(0); - } - - private int currentDirection; - - private bool dashing; - - protected bool Dashing - { - get => dashing; - set - { - if (value == dashing) return; - - dashing = value; - - Trail |= dashing; - } - } - - private bool trail; - - /// - /// Activate or deactive the trail. Will be automatically deactivated when conditions to keep the trail displayed are no longer met. - /// - protected bool Trail - { - get => trail; - set - { - if (value == trail) return; - - trail = value; - - if (Trail) - beginTrail(); - } - } - - private void beginTrail() - { - Trail &= dashing || HyperDashing; - Trail &= AdditiveTarget != null; - - if (!Trail) return; - - var additive = createAdditiveSprite(HyperDashing); - - additive.FadeTo(0.4f).FadeOut(800, Easing.OutQuint); - additive.Expire(true); - - Scheduler.AddDelayed(beginTrail, HyperDashing ? 25 : 50); - } - - private Drawable createAdditiveSprite(bool hyperDash) - { - var additive = createCatcherSprite(); - - additive.Anchor = Anchor; - additive.Scale = Scale; - additive.Colour = hyperDash ? Color4.Red : Color4.White; - additive.Blending = BlendingParameters.Additive; - additive.RelativePositionAxes = RelativePositionAxes; - additive.Position = Position; - - AdditiveTarget.Add(additive); - - return additive; - } - - private Drawable createCatcherSprite() => new CatcherSprite(CurrentState); - - /// - /// Add a caught fruit to the catcher's stack. - /// - /// The fruit that was caught. - public void PlaceOnPlate(DrawableCatchHitObject fruit) - { - float ourRadius = fruit.DisplayRadius; - float theirRadius = 0; - - const float allowance = 6; - - while (caughtFruit.Any(f => - f.LifetimeEnd == double.MaxValue && - Vector2Extensions.Distance(f.Position, fruit.Position) < (ourRadius + (theirRadius = f.DrawSize.X / 2 * f.Scale.X)) / (allowance / 2))) - { - float diff = (ourRadius + theirRadius) / allowance; - fruit.X += (RNG.NextSingle() - 0.5f) * 2 * diff; - fruit.Y -= RNG.NextSingle() * diff; - } - - fruit.X = Math.Clamp(fruit.X, -CATCHER_SIZE / 2, CATCHER_SIZE / 2); - - caughtFruit.Add(fruit); - - Add(new HitExplosion(fruit) - { - X = fruit.X, - Scale = new Vector2(fruit.HitObject.Scale) - }); - } - - /// - /// Let the catcher attempt to catch a fruit. - /// - /// The fruit to catch. - /// Whether the catch is possible. - public bool AttemptCatch(CatchHitObject fruit) - { - float halfCatchWidth = CatchWidth * 0.5f; - - // this stuff wil disappear once we move fruit to non-relative coordinate space in the future. - var catchObjectPosition = fruit.X * CatchPlayfield.BASE_WIDTH; - var catcherPosition = Position.X * CatchPlayfield.BASE_WIDTH; - - var validCatch = - catchObjectPosition >= catcherPosition - halfCatchWidth && - catchObjectPosition <= catcherPosition + halfCatchWidth; - - // only update hyperdash state if we are catching a fruit. - // exceptions are Droplets and JuiceStreams. - if (!(fruit is Fruit)) return validCatch; - - if (validCatch && fruit.HyperDash) - { - var target = fruit.HyperDashTarget; - double timeDifference = target.StartTime - fruit.StartTime; - double positionDifference = target.X * CatchPlayfield.BASE_WIDTH - catcherPosition; - double velocity = positionDifference / Math.Max(1.0, timeDifference - 1000.0 / 60.0); - - SetHyperDashState(Math.Abs(velocity), target.X); - } - else - { - SetHyperDashState(); - } - - if (validCatch) - updateState(fruit.Kiai ? CatcherAnimationState.Kiai : CatcherAnimationState.Idle); - else if (!(fruit is Banana)) - updateState(CatcherAnimationState.Fail); - - return validCatch; - } - - private void updateState(CatcherAnimationState state) - { - if (CurrentState == state) - return; - - CurrentState = state; - updateCatcher(); - } - - public CatcherAnimationState CurrentState { get; private set; } - - private double hyperDashModifier = 1; - private int hyperDashDirection; - private float hyperDashTargetPosition; - - /// - /// Whether we are hyper-dashing or not. - /// - public bool HyperDashing => hyperDashModifier != 1; - - /// - /// Set hyper-dash state. - /// - /// The speed multiplier. If this is less or equals to 1, this catcher will be non-hyper-dashing state. - /// When this catcher crosses this position, this catcher ends hyper-dashing. - public void SetHyperDashState(double modifier = 1, float targetPosition = -1) - { - const float hyper_dash_transition_length = 180; - - bool wasHyperDashing = HyperDashing; - - if (modifier <= 1 || X == targetPosition) - { - hyperDashModifier = 1; - hyperDashDirection = 0; - - if (wasHyperDashing) - { - this.FadeColour(Color4.White, hyper_dash_transition_length, Easing.OutQuint); - this.FadeTo(1, hyper_dash_transition_length, Easing.OutQuint); - Trail &= Dashing; - } - } - else - { - hyperDashModifier = modifier; - hyperDashDirection = Math.Sign(targetPosition - X); - hyperDashTargetPosition = targetPosition; - - if (!wasHyperDashing) - { - this.FadeColour(Color4.OrangeRed, hyper_dash_transition_length, Easing.OutQuint); - this.FadeTo(0.2f, hyper_dash_transition_length, Easing.OutQuint); - Trail = true; - - var hyperDashEndGlow = createAdditiveSprite(true); - - hyperDashEndGlow.MoveToOffset(new Vector2(0, -20), 1200, Easing.In); - hyperDashEndGlow.ScaleTo(hyperDashEndGlow.Scale * 0.9f).ScaleTo(hyperDashEndGlow.Scale * 1.2f, 1200, Easing.In); - hyperDashEndGlow.FadeOut(1200); - hyperDashEndGlow.Expire(true); - } - } - } - - public bool OnPressed(CatchAction action) - { - switch (action) - { - case CatchAction.MoveLeft: - currentDirection--; - return true; - - case CatchAction.MoveRight: - currentDirection++; - return true; - - case CatchAction.Dash: - Dashing = true; - return true; - } - - return false; - } - - public void OnReleased(CatchAction action) - { - switch (action) - { - case CatchAction.MoveLeft: - currentDirection++; - break; - - case CatchAction.MoveRight: - currentDirection--; - break; - - case CatchAction.Dash: - Dashing = false; - break; - } - } - - /// - /// The relative space to cover in 1 millisecond. based on 1 game pixel per millisecond as in osu-stable. - /// - public const double BASE_SPEED = 1.0 / 512; - - protected override void Update() - { - base.Update(); - - if (currentDirection == 0) return; - - var direction = Math.Sign(currentDirection); - - double dashModifier = Dashing ? 1 : 0.5; - double speed = BASE_SPEED * dashModifier * hyperDashModifier; - - UpdatePosition((float)(X + direction * Clock.ElapsedFrameTime * speed)); - - // Correct overshooting. - if ((hyperDashDirection > 0 && hyperDashTargetPosition < X) || - (hyperDashDirection < 0 && hyperDashTargetPosition > X)) - { - X = hyperDashTargetPosition; - SetHyperDashState(); - } - } - - public void UpdatePosition(float position) - { - position = Math.Clamp(position, 0, 1); - - if (position == X) - return; - - Scale = new Vector2(Math.Abs(Scale.X) * (position > X ? 1 : -1), Scale.Y); - X = position; - } - - /// - /// Drop any fruit off the plate. - /// - public void Drop() - { - foreach (var f in caughtFruit.ToArray()) - Drop(f); - } - - /// - /// Explode any fruit off the plate. - /// - public void Explode() - { - foreach (var f in caughtFruit.ToArray()) - Explode(f); - } - - public void Drop(DrawableHitObject fruit) => removeFromPlateWithTransform(fruit, f => - { - f.MoveToY(f.Y + 75, 750, Easing.InSine); - f.FadeOut(750); - }); - - public void Explode(DrawableHitObject fruit) - { - var originalX = fruit.X * Scale.X; - - removeFromPlateWithTransform(fruit, f => - { - f.MoveToY(f.Y - 50, 250, Easing.OutSine).Then().MoveToY(f.Y + 50, 500, Easing.InSine); - f.MoveToX(f.X + originalX * 6, 1000); - f.FadeOut(750); - }); - } - - private void removeFromPlateWithTransform(DrawableHitObject fruit, Action action) - { - if (ExplodingFruitTarget != null) - { - fruit.Anchor = Anchor.TopLeft; - fruit.Position = caughtFruit.ToSpaceOfOtherDrawable(fruit.DrawPosition, ExplodingFruitTarget); - - if (!caughtFruit.Remove(fruit)) - // we may have already been removed by a previous operation (due to the weird OnLoadComplete scheduling). - // this avoids a crash on potentially attempting to Add a fruit to ExplodingFruitTarget twice. - return; - - ExplodingFruitTarget.Add(fruit); - } - - double actionTime = Clock.CurrentTime; - - fruit.ApplyCustomUpdateState += onFruitOnApplyCustomUpdateState; - onFruitOnApplyCustomUpdateState(fruit, fruit.State.Value); - - void onFruitOnApplyCustomUpdateState(DrawableHitObject o, ArmedState state) - { - using (fruit.BeginAbsoluteSequence(actionTime)) - action(fruit); - - fruit.Expire(); - } - } - } - } - - public class HitExplosion : CompositeDrawable - { - private readonly CircularContainer largeFaint; - - public HitExplosion(DrawableCatchHitObject fruit) - { - Size = new Vector2(20); - Anchor = Anchor.TopCentre; - Origin = Anchor.BottomCentre; - - Color4 objectColour = fruit.AccentColour.Value; - - // scale roughly in-line with visual appearance of notes - - const float angle_variangle = 15; // should be less than 45 - - const float roundness = 100; - - const float initial_height = 10; - - var colour = Interpolation.ValueAt(0.4f, objectColour, Color4.White, 0, 1); - - InternalChildren = new Drawable[] - { - largeFaint = new CircularContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Masking = true, - // we want our size to be very small so the glow dominates it. - Size = new Vector2(0.8f), - Blending = BlendingParameters.Additive, - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Glow, - Colour = Interpolation.ValueAt(0.1f, objectColour, Color4.White, 0, 1).Opacity(0.3f), - Roundness = 160, - Radius = 200, - }, - }, - new CircularContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Masking = true, - Blending = BlendingParameters.Additive, - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Glow, - Colour = Interpolation.ValueAt(0.6f, objectColour, Color4.White, 0, 1), - Roundness = 20, - Radius = 50, - }, - }, - new CircularContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Masking = true, - Size = new Vector2(0.01f, initial_height), - Blending = BlendingParameters.Additive, - Rotation = RNG.NextSingle(-angle_variangle, angle_variangle), - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Glow, - Colour = colour, - Roundness = roundness, - Radius = 40, - }, - }, - new CircularContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Masking = true, - Size = new Vector2(0.01f, initial_height), - Blending = BlendingParameters.Additive, - Rotation = RNG.NextSingle(-angle_variangle, angle_variangle), - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Glow, - Colour = colour, - Roundness = roundness, - Radius = 40, - }, - } - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - const double duration = 400; - - largeFaint - .ResizeTo(largeFaint.Size * new Vector2(5, 1), duration, Easing.OutQuint) - .FadeOut(duration * 2); - - this.FadeInFromZero(50).Then().FadeOut(duration, Easing.Out); - Expire(true); - } + protected internal readonly Catcher MovableCatcher; } } diff --git a/osu.Game.Rulesets.Catch/UI/HitExplosion.cs b/osu.Game.Rulesets.Catch/UI/HitExplosion.cs new file mode 100644 index 0000000000..04a86f83be --- /dev/null +++ b/osu.Game.Rulesets.Catch/UI/HitExplosion.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 osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Utils; +using osu.Game.Rulesets.Catch.Objects.Drawables; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Catch.UI +{ + public class HitExplosion : CompositeDrawable + { + private readonly CircularContainer largeFaint; + + public HitExplosion(DrawableCatchHitObject fruit) + { + Size = new Vector2(20); + Anchor = Anchor.TopCentre; + Origin = Anchor.BottomCentre; + + Color4 objectColour = fruit.AccentColour.Value; + + // scale roughly in-line with visual appearance of notes + + const float angle_variangle = 15; // should be less than 45 + + const float roundness = 100; + + const float initial_height = 10; + + var colour = Interpolation.ValueAt(0.4f, objectColour, Color4.White, 0, 1); + + InternalChildren = new Drawable[] + { + largeFaint = new CircularContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Masking = true, + // we want our size to be very small so the glow dominates it. + Size = new Vector2(0.8f), + Blending = BlendingParameters.Additive, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = Interpolation.ValueAt(0.1f, objectColour, Color4.White, 0, 1).Opacity(0.3f), + Roundness = 160, + Radius = 200, + }, + }, + new CircularContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Masking = true, + Blending = BlendingParameters.Additive, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = Interpolation.ValueAt(0.6f, objectColour, Color4.White, 0, 1), + Roundness = 20, + Radius = 50, + }, + }, + new CircularContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Masking = true, + Size = new Vector2(0.01f, initial_height), + Blending = BlendingParameters.Additive, + Rotation = RNG.NextSingle(-angle_variangle, angle_variangle), + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = colour, + Roundness = roundness, + Radius = 40, + }, + }, + new CircularContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Masking = true, + Size = new Vector2(0.01f, initial_height), + Blending = BlendingParameters.Additive, + Rotation = RNG.NextSingle(-angle_variangle, angle_variangle), + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = colour, + Roundness = roundness, + Radius = 40, + }, + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + const double duration = 400; + + largeFaint + .ResizeTo(largeFaint.Size * new Vector2(5, 1), duration, Easing.OutQuint) + .FadeOut(duration * 2); + + this.FadeInFromZero(50).Then().FadeOut(duration, Easing.Out); + Expire(true); + } + } +} From 4a774d02e0cf639c8a20b98eaea80f3cd26be71e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 8 Mar 2020 14:43:27 +0900 Subject: [PATCH 0370/2376] Remove exo font loading --- osu.Game/OsuGameBase.cs | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index b2277e2abf..93a845a6ae 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -138,30 +138,17 @@ namespace osu.Game dependencies.Cache(LocalConfig); AddFont(Resources, @"Fonts/osuFont"); - AddFont(Resources, @"Fonts/Exo2.0-Medium"); - AddFont(Resources, @"Fonts/Exo2.0-MediumItalic"); - - AddFont(Resources, @"Fonts/Noto-Basic"); - AddFont(Resources, @"Fonts/Noto-Hangul"); - AddFont(Resources, @"Fonts/Noto-CJK-Basic"); - AddFont(Resources, @"Fonts/Noto-CJK-Compatibility"); - - AddFont(Resources, @"Fonts/Exo2.0-Regular"); - AddFont(Resources, @"Fonts/Exo2.0-RegularItalic"); - AddFont(Resources, @"Fonts/Exo2.0-SemiBold"); - AddFont(Resources, @"Fonts/Exo2.0-SemiBoldItalic"); - AddFont(Resources, @"Fonts/Exo2.0-Bold"); - AddFont(Resources, @"Fonts/Exo2.0-BoldItalic"); - AddFont(Resources, @"Fonts/Exo2.0-Light"); - AddFont(Resources, @"Fonts/Exo2.0-LightItalic"); - AddFont(Resources, @"Fonts/Exo2.0-Black"); - AddFont(Resources, @"Fonts/Exo2.0-BlackItalic"); AddFont(Resources, @"Fonts/Torus-SemiBold"); AddFont(Resources, @"Fonts/Torus-Bold"); AddFont(Resources, @"Fonts/Torus-Regular"); AddFont(Resources, @"Fonts/Torus-Light"); + AddFont(Resources, @"Fonts/Noto-Basic"); + AddFont(Resources, @"Fonts/Noto-Hangul"); + AddFont(Resources, @"Fonts/Noto-CJK-Basic"); + AddFont(Resources, @"Fonts/Noto-CJK-Compatibility"); + AddFont(Resources, @"Fonts/Venera-Light"); AddFont(Resources, @"Fonts/Venera-Bold"); AddFont(Resources, @"Fonts/Venera-Black"); From ae112cf14f2f419e73ae9c4e75d9d542e72b1312 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Mar 2020 13:31:59 +0900 Subject: [PATCH 0371/2376] Reorder torus loading to provide regular as default --- osu.Game/OsuGameBase.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 93a845a6ae..3c7ab27651 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -139,10 +139,10 @@ namespace osu.Game AddFont(Resources, @"Fonts/osuFont"); - AddFont(Resources, @"Fonts/Torus-SemiBold"); - AddFont(Resources, @"Fonts/Torus-Bold"); AddFont(Resources, @"Fonts/Torus-Regular"); AddFont(Resources, @"Fonts/Torus-Light"); + AddFont(Resources, @"Fonts/Torus-SemiBold"); + AddFont(Resources, @"Fonts/Torus-Bold"); AddFont(Resources, @"Fonts/Noto-Basic"); AddFont(Resources, @"Fonts/Noto-Hangul"); From 288470c3138713756b4f24fa1ef1e8f4ab63c20c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Mar 2020 13:32:16 +0900 Subject: [PATCH 0372/2376] Remove exo specification completely --- osu.Game/Graphics/OsuFont.cs | 6 +----- osu.Game/Graphics/UserInterface/OsuTabControl.cs | 2 +- osu.Game/Graphics/UserInterface/PageTabControl.cs | 2 +- osu.Game/Overlays/AccountCreation/ScreenEntry.cs | 2 +- osu.Game/Overlays/News/NewsArticleCover.cs | 6 +++--- osu.Game/Screens/Menu/Disclaimer.cs | 6 +++--- osu.Game/Screens/Multi/Lounge/Components/ParticipantInfo.cs | 2 +- osu.Game/Screens/Multi/Ranking/Pages/RoomLeaderboardPage.cs | 2 +- 8 files changed, 12 insertions(+), 16 deletions(-) diff --git a/osu.Game/Graphics/OsuFont.cs b/osu.Game/Graphics/OsuFont.cs index 841936d2c5..1b5a3199a2 100644 --- a/osu.Game/Graphics/OsuFont.cs +++ b/osu.Game/Graphics/OsuFont.cs @@ -30,7 +30,7 @@ namespace osu.Game.Graphics /// Whether the font is italic. /// Whether all characters should be spaced the same distance apart. /// The . - public static FontUsage GetFont(Typeface typeface = Typeface.Exo, float size = DEFAULT_FONT_SIZE, FontWeight weight = FontWeight.Medium, bool italics = false, bool fixedWidth = false) + public static FontUsage GetFont(Typeface typeface = Typeface.Torus, float size = DEFAULT_FONT_SIZE, FontWeight weight = FontWeight.Medium, bool italics = false, bool fixedWidth = false) => new FontUsage(GetFamilyString(typeface), size, GetWeightString(typeface, weight), italics, fixedWidth); /// @@ -42,9 +42,6 @@ namespace osu.Game.Graphics { switch (typeface) { - case Typeface.Exo: - return "Exo2.0"; - case Typeface.Venera: return "Venera"; @@ -96,7 +93,6 @@ namespace osu.Game.Graphics public enum Typeface { - Exo, Venera, Torus } diff --git a/osu.Game/Graphics/UserInterface/OsuTabControl.cs b/osu.Game/Graphics/UserInterface/OsuTabControl.cs index 6c883d9893..ca9f1330f9 100644 --- a/osu.Game/Graphics/UserInterface/OsuTabControl.cs +++ b/osu.Game/Graphics/UserInterface/OsuTabControl.cs @@ -173,7 +173,7 @@ namespace osu.Game.Graphics.UserInterface new HoverClickSounds() }; - Active.BindValueChanged(active => Text.Font = Text.Font.With(Typeface.Exo, weight: active.NewValue ? FontWeight.Bold : FontWeight.Medium), true); + Active.BindValueChanged(active => Text.Font = Text.Font.With(Typeface.Torus, weight: active.NewValue ? FontWeight.Bold : FontWeight.Medium), true); } protected override void OnActivated() => fadeActive(); diff --git a/osu.Game/Graphics/UserInterface/PageTabControl.cs b/osu.Game/Graphics/UserInterface/PageTabControl.cs index ddcb626701..d05a08108a 100644 --- a/osu.Game/Graphics/UserInterface/PageTabControl.cs +++ b/osu.Game/Graphics/UserInterface/PageTabControl.cs @@ -78,7 +78,7 @@ namespace osu.Game.Graphics.UserInterface new HoverClickSounds() }; - Active.BindValueChanged(active => Text.Font = Text.Font.With(Typeface.Exo, weight: active.NewValue ? FontWeight.Bold : FontWeight.Medium), true); + Active.BindValueChanged(active => Text.Font = Text.Font.With(Typeface.Torus, weight: active.NewValue ? FontWeight.Bold : FontWeight.Medium), true); } protected virtual string CreateText() => (Value as Enum)?.GetDescription() ?? Value.ToString(); diff --git a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs index 2576900db8..a0b1b27ebf 100644 --- a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs +++ b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs @@ -132,7 +132,7 @@ namespace osu.Game.Overlays.AccountCreation usernameDescription.AddText("This will be your public presence. No profanity, no impersonation. Avoid exposing your own personal details, too!"); emailAddressDescription.AddText("Will be used for notifications, account verification and in the case you forget your password. No spam, ever."); - emailAddressDescription.AddText(" Make sure to get it right!", cp => cp.Font = cp.Font.With(Typeface.Exo, weight: FontWeight.Bold)); + emailAddressDescription.AddText(" Make sure to get it right!", cp => cp.Font = cp.Font.With(Typeface.Torus, weight: FontWeight.Bold)); passwordDescription.AddText("At least "); characterCheckText = passwordDescription.AddText("8 characters long"); diff --git a/osu.Game/Overlays/News/NewsArticleCover.cs b/osu.Game/Overlays/News/NewsArticleCover.cs index f61b30b381..e381b629e4 100644 --- a/osu.Game/Overlays/News/NewsArticleCover.cs +++ b/osu.Game/Overlays/News/NewsArticleCover.cs @@ -75,7 +75,7 @@ namespace osu.Game.Overlays.News Left = 25, Bottom = 50, }, - Font = OsuFont.GetFont(Typeface.Exo, 24, FontWeight.Bold), + Font = OsuFont.GetFont(Typeface.Torus, 24, FontWeight.Bold), Text = info.Title, }, new OsuSpriteText @@ -87,7 +87,7 @@ namespace osu.Game.Overlays.News Left = 25, Bottom = 30, }, - Font = OsuFont.GetFont(Typeface.Exo, 16, FontWeight.Bold), + Font = OsuFont.GetFont(Typeface.Torus, 16, FontWeight.Bold), Text = "by " + info.Author } }; @@ -148,7 +148,7 @@ namespace osu.Game.Overlays.News { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Font = OsuFont.GetFont(Typeface.Exo, 12, FontWeight.Black, false, false), + Font = OsuFont.GetFont(Typeface.Torus, 12, FontWeight.Black, false, false), Text = date.ToString("d MMM yyy").ToUpper(), Margin = new MarginPadding { diff --git a/osu.Game/Screens/Menu/Disclaimer.cs b/osu.Game/Screens/Menu/Disclaimer.cs index ee8200321b..35091028ae 100644 --- a/osu.Game/Screens/Menu/Disclaimer.cs +++ b/osu.Game/Screens/Menu/Disclaimer.cs @@ -90,14 +90,14 @@ namespace osu.Game.Screens.Menu } }; - textFlow.AddText("This project is an ongoing ", t => t.Font = t.Font.With(Typeface.Exo, 30, FontWeight.Light)); - textFlow.AddText("work in progress", t => t.Font = t.Font.With(Typeface.Exo, 30, FontWeight.SemiBold)); + textFlow.AddText("This project is an ongoing ", t => t.Font = t.Font.With(Typeface.Torus, 30, FontWeight.Light)); + textFlow.AddText("work in progress", t => t.Font = t.Font.With(Typeface.Torus, 30, FontWeight.SemiBold)); textFlow.NewParagraph(); static void format(SpriteText t) => t.Font = OsuFont.GetFont(size: 15, weight: FontWeight.SemiBold); - textFlow.AddParagraph(getRandomTip(), t => t.Font = t.Font.With(Typeface.Exo, 20, FontWeight.SemiBold)); + textFlow.AddParagraph(getRandomTip(), t => t.Font = t.Font.With(Typeface.Torus, 20, FontWeight.SemiBold)); textFlow.NewParagraph(); textFlow.NewParagraph(); diff --git a/osu.Game/Screens/Multi/Lounge/Components/ParticipantInfo.cs b/osu.Game/Screens/Multi/Lounge/Components/ParticipantInfo.cs index a55db096af..4152a9a3b2 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/ParticipantInfo.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/ParticipantInfo.cs @@ -77,7 +77,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components if (host.NewValue != null) { hostText.AddText("hosted by "); - hostText.AddUserLink(host.NewValue, s => s.Font = s.Font.With(Typeface.Exo, weight: FontWeight.Bold, italics: true)); + hostText.AddUserLink(host.NewValue, s => s.Font = s.Font.With(Typeface.Torus, weight: FontWeight.Bold, italics: true)); flagContainer.Child = new UpdateableFlag(host.NewValue.Country) { RelativeSizeAxes = Axes.Both }; } diff --git a/osu.Game/Screens/Multi/Ranking/Pages/RoomLeaderboardPage.cs b/osu.Game/Screens/Multi/Ranking/Pages/RoomLeaderboardPage.cs index f8fb192b5c..0d31805774 100644 --- a/osu.Game/Screens/Multi/Ranking/Pages/RoomLeaderboardPage.cs +++ b/osu.Game/Screens/Multi/Ranking/Pages/RoomLeaderboardPage.cs @@ -91,7 +91,7 @@ namespace osu.Game.Screens.Multi.Ranking.Pages rankText.AddText($"#{index + 1} ", s => { - s.Font = s.Font.With(Typeface.Exo, weight: FontWeight.Bold); + s.Font = s.Font.With(Typeface.Torus, weight: FontWeight.Bold); s.Colour = colours.YellowDark; }); From c45f9cafd4384b3903952dfe74f6ca8b59dda587 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Mar 2020 13:33:55 +0900 Subject: [PATCH 0373/2376] Add medium -> regular fallback for torus --- osu.Game/Graphics/OsuFont.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/OsuFont.cs b/osu.Game/Graphics/OsuFont.cs index 1b5a3199a2..076cc241ed 100644 --- a/osu.Game/Graphics/OsuFont.cs +++ b/osu.Game/Graphics/OsuFont.cs @@ -59,7 +59,13 @@ namespace osu.Game.Graphics /// The . /// The string representation of in the specified . public static string GetWeightString(Typeface typeface, FontWeight weight) - => GetWeightString(GetFamilyString(typeface), weight); + { + if (typeface == Typeface.Torus && weight == FontWeight.Medium) + // torus doesn't have a medium; fallback to regular. + weight = FontWeight.Regular; + + return GetWeightString(GetFamilyString(typeface), weight); + } /// /// Retrieves the string representation of a . From f7c036726a72271a8418acd0919dbbe932d2a086 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 13 Mar 2020 13:52:40 +0900 Subject: [PATCH 0374/2376] Add beatmap loading timeout --- osu.Game/Beatmaps/IWorkingBeatmap.cs | 4 +- osu.Game/Beatmaps/WorkingBeatmap.cs | 98 ++++++++++++++++------------ 2 files changed, 61 insertions(+), 41 deletions(-) diff --git a/osu.Game/Beatmaps/IWorkingBeatmap.cs b/osu.Game/Beatmaps/IWorkingBeatmap.cs index 5f1f0d1e40..61bd962648 100644 --- a/osu.Game/Beatmaps/IWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/IWorkingBeatmap.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.Audio.Track; using osu.Framework.Graphics.Textures; @@ -60,8 +61,9 @@ namespace osu.Game.Beatmaps /// /// The to create a playable for. /// The s to apply to the . + /// The loading timeout. /// The converted . /// If could not be converted to . - IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList mods = null); + IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList mods = null, TimeSpan? timeout = null); } } diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 1e1ffad81e..bdcfc058b4 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -83,55 +83,73 @@ 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) + public IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList mods = null, TimeSpan? timeout = null) { - mods ??= Array.Empty(); - - var rulesetInstance = ruleset.CreateInstance(); - - IBeatmapConverter converter = CreateBeatmapConverter(Beatmap, rulesetInstance); - - // Check if the beatmap can be converted - if (Beatmap.HitObjects.Count > 0 && !converter.CanConvert()) - throw new BeatmapInvalidForRulesetException($"{nameof(Beatmaps.Beatmap)} can not be converted for the ruleset (ruleset: {ruleset.InstantiationInfo}, converter: {converter})."); - - // Apply conversion mods - foreach (var mod in mods.OfType()) - mod.ApplyToBeatmapConverter(converter); - - // Convert - IBeatmap converted = converter.Convert(); - - // Apply difficulty mods - if (mods.Any(m => m is IApplicableToDifficulty)) + using (var cancellationSource = new CancellationTokenSource(timeout ?? TimeSpan.FromSeconds(10))) { - converted.BeatmapInfo = converted.BeatmapInfo.Clone(); - converted.BeatmapInfo.BaseDifficulty = converted.BeatmapInfo.BaseDifficulty.Clone(); + mods ??= Array.Empty(); - foreach (var mod in mods.OfType()) - mod.ApplyToDifficulty(converted.BeatmapInfo.BaseDifficulty); - } + var rulesetInstance = ruleset.CreateInstance(); - IBeatmapProcessor processor = rulesetInstance.CreateBeatmapProcessor(converted); + IBeatmapConverter converter = CreateBeatmapConverter(Beatmap, rulesetInstance); - processor?.PreProcess(); + // Check if the beatmap can be converted + if (Beatmap.HitObjects.Count > 0 && !converter.CanConvert()) + throw new BeatmapInvalidForRulesetException($"{nameof(Beatmaps.Beatmap)} can not be converted for the ruleset (ruleset: {ruleset.InstantiationInfo}, converter: {converter})."); - // Compute default values for hitobjects, including creating nested hitobjects in-case they're needed - foreach (var obj in converted.HitObjects) - obj.ApplyDefaults(converted.ControlPointInfo, converted.BeatmapInfo.BaseDifficulty); + // Apply conversion mods + foreach (var mod in mods.OfType()) + { + cancellationSource.Token.ThrowIfCancellationRequested(); + mod.ApplyToBeatmapConverter(converter); + } - foreach (var mod in mods.OfType()) - { + // Convert + IBeatmap converted = converter.Convert(); + + // Apply difficulty mods + if (mods.Any(m => m is IApplicableToDifficulty)) + { + converted.BeatmapInfo = converted.BeatmapInfo.Clone(); + converted.BeatmapInfo.BaseDifficulty = converted.BeatmapInfo.BaseDifficulty.Clone(); + + foreach (var mod in mods.OfType()) + { + cancellationSource.Token.ThrowIfCancellationRequested(); + mod.ApplyToDifficulty(converted.BeatmapInfo.BaseDifficulty); + } + } + + IBeatmapProcessor processor = rulesetInstance.CreateBeatmapProcessor(converted); + + processor?.PreProcess(); + + // Compute default values for hitobjects, including creating nested hitobjects in-case they're needed foreach (var obj in converted.HitObjects) - mod.ApplyToHitObject(obj); + { + cancellationSource.Token.ThrowIfCancellationRequested(); + obj.ApplyDefaults(converted.ControlPointInfo, converted.BeatmapInfo.BaseDifficulty); + } + + foreach (var mod in mods.OfType()) + { + foreach (var obj in converted.HitObjects) + { + cancellationSource.Token.ThrowIfCancellationRequested(); + mod.ApplyToHitObject(obj); + } + } + + processor?.PostProcess(); + + foreach (var mod in mods.OfType()) + { + cancellationSource.Token.ThrowIfCancellationRequested(); + mod.ApplyToBeatmap(converted); + } + + return converted; } - - processor?.PostProcess(); - - foreach (var mod in mods.OfType()) - mod.ApplyToBeatmap(converted); - - return converted; } private CancellationTokenSource loadCancellation = new CancellationTokenSource(); From ef0acde458c7b9bea5fa42fc60e951fa6e3581fb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Mar 2020 14:12:56 +0900 Subject: [PATCH 0375/2376] Adjust to allow for extra row --- osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs index 1ef2916ca1..fad9f00dc8 100644 --- a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs +++ b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs @@ -254,7 +254,7 @@ namespace osu.Game.Tournament.Screens.MapPool } } - if (totalRows > 8) + if (totalRows > 9) // remove horizontal padding to increase flow width to 3 panels mapFlows.Padding = new MarginPadding(5); } From fbb7e9f12a3971a6f3afb46138ba2f67b499d54b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Mar 2020 15:34:52 +0900 Subject: [PATCH 0376/2376] Add tests (wip) --- osu.Game.Tournament.Tests/LadderTestScene.cs | 122 ++++++++++++++++++ .../Screens/TestSceneMapPoolScreen.cs | 83 +++++++++++- .../Screens/TestSceneSeedingEditorScreen.cs | 2 +- .../Screens/TestSceneSeedingScreen.cs | 101 --------------- .../Screens/MapPool/MapPoolScreen.cs | 4 +- 5 files changed, 201 insertions(+), 111 deletions(-) diff --git a/osu.Game.Tournament.Tests/LadderTestScene.cs b/osu.Game.Tournament.Tests/LadderTestScene.cs index dae0721023..4477ca8338 100644 --- a/osu.Game.Tournament.Tests/LadderTestScene.cs +++ b/osu.Game.Tournament.Tests/LadderTestScene.cs @@ -3,7 +3,10 @@ using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Utils; +using osu.Game.Beatmaps; using osu.Game.Tournament.Models; +using osu.Game.Users; namespace osu.Game.Tournament.Tests { @@ -12,5 +15,124 @@ namespace osu.Game.Tournament.Tests { [Resolved] protected LadderInfo Ladder { get; private set; } + + protected override void LoadComplete() + { + base.LoadComplete(); + + TournamentMatch match = CreateSampleMatch(); + + Ladder.Rounds.Clear(); + Ladder.Rounds.Add(match.Round.Value); + + Ladder.Matches.Clear(); + Ladder.Matches.Add(match); + + Ladder.Teams.Clear(); + Ladder.Teams.Add(match.Team1.Value); + Ladder.Teams.Add(match.Team2.Value); + + Ladder.CurrentMatch.Value = match; + } + + public static TournamentMatch CreateSampleMatch() => new TournamentMatch + { + Team1 = + { + Value = new TournamentTeam + { + FlagName = { Value = "JP" }, + FullName = { Value = "Japan" }, + LastYearPlacing = { Value = 10 }, + Seed = { Value = "Low" }, + SeedingResults = + { + new SeedingResult + { + Mod = { Value = "NM" }, + Seed = { Value = 10 }, + Beatmaps = + { + new SeedingBeatmap + { + BeatmapInfo = CreateSampleBeatmapInfo(), + Score = 12345672, + Seed = { Value = 24 }, + }, + new SeedingBeatmap + { + BeatmapInfo = CreateSampleBeatmapInfo(), + Score = 1234567, + Seed = { Value = 12 }, + }, + new SeedingBeatmap + { + BeatmapInfo = CreateSampleBeatmapInfo(), + Score = 1234567, + Seed = { Value = 16 }, + } + } + }, + new SeedingResult + { + Mod = { Value = "DT" }, + Seed = { Value = 5 }, + Beatmaps = + { + new SeedingBeatmap + { + BeatmapInfo = CreateSampleBeatmapInfo(), + Score = 234567, + Seed = { Value = 3 }, + }, + new SeedingBeatmap + { + BeatmapInfo = CreateSampleBeatmapInfo(), + Score = 234567, + Seed = { Value = 6 }, + }, + new SeedingBeatmap + { + BeatmapInfo = CreateSampleBeatmapInfo(), + Score = 234567, + Seed = { Value = 12 }, + } + } + } + }, + Players = + { + new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 12 } } }, + new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 16 } } }, + new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 20 } } }, + new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 24 } } }, + new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 30 } } }, + } + } + }, + Team2 = + { + Value = new TournamentTeam + { + FlagName = { Value = "US" }, + FullName = { Value = "United States" }, + Players = + { + new User { Username = "Hello" }, + new User { Username = "Hello" }, + new User { Username = "Hello" }, + new User { Username = "Hello" }, + new User { Username = "Hello" }, + } + } + }, + Round = + { + Value = new TournamentRound { Name = { Value = "Quarterfinals" } } + } + }; + + public static BeatmapInfo CreateSampleBeatmapInfo() => + new BeatmapInfo { Metadata = new BeatmapMetadata { Title = "Test Title", Artist = "Test Artist", ID = RNG.Next(0, 1000000) } }; } } diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs index a7011c6d3c..a73e4e53ba 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs @@ -1,24 +1,93 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Testing; +using osu.Game.Tournament.Components; +using osu.Game.Tournament.Models; using osu.Game.Tournament.Screens.MapPool; namespace osu.Game.Tournament.Tests.Screens { public class TestSceneMapPoolScreen : LadderTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(MapPoolScreen) - }; + private MapPoolScreen screen; [BackgroundDependencyLoader] private void load() { - Add(new MapPoolScreen { Width = 0.7f }); + Add(screen = new MapPoolScreen { Width = 0.7f }); + } + + [Test] + public void TestFewMaps() + { + AddStep("load few maps", () => + { + Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Clear(); + + for (int i = 0; i < 8; i++) + addBeatmap(); + }); + + AddStep("reset match", () => + { + Ladder.CurrentMatch.Value = new TournamentMatch(); + Ladder.CurrentMatch.Value = Ladder.Matches.First(); + }); + + AddAssert("ensure layout width is 2", () => screen.ChildrenOfType().ElementAt(2).Y > 0); + } + + [Test] + public void TestManyMaps() + { + AddStep("load many maps", () => + { + Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Clear(); + + for (int i = 0; i < 17; i++) + addBeatmap(); + }); + + AddStep("reset match", () => + { + Ladder.CurrentMatch.Value = new TournamentMatch(); + Ladder.CurrentMatch.Value = Ladder.Matches.First(); + }); + + AddAssert("ensure layout width is 3", () => screen.ChildrenOfType().ElementAt(2).Y == 0); + } + + [Test] + public void TestManyMods() + { + AddStep("load many maps", () => + { + Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Clear(); + + for (int i = 0; i < 13; i++) + addBeatmap(i < 4 ? $"M{i}" : "NM"); + }); + + AddStep("reset match", () => + { + Ladder.CurrentMatch.Value = new TournamentMatch(); + Ladder.CurrentMatch.Value = Ladder.Matches.First(); + }); + + AddAssert("ensure layout width is 3", () => screen.ChildrenOfType().ElementAt(2).Y == 0); + } + + private void addBeatmap(string mods = "nm") + { + Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Add(new RoundBeatmap + { + BeatmapInfo = CreateSampleBeatmapInfo(), + Mods = mods + }); } } } diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneSeedingEditorScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneSeedingEditorScreen.cs index 014cd4663b..17cccd34b6 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneSeedingEditorScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneSeedingEditorScreen.cs @@ -14,7 +14,7 @@ namespace osu.Game.Tournament.Tests.Screens public TestSceneSeedingEditorScreen() { - var match = TestSceneSeedingScreen.CreateSampleSeededMatch(); + var match = CreateSampleMatch(); Add(new SeedingEditorScreen(match.Team1.Value) { diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneSeedingScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneSeedingScreen.cs index 335a6c80a1..4269f8f56a 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneSeedingScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneSeedingScreen.cs @@ -3,10 +3,8 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Game.Beatmaps; using osu.Game.Tournament.Models; using osu.Game.Tournament.Screens.TeamIntro; -using osu.Game.Users; namespace osu.Game.Tournament.Tests.Screens { @@ -18,110 +16,11 @@ namespace osu.Game.Tournament.Tests.Screens [BackgroundDependencyLoader] private void load() { - ladder.CurrentMatch.Value = CreateSampleSeededMatch(); - Add(new SeedingScreen { FillMode = FillMode.Fit, FillAspectRatio = 16 / 9f }); } - - public static TournamentMatch CreateSampleSeededMatch() => new TournamentMatch - { - Team1 = - { - Value = new TournamentTeam - { - FlagName = { Value = "JP" }, - FullName = { Value = "Japan" }, - LastYearPlacing = { Value = 10 }, - Seed = { Value = "Low" }, - SeedingResults = - { - new SeedingResult - { - Mod = { Value = "NM" }, - Seed = { Value = 10 }, - Beatmaps = - { - new SeedingBeatmap - { - BeatmapInfo = new BeatmapInfo { Metadata = new BeatmapMetadata { Title = "Test Title", Artist = "Test Artist" } }, - Score = 12345672, - Seed = { Value = 24 }, - }, - new SeedingBeatmap - { - BeatmapInfo = new BeatmapInfo { Metadata = new BeatmapMetadata { Title = "Test Title", Artist = "Test Artist" } }, - Score = 1234567, - Seed = { Value = 12 }, - }, - new SeedingBeatmap - { - BeatmapInfo = new BeatmapInfo { Metadata = new BeatmapMetadata { Title = "Test Title", Artist = "Test Artist" } }, - Score = 1234567, - Seed = { Value = 16 }, - } - } - }, - new SeedingResult - { - Mod = { Value = "DT" }, - Seed = { Value = 5 }, - Beatmaps = - { - new SeedingBeatmap - { - BeatmapInfo = new BeatmapInfo { Metadata = new BeatmapMetadata { Title = "Test Title", Artist = "Test Artist" } }, - Score = 234567, - Seed = { Value = 3 }, - }, - new SeedingBeatmap - { - BeatmapInfo = new BeatmapInfo { Metadata = new BeatmapMetadata { Title = "Test Title", Artist = "Test Artist" } }, - Score = 234567, - Seed = { Value = 6 }, - }, - new SeedingBeatmap - { - BeatmapInfo = new BeatmapInfo { Metadata = new BeatmapMetadata { Title = "Test Title", Artist = "Test Artist" } }, - Score = 234567, - Seed = { Value = 12 }, - } - } - } - }, - Players = - { - new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 12 } } }, - new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 16 } } }, - new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 20 } } }, - new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 24 } } }, - new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 30 } } }, - } - } - }, - Team2 = - { - Value = new TournamentTeam - { - FlagName = { Value = "US" }, - FullName = { Value = "United States" }, - Players = - { - new User { Username = "Hello" }, - new User { Username = "Hello" }, - new User { Username = "Hello" }, - new User { Username = "Hello" }, - new User { Username = "Hello" }, - } - } - }, - Round = - { - Value = new TournamentRound { Name = { Value = "Quarterfinals" } } - } - }; } } diff --git a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs index fad9f00dc8..a9e1d1f226 100644 --- a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs +++ b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs @@ -52,7 +52,6 @@ namespace osu.Game.Tournament.Screens.MapPool { Y = 100, Spacing = new Vector2(10, 10), - Padding = new MarginPadding(5) { Horizontal = 100 }, Direction = FillDirection.Vertical, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, @@ -212,6 +211,7 @@ namespace osu.Game.Tournament.Screens.MapPool private void matchChanged(ValueChangedEvent match) { mapFlows.Clear(); + mapFlows.Padding = new MarginPadding(5) { Horizontal = 100 }; int totalRows = 0; @@ -243,7 +243,7 @@ namespace osu.Game.Tournament.Screens.MapPool if (++flowCount > 2) { totalRows++; - flowCount = 0; + flowCount = 1; } currentFlow.Add(new TournamentBeatmapPanel(b.BeatmapInfo, b.Mods) From 00d7dc19cc4932ac1af3ca1c6da315dbcd8b5ee1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Mar 2020 14:25:25 +0900 Subject: [PATCH 0377/2376] Update tests and logic --- .../Screens/TestSceneMapPoolScreen.cs | 46 +++++++++++++++++-- .../Screens/MapPool/MapPoolScreen.cs | 7 +-- 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs index a73e4e53ba..cc5f66761e 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs @@ -41,6 +41,26 @@ namespace osu.Game.Tournament.Tests.Screens AddAssert("ensure layout width is 2", () => screen.ChildrenOfType().ElementAt(2).Y > 0); } + [Test] + public void TestJustEnoughMaps() + { + AddStep("load just enough maps", () => + { + Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Clear(); + + for (int i = 0; i < 18; i++) + addBeatmap(); + }); + + AddStep("reset match", () => + { + Ladder.CurrentMatch.Value = new TournamentMatch(); + Ladder.CurrentMatch.Value = Ladder.Matches.First(); + }); + + AddAssert("ensure layout width is 2", () => screen.ChildrenOfType().ElementAt(2).Y > 0); + } + [Test] public void TestManyMaps() { @@ -48,7 +68,7 @@ namespace osu.Game.Tournament.Tests.Screens { Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Clear(); - for (int i = 0; i < 17; i++) + for (int i = 0; i < 19; i++) addBeatmap(); }); @@ -61,6 +81,26 @@ namespace osu.Game.Tournament.Tests.Screens AddAssert("ensure layout width is 3", () => screen.ChildrenOfType().ElementAt(2).Y == 0); } + [Test] + public void TestJustEnoughMods() + { + AddStep("load many maps", () => + { + Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Clear(); + + for (int i = 0; i < 11; i++) + addBeatmap(i > 4 ? $"M{i}" : "NM"); + }); + + AddStep("reset match", () => + { + Ladder.CurrentMatch.Value = new TournamentMatch(); + Ladder.CurrentMatch.Value = Ladder.Matches.First(); + }); + + AddAssert("ensure layout width is 2", () => screen.ChildrenOfType().ElementAt(2).Y > 0); + } + [Test] public void TestManyMods() { @@ -68,8 +108,8 @@ namespace osu.Game.Tournament.Tests.Screens { Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Clear(); - for (int i = 0; i < 13; i++) - addBeatmap(i < 4 ? $"M{i}" : "NM"); + for (int i = 0; i < 12; i++) + addBeatmap(i > 4 ? $"M{i}" : "NM"); }); AddStep("reset match", () => diff --git a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs index a9e1d1f226..da7e226103 100644 --- a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs +++ b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs @@ -211,7 +211,6 @@ namespace osu.Game.Tournament.Screens.MapPool private void matchChanged(ValueChangedEvent match) { mapFlows.Clear(); - mapFlows.Padding = new MarginPadding(5) { Horizontal = 100 }; int totalRows = 0; @@ -254,9 +253,11 @@ namespace osu.Game.Tournament.Screens.MapPool } } - if (totalRows > 9) + mapFlows.Padding = new MarginPadding(5) + { // remove horizontal padding to increase flow width to 3 panels - mapFlows.Padding = new MarginPadding(5); + Horizontal = totalRows > 9 ? 0 : 100 + }; } } } From c33ca6e99c60e1fd4bbc1d321a229a55f3e99de5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 13 Mar 2020 14:28:11 +0900 Subject: [PATCH 0378/2376] Decorate usages with exception management --- osu.Game/Screens/Edit/Editor.cs | 14 +++++++++- osu.Game/Screens/Select/BeatmapInfoWedge.cs | 30 +++++++++++++-------- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 3a6f02f811..cf13f8a3a1 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -23,6 +23,7 @@ using osuTK.Input; using System.Collections.Generic; using osu.Framework; using osu.Framework.Input.Bindings; +using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Graphics.Cursor; using osu.Game.Input.Bindings; @@ -86,7 +87,18 @@ namespace osu.Game.Screens.Edit // todo: remove caching of this and consume via editorBeatmap? dependencies.Cache(beatDivisor); - playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Beatmap.Value.BeatmapInfo.Ruleset); + try + { + playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Beatmap.Value.BeatmapInfo.Ruleset); + } + catch (Exception e) + { + Logger.Error(e, "Could not load beatmap sucessfully!"); + //couldn't load, hard abort! + this.Exit(); + return; + } + AddInternal(editorBeatmap = new EditorBeatmap(playableBeatmap)); dependencies.CacheAs(editorBeatmap); diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index f84aac3081..59d2aca17d 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -23,6 +23,7 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Framework.Logging; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; @@ -311,20 +312,27 @@ namespace osu.Game.Screens.Select Content = getBPMRange(b), })); - IBeatmap playableBeatmap; - try { - // Try to get the beatmap with the user's ruleset - playableBeatmap = beatmap.GetPlayableBeatmap(ruleset, Array.Empty()); - } - catch (BeatmapInvalidForRulesetException) - { - // Can't be converted to the user's ruleset, so use the beatmap's own ruleset - playableBeatmap = beatmap.GetPlayableBeatmap(beatmap.BeatmapInfo.Ruleset, Array.Empty()); - } + IBeatmap playableBeatmap; - labels.AddRange(playableBeatmap.GetStatistics().Select(s => new InfoLabel(s))); + try + { + // Try to get the beatmap with the user's ruleset + playableBeatmap = beatmap.GetPlayableBeatmap(ruleset, Array.Empty()); + } + catch (BeatmapInvalidForRulesetException) + { + // Can't be converted to the user's ruleset, so use the beatmap's own ruleset + playableBeatmap = beatmap.GetPlayableBeatmap(beatmap.BeatmapInfo.Ruleset, Array.Empty()); + } + + labels.AddRange(playableBeatmap.GetStatistics().Select(s => new InfoLabel(s))); + } + catch (Exception e) + { + Logger.Error(e, "Could not load beatmap sucessfully!"); + } } return labels.ToArray(); From 30ad580993b60944d395a3650214f53a8e6b75fe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Mar 2020 14:09:09 +0900 Subject: [PATCH 0379/2376] Fix map pool screen vertical layout --- osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs index da7e226103..7217629860 100644 --- a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs +++ b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs @@ -50,7 +50,7 @@ namespace osu.Game.Tournament.Screens.MapPool new MatchHeader(), mapFlows = new FillFlowContainer> { - Y = 100, + Y = 160, Spacing = new Vector2(10, 10), Direction = FillDirection.Vertical, RelativeSizeAxes = Axes.X, From edd444ea73fdc0df668b2e2c11f8fa15da1ac438 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Mar 2020 14:36:46 +0900 Subject: [PATCH 0380/2376] Fix mod sprite bleeding border colour --- .../Components/TournamentBeatmapPanel.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs index 4116ffbec6..dc4c4b9ec6 100644 --- a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs +++ b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs @@ -125,13 +125,21 @@ namespace osu.Game.Tournament.Components if (!string.IsNullOrEmpty(mods)) { - AddInternal(new Sprite + AddInternal(new Container { - Texture = textures.Get($"mods/{mods}"), + RelativeSizeAxes = Axes.Y, + Width = 60, Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, Margin = new MarginPadding(10), - Scale = new Vector2(0.8f) + Child = new Sprite + { + FillMode = FillMode.Fit, + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Texture = textures.Get($"mods/{mods}"), + } }); } } From c27751050be0ac704c76ceb11cfc2b25f0213c7e Mon Sep 17 00:00:00 2001 From: voidedWarranties Date: Thu, 12 Mar 2020 23:29:11 -0700 Subject: [PATCH 0381/2376] Switch back to strings and update setting labels --- .../Beatmaps/Formats/LegacyStoryboardDecoder.cs | 2 +- osu.Game/Beatmaps/Legacy/LegacyStoryLayer.cs | 2 +- .../Settings/Sections/Graphics/DetailSettings.cs | 2 +- .../Screens/Play/PlayerSettings/VisualSettings.cs | 2 +- osu.Game/Storyboards/Storyboard.cs | 13 +++++-------- 5 files changed, 9 insertions(+), 12 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs index 8cb717076a..ba4eb7209b 100644 --- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs @@ -92,7 +92,7 @@ namespace osu.Game.Beatmaps.Formats var offset = Parsing.ParseInt(split[1]); var filename = CleanFilename(split[2]); - storyboard.GetLayer(LegacyStoryLayer.Video).Add(new StoryboardVideo(filename, offset)); + storyboard.GetLayer("Video").Add(new StoryboardVideo(filename, offset)); break; } diff --git a/osu.Game/Beatmaps/Legacy/LegacyStoryLayer.cs b/osu.Game/Beatmaps/Legacy/LegacyStoryLayer.cs index c1329921ec..48e8bdbb76 100644 --- a/osu.Game/Beatmaps/Legacy/LegacyStoryLayer.cs +++ b/osu.Game/Beatmaps/Legacy/LegacyStoryLayer.cs @@ -3,7 +3,7 @@ namespace osu.Game.Beatmaps.Legacy { - public enum LegacyStoryLayer + internal enum LegacyStoryLayer { Background = 0, Fail = 1, diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs index acf33f00b3..3089040f96 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs @@ -18,7 +18,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics { new SettingsCheckbox { - LabelText = "Storyboards", + LabelText = "Storyboard / Video", Bindable = config.GetBindable(OsuSetting.ShowStoryboard) }, new SettingsCheckbox diff --git a/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs b/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs index bfb77e823f..d6c66d0751 100644 --- a/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs @@ -42,7 +42,7 @@ namespace osu.Game.Screens.Play.PlayerSettings { Text = "Toggles:" }, - showStoryboardToggle = new PlayerCheckbox { LabelText = "Storyboards" }, + showStoryboardToggle = new PlayerCheckbox { LabelText = "Storyboard / Video" }, beatmapSkinsToggle = new PlayerCheckbox { LabelText = "Beatmap skins" }, beatmapHitsoundsToggle = new PlayerCheckbox { LabelText = "Beatmap hitsounds" } }; diff --git a/osu.Game/Storyboards/Storyboard.cs b/osu.Game/Storyboards/Storyboard.cs index 64ad0208c9..7cfb104576 100644 --- a/osu.Game/Storyboards/Storyboard.cs +++ b/osu.Game/Storyboards/Storyboard.cs @@ -1,11 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.Legacy; -using osu.Game.Storyboards.Drawables; using System.Collections.Generic; using System.Linq; +using osu.Game.Beatmaps; +using osu.Game.Storyboards.Drawables; namespace osu.Game.Storyboards { @@ -29,8 +28,6 @@ namespace osu.Game.Storyboards layers.Add("Foreground", new StoryboardLayer("Foreground", 0)); } - public StoryboardLayer GetLayer(LegacyStoryLayer layer) => GetLayer(layer.ToString()); - public StoryboardLayer GetLayer(string name) { if (!layers.TryGetValue(name, out var layer)) @@ -50,17 +47,17 @@ namespace osu.Game.Storyboards if (backgroundPath == null) return false; - if (GetLayer(LegacyStoryLayer.Video).Elements.Any()) + if (GetLayer("Video").Elements.Any()) return true; - return GetLayer(LegacyStoryLayer.Background).Elements.Any(e => e.Path.ToLowerInvariant() == backgroundPath); + return GetLayer("Background").Elements.Any(e => e.Path.ToLowerInvariant() == backgroundPath); } } public DrawableStoryboard CreateDrawable(WorkingBeatmap working = null) { var drawable = new DrawableStoryboard(this); - drawable.Width = drawable.Height * (BeatmapInfo.WidescreenStoryboard || GetLayer(LegacyStoryLayer.Video).Elements.Any() ? 16 / 9f : 4 / 3f); + drawable.Width = drawable.Height * (BeatmapInfo.WidescreenStoryboard || GetLayer("Video").Elements.Any() ? 16 / 9f : 4 / 3f); return drawable; } } From b902e5039636ec42a975e3b355f8b1f5dddad871 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Mar 2020 15:44:13 +0900 Subject: [PATCH 0382/2376] Add resolution selector in tournament setup screen --- .../Components/ControlPanel.cs | 6 ++-- osu.Game.Tournament/Screens/SetupScreen.cs | 29 ++++++++++++++++++- osu.Game.Tournament/TournamentGameBase.cs | 2 +- osu.Game.Tournament/TournamentSceneManager.cs | 18 ++++++++---- 4 files changed, 44 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tournament/Components/ControlPanel.cs b/osu.Game.Tournament/Components/ControlPanel.cs index fa5c941f1a..ef8c8767e0 100644 --- a/osu.Game.Tournament/Components/ControlPanel.cs +++ b/osu.Game.Tournament/Components/ControlPanel.cs @@ -22,9 +22,9 @@ namespace osu.Game.Tournament.Components public ControlPanel() { - RelativeSizeAxes = Axes.Both; + RelativeSizeAxes = Axes.Y; AlwaysPresent = true; - Width = 0.15f; + Width = TournamentSceneManager.CONTROL_AREA_WIDTH; Anchor = Anchor.TopRight; InternalChildren = new Drawable[] @@ -47,8 +47,8 @@ namespace osu.Game.Tournament.Components Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Width = 0.75f, Position = new Vector2(0, 35f), + Padding = new MarginPadding(5), Direction = FillDirection.Vertical, Spacing = new Vector2(0, 5f), }, diff --git a/osu.Game.Tournament/Screens/SetupScreen.cs b/osu.Game.Tournament/Screens/SetupScreen.cs index 023582166c..b7f8b2bfd6 100644 --- a/osu.Game.Tournament/Screens/SetupScreen.cs +++ b/osu.Game.Tournament/Screens/SetupScreen.cs @@ -3,7 +3,10 @@ using System; using System.Collections.Generic; +using System.Drawing; using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.UserInterface; @@ -22,6 +25,7 @@ namespace osu.Game.Tournament.Screens private FillFlowContainer fillFlow; private LoginOverlay loginOverlay; + private ActionableInfo resolution; [Resolved] private MatchIPCInfo ipc { get; set; } @@ -32,9 +36,13 @@ namespace osu.Game.Tournament.Screens [Resolved] private RulesetStore rulesets { get; set; } + private Bindable windowSize; + [BackgroundDependencyLoader] - private void load() + private void load(FrameworkConfigManager frameworkConfig) { + windowSize = frameworkConfig.GetBindable(FrameworkSetting.WindowedSize); + InternalChild = fillFlow = new FillFlowContainer { RelativeSizeAxes = Axes.X, @@ -48,6 +56,9 @@ namespace osu.Game.Tournament.Screens reload(); } + [Resolved] + private Framework.Game game { get; set; } + private void reload() { var fileBasedIpc = ipc as FileBasedIPC; @@ -97,9 +108,25 @@ namespace osu.Game.Tournament.Screens Items = rulesets.AvailableRulesets, Current = LadderInfo.Ruleset, }, + resolution = new ActionableInfo + { + Label = "Stream area resolution", + ButtonText = "Set to 1080p", + Action = () => + { + windowSize.Value = new Size((int)(1920 / TournamentSceneManager.STREAM_AREA_WIDTH * TournamentSceneManager.REQUIRED_WIDTH), 1080); + } + } }; } + protected override void Update() + { + base.Update(); + + resolution.Value = $"{ScreenSpaceDrawQuad.Width:N0}x{ScreenSpaceDrawQuad.Height:N0}"; + } + public class LabelledDropdown : LabelledComponent, T> { public LabelledDropdown() diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 41822ae2c3..85db9e61fb 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -65,7 +65,7 @@ namespace osu.Game.Tournament windowSize = frameworkConfig.GetBindable(FrameworkSetting.WindowedSize); windowSize.BindValueChanged(size => ScheduleAfterChildren(() => { - var minWidth = (int)(size.NewValue.Height / 9f * 16 + 400); + var minWidth = (int)(size.NewValue.Height / 768f * TournamentSceneManager.REQUIRED_WIDTH) - 1; heightWarning.Alpha = size.NewValue.Width < minWidth ? 1 : 0; }), true); diff --git a/osu.Game.Tournament/TournamentSceneManager.cs b/osu.Game.Tournament/TournamentSceneManager.cs index ef8d16011d..23fcb01db7 100644 --- a/osu.Game.Tournament/TournamentSceneManager.cs +++ b/osu.Game.Tournament/TournamentSceneManager.cs @@ -33,6 +33,12 @@ namespace osu.Game.Tournament private Container screens; private TourneyVideo video; + public const float CONTROL_AREA_WIDTH = 160; + + public const float STREAM_AREA_WIDTH = 1366; + + public const double REQUIRED_WIDTH = TournamentSceneManager.CONTROL_AREA_WIDTH * 2 + TournamentSceneManager.STREAM_AREA_WIDTH; + [Cached] private TournamentMatchChatDisplay chat = new TournamentMatchChatDisplay(); @@ -51,13 +57,13 @@ namespace osu.Game.Tournament { new Container { - RelativeSizeAxes = Axes.Both, - X = 200, + RelativeSizeAxes = Axes.Y, + X = CONTROL_AREA_WIDTH, FillMode = FillMode.Fit, FillAspectRatio = 16 / 9f, Anchor = Anchor.TopLeft, Origin = Anchor.TopLeft, - Size = new Vector2(0.8f, 1), + Width = STREAM_AREA_WIDTH, //Masking = true, Children = new Drawable[] { @@ -96,7 +102,7 @@ namespace osu.Game.Tournament new Container { RelativeSizeAxes = Axes.Y, - Width = 200, + Width = CONTROL_AREA_WIDTH, Children = new Drawable[] { new Box @@ -108,8 +114,8 @@ namespace osu.Game.Tournament { RelativeSizeAxes = Axes.Both, Direction = FillDirection.Vertical, - Spacing = new Vector2(2), - Padding = new MarginPadding(2), + Spacing = new Vector2(5), + Padding = new MarginPadding(5), Children = new Drawable[] { new ScreenButton(typeof(SetupScreen)) { Text = "Setup", RequestSelection = SetScreen }, From bee855bd1d914fd17cd108fb9fb55f8b6f1519fa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Mar 2020 15:54:46 +0900 Subject: [PATCH 0383/2376] Remove using --- osu.Game.Tournament/Components/TournamentBeatmapPanel.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs index dc4c4b9ec6..477bf4bd63 100644 --- a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs +++ b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs @@ -16,7 +16,6 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; using osu.Game.Tournament.Models; -using osuTK; using osuTK.Graphics; namespace osu.Game.Tournament.Components From 7e9d28b1b13120372a7afd6599d7d905817043b8 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Fri, 13 Mar 2020 13:42:33 +0300 Subject: [PATCH 0384/2376] Fix slider ball colour affects follow circle --- .../Objects/Drawables/Pieces/SliderBall.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs index c871089acd..287a2d7e92 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs @@ -23,9 +23,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { public Func GetInitialHitAction; + public new Color4 Colour + { + get => ball.Colour; + set => ball.Colour = value; + } + private readonly Slider slider; private readonly Drawable followCircle; private readonly DrawableSlider drawableSlider; + private readonly CircularContainer ball; public SliderBall(Slider slider, DrawableSlider drawableSlider = null) { @@ -47,7 +54,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces Alpha = 0, Child = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderFollowCircle), _ => new DefaultFollowCircle()), }, - new CircularContainer + ball = new CircularContainer { Masking = true, RelativeSizeAxes = Axes.Both, From 097bd37e37c38095113bd706717a9601afc1f3c0 Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Fri, 13 Mar 2020 18:27:33 +0100 Subject: [PATCH 0385/2376] Fix SelectorTab crashing tests after a reload For some reason, the default channel type (Public) caused the channel manager to attempt to connect to an API, which was null at that time, after hot reloading the test environment (via dynamic compilation). Changing the channel type seems to fix that. --- osu.Game/Overlays/Chat/Tabs/ChannelSelectorTabItem.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Overlays/Chat/Tabs/ChannelSelectorTabItem.cs b/osu.Game/Overlays/Chat/Tabs/ChannelSelectorTabItem.cs index d5d9a6c2ce..5fb56a3f75 100644 --- a/osu.Game/Overlays/Chat/Tabs/ChannelSelectorTabItem.cs +++ b/osu.Game/Overlays/Chat/Tabs/ChannelSelectorTabItem.cs @@ -39,6 +39,7 @@ namespace osu.Game.Overlays.Chat.Tabs public ChannelSelectorTabChannel() { Name = "+"; + Type = ChannelType.Temporary; } } } From 8991e88039b0af4e39dd7122588542f7a916ccf8 Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Fri, 13 Mar 2020 18:33:06 +0100 Subject: [PATCH 0386/2376] Fix active tab closing behaviour --- .../Overlays/Chat/Tabs/ChannelTabControl.cs | 33 +++++++------------ 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs b/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs index 104495ae01..a72f182450 100644 --- a/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs +++ b/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs @@ -41,7 +41,7 @@ namespace osu.Game.Overlays.Chat.Tabs // performTabSort might've made selectorTab's position wonky, fix it TabContainer.SetLayoutPosition(selectorTab, float.MaxValue); - ((ChannelTabItem)item).OnRequestClose += tabCloseRequested; + ((ChannelTabItem)item).OnRequestClose += channelItem => OnRequestLeave?.Invoke(channelItem.Value); base.AddTabItem(item, addToDropdown); } @@ -74,18 +74,24 @@ namespace osu.Game.Overlays.Chat.Tabs /// /// Removes a channel from the ChannelTabControl. - /// If the selected channel is the one that is beeing removed, the next available channel will be selected. + /// If the selected channel is the one that is being removed, the next available channel will be selected. /// /// The channel that is going to be removed. public void RemoveChannel(Channel channel) { - RemoveItem(channel); - if (Current.Value == channel) { - // Prefer non-selector channels first - Current.Value = Items.FirstOrDefault(c => !(c is ChannelSelectorTabItem.ChannelSelectorTabChannel)) ?? Items.FirstOrDefault(); + var allChannels = TabContainer.AllTabItems.Select(tab => tab.Value).ToList(); + var isNextTabSelector = allChannels[allChannels.IndexOf(channel) + 1] == selectorTab.Value; + + // selectorTab is not switchable, so we have to explicitly select it if it's the only tab left + if (isNextTabSelector && allChannels.Count == 2) + SelectTab(selectorTab); + else + SwitchTab(isNextTabSelector ? -1 : 1); } + + RemoveItem(channel); } protected override void SelectTab(TabItem tab) @@ -100,21 +106,6 @@ namespace osu.Game.Overlays.Chat.Tabs selectorTab.Active.Value = false; } - private void tabCloseRequested(TabItem tab) - { - int totalTabs = TabContainer.Count - 1; // account for selectorTab - int currentIndex = Math.Clamp(TabContainer.IndexOf(tab), 1, totalTabs); - - if (tab == SelectedTab && totalTabs > 1) - // Select the tab after tab-to-be-removed's index, or the tab before if current == last - SelectTab(TabContainer[currentIndex == totalTabs ? currentIndex - 1 : currentIndex + 1]); - else if (totalTabs == 1 && !selectorTab.Active.Value) - // Open channel selection overlay if all channel tabs will be closed after removing this tab - SelectTab(selectorTab); - - OnRequestLeave?.Invoke(tab.Value); - } - protected override TabFillFlowContainer CreateTabFlow() => new ChannelTabFillFlowContainer { Direction = FillDirection.Full, From 694e56b0d13cc90e0f82b1f14661dd06fbf03f8d Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Fri, 13 Mar 2020 18:33:58 +0100 Subject: [PATCH 0387/2376] Add non-PM chat tabs to tests --- .../Visual/Online/TestSceneChatOverlay.cs | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index 19bdaff6ff..8aa30c7fa3 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; @@ -196,12 +196,22 @@ namespace osu.Game.Tests.Visual.Online private class TestTabControl : ChannelTabControl { - protected override TabItem CreateTabItem(Channel value) => new TestChannelTabItem(value); + protected override TabItem CreateTabItem(Channel value) + { + switch (value.Type) + { + case ChannelType.PM: + return new TestPrivateChannelTabItem(value); + + default: + return new TestChannelTabItem(value); + } + } public new IReadOnlyDictionary> TabMap => base.TabMap; } - private class TestChannelTabItem : PrivateChannelTabItem + private class TestChannelTabItem : ChannelTabItem { public TestChannelTabItem(Channel channel) : base(channel) @@ -210,5 +220,15 @@ namespace osu.Game.Tests.Visual.Online public new ClickableContainer CloseButton => base.CloseButton; } + + private class TestPrivateChannelTabItem : PrivateChannelTabItem + { + public TestPrivateChannelTabItem(Channel channel) + : base(channel) + { + } + + public new ClickableContainer CloseButton => base.CloseButton; + } } } From 0bbae094ddf854d6e9d6ee3c3197df0577071231 Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Fri, 13 Mar 2020 18:34:22 +0100 Subject: [PATCH 0388/2376] Add active tab closing behaviour tests --- .../Visual/Online/TestSceneChatOverlay.cs | 76 +++++++++++++++++-- 1 file changed, 70 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index 8aa30c7fa3..ede99c06be 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; @@ -38,8 +38,13 @@ namespace osu.Game.Tests.Visual.Online private TestChatOverlay chatOverlay; private ChannelManager channelManager; + private IEnumerable visibleChannels => chatOverlay.ChannelTabControl.VisibleItems.Where(channel => channel.Name != "+"); + private IEnumerable joinedChannels => chatOverlay.ChannelTabControl.Items.Where(channel => channel.Name != "+"); private readonly List channels; + private Channel currentChannel => channelManager.CurrentChannel.Value; + private Channel nextChannel => joinedChannels.ElementAt(joinedChannels.ToList().IndexOf(currentChannel) + 1); + private Channel previousChannel => joinedChannels.ElementAt(joinedChannels.ToList().IndexOf(currentChannel) - 1); private Channel channel1 => channels[0]; private Channel channel2 => channels[1]; @@ -91,7 +96,7 @@ namespace osu.Game.Tests.Visual.Online AddStep("Join channel 1", () => channelManager.JoinChannel(channel1)); AddStep("Switch to channel 1", () => clickDrawable(chatOverlay.TabMap[channel1])); - AddAssert("Current channel is channel 1", () => channelManager.CurrentChannel.Value == channel1); + AddAssert("Current channel is channel 1", () => currentChannel == channel1); AddAssert("Channel selector was closed", () => chatOverlay.SelectionOverlayState == Visibility.Hidden); } @@ -102,12 +107,12 @@ namespace osu.Game.Tests.Visual.Online AddStep("Join channel 2", () => channelManager.JoinChannel(channel2)); AddStep("Switch to channel 2", () => clickDrawable(chatOverlay.TabMap[channel2])); - AddStep("Close channel 2", () => clickDrawable(((TestChannelTabItem)chatOverlay.TabMap[channel2]).CloseButton.Child)); + AddStep("Close channel 2", () => clickDrawable(((TestPrivateChannelTabItem)chatOverlay.TabMap[channel2]).CloseButton.Child)); AddAssert("Selector remained closed", () => chatOverlay.SelectionOverlayState == Visibility.Hidden); - AddAssert("Current channel is channel 1", () => channelManager.CurrentChannel.Value == channel1); + AddAssert("Current channel is channel 1", () => currentChannel == channel1); - AddStep("Close channel 1", () => clickDrawable(((TestChannelTabItem)chatOverlay.TabMap[channel1]).CloseButton.Child)); + AddStep("Close channel 1", () => clickDrawable(((TestPrivateChannelTabItem)chatOverlay.TabMap[channel1]).CloseButton.Child)); AddAssert("Selector is visible", () => chatOverlay.SelectionOverlayState == Visibility.Visible); } @@ -140,10 +145,67 @@ namespace osu.Game.Tests.Visual.Online var targetNumberKey = oneBasedIndex % 10; var targetChannel = channels[zeroBasedIndex]; AddStep($"press Alt+{targetNumberKey}", () => pressChannelHotkey(targetNumberKey)); - AddAssert($"channel #{oneBasedIndex} is selected", () => channelManager.CurrentChannel.Value == targetChannel); + AddAssert($"channel #{oneBasedIndex} is selected", () => currentChannel == targetChannel); } } + private Channel expectedChannel; + + [Test] + public void TestCloseChannelWhileActive() + { + AddUntilStep("Join until dropdown has channels", () => + { + if (visibleChannels.Count() < joinedChannels.Count()) + return true; + + // Using temporary channels because they don't hide their names when not active + Channel toAdd = new Channel { Name = $"test channel {joinedChannels.Count()}", Type = ChannelType.Temporary }; + channelManager.JoinChannel(toAdd); + + return false; + }); + + AddStep("Switch to last tab", () => clickDrawable(chatOverlay.TabMap[visibleChannels.Last()])); + AddAssert("Last visible selected", () => currentChannel == visibleChannels.Last()); + + // Closing the last channel before dropdown + AddStep("Close current channel", () => + { + expectedChannel = nextChannel; + chatOverlay.ChannelTabControl.RemoveChannel(currentChannel); + }); + AddAssert("Next channel selected", () => currentChannel == expectedChannel); + + // Depending on the window size, one more channel might need to be closed for the selectorTab to appear + AddUntilStep("Close channels until selector visible", () => + { + if (chatOverlay.ChannelTabControl.VisibleItems.Last().Name == "+") + return true; + + chatOverlay.ChannelTabControl.RemoveChannel(visibleChannels.Last()); + return false; + }); + AddAssert("Last visible selected", () => currentChannel == visibleChannels.Last()); + + // Closing the last channel with dropdown no longer present + AddStep("Close last when selector next", () => + { + expectedChannel = previousChannel; + chatOverlay.ChannelTabControl.RemoveChannel(currentChannel); + }); + AddAssert("Channel changed to previous", () => currentChannel == expectedChannel); + + // Standard channel closing + AddStep("Switch to previous channel", () => chatOverlay.ChannelTabControl.SwitchTab(-1)); + AddStep("Close current channel", () => + { + expectedChannel = nextChannel; + chatOverlay.ChannelTabControl.RemoveChannel(currentChannel); + }); + AddAssert("Channel changed to next", () => currentChannel == expectedChannel); + } + private void pressChannelHotkey(int number) { var channelKey = Key.Number0 + number; @@ -187,6 +249,8 @@ namespace osu.Game.Tests.Visual.Online { public Visibility SelectionOverlayState => ChannelSelectionOverlay.State.Value; + public new ChannelTabControl ChannelTabControl => base.ChannelTabControl; + public new ChannelSelectionOverlay ChannelSelectionOverlay => base.ChannelSelectionOverlay; protected override ChannelTabControl CreateChannelTabControl() => new TestTabControl(); From 8d3cab0e1696e62c3c018cab91a1ee797b8bcc03 Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Fri, 13 Mar 2020 18:58:32 +0100 Subject: [PATCH 0389/2376] Trim whitespace --- osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index ede99c06be..297a51c4a5 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; @@ -107,12 +107,12 @@ namespace osu.Game.Tests.Visual.Online AddStep("Join channel 2", () => channelManager.JoinChannel(channel2)); AddStep("Switch to channel 2", () => clickDrawable(chatOverlay.TabMap[channel2])); - AddStep("Close channel 2", () => clickDrawable(((TestPrivateChannelTabItem)chatOverlay.TabMap[channel2]).CloseButton.Child)); + AddStep("Close channel 2", () => clickDrawable(((TestChannelTabItem)chatOverlay.TabMap[channel2]).CloseButton.Child)); AddAssert("Selector remained closed", () => chatOverlay.SelectionOverlayState == Visibility.Hidden); AddAssert("Current channel is channel 1", () => currentChannel == channel1); - AddStep("Close channel 1", () => clickDrawable(((TestPrivateChannelTabItem)chatOverlay.TabMap[channel1]).CloseButton.Child)); + AddStep("Close channel 1", () => clickDrawable(((TestChannelTabItem)chatOverlay.TabMap[channel1]).CloseButton.Child)); AddAssert("Selector is visible", () => chatOverlay.SelectionOverlayState == Visibility.Visible); } From 38d00c7f0ae6abb0283a366fadfca6cabb048948 Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Fri, 13 Mar 2020 21:29:10 +0100 Subject: [PATCH 0390/2376] Revert unnecessary changes and actually trim the whitespace --- osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index 297a51c4a5..736bfd8e7d 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; @@ -107,12 +107,12 @@ namespace osu.Game.Tests.Visual.Online AddStep("Join channel 2", () => channelManager.JoinChannel(channel2)); AddStep("Switch to channel 2", () => clickDrawable(chatOverlay.TabMap[channel2])); - AddStep("Close channel 2", () => clickDrawable(((TestChannelTabItem)chatOverlay.TabMap[channel2]).CloseButton.Child)); + AddStep("Close channel 2", () => clickDrawable(((TestPrivateChannelTabItem)chatOverlay.TabMap[channel2]).CloseButton.Child)); AddAssert("Selector remained closed", () => chatOverlay.SelectionOverlayState == Visibility.Hidden); AddAssert("Current channel is channel 1", () => currentChannel == channel1); - AddStep("Close channel 1", () => clickDrawable(((TestChannelTabItem)chatOverlay.TabMap[channel1]).CloseButton.Child)); + AddStep("Close channel 1", () => clickDrawable(((TestPrivateChannelTabItem)chatOverlay.TabMap[channel1]).CloseButton.Child)); AddAssert("Selector is visible", () => chatOverlay.SelectionOverlayState == Visibility.Visible); } @@ -189,7 +189,7 @@ namespace osu.Game.Tests.Visual.Online AddAssert("Last visible selected", () => currentChannel == visibleChannels.Last()); // Closing the last channel with dropdown no longer present - AddStep("Close last when selector next", () => + AddStep("Close last when selector next", () => { expectedChannel = previousChannel; chatOverlay.ChannelTabControl.RemoveChannel(currentChannel); @@ -198,7 +198,7 @@ namespace osu.Game.Tests.Visual.Online // Standard channel closing AddStep("Switch to previous channel", () => chatOverlay.ChannelTabControl.SwitchTab(-1)); - AddStep("Close current channel", () => + AddStep("Close current channel", () => { expectedChannel = nextChannel; chatOverlay.ChannelTabControl.RemoveChannel(currentChannel); From 202c8cdad8ef0cf223b48f6a0590622004fa2efd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 14 Mar 2020 15:35:59 +0900 Subject: [PATCH 0391/2376] Add braces to satisfy codefactor --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 2c8b080ee3..a3dc58bc19 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -348,8 +348,8 @@ namespace osu.Game.Rulesets.Catch.UI UpdatePosition((float)(X + direction * Clock.ElapsedFrameTime * speed)); // Correct overshooting. - if (hyperDashDirection > 0 && hyperDashTargetPosition < X || - hyperDashDirection < 0 && hyperDashTargetPosition > X) + if ((hyperDashDirection > 0 && hyperDashTargetPosition < X) || + (hyperDashDirection < 0 && hyperDashTargetPosition > X)) { X = hyperDashTargetPosition; SetHyperDashState(); From cd0f1c98ba710cfd29130859918e5b31657a7f62 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 14 Mar 2020 16:17:14 +0900 Subject: [PATCH 0392/2376] 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 f623a92ade..d7a76c97af 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index ba6f0e2251..882dd82a53 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -23,7 +23,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 54cd400d51..7872db676b 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + @@ -79,7 +79,7 @@ - + From 45dfb22bd50ae25884e2b159d27747acd8a5819d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 14 Mar 2020 16:25:10 +0900 Subject: [PATCH 0393/2376] Centralise additive texture creation --- osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 39 +++++++++++++---------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 13f1ddf1d7..2caae8c0c7 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -251,22 +251,10 @@ namespace osu.Game.Rulesets.Catch.UI return; } - Texture tex = (currentCatcher.Drawable as TextureAnimation)?.CurrentFrame ?? ((Sprite)currentCatcher.Drawable).Texture; - - var additive = new CatcherTrailSprite(tex) - { - Anchor = Anchor, - Scale = Scale, - Colour = HyperDashing ? Color4.Red : Color4.White, - Blending = BlendingParameters.Additive, - RelativePositionAxes = RelativePositionAxes, - Position = Position - }; + var additive = createAdditiveSprite(); additive.FadeTo(0.4f).FadeOut(800, Easing.OutQuint); additive.Expire(true); - - AdditiveTarget?.Add(additive); Scheduler.AddDelayed(beginTrail, HyperDashing ? 25 : 50); } @@ -400,10 +388,10 @@ namespace osu.Game.Rulesets.Catch.UI this.FadeTo(0.2f, hyper_dash_transition_length, Easing.OutQuint); Trail = true; - var hyperDashEndGlow = createAdditiveSprite(true); + var hyperDashEndGlow = createAdditiveSprite(); - hyperDashEndGlow.MoveToOffset(new Vector2(0, -20), 1200, Easing.In); - hyperDashEndGlow.ScaleTo(hyperDashEndGlow.Scale * 0.9f).ScaleTo(hyperDashEndGlow.Scale * 1.2f, 1200, Easing.In); + hyperDashEndGlow.MoveToOffset(new Vector2(0, -10), 1200, Easing.In); + hyperDashEndGlow.ScaleTo(hyperDashEndGlow.Scale * 0.95f).ScaleTo(hyperDashEndGlow.Scale * 1.2f, 1200, Easing.In); hyperDashEndGlow.FadeOut(1200); hyperDashEndGlow.Expire(true); } @@ -522,6 +510,25 @@ namespace osu.Game.Rulesets.Catch.UI }); } + private CatcherTrailSprite createAdditiveSprite() + { + Texture tex = (currentCatcher.Drawable as TextureAnimation)?.CurrentFrame ?? ((Sprite)currentCatcher.Drawable).Texture; + + var sprite = new CatcherTrailSprite(tex) + { + Anchor = Anchor, + Scale = Scale, + Colour = HyperDashing ? Color4.Red : Color4.White, + Blending = BlendingParameters.Additive, + RelativePositionAxes = RelativePositionAxes, + Position = Position + }; + + AdditiveTarget?.Add(sprite); + + return sprite; + } + private void removeFromPlateWithTransform(DrawableHitObject fruit, Action action) { if (ExplodingFruitTarget != null) From d3f23b766e775e068199487c9859091b5c395eb0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 14 Mar 2020 17:06:23 +0900 Subject: [PATCH 0394/2376] Move across to new file in line with master --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 59 ++++++++++++++++++--------- 1 file changed, 39 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index a3dc58bc19..29bed00d61 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -7,6 +7,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; using osu.Framework.Utils; using osu.Game.Beatmaps; @@ -55,14 +56,14 @@ namespace osu.Game.Rulesets.Catch.UI } /// - /// Activate or deactive the trail. Will be automatically deactivated when conditions to keep the trail displayed are no longer met. + /// Activate or deactivate the trail. Will be automatically deactivated when conditions to keep the trail displayed are no longer met. /// protected bool Trail { get => trail; set { - if (value == trail) return; + if (value == trail || AdditiveTarget == null) return; trail = value; @@ -77,6 +78,8 @@ namespace osu.Game.Rulesets.Catch.UI private CatcherSprite catcherKiai; private CatcherSprite catcherFail; + private CatcherSprite currentCatcher; + private int currentDirection; private bool dashing; @@ -236,10 +239,10 @@ namespace osu.Game.Rulesets.Catch.UI this.FadeTo(0.2f, hyper_dash_transition_length, Easing.OutQuint); Trail = true; - var hyperDashEndGlow = createAdditiveSprite(true); + var hyperDashEndGlow = createAdditiveSprite(); - hyperDashEndGlow.MoveToOffset(new Vector2(0, -20), 1200, Easing.In); - hyperDashEndGlow.ScaleTo(hyperDashEndGlow.Scale * 0.9f).ScaleTo(hyperDashEndGlow.Scale * 1.2f, 1200, Easing.In); + hyperDashEndGlow.MoveToOffset(new Vector2(0, -10), 1200, Easing.In); + hyperDashEndGlow.ScaleTo(hyperDashEndGlow.Scale * 0.95f).ScaleTo(hyperDashEndGlow.Scale * 1.2f, 1200, Easing.In); hyperDashEndGlow.FadeOut(1200); hyperDashEndGlow.Expire(true); } @@ -358,39 +361,36 @@ namespace osu.Game.Rulesets.Catch.UI private void updateCatcher() { - catcherIdle.Hide(); - catcherKiai.Hide(); - catcherFail.Hide(); - - CatcherSprite current; + currentCatcher?.Hide(); switch (CurrentState) { default: - current = catcherIdle; + currentCatcher = catcherIdle; break; case CatcherAnimationState.Fail: - current = catcherFail; + currentCatcher = catcherFail; break; case CatcherAnimationState.Kiai: - current = catcherKiai; + currentCatcher = catcherKiai; break; } - current.Show(); - (current.Drawable as IAnimation)?.GotoFrame(0); + currentCatcher.Show(); + (currentCatcher.Drawable as IAnimation)?.GotoFrame(0); } private void beginTrail() { - Trail &= dashing || HyperDashing; - Trail &= AdditiveTarget != null; + if (!dashing && !HyperDashing) + { + Trail = false; + return; + } - if (!Trail) return; - - var additive = createAdditiveSprite(HyperDashing); + var additive = createAdditiveSprite(); additive.FadeTo(0.4f).FadeOut(800, Easing.OutQuint); additive.Expire(true); @@ -428,6 +428,25 @@ namespace osu.Game.Rulesets.Catch.UI updateCatcher(); } + private CatcherTrailSprite createAdditiveSprite() + { + var tex = (currentCatcher.Drawable as TextureAnimation)?.CurrentFrame ?? ((Sprite)currentCatcher.Drawable).Texture; + + var sprite = new CatcherTrailSprite(tex) + { + Anchor = Anchor, + Scale = Scale, + Colour = HyperDashing ? Color4.Red : Color4.White, + Blending = BlendingParameters.Additive, + RelativePositionAxes = RelativePositionAxes, + Position = Position + }; + + AdditiveTarget?.Add(sprite); + + return sprite; + } + private void removeFromPlateWithTransform(DrawableHitObject fruit, Action action) { if (ExplodingFruitTarget != null) From 74c9d5fc93f85780df7da15124e92b386e4ea517 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 14 Mar 2020 13:45:55 +0300 Subject: [PATCH 0395/2376] Use AccentColour --- .../Objects/Drawables/DrawableSlider.cs | 2 +- .../Objects/Drawables/Pieces/SliderBall.cs | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 7403649184..ccc731779d 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -185,7 +185,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables base.ApplySkin(skin, allowFallback); bool allowBallTint = skin.GetConfig(OsuSkinConfiguration.AllowSliderBallTint)?.Value ?? false; - Ball.Colour = allowBallTint ? AccentColour.Value : Color4.White; + Ball.AccentColour = allowBallTint ? AccentColour.Value : Color4.White; } protected override void CheckForResult(bool userTriggered, double timeOffset) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs index 287a2d7e92..0c046604c0 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs @@ -16,17 +16,24 @@ using osu.Game.Rulesets.Osu.Skinning; using osuTK.Graphics; using osu.Game.Skinning; using osuTK; +using osu.Game.Graphics; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { - public class SliderBall : CircularContainer, ISliderProgress, IRequireHighFrequencyMousePosition + public class SliderBall : CircularContainer, ISliderProgress, IRequireHighFrequencyMousePosition, IHasAccentColour { public Func GetInitialHitAction; - public new Color4 Colour + private Color4 accentColour; + + public Color4 AccentColour { - get => ball.Colour; - set => ball.Colour = value; + get => accentColour; + set + { + accentColour = value; + ball.Colour = value; + } } private readonly Slider slider; From c271d1755715822bd0dd5d47040a91da74ab23f6 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 14 Mar 2020 14:07:52 +0300 Subject: [PATCH 0396/2376] Remove useless field --- .../Objects/Drawables/Pieces/SliderBall.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs index 0c046604c0..5a6dd49c44 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs @@ -24,16 +24,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { public Func GetInitialHitAction; - private Color4 accentColour; - public Color4 AccentColour { - get => accentColour; - set - { - accentColour = value; - ball.Colour = value; - } + get => ball.Colour; + set => ball.Colour = value; } private readonly Slider slider; From 62ce5031269446adbc8c107fa16f832678a5d441 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 15 Mar 2020 00:36:21 +0900 Subject: [PATCH 0397/2376] Fix changelog alignment and italics usage --- osu.Game/Overlays/Changelog/ChangelogBuild.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Changelog/ChangelogBuild.cs b/osu.Game/Overlays/Changelog/ChangelogBuild.cs index 8aee76cb08..48bf6c2ddd 100644 --- a/osu.Game/Overlays/Changelog/ChangelogBuild.cs +++ b/osu.Game/Overlays/Changelog/ChangelogBuild.cs @@ -93,6 +93,7 @@ namespace osu.Game.Overlays.Changelog Direction = FillDirection.Full, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, + TextAnchor = Anchor.BottomLeft, } } }; @@ -125,7 +126,7 @@ namespace osu.Game.Overlays.Changelog title.AddText("by ", t => { - t.Font = fontMedium.With(italics: true); + t.Font = fontMedium; t.Colour = entryColour; t.Padding = new MarginPadding { Left = 10 }; }); @@ -138,7 +139,7 @@ namespace osu.Game.Overlays.Changelog Id = entry.GithubUser.UserId.Value }, t => { - t.Font = fontMedium.With(italics: true); + t.Font = fontMedium; t.Colour = entryColour; }); } @@ -146,7 +147,7 @@ namespace osu.Game.Overlays.Changelog { title.AddLink(entry.GithubUser.DisplayName, entry.GithubUser.GithubUrl, t => { - t.Font = fontMedium.With(italics: true); + t.Font = fontMedium; t.Colour = entryColour; }); } @@ -154,7 +155,7 @@ namespace osu.Game.Overlays.Changelog { title.AddText(entry.GithubUser.DisplayName, t => { - t.Font = fontMedium.With(italics: true); + t.Font = fontMedium; t.Colour = entryColour; }); } From cd604785a87e14dfec1bf2317762f44325d1c196 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 15 Mar 2020 00:38:27 +0900 Subject: [PATCH 0398/2376] Ignore italics specification for now --- osu.Game/Graphics/OsuFont.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/OsuFont.cs b/osu.Game/Graphics/OsuFont.cs index 076cc241ed..7c78141b4d 100644 --- a/osu.Game/Graphics/OsuFont.cs +++ b/osu.Game/Graphics/OsuFont.cs @@ -31,7 +31,14 @@ namespace osu.Game.Graphics /// Whether all characters should be spaced the same distance apart. /// The . public static FontUsage GetFont(Typeface typeface = Typeface.Torus, float size = DEFAULT_FONT_SIZE, FontWeight weight = FontWeight.Medium, bool italics = false, bool fixedWidth = false) - => new FontUsage(GetFamilyString(typeface), size, GetWeightString(typeface, weight), italics, fixedWidth); + => new FontUsage(GetFamilyString(typeface), size, GetWeightString(typeface, weight), getItalics(italics), fixedWidth); + + private static bool getItalics(in bool italicsRequested) + { + // right now none of our fonts support italics. + // should add exceptions to this rule if they come up. + return false; + } /// /// Retrieves the string representation of a . From 0b788065b46a0a579e849bfe893ba0b3b57d301a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 15 Mar 2020 01:00:25 +0900 Subject: [PATCH 0399/2376] Update resources package --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index f623a92ade..1a63b893a1 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 ba6f0e2251..95d09e84eb 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -22,7 +22,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 54cd400d51..ce7ff38988 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + From 1a6056637b9918f66ad0dc5282d660665c17ebdc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 15 Mar 2020 01:43:29 +0900 Subject: [PATCH 0400/2376] Turn off italics test for now (may come back if we switch chat to content font) --- osu.Game.Tests/Visual/Online/TestSceneChatLink.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs index c76d4fd5b8..7a257a1603 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs @@ -12,7 +12,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Sprites; using osu.Game.Online.Chat; using osu.Game.Overlays; using osu.Game.Overlays.Chat; @@ -78,7 +77,7 @@ namespace osu.Game.Tests.Visual.Online AddAssert($"msg #{index} has {linkAmount} link(s)", () => newLine.Message.Links.Count == linkAmount); AddAssert($"msg #{index} has the right action", hasExpectedActions); - AddAssert($"msg #{index} is " + (isAction ? "italic" : "not italic"), () => newLine.ContentFlow.Any() && isAction == isItalic()); + //AddAssert($"msg #{index} is " + (isAction ? "italic" : "not italic"), () => newLine.ContentFlow.Any() && isAction == isItalic()); AddAssert($"msg #{index} shows {linkAmount} link(s)", isShowingLinks); bool hasExpectedActions() @@ -97,7 +96,7 @@ namespace osu.Game.Tests.Visual.Online return true; } - bool isItalic() => newLine.ContentFlow.Where(d => d is OsuSpriteText).Cast().All(sprite => sprite.Font.Italics); + //bool isItalic() => newLine.ContentFlow.Where(d => d is OsuSpriteText).Cast().All(sprite => sprite.Font.Italics); bool isShowingLinks() { From 12b7727af6022d1d4f88397f542a12497899b7be Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 15 Mar 2020 03:25:01 +0900 Subject: [PATCH 0401/2376] 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 1a63b893a1..66a1523843 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 95d09e84eb..647f05b428 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -23,7 +23,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index ce7ff38988..0e5c64cf0f 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + @@ -79,7 +79,7 @@ - + From 340d362d69230a79a005cf86cc4bee0295709e00 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 15 Mar 2020 03:51:30 +0900 Subject: [PATCH 0402/2376] Appease inspectcode --- osu.Game/Screens/Select/BeatmapCarousel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 20f9bb1be8..e2e7ba8031 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -779,7 +779,7 @@ namespace osu.Game.Screens.Select /// public bool UserScrolling { get; private set; } - protected override void OnUserScroll(float value, bool animated = true, double? distanceDecay = null) + protected override void OnUserScroll(float value, bool animated = true, double? distanceDecay = default) { UserScrolling = true; base.OnUserScroll(value, animated, distanceDecay); From f90485994367f5b6b1b57da6cb452536f92cc04b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 15 Mar 2020 15:45:13 +0100 Subject: [PATCH 0403/2376] Remove leftover unused private methods --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 29bed00d61..e361b29a9d 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -398,27 +398,6 @@ namespace osu.Game.Rulesets.Catch.UI Scheduler.AddDelayed(beginTrail, HyperDashing ? 25 : 50); } - private Drawable createAdditiveSprite(bool hyperDash) - { - var additive = createCatcherSprite(); - - additive.Anchor = Anchor; - additive.Scale = Scale; - additive.Colour = hyperDash ? Color4.Red : Color4.White; - additive.Blending = BlendingParameters.Additive; - additive.RelativePositionAxes = RelativePositionAxes; - additive.Position = Position; - - AdditiveTarget.Add(additive); - - return additive; - } - - private Drawable createCatcherSprite() - { - return new CatcherSprite(CurrentState); - } - private void updateState(CatcherAnimationState state) { if (CurrentState == state) From e68d4f92f5f5694d306a8b11d35eb7545685de4a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Mar 2020 01:19:10 +0900 Subject: [PATCH 0404/2376] Fix framework version mismatch --- osu.iOS.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.iOS.props b/osu.iOS.props index 1387026799..0e5c64cf0f 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -79,7 +79,7 @@ - + From acd280c85552ec22073606807e75eeda663ebe11 Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Sun, 15 Mar 2020 22:13:26 +0100 Subject: [PATCH 0405/2376] Add System channel type and use it for the ChannelSelectorTab --- osu.Game/Online/Chat/ChannelType.cs | 1 + osu.Game/Overlays/Chat/Tabs/ChannelSelectorTabItem.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Chat/ChannelType.cs b/osu.Game/Online/Chat/ChannelType.cs index 7d2b661164..151efc4645 100644 --- a/osu.Game/Online/Chat/ChannelType.cs +++ b/osu.Game/Online/Chat/ChannelType.cs @@ -12,5 +12,6 @@ namespace osu.Game.Online.Chat Temporary, PM, Group, + System, } } diff --git a/osu.Game/Overlays/Chat/Tabs/ChannelSelectorTabItem.cs b/osu.Game/Overlays/Chat/Tabs/ChannelSelectorTabItem.cs index 5fb56a3f75..e3ede04edd 100644 --- a/osu.Game/Overlays/Chat/Tabs/ChannelSelectorTabItem.cs +++ b/osu.Game/Overlays/Chat/Tabs/ChannelSelectorTabItem.cs @@ -39,7 +39,7 @@ namespace osu.Game.Overlays.Chat.Tabs public ChannelSelectorTabChannel() { Name = "+"; - Type = ChannelType.Temporary; + Type = ChannelType.System; } } } From f390c1995db413005cea88a68d832e92382768e2 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 16 Mar 2020 11:29:28 +0900 Subject: [PATCH 0406/2376] Apply comment suggestions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Dean Herbert Co-Authored-By: Bartłomiej Dach --- osu.Game/Beatmaps/IWorkingBeatmap.cs | 2 +- osu.Game/Screens/Edit/Editor.cs | 4 ++-- osu.Game/Screens/Select/BeatmapInfoWedge.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Beatmaps/IWorkingBeatmap.cs b/osu.Game/Beatmaps/IWorkingBeatmap.cs index 61bd962648..526bc668af 100644 --- a/osu.Game/Beatmaps/IWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/IWorkingBeatmap.cs @@ -61,7 +61,7 @@ namespace osu.Game.Beatmaps /// /// The to create a playable for. /// The s to apply to the . - /// The loading timeout. + /// The maximum length in milliseconds to wait for load to complete. Defaults to 10,000ms. /// The converted . /// If could not be converted to . IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList mods = null, TimeSpan? timeout = null); diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index cf13f8a3a1..f1cbed57f1 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -93,8 +93,8 @@ namespace osu.Game.Screens.Edit } catch (Exception e) { - Logger.Error(e, "Could not load beatmap sucessfully!"); - //couldn't load, hard abort! + Logger.Error(e, "Could not load beatmap successfully!"); + // couldn't load, hard abort! this.Exit(); return; } diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index 59d2aca17d..7a8a1593b9 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -331,7 +331,7 @@ namespace osu.Game.Screens.Select } catch (Exception e) { - Logger.Error(e, "Could not load beatmap sucessfully!"); + Logger.Error(e, "Could not load beatmap successfully!"); } } From 9c5423734a30587063c9a1375eeccb6243cc0d78 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 16 Mar 2020 11:33:26 +0900 Subject: [PATCH 0407/2376] Throw timeout exceptions instead --- osu.Game/Beatmaps/WorkingBeatmap.cs | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index bdcfc058b4..dd4f893ac2 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -100,7 +100,9 @@ namespace osu.Game.Beatmaps // Apply conversion mods foreach (var mod in mods.OfType()) { - cancellationSource.Token.ThrowIfCancellationRequested(); + if (cancellationSource.IsCancellationRequested) + throw new BeatmapLoadTimeoutException(BeatmapInfo); + mod.ApplyToBeatmapConverter(converter); } @@ -115,7 +117,9 @@ namespace osu.Game.Beatmaps foreach (var mod in mods.OfType()) { - cancellationSource.Token.ThrowIfCancellationRequested(); + if (cancellationSource.IsCancellationRequested) + throw new BeatmapLoadTimeoutException(BeatmapInfo); + mod.ApplyToDifficulty(converted.BeatmapInfo.BaseDifficulty); } } @@ -127,7 +131,9 @@ namespace osu.Game.Beatmaps // Compute default values for hitobjects, including creating nested hitobjects in-case they're needed foreach (var obj in converted.HitObjects) { - cancellationSource.Token.ThrowIfCancellationRequested(); + if (cancellationSource.IsCancellationRequested) + throw new BeatmapLoadTimeoutException(BeatmapInfo); + obj.ApplyDefaults(converted.ControlPointInfo, converted.BeatmapInfo.BaseDifficulty); } @@ -135,7 +141,9 @@ namespace osu.Game.Beatmaps { foreach (var obj in converted.HitObjects) { - cancellationSource.Token.ThrowIfCancellationRequested(); + if (cancellationSource.IsCancellationRequested) + throw new BeatmapLoadTimeoutException(BeatmapInfo); + mod.ApplyToHitObject(obj); } } @@ -315,5 +323,13 @@ namespace osu.Game.Beatmaps private void recreate() => lazy = new Lazy(valueFactory, LazyThreadSafetyMode.ExecutionAndPublication); } + + private class BeatmapLoadTimeoutException : TimeoutException + { + public BeatmapLoadTimeoutException(BeatmapInfo beatmapInfo) + : base($"Timed out while loading beatmap ({beatmapInfo}).") + { + } + } } } From 58fc947be3417ce9edbeadbd00b885bcb569dba7 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 16 Mar 2020 14:06:42 +0900 Subject: [PATCH 0408/2376] Privatise some setters --- osu.Game/Users/UserPanel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index 5676113aad..289244cdc3 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -32,9 +32,9 @@ namespace osu.Game.Users public new Action Action; - protected Action ViewProfile; + protected Action ViewProfile { get; private set; } - protected DelayedLoadUnloadWrapper Background; + protected DelayedLoadUnloadWrapper Background { get; private set; } private SpriteIcon statusIcon; private OsuSpriteText statusMessage; From 544dfe7dd39266a4e0c4906413fa3b26db161d10 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 16 Mar 2020 09:42:21 +0300 Subject: [PATCH 0409/2376] Implement FriendsLayout component --- .../Visual/Online/TestSceneFriendsLayout.cs | 80 ++++++ .../TestSceneFriendsOnlineStatusControl.cs | 12 +- .../UserInterface/TestSceneUserListToolbar.cs | 2 +- .../Online/API/Requests/GetFriendsRequest.cs | 4 +- .../API/Requests/Responses/APIFriend.cs | 14 + .../Friends/FriendsBundle.cs | 13 +- .../Dashboard/Friends/FriendsLayout.cs | 256 ++++++++++++++++++ .../Friends/FriendsOnlineStatusControl.cs | 15 +- .../Friends/FriendsOnlineStatusItem.cs | 2 +- .../Friends/UserListToolbar.cs | 2 +- .../Friends/UserSortTabControl.cs | 2 +- osu.Game/Overlays/SocialOverlay.cs | 2 +- 12 files changed, 379 insertions(+), 25 deletions(-) create mode 100644 osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs create mode 100644 osu.Game/Online/API/Requests/Responses/APIFriend.cs rename osu.Game/Overlays/{Home => Dashboard}/Friends/FriendsBundle.cs (55%) create mode 100644 osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs rename osu.Game/Overlays/{Home => Dashboard}/Friends/FriendsOnlineStatusControl.cs (70%) rename osu.Game/Overlays/{Home => Dashboard}/Friends/FriendsOnlineStatusItem.cs (96%) rename osu.Game/Overlays/{Home => Dashboard}/Friends/UserListToolbar.cs (96%) rename osu.Game/Overlays/{Home => Dashboard}/Friends/UserSortTabControl.cs (90%) diff --git a/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs b/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs new file mode 100644 index 0000000000..0e9fafb1b6 --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs @@ -0,0 +1,80 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using osu.Framework.Graphics.Containers; +using osu.Game.Overlays.Dashboard.Friends; +using osu.Framework.Graphics; +using osu.Game.Users; +using osu.Game.Overlays; +using osu.Framework.Allocation; +using NUnit.Framework; +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Tests.Visual.Online +{ + public class TestSceneFriendsLayout : OsuTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(FriendsLayout), + typeof(FriendsOnlineStatusControl), + typeof(UserListToolbar) + }; + + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); + + private FriendsLayout layout; + + [SetUp] + public void Setup() => Schedule(() => + { + Child = new BasicScrollContainer + { + RelativeSizeAxes = Axes.Both, + Child = layout = new FriendsLayout() + }; + }); + + [Test] + public void TestPopulate() + { + AddStep("Populate", () => layout.Users = getUsers()); + } + + private List getUsers() => new List + { + new APIFriend + { + Username = @"flyte", + Id = 3103765, + IsOnline = true, + CurrentModeRank = 1111, + Country = new Country { FlagName = @"JP" }, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg" + }, + new APIFriend + { + Username = @"peppy", + Id = 2, + IsOnline = false, + CurrentModeRank = 2222, + Country = new Country { FlagName = @"AU" }, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + IsSupporter = true, + SupportLevel = 3, + }, + new APIFriend + { + Username = @"Evast", + Id = 8195163, + Country = new Country { FlagName = @"BY" }, + CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", + IsOnline = false, + LastVisit = DateTimeOffset.Now + } + }; + } +} diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs index 0d841dfef1..8bdf3c5dc1 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs @@ -7,9 +7,9 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; -using osu.Game.Overlays.Home.Friends; -using osu.Game.Users; +using osu.Game.Overlays.Dashboard.Friends; namespace osu.Game.Tests.Visual.UserInterface { @@ -39,17 +39,17 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void Populate() { - AddStep("Populate", () => control.Populate(new List + AddStep("Populate", () => control.Populate(new List { - new User + new APIFriend { IsOnline = true }, - new User + new APIFriend { IsOnline = false }, - new User + new APIFriend { IsOnline = false } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneUserListToolbar.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneUserListToolbar.cs index 02b8839922..1546972580 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneUserListToolbar.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneUserListToolbar.cs @@ -8,7 +8,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; -using osu.Game.Overlays.Home.Friends; +using osu.Game.Overlays.Dashboard.Friends; using osuTK; namespace osu.Game.Tests.Visual.UserInterface diff --git a/osu.Game/Online/API/Requests/GetFriendsRequest.cs b/osu.Game/Online/API/Requests/GetFriendsRequest.cs index 46890aa889..321f675aae 100644 --- a/osu.Game/Online/API/Requests/GetFriendsRequest.cs +++ b/osu.Game/Online/API/Requests/GetFriendsRequest.cs @@ -2,11 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using osu.Game.Users; +using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Online.API.Requests { - public class GetFriendsRequest : APIRequest> + public class GetFriendsRequest : APIRequest> { protected override string Target => @"friends"; } diff --git a/osu.Game/Online/API/Requests/Responses/APIFriend.cs b/osu.Game/Online/API/Requests/Responses/APIFriend.cs new file mode 100644 index 0000000000..91fed28d44 --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/APIFriend.cs @@ -0,0 +1,14 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Newtonsoft.Json; +using osu.Game.Users; + +namespace osu.Game.Online.API.Requests.Responses +{ + public class APIFriend : User + { + [JsonProperty(@"current_mode_rank")] + public int? CurrentModeRank; + } +} diff --git a/osu.Game/Overlays/Home/Friends/FriendsBundle.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsBundle.cs similarity index 55% rename from osu.Game/Overlays/Home/Friends/FriendsBundle.cs rename to osu.Game/Overlays/Dashboard/Friends/FriendsBundle.cs index 75d00dfef8..0062c49c91 100644 --- a/osu.Game/Overlays/Home/Friends/FriendsBundle.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsBundle.cs @@ -1,18 +1,23 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -namespace osu.Game.Overlays.Home.Friends +using System.Collections.Generic; +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Overlays.Dashboard.Friends { public class FriendsBundle { public FriendsOnlineStatus Status { get; } - public int Count { get; } + public int Count => Users.Count; - public FriendsBundle(FriendsOnlineStatus status, int count) + public List Users { get; } + + public FriendsBundle(FriendsOnlineStatus status, List users) { Status = status; - Count = count; + Users = users; } } diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs new file mode 100644 index 0000000000..55f394cb78 --- /dev/null +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs @@ -0,0 +1,256 @@ +// 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 System.Threading; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Users; +using osuTK; + +namespace osu.Game.Overlays.Dashboard.Friends +{ + public class FriendsLayout : CompositeDrawable + { + private List users = new List(); + + public List Users + { + get => users; + set + { + users = value; + + usersLoaded = true; + + onlineStatusControl.Populate(value); + } + } + + [Resolved] + private IAPIProvider api { get; set; } + + private GetFriendsRequest request; + private CancellationTokenSource cancellationToken; + + private Drawable currentContent; + + private readonly Box background; + private readonly Box controlBackground; + private readonly FriendsOnlineStatusControl onlineStatusControl; + private readonly UserListToolbar userListToolbar; + private readonly Container itemsPlaceholder; + private readonly LoadingLayer loading; + + public FriendsLayout() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + InternalChild = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + controlBackground = new Box + { + RelativeSizeAxes = Axes.Both + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding + { + Top = 20, + Horizontal = 45 + }, + Child = onlineStatusControl = new FriendsOnlineStatusControl(), + } + } + }, + new Container + { + Name = "User List", + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Margin = new MarginPadding { Bottom = 20 }, + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding + { + Horizontal = 40, + Vertical = 20 + }, + Child = userListToolbar = new UserListToolbar + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + } + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + itemsPlaceholder = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Horizontal = 50 } + }, + loading = new LoadingLayer(itemsPlaceholder) + } + } + } + } + } + } + } + }; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + background.Colour = colourProvider.Background4; + controlBackground.Colour = colourProvider.Background5; + } + + private bool usersLoaded; + + protected override void LoadComplete() + { + base.LoadComplete(); + + onlineStatusControl.Current.BindValueChanged(_ => recreatePanels()); + userListToolbar.DisplayStyle.BindValueChanged(_ => recreatePanels()); + userListToolbar.SortCriteria.BindValueChanged(_ => recreatePanels()); + + if (!api.IsLoggedIn) + return; + + request = new GetFriendsRequest(); + request.Success += response => Schedule(() => Users = response); + api.Queue(request); + } + + private void recreatePanels() + { + // Don't allow any changes until we have users loaded + if (!usersLoaded) + return; + + cancellationToken?.Cancel(); + + if (itemsPlaceholder.Any()) + loading.Show(); + + var groupedUsers = onlineStatusControl.Current.Value?.Users ?? new List(); + + var sortedUsers = sortUsers(groupedUsers); + + LoadComponentAsync(createTable(sortedUsers), addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token); + } + + private void addContentToPlaceholder(Drawable content) + { + loading.Hide(); + + var lastContent = currentContent; + + if (lastContent != null) + { + lastContent.FadeOut(100, Easing.OutQuint).Expire(); + lastContent.Delay(25).Schedule(() => lastContent.BypassAutoSizeAxes = Axes.Y); + } + + itemsPlaceholder.Add(currentContent = content); + currentContent.FadeIn(200, Easing.OutQuint); + } + + private FillFlowContainer createTable(List users) + { + var style = userListToolbar.DisplayStyle.Value; + + return new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(style == OverlayPanelDisplayStyle.Card ? 10 : 2), + Children = users.Select(u => createUserPanel(u, style)).ToList() + }; + } + + private UserPanel createUserPanel(User user, OverlayPanelDisplayStyle style) + { + switch (style) + { + default: + case OverlayPanelDisplayStyle.Card: + return new UserGridPanel(user).With(panel => + { + panel.Anchor = Anchor.TopCentre; + panel.Origin = Anchor.TopCentre; + panel.Width = 290; + }); + + case OverlayPanelDisplayStyle.List: + return new UserListPanel(user); + } + } + + private List sortUsers(List unsorted) + { + switch (userListToolbar.SortCriteria.Value) + { + default: + case UserSortCriteria.LastVisit: + return unsorted.OrderBy(u => u.LastVisit).Reverse().ToList(); + + case UserSortCriteria.Rank: + return unsorted.Where(u => u.CurrentModeRank.HasValue).OrderBy(u => u.CurrentModeRank).Concat(unsorted.Where(u => u.CurrentModeRank == null)).ToList(); + + case UserSortCriteria.Username: + return unsorted.OrderBy(u => u.Username).ToList(); + } + } + + protected override void Dispose(bool isDisposing) + { + request?.Cancel(); + cancellationToken?.Cancel(); + + base.Dispose(isDisposing); + } + } +} diff --git a/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusControl.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusControl.cs similarity index 70% rename from osu.Game/Overlays/Home/Friends/FriendsOnlineStatusControl.cs rename to osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusControl.cs index 196f01ab4a..2b716f228d 100644 --- a/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusControl.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusControl.cs @@ -3,22 +3,21 @@ using System.Collections.Generic; using System.Linq; -using osu.Game.Users; +using osu.Game.Online.API.Requests.Responses; -namespace osu.Game.Overlays.Home.Friends +namespace osu.Game.Overlays.Dashboard.Friends { public class FriendsOnlineStatusControl : OverlayStreamControl { protected override OverlayStreamItem CreateStreamItem(FriendsBundle value) => new FriendsOnlineStatusItem(value); - public void Populate(List users) + public void Populate(List users) { - var userCount = users.Count; - var onlineUsersCount = users.Count(user => user.IsOnline); + Clear(); - AddItem(new FriendsBundle(FriendsOnlineStatus.All, userCount)); - AddItem(new FriendsBundle(FriendsOnlineStatus.Online, onlineUsersCount)); - AddItem(new FriendsBundle(FriendsOnlineStatus.Offline, userCount - onlineUsersCount)); + AddItem(new FriendsBundle(FriendsOnlineStatus.All, users)); + AddItem(new FriendsBundle(FriendsOnlineStatus.Online, users.Where(u => u.IsOnline).ToList())); + AddItem(new FriendsBundle(FriendsOnlineStatus.Offline, users.Where(u => !u.IsOnline).ToList())); Current.Value = Items.FirstOrDefault(); } diff --git a/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusItem.cs similarity index 96% rename from osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs rename to osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusItem.cs index d9b780ce46..eada9420ea 100644 --- a/osu.Game/Overlays/Home/Friends/FriendsOnlineStatusItem.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusItem.cs @@ -5,7 +5,7 @@ using System; using osu.Game.Graphics; using osuTK.Graphics; -namespace osu.Game.Overlays.Home.Friends +namespace osu.Game.Overlays.Dashboard.Friends { public class FriendsOnlineStatusItem : OverlayStreamItem { diff --git a/osu.Game/Overlays/Home/Friends/UserListToolbar.cs b/osu.Game/Overlays/Dashboard/Friends/UserListToolbar.cs similarity index 96% rename from osu.Game/Overlays/Home/Friends/UserListToolbar.cs rename to osu.Game/Overlays/Dashboard/Friends/UserListToolbar.cs index f7c5e9f4fd..fb4b938183 100644 --- a/osu.Game/Overlays/Home/Friends/UserListToolbar.cs +++ b/osu.Game/Overlays/Dashboard/Friends/UserListToolbar.cs @@ -6,7 +6,7 @@ using osu.Framework.Graphics.Containers; using osuTK; using osu.Framework.Bindables; -namespace osu.Game.Overlays.Home.Friends +namespace osu.Game.Overlays.Dashboard.Friends { public class UserListToolbar : CompositeDrawable { diff --git a/osu.Game/Overlays/Home/Friends/UserSortTabControl.cs b/osu.Game/Overlays/Dashboard/Friends/UserSortTabControl.cs similarity index 90% rename from osu.Game/Overlays/Home/Friends/UserSortTabControl.cs rename to osu.Game/Overlays/Dashboard/Friends/UserSortTabControl.cs index 2479fa4638..3a5f65212d 100644 --- a/osu.Game/Overlays/Home/Friends/UserSortTabControl.cs +++ b/osu.Game/Overlays/Dashboard/Friends/UserSortTabControl.cs @@ -3,7 +3,7 @@ using System.ComponentModel; -namespace osu.Game.Overlays.Home.Friends +namespace osu.Game.Overlays.Dashboard.Friends { public class UserSortTabControl : OverlaySortTabControl { diff --git a/osu.Game/Overlays/SocialOverlay.cs b/osu.Game/Overlays/SocialOverlay.cs index 02f7c9b0d3..ba572b0e78 100644 --- a/osu.Game/Overlays/SocialOverlay.cs +++ b/osu.Game/Overlays/SocialOverlay.cs @@ -120,7 +120,7 @@ namespace osu.Game.Overlays { case SocialTab.Friends: var friendRequest = new GetFriendsRequest(); // TODO filter arguments? - friendRequest.Success += users => Users = users.ToArray(); + friendRequest.Success += users => Users = users.Select(u => (User)u).ToArray(); API.Queue(getUsersRequest = friendRequest); break; From 24fe7538fd0e7eb7d9592760978e716636751069 Mon Sep 17 00:00:00 2001 From: "Marcus \"Mestro\" Nordgren" Date: Mon, 16 Mar 2020 13:09:15 +0100 Subject: [PATCH 0410/2376] Use new logo name for showcase screen --- osu.Game.Tournament/Screens/Showcase/TournamentLogo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/Showcase/TournamentLogo.cs b/osu.Game.Tournament/Screens/Showcase/TournamentLogo.cs index 6ad5ccaf0c..bd5aa2f5d9 100644 --- a/osu.Game.Tournament/Screens/Showcase/TournamentLogo.cs +++ b/osu.Game.Tournament/Screens/Showcase/TournamentLogo.cs @@ -28,7 +28,7 @@ namespace osu.Game.Tournament.Screens.Showcase Origin = Anchor.TopCentre, FillMode = FillMode.Fit, RelativeSizeAxes = Axes.Both, - Texture = textures.Get("game-screen-logo"), + Texture = textures.Get("header-logo"), }; } } From 50c2e65e3c2b5da4378171d3c920e1648521e4f0 Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Mon, 16 Mar 2020 19:10:42 +0100 Subject: [PATCH 0411/2376] Improve TestSceneChatOverlay --- .../Visual/Online/TestSceneChatOverlay.cs | 86 ++++++++++++------- 1 file changed, 55 insertions(+), 31 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index 736bfd8e7d..03251f1d5e 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -47,14 +47,20 @@ namespace osu.Game.Tests.Visual.Online private Channel previousChannel => joinedChannels.ElementAt(joinedChannels.ToList().IndexOf(currentChannel) - 1); private Channel channel1 => channels[0]; private Channel channel2 => channels[1]; + private Channel channelPM => channels.Last(); public TestSceneChatOverlay() { channels = Enumerable.Range(1, 10) - .Select(index => new Channel(new User()) + .Select(index => new Channel { Name = $"Channel no. {index}", - Topic = index == 3 ? null : $"We talk about the number {index} here" + Topic = index == 3 ? null : $"We talk about the number {index} here", + Type = ChannelType.Temporary + }) + .Append(new Channel(new User()) + { + Name = "PM channel" }) .ToList(); } @@ -100,28 +106,11 @@ namespace osu.Game.Tests.Visual.Online AddAssert("Channel selector was closed", () => chatOverlay.SelectionOverlayState == Visibility.Hidden); } - [Test] - public void TestCloseChannelWhileSelectorClosed() - { - AddStep("Join channel 1", () => channelManager.JoinChannel(channel1)); - AddStep("Join channel 2", () => channelManager.JoinChannel(channel2)); - - AddStep("Switch to channel 2", () => clickDrawable(chatOverlay.TabMap[channel2])); - AddStep("Close channel 2", () => clickDrawable(((TestPrivateChannelTabItem)chatOverlay.TabMap[channel2]).CloseButton.Child)); - - AddAssert("Selector remained closed", () => chatOverlay.SelectionOverlayState == Visibility.Hidden); - AddAssert("Current channel is channel 1", () => currentChannel == channel1); - - AddStep("Close channel 1", () => clickDrawable(((TestPrivateChannelTabItem)chatOverlay.TabMap[channel1]).CloseButton.Child)); - - AddAssert("Selector is visible", () => chatOverlay.SelectionOverlayState == Visibility.Visible); - } - [Test] public void TestSearchInSelector() { - AddStep("search for 'no. 2'", () => chatOverlay.ChildrenOfType().First().Text = "no. 2"); - AddUntilStep("only channel 2 visible", () => + AddStep("Search for 'no. 2'", () => chatOverlay.ChildrenOfType().First().Text = "no. 2"); + AddUntilStep("Only channel 2 visible", () => { var listItems = chatOverlay.ChildrenOfType().Where(c => c.IsPresent); return listItems.Count() == 1 && listItems.Single().Channel == channel2; @@ -131,28 +120,28 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestChannelShortcutKeys() { - AddStep("join 10 channels", () => channels.ForEach(channel => channelManager.JoinChannel(channel))); - AddStep("close channel selector", () => + AddStep("Join channels", () => channels.ForEach(channel => channelManager.JoinChannel(channel))); + AddStep("Close channel selector", () => { InputManager.PressKey(Key.Escape); InputManager.ReleaseKey(Key.Escape); }); - AddUntilStep("wait for close", () => chatOverlay.SelectionOverlayState == Visibility.Hidden); + AddUntilStep("Wait for close", () => chatOverlay.SelectionOverlayState == Visibility.Hidden); for (int zeroBasedIndex = 0; zeroBasedIndex < 10; ++zeroBasedIndex) { var oneBasedIndex = zeroBasedIndex + 1; var targetNumberKey = oneBasedIndex % 10; var targetChannel = channels[zeroBasedIndex]; - AddStep($"press Alt+{targetNumberKey}", () => pressChannelHotkey(targetNumberKey)); - AddAssert($"channel #{oneBasedIndex} is selected", () => currentChannel == targetChannel); + AddStep($"Press Alt+{targetNumberKey}", () => pressChannelHotkey(targetNumberKey)); + AddAssert($"Channel #{oneBasedIndex} is selected", () => currentChannel == targetChannel); } } private Channel expectedChannel; [Test] - public void TestCloseChannelWhileActive() + public void TestCloseChannelBehaviour() { AddUntilStep("Join until dropdown has channels", () => { @@ -160,8 +149,11 @@ namespace osu.Game.Tests.Visual.Online return true; // Using temporary channels because they don't hide their names when not active - Channel toAdd = new Channel { Name = $"test channel {joinedChannels.Count()}", Type = ChannelType.Temporary }; - channelManager.JoinChannel(toAdd); + channelManager.JoinChannel(new Channel + { + Name = $"Channel no. {joinedChannels.Count() + 1}", + Type = ChannelType.Temporary + }); return false; }); @@ -176,6 +168,7 @@ namespace osu.Game.Tests.Visual.Online chatOverlay.ChannelTabControl.RemoveChannel(currentChannel); }); AddAssert("Next channel selected", () => currentChannel == expectedChannel); + AddAssert("Selector remained closed", () => chatOverlay.SelectionOverlayState == Visibility.Hidden); // Depending on the window size, one more channel might need to be closed for the selectorTab to appear AddUntilStep("Close channels until selector visible", () => @@ -194,7 +187,7 @@ namespace osu.Game.Tests.Visual.Online expectedChannel = previousChannel; chatOverlay.ChannelTabControl.RemoveChannel(currentChannel); }); - AddAssert("Channel changed to previous", () => currentChannel == expectedChannel); + AddAssert("Previous channel selected", () => currentChannel == expectedChannel); // Standard channel closing AddStep("Switch to previous channel", () => chatOverlay.ChannelTabControl.SwitchTab(-1)); @@ -203,7 +196,38 @@ namespace osu.Game.Tests.Visual.Online expectedChannel = nextChannel; chatOverlay.ChannelTabControl.RemoveChannel(currentChannel); }); - AddAssert("Channel changed to next", () => currentChannel == expectedChannel); + AddAssert("Next channel selected", () => currentChannel == expectedChannel); + + // Selector reappearing after all channels closed + AddUntilStep("Close all channels", () => + { + if (!joinedChannels.Any()) + return true; + + chatOverlay.ChannelTabControl.RemoveChannel(joinedChannels.Last()); + return false; + }); + AddAssert("Selector is visible", () => chatOverlay.SelectionOverlayState == Visibility.Visible); + } + + [Test] + public void TestChannelCloseButton() + { + AddStep("Join channels", () => + { + channelManager.JoinChannel(channel1); + channelManager.JoinChannel(channelPM); + }); + + // PM channel close button only appears when active + AddStep("Select PM channel", () => clickDrawable(chatOverlay.TabMap[channelPM])); + AddStep("Click PM close button", () => clickDrawable(((TestPrivateChannelTabItem)chatOverlay.TabMap[channelPM]).CloseButton.Child)); + AddAssert("PM channel closed", () => !channelManager.JoinedChannels.Contains(channelPM)); + + // Non-PM chat channel close button only appears when hovered + AddStep("Hover normal channel tab", () => InputManager.MoveMouseTo(chatOverlay.TabMap[channel1])); + AddStep("Click normal close button", () => clickDrawable(((TestChannelTabItem)chatOverlay.TabMap[channel1]).CloseButton.Child)); + AddAssert("All channels closed", () => !channelManager.JoinedChannels.Any()); } private void pressChannelHotkey(int number) From 0f40671e69ea9d2baaa426f0d841223c2bb65959 Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Mon, 16 Mar 2020 19:44:03 +0100 Subject: [PATCH 0412/2376] Mix normal channel tabs with PM ones --- .../Visual/Online/TestSceneChatOverlay.cs | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index 03251f1d5e..02460282d8 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -47,20 +47,15 @@ namespace osu.Game.Tests.Visual.Online private Channel previousChannel => joinedChannels.ElementAt(joinedChannels.ToList().IndexOf(currentChannel) - 1); private Channel channel1 => channels[0]; private Channel channel2 => channels[1]; - private Channel channelPM => channels.Last(); public TestSceneChatOverlay() { channels = Enumerable.Range(1, 10) - .Select(index => new Channel + .Select(index => new Channel(new User()) { Name = $"Channel no. {index}", Topic = index == 3 ? null : $"We talk about the number {index} here", - Type = ChannelType.Temporary - }) - .Append(new Channel(new User()) - { - Name = "PM channel" + Type = index % 2 == 0 ? ChannelType.PM : ChannelType.Temporary }) .ToList(); } @@ -150,9 +145,9 @@ namespace osu.Game.Tests.Visual.Online // Using temporary channels because they don't hide their names when not active channelManager.JoinChannel(new Channel - { + { Name = $"Channel no. {joinedChannels.Count() + 1}", - Type = ChannelType.Temporary + Type = ChannelType.Temporary }); return false; @@ -203,7 +198,7 @@ namespace osu.Game.Tests.Visual.Online { if (!joinedChannels.Any()) return true; - + chatOverlay.ChannelTabControl.RemoveChannel(joinedChannels.Last()); return false; }); @@ -213,16 +208,16 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestChannelCloseButton() { - AddStep("Join channels", () => + AddStep("Join 2 channels", () => { channelManager.JoinChannel(channel1); - channelManager.JoinChannel(channelPM); + channelManager.JoinChannel(channel2); }); // PM channel close button only appears when active - AddStep("Select PM channel", () => clickDrawable(chatOverlay.TabMap[channelPM])); - AddStep("Click PM close button", () => clickDrawable(((TestPrivateChannelTabItem)chatOverlay.TabMap[channelPM]).CloseButton.Child)); - AddAssert("PM channel closed", () => !channelManager.JoinedChannels.Contains(channelPM)); + AddStep("Select PM channel", () => clickDrawable(chatOverlay.TabMap[channel2])); + AddStep("Click PM close button", () => clickDrawable(((TestPrivateChannelTabItem)chatOverlay.TabMap[channel2]).CloseButton.Child)); + AddAssert("PM channel closed", () => !channelManager.JoinedChannels.Contains(channel2)); // Non-PM chat channel close button only appears when hovered AddStep("Hover normal channel tab", () => InputManager.MoveMouseTo(chatOverlay.TabMap[channel1])); From 1aacd1aaa2568eecb1f00ecc1b77cb0ae406082f Mon Sep 17 00:00:00 2001 From: Lucas A Date: Mon, 16 Mar 2020 20:43:02 +0100 Subject: [PATCH 0413/2376] Initial implementation of LowHealthLayer --- osu.Game/Configuration/OsuConfigManager.cs | 2 + .../Sections/Gameplay/GeneralSettings.cs | 6 +++ osu.Game/Screens/Play/HUD/LowHealthLayer.cs | 47 +++++++++++++++++++ 3 files changed, 55 insertions(+) create mode 100644 osu.Game/Screens/Play/HUD/LowHealthLayer.cs diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 21de654670..895bacafc4 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -88,6 +88,7 @@ namespace osu.Game.Configuration Set(OsuSetting.ShowInterface, true); Set(OsuSetting.ShowProgressGraph, true); Set(OsuSetting.ShowHealthDisplayWhenCantFail, true); + Set(OsuSetting.FadePlayfieldWhenLowHealth, true); Set(OsuSetting.KeyOverlay, false); Set(OsuSetting.ScoreMeter, ScoreMeterType.HitErrorBoth); @@ -183,6 +184,7 @@ namespace osu.Game.Configuration ShowInterface, ShowProgressGraph, ShowHealthDisplayWhenCantFail, + FadePlayfieldWhenLowHealth, MouseDisableButtons, MouseDisableWheel, AudioOffset, diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs index 2d2cd42213..6b6b3e8fa4 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs @@ -53,6 +53,12 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay Keywords = new[] { "hp", "bar" } }, new SettingsCheckbox + { + LabelText = "Fade playfield to red when health is low", + Bindable = config.GetBindable(OsuSetting.FadePlayfieldWhenLowHealth), + Keywords = new[] { "hp", "playfield", "health" } + }, + new SettingsCheckbox { LabelText = "Always show key overlay", Bindable = config.GetBindable(OsuSetting.KeyOverlay) diff --git a/osu.Game/Screens/Play/HUD/LowHealthLayer.cs b/osu.Game/Screens/Play/HUD/LowHealthLayer.cs new file mode 100644 index 0000000000..8f03a95877 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/LowHealthLayer.cs @@ -0,0 +1,47 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Game.Configuration; +using osu.Game.Graphics; + +namespace osu.Game.Screens.Play.HUD +{ + public class LowHealthLayer : HealthDisplay + { + private const float max_alpha = 0.4f; + + private const double fade_time = 300; + + private readonly Box box; + + private Bindable configFadeRedWhenLowHealth; + + public LowHealthLayer() + { + Child = box = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0 + }; + } + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config, OsuColour color) + { + configFadeRedWhenLowHealth = config.GetBindable(OsuSetting.FadePlayfieldWhenLowHealth); + box.Colour = color.Red; + + configFadeRedWhenLowHealth.BindValueChanged(value => + { + if (value.NewValue) + this.FadeIn(fade_time, Easing.OutQuint); + else + this.FadeOut(fade_time, Easing.OutQuint); + }, true); + } + } +} From 4153f8d49db453b613f7bd1ed7624892d060f3b3 Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Mon, 16 Mar 2020 21:31:22 +0100 Subject: [PATCH 0414/2376] Fix edge case making test fail Forgot that if a PM channel was the last tab, it hid itself upon selecting due to changing its width, which made the last-visible-selected assert fail. Made this particular test only use non-PM channels. --- osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index 02460282d8..6665452d94 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -146,7 +146,7 @@ namespace osu.Game.Tests.Visual.Online // Using temporary channels because they don't hide their names when not active channelManager.JoinChannel(new Channel { - Name = $"Channel no. {joinedChannels.Count() + 1}", + Name = $"Channel no. {joinedChannels.Count() + 11}", Type = ChannelType.Temporary }); From cc5833db80568e8e300dd434f4ad636c9582f573 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 17 Mar 2020 01:36:48 +0300 Subject: [PATCH 0415/2376] Remove string prefixes in the test scene --- .../Visual/Online/TestSceneFriendsLayout.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs b/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs index 0e9fafb1b6..46f22073f2 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs @@ -48,30 +48,30 @@ namespace osu.Game.Tests.Visual.Online { new APIFriend { - Username = @"flyte", + Username = "flyte", Id = 3103765, IsOnline = true, CurrentModeRank = 1111, - Country = new Country { FlagName = @"JP" }, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg" + Country = new Country { FlagName = "JP" }, + CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c6.jpg" }, new APIFriend { - Username = @"peppy", + Username = "peppy", Id = 2, IsOnline = false, CurrentModeRank = 2222, - Country = new Country { FlagName = @"AU" }, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + Country = new Country { FlagName = "AU" }, + CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", IsSupporter = true, SupportLevel = 3, }, new APIFriend { - Username = @"Evast", + Username = "Evast", Id = 8195163, - Country = new Country { FlagName = @"BY" }, - CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", + Country = new Country { FlagName = "BY" }, + CoverUrl = "https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", IsOnline = false, LastVisit = DateTimeOffset.Now } From 6ec01a67af0d3d3a7af3db542e00dd69e725b008 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 17 Mar 2020 01:38:45 +0300 Subject: [PATCH 0416/2376] Use cast in SocialOverlay --- osu.Game/Overlays/SocialOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/SocialOverlay.cs b/osu.Game/Overlays/SocialOverlay.cs index ba572b0e78..ff6f7de436 100644 --- a/osu.Game/Overlays/SocialOverlay.cs +++ b/osu.Game/Overlays/SocialOverlay.cs @@ -120,7 +120,7 @@ namespace osu.Game.Overlays { case SocialTab.Friends: var friendRequest = new GetFriendsRequest(); // TODO filter arguments? - friendRequest.Success += users => Users = users.Select(u => (User)u).ToArray(); + friendRequest.Success += users => Users = users.Cast().ToArray(); API.Queue(getUsersRequest = friendRequest); break; From 6a151b8e75a4bcc3ed48d2dfd6c8f2f2ddd393b1 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 17 Mar 2020 01:50:19 +0300 Subject: [PATCH 0417/2376] Add online test --- .../Visual/Online/TestSceneFriendsLayout.cs | 18 ++++++++++++++++-- .../Dashboard/Friends/FriendsLayout.cs | 12 +++++++----- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs b/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs index 46f22073f2..90474e5178 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs @@ -11,6 +11,7 @@ using osu.Game.Overlays; using osu.Framework.Allocation; using NUnit.Framework; using osu.Game.Online.API.Requests.Responses; +using System.Linq; namespace osu.Game.Tests.Visual.Online { @@ -23,10 +24,12 @@ namespace osu.Game.Tests.Visual.Online typeof(UserListToolbar) }; + protected override bool UseOnlineAPI => true; + [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); - private FriendsLayout layout; + private TestFriendsLayout layout; [SetUp] public void Setup() => Schedule(() => @@ -34,10 +37,16 @@ namespace osu.Game.Tests.Visual.Online Child = new BasicScrollContainer { RelativeSizeAxes = Axes.Both, - Child = layout = new FriendsLayout() + Child = layout = new TestFriendsLayout() }; }); + [Test] + public void TestOnline() + { + AddUntilStep("Users loaded", () => layout?.StatusControl.Items.Any() ?? false); + } + [Test] public void TestPopulate() { @@ -76,5 +85,10 @@ namespace osu.Game.Tests.Visual.Online LastVisit = DateTimeOffset.Now } }; + + private class TestFriendsLayout : FriendsLayout + { + public FriendsOnlineStatusControl StatusControl => OnlineStatusControl; + } } } diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs index 55f394cb78..cd358bcf84 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs @@ -26,11 +26,13 @@ namespace osu.Game.Overlays.Dashboard.Friends get => users; set { + request?.Cancel(); + users = value; usersLoaded = true; - onlineStatusControl.Populate(value); + OnlineStatusControl.Populate(value); } } @@ -42,9 +44,9 @@ namespace osu.Game.Overlays.Dashboard.Friends private Drawable currentContent; + protected readonly FriendsOnlineStatusControl OnlineStatusControl; private readonly Box background; private readonly Box controlBackground; - private readonly FriendsOnlineStatusControl onlineStatusControl; private readonly UserListToolbar userListToolbar; private readonly Container itemsPlaceholder; private readonly LoadingLayer loading; @@ -78,7 +80,7 @@ namespace osu.Game.Overlays.Dashboard.Friends Top = 20, Horizontal = 45 }, - Child = onlineStatusControl = new FriendsOnlineStatusControl(), + Child = OnlineStatusControl = new FriendsOnlineStatusControl(), } } }, @@ -152,7 +154,7 @@ namespace osu.Game.Overlays.Dashboard.Friends { base.LoadComplete(); - onlineStatusControl.Current.BindValueChanged(_ => recreatePanels()); + OnlineStatusControl.Current.BindValueChanged(_ => recreatePanels()); userListToolbar.DisplayStyle.BindValueChanged(_ => recreatePanels()); userListToolbar.SortCriteria.BindValueChanged(_ => recreatePanels()); @@ -175,7 +177,7 @@ namespace osu.Game.Overlays.Dashboard.Friends if (itemsPlaceholder.Any()) loading.Show(); - var groupedUsers = onlineStatusControl.Current.Value?.Users ?? new List(); + var groupedUsers = OnlineStatusControl.Current.Value?.Users ?? new List(); var sortedUsers = sortUsers(groupedUsers); From da97a02e6660ef516762d9f0c742df3eb39f7247 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 17 Mar 2020 01:53:53 +0300 Subject: [PATCH 0418/2376] Remove pointless flag --- osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs index cd358bcf84..1098fdee8f 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs @@ -30,8 +30,6 @@ namespace osu.Game.Overlays.Dashboard.Friends users = value; - usersLoaded = true; - OnlineStatusControl.Populate(value); } } @@ -148,8 +146,6 @@ namespace osu.Game.Overlays.Dashboard.Friends controlBackground.Colour = colourProvider.Background5; } - private bool usersLoaded; - protected override void LoadComplete() { base.LoadComplete(); @@ -168,8 +164,7 @@ namespace osu.Game.Overlays.Dashboard.Friends private void recreatePanels() { - // Don't allow any changes until we have users loaded - if (!usersLoaded) + if (!users.Any()) return; cancellationToken?.Cancel(); From bd84980aa61af03d6c398db2942d8d5998d049ab Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 17 Mar 2020 01:56:10 +0300 Subject: [PATCH 0419/2376] Simplify order by last visit --- osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs index 1098fdee8f..1c56227521 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs @@ -232,7 +232,7 @@ namespace osu.Game.Overlays.Dashboard.Friends { default: case UserSortCriteria.LastVisit: - return unsorted.OrderBy(u => u.LastVisit).Reverse().ToList(); + return unsorted.OrderByDescending(u => u.LastVisit).ToList(); case UserSortCriteria.Rank: return unsorted.Where(u => u.CurrentModeRank.HasValue).OrderBy(u => u.CurrentModeRank).Concat(unsorted.Where(u => u.CurrentModeRank == null)).ToList(); From f816479ff8972c4e2fb139b55746b36432a05f34 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 17 Mar 2020 02:03:57 +0300 Subject: [PATCH 0420/2376] Simplify order by rank --- osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs index 1c56227521..f069d3f384 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs @@ -235,7 +235,7 @@ namespace osu.Game.Overlays.Dashboard.Friends return unsorted.OrderByDescending(u => u.LastVisit).ToList(); case UserSortCriteria.Rank: - return unsorted.Where(u => u.CurrentModeRank.HasValue).OrderBy(u => u.CurrentModeRank).Concat(unsorted.Where(u => u.CurrentModeRank == null)).ToList(); + return unsorted.OrderByDescending(u => u.CurrentModeRank.HasValue).ThenBy(u => u.CurrentModeRank ?? 0).ToList(); case UserSortCriteria.Username: return unsorted.OrderBy(u => u.Username).ToList(); From d9d812a8fe09d7006c0e64bd20a79dd9f049efec Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 17 Mar 2020 02:10:20 +0300 Subject: [PATCH 0421/2376] Fix status icon flash on first status change --- osu.Game/Users/UserPanel.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index 289244cdc3..d5e6d5f13e 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -99,6 +99,9 @@ namespace osu.Game.Users { base.LoadComplete(); Status.TriggerChange(); + + // Colour should be applied immediately on first load. + statusIcon.FinishTransforms(); } protected override bool OnHover(HoverEvent e) From bf9c6f8a3b0f36f1fb984ddc7894cb00d2ec0a8d Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 17 Mar 2020 02:19:03 +0300 Subject: [PATCH 0422/2376] Skip online test if user is not logged-in --- osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs b/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs index 90474e5178..788cbd82c9 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs @@ -12,6 +12,7 @@ using osu.Framework.Allocation; using NUnit.Framework; using osu.Game.Online.API.Requests.Responses; using System.Linq; +using osu.Game.Online.API; namespace osu.Game.Tests.Visual.Online { @@ -29,6 +30,9 @@ namespace osu.Game.Tests.Visual.Online [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); + [Resolved] + private IAPIProvider api { get; set; } + private TestFriendsLayout layout; [SetUp] @@ -44,7 +48,8 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestOnline() { - AddUntilStep("Users loaded", () => layout?.StatusControl.Items.Any() ?? false); + // Skip online test if user is not logged-in + AddUntilStep("Users loaded", () => !api.IsLoggedIn || (layout?.StatusControl.Items.Any() ?? false)); } [Test] From f7ea20a926d9cf512ec4ec0533777336de3744c4 Mon Sep 17 00:00:00 2001 From: Joehu Date: Mon, 16 Mar 2020 16:32:25 -0700 Subject: [PATCH 0423/2376] Limit font weight to bold --- osu.Game/Graphics/OsuFont.cs | 5 ----- osu.Game/Overlays/News/NewsArticleCover.cs | 2 +- osu.Game/Overlays/Notifications/NotificationSection.cs | 4 ++-- osu.Game/Overlays/OSD/Toast.cs | 2 +- osu.Game/Overlays/OverlayStreamItem.cs | 2 +- osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs | 2 +- osu.Game/Overlays/Settings/SettingsSubsection.cs | 2 +- osu.Game/Screens/Edit/Timing/ControlPointTable.cs | 2 +- osu.Game/Screens/Play/Break/BreakInfo.cs | 2 +- 9 files changed, 9 insertions(+), 14 deletions(-) diff --git a/osu.Game/Graphics/OsuFont.cs b/osu.Game/Graphics/OsuFont.cs index 7c78141b4d..255f7f24f7 100644 --- a/osu.Game/Graphics/OsuFont.cs +++ b/osu.Game/Graphics/OsuFont.cs @@ -136,10 +136,5 @@ namespace osu.Game.Graphics /// Equivalent to weight 700. ///
Bold = 700, - - /// - /// Equivalent to weight 900. - /// - Black = 900 } } diff --git a/osu.Game/Overlays/News/NewsArticleCover.cs b/osu.Game/Overlays/News/NewsArticleCover.cs index e381b629e4..cca0cfb4a0 100644 --- a/osu.Game/Overlays/News/NewsArticleCover.cs +++ b/osu.Game/Overlays/News/NewsArticleCover.cs @@ -148,7 +148,7 @@ namespace osu.Game.Overlays.News { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Font = OsuFont.GetFont(Typeface.Torus, 12, FontWeight.Black, false, false), + Font = OsuFont.GetFont(Typeface.Torus, 12, FontWeight.Bold, false, false), Text = date.ToString("d MMM yyy").ToUpper(), Margin = new MarginPadding { diff --git a/osu.Game/Overlays/Notifications/NotificationSection.cs b/osu.Game/Overlays/Notifications/NotificationSection.cs index 17a2d4cf9f..c2a958b65e 100644 --- a/osu.Game/Overlays/Notifications/NotificationSection.cs +++ b/osu.Game/Overlays/Notifications/NotificationSection.cs @@ -84,13 +84,13 @@ namespace osu.Game.Overlays.Notifications new OsuSpriteText { Text = titleText.ToUpperInvariant(), - Font = OsuFont.GetFont(weight: FontWeight.Black) + Font = OsuFont.GetFont(weight: FontWeight.Bold) }, countDrawable = new OsuSpriteText { Text = "3", Colour = colours.Yellow, - Font = OsuFont.GetFont(weight: FontWeight.Black) + Font = OsuFont.GetFont(weight: FontWeight.Bold) }, } }, diff --git a/osu.Game/Overlays/OSD/Toast.cs b/osu.Game/Overlays/OSD/Toast.cs index 46c53ec409..5d36cac20e 100644 --- a/osu.Game/Overlays/OSD/Toast.cs +++ b/osu.Game/Overlays/OSD/Toast.cs @@ -53,7 +53,7 @@ namespace osu.Game.Overlays.OSD { Padding = new MarginPadding(10), Name = "Description", - Font = OsuFont.GetFont(size: 14, weight: FontWeight.Black), + Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold), Spacing = new Vector2(1, 0), Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, diff --git a/osu.Game/Overlays/OverlayStreamItem.cs b/osu.Game/Overlays/OverlayStreamItem.cs index 630d3a0a22..7f8559e7de 100644 --- a/osu.Game/Overlays/OverlayStreamItem.cs +++ b/osu.Game/Overlays/OverlayStreamItem.cs @@ -59,7 +59,7 @@ namespace osu.Game.Overlays new OsuSpriteText { Text = MainText, - Font = OsuFont.GetFont(size: 12, weight: FontWeight.Black), + Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold), }, new OsuSpriteText { diff --git a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs index 3ab64786a2..52b712a40e 100644 --- a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs @@ -80,7 +80,7 @@ namespace osu.Game.Overlays.Settings.Sections.General { Text = "ACCOUNT", Margin = new MarginPadding { Bottom = 5 }, - Font = OsuFont.GetFont(weight: FontWeight.Black), + Font = OsuFont.GetFont(weight: FontWeight.Bold), }, form = new LoginForm { diff --git a/osu.Game/Overlays/Settings/SettingsSubsection.cs b/osu.Game/Overlays/Settings/SettingsSubsection.cs index 9b3b2f570c..b096c146a6 100644 --- a/osu.Game/Overlays/Settings/SettingsSubsection.cs +++ b/osu.Game/Overlays/Settings/SettingsSubsection.cs @@ -54,7 +54,7 @@ namespace osu.Game.Overlays.Settings { Text = Header.ToUpperInvariant(), Margin = new MarginPadding { Bottom = 10, Left = SettingsPanel.CONTENT_MARGINS, Right = SettingsPanel.CONTENT_MARGINS }, - Font = OsuFont.GetFont(weight: FontWeight.Black), + Font = OsuFont.GetFont(weight: FontWeight.Bold), }, FlowContent }); diff --git a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs index 96e3ab48f2..5c59cfbfe8 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs @@ -152,7 +152,7 @@ namespace osu.Game.Screens.Edit.Timing public HeaderText(string text) { Text = text.ToUpper(); - Font = OsuFont.GetFont(size: 12, weight: FontWeight.Black); + Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold); } } diff --git a/osu.Game/Screens/Play/Break/BreakInfo.cs b/osu.Game/Screens/Play/Break/BreakInfo.cs index a3d64d05a3..6e129b20ea 100644 --- a/osu.Game/Screens/Play/Break/BreakInfo.cs +++ b/osu.Game/Screens/Play/Break/BreakInfo.cs @@ -30,7 +30,7 @@ namespace osu.Game.Screens.Play.Break Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Text = "current progress".ToUpperInvariant(), - Font = OsuFont.GetFont(weight: FontWeight.Black, size: 15), + Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 15), }, new FillFlowContainer { From 8895d52d298b8b474ec40dd5490d0543545a2782 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Mar 2020 13:16:18 +0900 Subject: [PATCH 0424/2376] Fix header-text scaling on intro/winner screens --- .../Components/TestSceneRoundDisplay.cs | 40 +++++++++++++++++++ .../DrawableTournamentHeaderText.cs | 11 ++--- .../Components/RoundDisplay.cs | 14 +++++-- 3 files changed, 57 insertions(+), 8 deletions(-) create mode 100644 osu.Game.Tournament.Tests/Components/TestSceneRoundDisplay.cs diff --git a/osu.Game.Tournament.Tests/Components/TestSceneRoundDisplay.cs b/osu.Game.Tournament.Tests/Components/TestSceneRoundDisplay.cs new file mode 100644 index 0000000000..6f71627ce4 --- /dev/null +++ b/osu.Game.Tournament.Tests/Components/TestSceneRoundDisplay.cs @@ -0,0 +1,40 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using osu.Framework.Graphics; +using osu.Game.Tournament.Components; +using osu.Game.Tournament.Models; + +namespace osu.Game.Tournament.Tests.Components +{ + public class TestSceneRoundDisplay : TournamentTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(DrawableTournamentHeaderText), + typeof(DrawableTournamentHeaderLogo), + }; + + public TestSceneRoundDisplay() + { + Children = new Drawable[] + { + new RoundDisplay(new TournamentMatch + { + Round = + { + Value = new TournamentRound + { + Name = { Value = "Test Round" } + } + } + }) + { + Margin = new MarginPadding(20) + } + }; + } + } +} diff --git a/osu.Game.Tournament/Components/DrawableTournamentHeaderText.cs b/osu.Game.Tournament/Components/DrawableTournamentHeaderText.cs index bda696ba00..99d914fed4 100644 --- a/osu.Game.Tournament/Components/DrawableTournamentHeaderText.cs +++ b/osu.Game.Tournament/Components/DrawableTournamentHeaderText.cs @@ -11,9 +11,13 @@ namespace osu.Game.Tournament.Components { public class DrawableTournamentHeaderText : CompositeDrawable { - public DrawableTournamentHeaderText() + public DrawableTournamentHeaderText(bool center = true) { - InternalChild = new TextSprite(); + InternalChild = new TextSprite + { + Anchor = center ? Anchor.Centre : Anchor.TopLeft, + Origin = center ? Anchor.Centre : Anchor.TopLeft, + }; Height = 22; RelativeSizeAxes = Axes.X; @@ -27,9 +31,6 @@ namespace osu.Game.Tournament.Components RelativeSizeAxes = Axes.Both; FillMode = FillMode.Fit; - Anchor = Anchor.Centre; - Origin = Anchor.Centre; - Texture = textures.Get("header-text"); } } diff --git a/osu.Game.Tournament/Components/RoundDisplay.cs b/osu.Game.Tournament/Components/RoundDisplay.cs index bebede6782..c0002e6804 100644 --- a/osu.Game.Tournament/Components/RoundDisplay.cs +++ b/osu.Game.Tournament/Components/RoundDisplay.cs @@ -12,19 +12,27 @@ namespace osu.Game.Tournament.Components { public RoundDisplay(TournamentMatch match) { - AutoSizeAxes = Axes.Both; + AutoSizeAxes = Axes.Y; + RelativeSizeAxes = Axes.X; InternalChildren = new Drawable[] { new FillFlowContainer { - AutoSizeAxes = Axes.Both, + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, Direction = FillDirection.Vertical, Children = new Drawable[] { - new DrawableTournamentHeaderText(), + new DrawableTournamentHeaderText(false) + { + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + }, new TournamentSpriteText { + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, Text = match.Round.Value?.Name.Value ?? "Unknown Round", Font = OsuFont.Torus.With(size: 26, weight: FontWeight.SemiBold) }, From 99f28efc96a5b6b459a4b81ccccb8478e539fe02 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Mar 2020 13:16:52 +0900 Subject: [PATCH 0425/2376] Automatically mark the currently selected match as stsrated on entering gameplay screen --- .../Gameplay/Components/TeamScoreDisplay.cs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs index 462015f004..3e60a03f92 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs @@ -35,7 +35,9 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components private void load(LadderInfo ladder) { currentMatch.BindTo(ladder.CurrentMatch); - currentMatch.BindValueChanged(matchChanged, true); + currentMatch.BindValueChanged(matchChanged); + + updateMatch(); } private void matchChanged(ValueChangedEvent match) @@ -43,10 +45,19 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components currentTeamScore.UnbindBindings(); currentTeam.UnbindBindings(); - if (match.NewValue != null) + Scheduler.AddOnce(updateMatch); + } + + private void updateMatch() + { + var match = currentMatch.Value; + + if (match != null) { - currentTeamScore.BindTo(teamColour == TeamColour.Red ? match.NewValue.Team1Score : match.NewValue.Team2Score); - currentTeam.BindTo(teamColour == TeamColour.Red ? match.NewValue.Team1 : match.NewValue.Team2); + match.StartMatch(); + + currentTeamScore.BindTo(teamColour == TeamColour.Red ? match.Team1Score : match.Team2Score); + currentTeam.BindTo(teamColour == TeamColour.Red ? match.Team1 : match.Team2); } // team may change to same team, which means score is not in a good state. From 4ac740b12baceb300e3c582e37804587568acb8c Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 17 Mar 2020 08:51:54 +0300 Subject: [PATCH 0426/2376] Remove APIFriend --- .../Visual/Online/TestSceneFriendsLayout.cs | 9 ++++----- .../TestSceneFriendsOnlineStatusControl.cs | 10 +++++----- osu.Game/Online/API/Requests/GetFriendsRequest.cs | 4 ++-- .../Online/API/Requests/Responses/APIFriend.cs | 14 -------------- .../Overlays/Dashboard/Friends/FriendsBundle.cs | 6 +++--- .../Overlays/Dashboard/Friends/FriendsLayout.cs | 11 +++++------ .../Friends/FriendsOnlineStatusControl.cs | 4 ++-- osu.Game/Overlays/SocialOverlay.cs | 2 +- osu.Game/Users/User.cs | 3 +++ 9 files changed, 25 insertions(+), 38 deletions(-) delete mode 100644 osu.Game/Online/API/Requests/Responses/APIFriend.cs diff --git a/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs b/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs index 788cbd82c9..c6971a971d 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs @@ -10,7 +10,6 @@ using osu.Game.Users; using osu.Game.Overlays; using osu.Framework.Allocation; using NUnit.Framework; -using osu.Game.Online.API.Requests.Responses; using System.Linq; using osu.Game.Online.API; @@ -58,9 +57,9 @@ namespace osu.Game.Tests.Visual.Online AddStep("Populate", () => layout.Users = getUsers()); } - private List getUsers() => new List + private List getUsers() => new List { - new APIFriend + new User { Username = "flyte", Id = 3103765, @@ -69,7 +68,7 @@ namespace osu.Game.Tests.Visual.Online Country = new Country { FlagName = "JP" }, CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c6.jpg" }, - new APIFriend + new User { Username = "peppy", Id = 2, @@ -80,7 +79,7 @@ namespace osu.Game.Tests.Visual.Online IsSupporter = true, SupportLevel = 3, }, - new APIFriend + new User { Username = "Evast", Id = 8195163, diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs index 8bdf3c5dc1..d72818ed89 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs @@ -7,9 +7,9 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.Dashboard.Friends; +using osu.Game.Users; namespace osu.Game.Tests.Visual.UserInterface { @@ -39,17 +39,17 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void Populate() { - AddStep("Populate", () => control.Populate(new List + AddStep("Populate", () => control.Populate(new List { - new APIFriend + new User { IsOnline = true }, - new APIFriend + new User { IsOnline = false }, - new APIFriend + new User { IsOnline = false } diff --git a/osu.Game/Online/API/Requests/GetFriendsRequest.cs b/osu.Game/Online/API/Requests/GetFriendsRequest.cs index 321f675aae..46890aa889 100644 --- a/osu.Game/Online/API/Requests/GetFriendsRequest.cs +++ b/osu.Game/Online/API/Requests/GetFriendsRequest.cs @@ -2,11 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using osu.Game.Online.API.Requests.Responses; +using osu.Game.Users; namespace osu.Game.Online.API.Requests { - public class GetFriendsRequest : APIRequest> + public class GetFriendsRequest : APIRequest> { protected override string Target => @"friends"; } diff --git a/osu.Game/Online/API/Requests/Responses/APIFriend.cs b/osu.Game/Online/API/Requests/Responses/APIFriend.cs deleted file mode 100644 index 91fed28d44..0000000000 --- a/osu.Game/Online/API/Requests/Responses/APIFriend.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using Newtonsoft.Json; -using osu.Game.Users; - -namespace osu.Game.Online.API.Requests.Responses -{ - public class APIFriend : User - { - [JsonProperty(@"current_mode_rank")] - public int? CurrentModeRank; - } -} diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsBundle.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsBundle.cs index 0062c49c91..772d9c67a0 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendsBundle.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsBundle.cs @@ -2,7 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using osu.Game.Online.API.Requests.Responses; +using osu.Game.Users; namespace osu.Game.Overlays.Dashboard.Friends { @@ -12,9 +12,9 @@ namespace osu.Game.Overlays.Dashboard.Friends public int Count => Users.Count; - public List Users { get; } + public List Users { get; } - public FriendsBundle(FriendsOnlineStatus status, List users) + public FriendsBundle(FriendsOnlineStatus status, List users) { Status = status; Users = users; diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs index f069d3f384..c02f07fe4a 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs @@ -11,7 +11,6 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.API.Requests; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Users; using osuTK; @@ -19,9 +18,9 @@ namespace osu.Game.Overlays.Dashboard.Friends { public class FriendsLayout : CompositeDrawable { - private List users = new List(); + private List users = new List(); - public List Users + public List Users { get => users; set @@ -172,7 +171,7 @@ namespace osu.Game.Overlays.Dashboard.Friends if (itemsPlaceholder.Any()) loading.Show(); - var groupedUsers = OnlineStatusControl.Current.Value?.Users ?? new List(); + var groupedUsers = OnlineStatusControl.Current.Value?.Users ?? new List(); var sortedUsers = sortUsers(groupedUsers); @@ -195,7 +194,7 @@ namespace osu.Game.Overlays.Dashboard.Friends currentContent.FadeIn(200, Easing.OutQuint); } - private FillFlowContainer createTable(List users) + private FillFlowContainer createTable(List users) { var style = userListToolbar.DisplayStyle.Value; @@ -226,7 +225,7 @@ namespace osu.Game.Overlays.Dashboard.Friends } } - private List sortUsers(List unsorted) + private List sortUsers(List unsorted) { switch (userListToolbar.SortCriteria.Value) { diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusControl.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusControl.cs index 2b716f228d..88035e0a34 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusControl.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusControl.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Linq; -using osu.Game.Online.API.Requests.Responses; +using osu.Game.Users; namespace osu.Game.Overlays.Dashboard.Friends { @@ -11,7 +11,7 @@ namespace osu.Game.Overlays.Dashboard.Friends { protected override OverlayStreamItem CreateStreamItem(FriendsBundle value) => new FriendsOnlineStatusItem(value); - public void Populate(List users) + public void Populate(List users) { Clear(); diff --git a/osu.Game/Overlays/SocialOverlay.cs b/osu.Game/Overlays/SocialOverlay.cs index ff6f7de436..02f7c9b0d3 100644 --- a/osu.Game/Overlays/SocialOverlay.cs +++ b/osu.Game/Overlays/SocialOverlay.cs @@ -120,7 +120,7 @@ namespace osu.Game.Overlays { case SocialTab.Friends: var friendRequest = new GetFriendsRequest(); // TODO filter arguments? - friendRequest.Success += users => Users = users.Cast().ToArray(); + friendRequest.Success += users => Users = users.ToArray(); API.Queue(getUsersRequest = friendRequest); break; diff --git a/osu.Game/Users/User.cs b/osu.Game/Users/User.cs index d25c552160..2a6f7844a2 100644 --- a/osu.Game/Users/User.cs +++ b/osu.Game/Users/User.cs @@ -69,6 +69,9 @@ namespace osu.Game.Users [JsonProperty(@"support_level")] public int SupportLevel; + [JsonProperty(@"current_mode_rank")] + public int? CurrentModeRank; + [JsonProperty(@"is_gmt")] public bool IsGMT; From 9e7c388202b2cdc59b4b43a0bd55e54c83ed9135 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 17 Mar 2020 16:24:33 +0900 Subject: [PATCH 0427/2376] Expose Spacing and UseFullGlyphHeight --- osu.Game/Graphics/Sprites/GlowingSpriteText.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/osu.Game/Graphics/Sprites/GlowingSpriteText.cs b/osu.Game/Graphics/Sprites/GlowingSpriteText.cs index 12688da9df..4aea5aa518 100644 --- a/osu.Game/Graphics/Sprites/GlowingSpriteText.cs +++ b/osu.Game/Graphics/Sprites/GlowingSpriteText.cs @@ -43,6 +43,18 @@ namespace osu.Game.Graphics.Sprites set => blurredText.Colour = value; } + public Vector2 Spacing + { + get => spriteText.Spacing; + set => spriteText.Spacing = blurredText.Spacing = value; + } + + public bool UseFullGlyphHeight + { + get => spriteText.UseFullGlyphHeight; + set => spriteText.UseFullGlyphHeight = blurredText.UseFullGlyphHeight = value; + } + public GlowingSpriteText() { AutoSizeAxes = Axes.Both; From d77b0acd906a3fbb1af6bf10ca1f82fff6b9c439 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 17 Mar 2020 16:25:41 +0900 Subject: [PATCH 0428/2376] Move rank colour to OsuColour --- osu.Game/Graphics/OsuColour.cs | 30 +++++++++++++++++++ osu.Game/Online/Leaderboards/DrawableRank.cs | 31 +------------------- 2 files changed, 31 insertions(+), 30 deletions(-) diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs index 984f5e52d1..f7ed55410c 100644 --- a/osu.Game/Graphics/OsuColour.cs +++ b/osu.Game/Graphics/OsuColour.cs @@ -3,6 +3,7 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Game.Beatmaps; +using osu.Game.Scoring; using osuTK.Graphics; namespace osu.Game.Graphics @@ -37,6 +38,35 @@ namespace osu.Game.Graphics } } + /// + /// Retrieves the colour for a . + /// + public static Color4 ForRank(ScoreRank rank) + { + switch (rank) + { + case ScoreRank.XH: + case ScoreRank.X: + return Color4Extensions.FromHex(@"ce1c9d"); + + case ScoreRank.SH: + case ScoreRank.S: + return Color4Extensions.FromHex(@"00a8b5"); + + case ScoreRank.A: + return Color4Extensions.FromHex(@"7cce14"); + + case ScoreRank.B: + return Color4Extensions.FromHex(@"e3b130"); + + case ScoreRank.C: + return Color4Extensions.FromHex(@"f18252"); + + default: + return Color4Extensions.FromHex(@"e95353"); + } + } + // See https://github.com/ppy/osu-web/blob/master/resources/assets/less/colors.less public readonly Color4 PurpleLighter = Color4Extensions.FromHex(@"eeeeff"); public readonly Color4 PurpleLight = Color4Extensions.FromHex(@"aa88ff"); diff --git a/osu.Game/Online/Leaderboards/DrawableRank.cs b/osu.Game/Online/Leaderboards/DrawableRank.cs index 45b91bbf81..0c3ab25044 100644 --- a/osu.Game/Online/Leaderboards/DrawableRank.cs +++ b/osu.Game/Online/Leaderboards/DrawableRank.cs @@ -28,7 +28,7 @@ namespace osu.Game.Online.Leaderboards FillMode = FillMode.Fit; FillAspectRatio = 2; - var rankColour = getRankColour(); + var rankColour = OsuColour.ForRank(rank); InternalChild = new DrawSizePreservingFillContainer { TargetDrawSize = new Vector2(64, 32), @@ -71,35 +71,6 @@ namespace osu.Game.Online.Leaderboards private string getRankName() => rank.GetDescription().TrimEnd('+'); - /// - /// Retrieves the grade background colour. - /// - private Color4 getRankColour() - { - switch (rank) - { - case ScoreRank.XH: - case ScoreRank.X: - return Color4Extensions.FromHex(@"ce1c9d"); - - case ScoreRank.SH: - case ScoreRank.S: - return Color4Extensions.FromHex(@"00a8b5"); - - case ScoreRank.A: - return Color4Extensions.FromHex(@"7cce14"); - - case ScoreRank.B: - return Color4Extensions.FromHex(@"e3b130"); - - case ScoreRank.C: - return Color4Extensions.FromHex(@"f18252"); - - default: - return Color4Extensions.FromHex(@"e95353"); - } - } - /// /// Retrieves the grade text colour. /// From e586249db70e979fd884bd050019b3a0a220f3cf Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 17 Mar 2020 16:25:51 +0900 Subject: [PATCH 0429/2376] Expose GetRankName from DrawableRank --- osu.Game/Online/Leaderboards/DrawableRank.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Leaderboards/DrawableRank.cs b/osu.Game/Online/Leaderboards/DrawableRank.cs index 0c3ab25044..4d41230799 100644 --- a/osu.Game/Online/Leaderboards/DrawableRank.cs +++ b/osu.Game/Online/Leaderboards/DrawableRank.cs @@ -59,7 +59,7 @@ namespace osu.Game.Online.Leaderboards Padding = new MarginPadding { Top = 5 }, Colour = getRankNameColour(), Font = OsuFont.Numeric.With(size: 25), - Text = getRankName(), + Text = GetRankName(rank), ShadowColour = Color4.Black.Opacity(0.3f), ShadowOffset = new Vector2(0, 0.08f), Shadow = true, @@ -69,7 +69,7 @@ namespace osu.Game.Online.Leaderboards }; } - private string getRankName() => rank.GetDescription().TrimEnd('+'); + public static string GetRankName(ScoreRank rank) => rank.GetDescription().TrimEnd('+'); /// /// Retrieves the grade text colour. From dca2e1d816971316351eec8bb266c0019abdf188 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 17 Mar 2020 16:37:56 +0900 Subject: [PATCH 0430/2376] Implement the accuracy circle --- .../Visual/Ranking/TestSceneAccuracyCircle.cs | 155 +++++++++++ .../Expanded/Accuracy/AccuracyCircle.cs | 253 ++++++++++++++++++ .../Ranking/Expanded/Accuracy/RankBadge.cs | 99 +++++++ .../Ranking/Expanded/Accuracy/RankNotch.cs | 49 ++++ .../Ranking/Expanded/Accuracy/RankText.cs | 83 ++++++ .../Accuracy/SmoothCircularProgress.cs | 126 +++++++++ 6 files changed, 765 insertions(+) create mode 100644 osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs create mode 100644 osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs create mode 100644 osu.Game/Screens/Ranking/Expanded/Accuracy/RankBadge.cs create mode 100644 osu.Game/Screens/Ranking/Expanded/Accuracy/RankNotch.cs create mode 100644 osu.Game/Screens/Ranking/Expanded/Accuracy/RankText.cs create mode 100644 osu.Game/Screens/Ranking/Expanded/Accuracy/SmoothCircularProgress.cs diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs new file mode 100644 index 0000000000..d0b9d43f51 --- /dev/null +++ b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs @@ -0,0 +1,155 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using NUnit.Framework; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Screens.Ranking.Expanded.Accuracy; +using osu.Game.Tests.Beatmaps; +using osu.Game.Users; +using osuTK; + +namespace osu.Game.Tests.Visual.Ranking +{ + public class TestSceneAccuracyCircle : OsuTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(AccuracyCircle), + typeof(RankBadge), + typeof(RankNotch), + typeof(RankText), + typeof(SmoothCircularProgress) + }; + + [Test] + public void TestDRank() + { + var score = createScore(); + score.Accuracy = 0.5; + score.Rank = ScoreRank.D; + + addCircleStep(score); + } + + [Test] + public void TestCRank() + { + var score = createScore(); + score.Accuracy = 0.75; + score.Rank = ScoreRank.C; + + addCircleStep(score); + } + + [Test] + public void TestBRank() + { + var score = createScore(); + score.Accuracy = 0.85; + score.Rank = ScoreRank.B; + + addCircleStep(score); + } + + [Test] + public void TestARank() + { + var score = createScore(); + score.Accuracy = 0.925; + score.Rank = ScoreRank.A; + + addCircleStep(score); + } + + [Test] + public void TestSRank() + { + var score = createScore(); + score.Accuracy = 0.975; + score.Rank = ScoreRank.S; + + addCircleStep(score); + } + + [Test] + public void TestAlmostSSRank() + { + var score = createScore(); + score.Accuracy = 0.9999; + score.Rank = ScoreRank.S; + + addCircleStep(score); + } + + [Test] + public void TestSSRank() + { + var score = createScore(); + score.Accuracy = 1; + score.Rank = ScoreRank.X; + + addCircleStep(score); + } + + private void addCircleStep(ScoreInfo score) => AddStep("add panel", () => + { + Children = new Drawable[] + { + new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(500, 700), + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientVertical(Color4Extensions.FromHex("#555"), Color4Extensions.FromHex("#333")) + } + } + }, + new AccuracyCircle(score) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(230) + } + }; + }); + + private ScoreInfo createScore() => new ScoreInfo + { + User = new User + { + Id = 2, + Username = "peppy", + }, + Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, + Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }, + TotalScore = 2845370, + Accuracy = 0.95, + MaxCombo = 999, + Rank = ScoreRank.S, + Date = DateTimeOffset.Now, + Statistics = + { + { HitResult.Miss, 1 }, + { HitResult.Meh, 50 }, + { HitResult.Good, 100 }, + { HitResult.Great, 300 }, + } + }; + } +} diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs new file mode 100644 index 0000000000..873c20cc2b --- /dev/null +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs @@ -0,0 +1,253 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Utils; +using osu.Game.Graphics; +using osu.Game.Scoring; +using osuTK; + +namespace osu.Game.Screens.Ranking.Expanded.Accuracy +{ + /// + /// The component that displays the player's accuracy on the results screen. + /// + public class AccuracyCircle : CompositeDrawable + { + /// + /// Duration for the transforms causing this component to appear. + /// + public const double APPEAR_DURATION = 200; + + /// + /// Delay before the accuracy circle starts filling. + /// + public const double ACCURACY_TRANSFORM_DELAY = 450; + + /// + /// Duration for the accuracy circle fill. + /// + public const double ACCURACY_TRANSFORM_DURATION = 3000; + + /// + /// Delay after for the rank text (A/B/C/D/S/SS) to appear. + /// + public const double TEXT_APPEAR_DELAY = ACCURACY_TRANSFORM_DURATION / 2; + + /// + /// Delay before the rank circles start filling. + /// + public const double RANK_CIRCLE_TRANSFORM_DELAY = 150; + + /// + /// Duration for the rank circle fills. + /// + public const double RANK_CIRCLE_TRANSFORM_DURATION = 800; + + /// + /// Relative width of the rank circles. + /// + public const float RANK_CIRCLE_RADIUS = 0.06f; + + /// + /// Relative width of the circle showing the accuracy. + /// + private const float accuracy_circle_radius = 0.2f; + + /// + /// SS is displayed as a 1% region, otherwise it would be invisible. + /// + private const double virtual_ss_percentage = 0.01; + + /// + /// The easing for the circle filling transforms. + /// + public static readonly Easing ACCURACY_TRANSFORM_EASING = Easing.OutPow10; + + private readonly ScoreInfo score; + + private SmoothCircularProgress accuracyCircle; + private SmoothCircularProgress innerMask; + private Container badges; + private RankText rankText; + + public AccuracyCircle(ScoreInfo score) + { + this.score = score; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChildren = new Drawable[] + { + new SmoothCircularProgress + { + Name = "Background circle", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.Gray(47), + Alpha = 0.5f, + InnerRadius = accuracy_circle_radius + 0.01f, // Extends a little bit into the circle + Current = { Value = 1 }, + }, + accuracyCircle = new SmoothCircularProgress + { + Name = "Accuracy circle", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientVertical(Color4Extensions.FromHex("#7CF6FF"), Color4Extensions.FromHex("#BAFFA9")), + InnerRadius = accuracy_circle_radius, + }, + new BufferedContainer + { + Name = "Graded circles", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.8f), + Padding = new MarginPadding(2), + Children = new Drawable[] + { + new SmoothCircularProgress + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#BE0089"), + InnerRadius = RANK_CIRCLE_RADIUS, + Current = { Value = 1 } + }, + new SmoothCircularProgress + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#0096A2"), + InnerRadius = RANK_CIRCLE_RADIUS, + Current = { Value = 1 - virtual_ss_percentage } + }, + new SmoothCircularProgress + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#72C904"), + InnerRadius = RANK_CIRCLE_RADIUS, + Current = { Value = 0.95f } + }, + new SmoothCircularProgress + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#D99D03"), + InnerRadius = RANK_CIRCLE_RADIUS, + Current = { Value = 0.9f } + }, + new SmoothCircularProgress + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#EA7948"), + InnerRadius = RANK_CIRCLE_RADIUS, + Current = { Value = 0.8f } + }, + new SmoothCircularProgress + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#FF5858"), + InnerRadius = RANK_CIRCLE_RADIUS, + Current = { Value = 0.7f } + }, + new RankNotch(0), + new RankNotch((float)(1 - virtual_ss_percentage)), + new RankNotch(0.95f), + new RankNotch(0.9f), + new RankNotch(0.8f), + new RankNotch(0.7f), + new BufferedContainer + { + Name = "Graded circle mask", + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(1), + Blending = new BlendingParameters + { + Source = BlendingType.DstColor, + Destination = BlendingType.OneMinusSrcAlpha, + SourceAlpha = BlendingType.One, + DestinationAlpha = BlendingType.SrcAlpha + }, + Child = innerMask = new SmoothCircularProgress + { + RelativeSizeAxes = Axes.Both, + InnerRadius = RANK_CIRCLE_RADIUS - 0.01f, + } + } + } + }, + badges = new Container + { + Name = "Rank badges", + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Vertical = -15, Horizontal = -20 }, + Children = new[] + { + new RankBadge(1f, ScoreRank.X), + new RankBadge(0.95f, ScoreRank.S), + new RankBadge(0.9f, ScoreRank.A), + new RankBadge(0.8f, ScoreRank.B), + new RankBadge(0.7f, ScoreRank.C), + } + }, + rankText = new RankText(score.Rank) + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + this.ScaleTo(0).Then().ScaleTo(1, APPEAR_DURATION, Easing.OutQuint); + + using (BeginDelayedSequence(RANK_CIRCLE_TRANSFORM_DELAY, true)) + innerMask.FillTo(1f, RANK_CIRCLE_TRANSFORM_DURATION, ACCURACY_TRANSFORM_EASING); + + using (BeginDelayedSequence(ACCURACY_TRANSFORM_DELAY, true)) + { + double targetAccuracy = score.Rank == ScoreRank.X || score.Rank == ScoreRank.XH ? 1 : Math.Min(1 - virtual_ss_percentage, score.Accuracy); + + accuracyCircle.FillTo(targetAccuracy, ACCURACY_TRANSFORM_DURATION, ACCURACY_TRANSFORM_EASING); + + foreach (var badge in badges) + { + if (badge.Accuracy > score.Accuracy) + continue; + + using (BeginDelayedSequence(inverseEasing(ACCURACY_TRANSFORM_EASING, badge.Accuracy / targetAccuracy) * ACCURACY_TRANSFORM_DURATION, true)) + badge.Appear(); + } + + using (BeginDelayedSequence(TEXT_APPEAR_DELAY, true)) + rankText.Appear(); + } + } + + private double inverseEasing(Easing easing, double targetValue) + { + double test = 0; + double result = 0; + int count = 2; + + while (Math.Abs(result - targetValue) > 0.005) + { + int dir = Math.Sign(targetValue - result); + + test += dir * 1.0 / count; + result = Interpolation.ApplyEasing(easing, test); + + count++; + } + + return test; + } + } +} diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/RankBadge.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/RankBadge.cs new file mode 100644 index 0000000000..76cd408daa --- /dev/null +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/RankBadge.cs @@ -0,0 +1,99 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Online.Leaderboards; +using osu.Game.Scoring; +using osuTK; + +namespace osu.Game.Screens.Ranking.Expanded.Accuracy +{ + /// + /// Contains a that is positioned around the . + /// + public class RankBadge : CompositeDrawable + { + /// + /// The accuracy value corresponding to the displayed by this badge. + /// + public readonly float Accuracy; + + private readonly ScoreRank rank; + + private Drawable rankContainer; + private Drawable overlay; + + /// + /// Creates a new . + /// + /// The accuracy value corresponding to . + /// The to be displayed in this . + public RankBadge(float accuracy, ScoreRank rank) + { + Accuracy = accuracy; + this.rank = rank; + + RelativeSizeAxes = Axes.Both; + Alpha = 0; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = rankContainer = new Container + { + Origin = Anchor.Centre, + Size = new Vector2(28, 14), + Children = new[] + { + new DrawableRank(rank), + overlay = new CircularContainer + { + RelativeSizeAxes = Axes.Both, + Blending = BlendingParameters.Additive, + Masking = true, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = OsuColour.ForRank(rank).Opacity(0.2f), + Radius = 10, + }, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true, + } + } + } + }; + } + + /// + /// Shows this . + /// + public void Appear() + { + this.FadeIn(50); + overlay.FadeIn().FadeOut(500, Easing.In); + } + + protected override void Update() + { + base.Update(); + + // Starts at -90deg (top) and moves counter-clockwise by the accuracy + rankContainer.Position = circlePosition(-MathF.PI / 2 - (1 - Accuracy) * MathF.PI * 2); + } + + private Vector2 circlePosition(float t) + => DrawSize / 2 + new Vector2(MathF.Cos(t), MathF.Sin(t)) * DrawSize / 2; + } +} diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/RankNotch.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/RankNotch.cs new file mode 100644 index 0000000000..894790b5b6 --- /dev/null +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/RankNotch.cs @@ -0,0 +1,49 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osuTK; + +namespace osu.Game.Screens.Ranking.Expanded.Accuracy +{ + /// + /// A solid "notch" of the that appears at the ends of the rank circles to add separation. + /// + public class RankNotch : CompositeDrawable + { + private readonly float position; + + public RankNotch(float position) + { + this.position = position; + + RelativeSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Rotation = position * 360f, + Child = new Box + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.Y, + Height = AccuracyCircle.RANK_CIRCLE_RADIUS, + Width = 1f, + Colour = OsuColour.Gray(0.3f), + EdgeSmoothness = new Vector2(1f) + } + }; + } + } +} diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/RankText.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/RankText.cs new file mode 100644 index 0000000000..b803fe6022 --- /dev/null +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/RankText.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 osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.Leaderboards; +using osu.Game.Scoring; +using osuTK; + +namespace osu.Game.Screens.Ranking.Expanded.Accuracy +{ + /// + /// The text that appears in the middle of the displaying the user's rank. + /// + public class RankText : CompositeDrawable + { + private readonly ScoreRank rank; + + private Drawable flash; + + public RankText(ScoreRank rank) + { + this.rank = rank; + + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + Alpha = 0; + AutoSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChildren = new[] + { + new GlowingSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Spacing = new Vector2(-15, 0), + Text = DrawableRank.GetRankName(rank), + Font = OsuFont.Numeric.With(size: 76), + UseFullGlyphHeight = false + }, + flash = new BufferedContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + BlurSigma = new Vector2(35), + BypassAutoSizeAxes = Axes.Both, + RelativeSizeAxes = Axes.Both, + Blending = BlendingParameters.Additive, + Size = new Vector2(2f), + Scale = new Vector2(1.8f), + Children = new[] + { + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Spacing = new Vector2(-15, 0), + Text = DrawableRank.GetRankName(rank), + Font = OsuFont.Numeric.With(size: 76), + UseFullGlyphHeight = false, + Shadow = false + }, + }, + }, + }; + } + + public void Appear() + { + this.FadeIn(0, Easing.In); + + flash.FadeIn(0, Easing.In).Then().FadeOut(800, Easing.OutQuint); + } + } +} diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/SmoothCircularProgress.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/SmoothCircularProgress.cs new file mode 100644 index 0000000000..106af31cae --- /dev/null +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/SmoothCircularProgress.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 osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Transforms; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics; +using osuTK; + +namespace osu.Game.Screens.Ranking.Expanded.Accuracy +{ + /// + /// Contains a with smoothened edges. + /// + public class SmoothCircularProgress : CompositeDrawable + { + public Bindable Current + { + get => progress.Current; + set => progress.Current = value; + } + + public float InnerRadius + { + get => progress.InnerRadius; + set + { + progress.InnerRadius = value; + innerSmoothingContainer.Size = new Vector2(1 - value); + smoothingWedge.Height = value / 2; + } + } + + private readonly CircularProgress progress; + private readonly Container innerSmoothingContainer; + private readonly Drawable smoothingWedge; + + public SmoothCircularProgress() + { + Container smoothingWedgeContainer; + + InternalChild = new BufferedContainer + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + progress = new CircularProgress { RelativeSizeAxes = Axes.Both }, + smoothingWedgeContainer = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Child = smoothingWedge = new Box + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.Y, + Width = 1f, + EdgeSmoothness = new Vector2(2, 0), + } + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(-1), + Child = new CircularContainer + { + RelativeSizeAxes = Axes.Both, + BorderThickness = 2, + Masking = true, + BorderColour = OsuColour.Gray(0.5f).Opacity(0.75f), + Blending = new BlendingParameters + { + AlphaEquation = BlendingEquation.ReverseSubtract, + }, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true + } + } + }, + innerSmoothingContainer = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Size = Vector2.Zero, + Padding = new MarginPadding(-1), + Child = new CircularContainer + { + RelativeSizeAxes = Axes.Both, + BorderThickness = 2, + BorderColour = OsuColour.Gray(0.5f).Opacity(0.75f), + Masking = true, + Blending = new BlendingParameters + { + AlphaEquation = BlendingEquation.ReverseSubtract, + }, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true + } + } + }, + } + }; + + Current.BindValueChanged(c => + { + smoothingWedgeContainer.Alpha = c.NewValue > 0 ? 1 : 0; + smoothingWedgeContainer.Rotation = (float)(360 * c.NewValue); + }, true); + } + + public TransformSequence FillTo(double newValue, double duration = 0, Easing easing = Easing.None) + => progress.FillTo(newValue, duration, easing); + } +} From daa5e63d0d40e79787fed6d5729c9382854808b7 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 17 Mar 2020 16:42:55 +0900 Subject: [PATCH 0431/2376] Fix replay scores not being populated via player --- osu.Game/Screens/Play/Player.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index bcadba14af..79f92c3762 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -401,14 +401,18 @@ namespace osu.Game.Screens.Play protected virtual ScoreInfo CreateScore() { - var score = DrawableRuleset.ReplayScore?.ScoreInfo ?? new ScoreInfo + var score = new ScoreInfo { Beatmap = Beatmap.Value.BeatmapInfo, Ruleset = rulesetInfo, Mods = Mods.Value.ToArray(), - User = api.LocalUser.Value, }; + if (DrawableRuleset.ReplayScore != null) + score.User = DrawableRuleset.ReplayScore.ScoreInfo?.User ?? new GuestUser(); + else + score.User = api.LocalUser.Value; + ScoreProcessor.PopulateScore(score); return score; From d322c8c2d76b9030838cdaf3ad8a0543699ae539 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Mar 2020 16:47:23 +0900 Subject: [PATCH 0432/2376] 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 66a1523843..942970c890 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 647f05b428..54f1ad2845 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -23,7 +23,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 0e5c64cf0f..816a430b52 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + @@ -79,7 +79,7 @@ - + From 05789e6fe4c91b20edb3ce3bcdb3a8c4e6c986d6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 17 Mar 2020 16:59:34 +0900 Subject: [PATCH 0433/2376] Implement the score panel --- .../Visual/Ranking/TestSceneScorePanel.cs | 143 +++++++++++ .../Expanded/ExpandedPanelMiddleContent.cs | 15 ++ .../Expanded/ExpandedPanelTopContent.cs | 15 ++ osu.Game/Screens/Ranking/PanelState.cs | 11 + osu.Game/Screens/Ranking/ScorePanel.cs | 223 ++++++++++++++++++ 5 files changed, 407 insertions(+) create mode 100644 osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs create mode 100644 osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs create mode 100644 osu.Game/Screens/Ranking/Expanded/ExpandedPanelTopContent.cs create mode 100644 osu.Game/Screens/Ranking/PanelState.cs create mode 100644 osu.Game/Screens/Ranking/ScorePanel.cs diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs new file mode 100644 index 0000000000..1e55885385 --- /dev/null +++ b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs @@ -0,0 +1,143 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Screens.Ranking; +using osu.Game.Screens.Ranking.Expanded; +using osu.Game.Tests.Beatmaps; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual.Ranking +{ + public class TestSceneScorePanel : OsuTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(ScorePanel), + typeof(PanelState), + typeof(ExpandedPanelMiddleContent), + typeof(ExpandedPanelTopContent), + }; + + [Test] + public void TestDRank() + { + var score = createScore(); + score.Accuracy = 0.5; + score.Rank = ScoreRank.D; + + addPanelStep(score); + } + + [Test] + public void TestCRank() + { + var score = createScore(); + score.Accuracy = 0.75; + score.Rank = ScoreRank.C; + + addPanelStep(score); + } + + [Test] + public void TestBRank() + { + var score = createScore(); + score.Accuracy = 0.85; + score.Rank = ScoreRank.B; + + addPanelStep(score); + } + + [Test] + public void TestARank() + { + var score = createScore(); + score.Accuracy = 0.925; + score.Rank = ScoreRank.A; + + addPanelStep(score); + } + + [Test] + public void TestSRank() + { + var score = createScore(); + score.Accuracy = 0.975; + score.Rank = ScoreRank.S; + + addPanelStep(score); + } + + [Test] + public void TestAlmostSSRank() + { + var score = createScore(); + score.Accuracy = 0.9999; + score.Rank = ScoreRank.S; + + addPanelStep(score); + } + + [Test] + public void TestSSRank() + { + var score = createScore(); + score.Accuracy = 1; + score.Rank = ScoreRank.X; + + addPanelStep(score); + } + + [Test] + public void TestAllHitResults() + { + var score = createScore(); + score.Statistics[HitResult.Perfect] = 350; + score.Statistics[HitResult.Ok] = 200; + + addPanelStep(score); + } + + private void addPanelStep(ScoreInfo score) => AddStep("add panel", () => + { + Child = new ScorePanel(score) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + State = PanelState.Expanded + }; + }); + + private ScoreInfo createScore() => new ScoreInfo + { + User = new User + { + Id = 2, + Username = "peppy", + }, + Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, + Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }, + TotalScore = 2845370, + Accuracy = 0.95, + MaxCombo = 999, + Rank = ScoreRank.S, + Date = DateTimeOffset.Now, + Statistics = + { + { HitResult.Miss, 1 }, + { HitResult.Meh, 50 }, + { HitResult.Good, 100 }, + { HitResult.Great, 300 }, + } + }; + } +} diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs new file mode 100644 index 0000000000..c41829051a --- /dev/null +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -0,0 +1,15 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics.Containers; +using osu.Game.Scoring; + +namespace osu.Game.Screens.Ranking.Expanded +{ + public class ExpandedPanelMiddleContent : CompositeDrawable + { + public ExpandedPanelMiddleContent(ScoreInfo score) + { + } + } +} diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelTopContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelTopContent.cs new file mode 100644 index 0000000000..064d1ed7b9 --- /dev/null +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelTopContent.cs @@ -0,0 +1,15 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics.Containers; +using osu.Game.Users; + +namespace osu.Game.Screens.Ranking.Expanded +{ + public class ExpandedPanelTopContent : CompositeDrawable + { + public ExpandedPanelTopContent(User user) + { + } + } +} diff --git a/osu.Game/Screens/Ranking/PanelState.cs b/osu.Game/Screens/Ranking/PanelState.cs new file mode 100644 index 0000000000..94e2c7cef4 --- /dev/null +++ b/osu.Game/Screens/Ranking/PanelState.cs @@ -0,0 +1,11 @@ +// 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.Screens.Ranking +{ + public enum PanelState + { + Expanded, + Contracted + } +} diff --git a/osu.Game/Screens/Ranking/ScorePanel.cs b/osu.Game/Screens/Ranking/ScorePanel.cs new file mode 100644 index 0000000000..a1adfcc500 --- /dev/null +++ b/osu.Game/Screens/Ranking/ScorePanel.cs @@ -0,0 +1,223 @@ +// 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; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Scoring; +using osu.Game.Screens.Ranking.Expanded; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.Ranking +{ + public class ScorePanel : CompositeDrawable, IStateful + { + /// + /// Width of the panel when contracted. + /// + private const float contracted_width = 160; + + /// + /// Height of the panel when contracted. + /// + private const float contracted_height = 320; + + /// + /// Width of the panel when expanded. + /// + private const float expanded_width = 360; + + /// + /// Height of the panel when expanded. + /// + private const float expanded_height = 560; + + /// + /// Height of the top layer when the panel is expanded. + /// + private const float expanded_top_layer_height = 53; + + /// + /// Height of the top layer when the panel is contracted. + /// + private const float contracted_top_layer_height = 40; + + /// + /// Duration for the panel to resize into its expanded/contracted size. + /// + private const double resize_duration = 200; + + /// + /// Delay after before the top layer is expanded. + /// + private const double top_layer_expand_delay = 100; + + /// + /// Duration for the top layer expansion. + /// + private const double top_layer_expand_duration = 200; + + /// + /// Duration for the panel contents to fade in. + /// + private const double content_fade_duration = 50; + + private static readonly ColourInfo expanded_top_layer_colour = ColourInfo.GradientVertical(Color4Extensions.FromHex("#444"), Color4Extensions.FromHex("#333")); + private static readonly ColourInfo expanded_middle_layer_colour = ColourInfo.GradientVertical(Color4Extensions.FromHex("#555"), Color4Extensions.FromHex("#333")); + private static readonly Color4 contracted_top_layer_colour = Color4Extensions.FromHex("#353535"); + private static readonly Color4 contracted_middle_layer_colour = Color4Extensions.FromHex("#444"); + + public event Action StateChanged; + + private readonly ScoreInfo score; + + private Container topLayerContainer; + private Drawable topLayerBackground; + private Container topLayerContentContainer; + private Drawable topLayerContent; + + private Container middleLayerContainer; + private Drawable middleLayerBackground; + private Container middleLayerContentContainer; + private Drawable middleLayerContent; + + public ScorePanel(ScoreInfo score) + { + this.score = score; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChildren = new Drawable[] + { + topLayerContainer = new Container + { + Name = "Top layer", + RelativeSizeAxes = Axes.X, + Height = 120, + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + CornerRadius = 20, + CornerExponent = 2.5f, + Masking = true, + Child = topLayerBackground = new Box { RelativeSizeAxes = Axes.Both } + }, + topLayerContentContainer = new Container { RelativeSizeAxes = Axes.Both } + } + }, + middleLayerContainer = new Container + { + Name = "Middle layer", + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + CornerRadius = 20, + CornerExponent = 2.5f, + Masking = true, + Child = middleLayerBackground = new Box { RelativeSizeAxes = Axes.Both } + }, + middleLayerContentContainer = new Container { RelativeSizeAxes = Axes.Both } + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + if (state == PanelState.Expanded) + { + topLayerBackground.FadeColour(expanded_top_layer_colour); + middleLayerBackground.FadeColour(expanded_middle_layer_colour); + } + else + { + topLayerBackground.FadeColour(contracted_top_layer_colour); + middleLayerBackground.FadeColour(contracted_middle_layer_colour); + } + + updateState(); + } + + private PanelState state = PanelState.Contracted; + + public PanelState State + { + get => state; + set + { + if (state == value) + return; + + state = value; + + if (LoadState >= LoadState.Ready) + updateState(); + + StateChanged?.Invoke(value); + } + } + + private void updateState() + { + topLayerContainer.MoveToY(0, resize_duration, Easing.OutQuint); + middleLayerContainer.MoveToY(0, resize_duration, Easing.OutQuint); + + topLayerContent?.FadeOut(content_fade_duration).Expire(); + middleLayerContent?.FadeOut(content_fade_duration).Expire(); + + switch (state) + { + case PanelState.Expanded: + this.ResizeTo(new Vector2(expanded_width, expanded_height), resize_duration, Easing.OutQuint); + + topLayerBackground.FadeColour(expanded_top_layer_colour, resize_duration, Easing.OutQuint); + middleLayerBackground.FadeColour(expanded_middle_layer_colour, resize_duration, Easing.OutQuint); + + topLayerContentContainer.Add(middleLayerContent = new ExpandedPanelTopContent(score.User).With(d => d.Alpha = 0)); + middleLayerContentContainer.Add(topLayerContent = new ExpandedPanelMiddleContent(score).With(d => d.Alpha = 0)); + break; + + case PanelState.Contracted: + this.ResizeTo(new Vector2(contracted_width, contracted_height), resize_duration, Easing.OutQuint); + + topLayerBackground.FadeColour(contracted_top_layer_colour, resize_duration, Easing.OutQuint); + middleLayerBackground.FadeColour(contracted_middle_layer_colour, resize_duration, Easing.OutQuint); + break; + } + + using (BeginDelayedSequence(resize_duration + top_layer_expand_delay, true)) + { + switch (state) + { + case PanelState.Expanded: + topLayerContainer.MoveToY(-expanded_top_layer_height / 2, top_layer_expand_duration, Easing.OutQuint); + middleLayerContainer.MoveToY(expanded_top_layer_height / 2, top_layer_expand_duration, Easing.OutQuint); + break; + + case PanelState.Contracted: + topLayerContainer.MoveToY(-contracted_top_layer_height / 2, top_layer_expand_duration, Easing.OutQuint); + middleLayerContainer.MoveToY(contracted_top_layer_height / 2, top_layer_expand_duration, Easing.OutQuint); + break; + } + + topLayerContent?.FadeIn(content_fade_duration); + middleLayerContent?.FadeIn(content_fade_duration); + } + } + } +} From 7cc1a6040fca3fff5f6870252662c3f4e575086c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 17 Mar 2020 17:01:38 +0900 Subject: [PATCH 0434/2376] Implement top panel contents --- .../TestSceneExpandedPanelTopContent.cs | 35 ++++++++++++++++ .../Expanded/ExpandedPanelTopContent.cs | 42 +++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelTopContent.cs diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelTopContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelTopContent.cs new file mode 100644 index 0000000000..afaa607099 --- /dev/null +++ b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelTopContent.cs @@ -0,0 +1,35 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Screens.Ranking.Expanded; +using osu.Game.Users; +using osuTK; + +namespace osu.Game.Tests.Visual.Ranking +{ + public class TestSceneExpandedPanelTopContent : OsuTestScene + { + public TestSceneExpandedPanelTopContent() + { + Child = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(500, 200), + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#444"), + }, + new ExpandedPanelTopContent(new User { Id = 2, Username = "peppy" }), + } + }; + } + } +} diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelTopContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelTopContent.cs index 064d1ed7b9..a9853c217c 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelTopContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelTopContent.cs @@ -1,15 +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 osu.Framework.Allocation; +using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; using osu.Game.Users; +using osu.Game.Users.Drawables; +using osuTK; namespace osu.Game.Screens.Ranking.Expanded { public class ExpandedPanelTopContent : CompositeDrawable { + private readonly User user; + public ExpandedPanelTopContent(User user) { + this.user = user; + Anchor = Anchor.TopCentre; + Origin = Anchor.Centre; + + AutoSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new UpdateableAvatar(user) + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Size = new Vector2(80), + CornerRadius = 20, + CornerExponent = 2.5f, + Masking = true, + }, + new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = user.Username, + Font = OsuFont.Torus.With(size: 16, weight: FontWeight.SemiBold) + } + } + }; } } } From e56d0f2eeaaed4fce4faded742b5fdb6ac3b93d2 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 17 Mar 2020 17:16:10 +0900 Subject: [PATCH 0435/2376] Add black font weighting --- osu.Game/Graphics/OsuFont.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Graphics/OsuFont.cs b/osu.Game/Graphics/OsuFont.cs index 255f7f24f7..7c78141b4d 100644 --- a/osu.Game/Graphics/OsuFont.cs +++ b/osu.Game/Graphics/OsuFont.cs @@ -136,5 +136,10 @@ namespace osu.Game.Graphics /// Equivalent to weight 700. /// Bold = 700, + + /// + /// Equivalent to weight 900. + /// + Black = 900 } } From 1521f25c96eec4c30119252d7a51311794a1c580 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 17 Mar 2020 17:25:24 +0900 Subject: [PATCH 0436/2376] Implement middle panel contents --- .../TestSceneExpandedPanelMiddleContent.cs | 80 +++++++ .../Expanded/ExpandedPanelMiddleContent.cs | 204 ++++++++++++++++++ .../Ranking/Expanded/StarRatingDisplay.cs | 95 ++++++++ .../Expanded/Statistics/AccuracyStatistic.cs | 51 +++++ .../Expanded/Statistics/ComboStatistic.cs | 66 ++++++ .../Expanded/Statistics/CounterStatistic.cs | 48 +++++ .../Expanded/Statistics/StatisticDisplay.cs | 82 +++++++ .../Ranking/Expanded/TotalScoreCounter.cs | 35 +++ 8 files changed, 661 insertions(+) create mode 100644 osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs create mode 100644 osu.Game/Screens/Ranking/Expanded/StarRatingDisplay.cs create mode 100644 osu.Game/Screens/Ranking/Expanded/Statistics/AccuracyStatistic.cs create mode 100644 osu.Game/Screens/Ranking/Expanded/Statistics/ComboStatistic.cs create mode 100644 osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs create mode 100644 osu.Game/Screens/Ranking/Expanded/Statistics/StatisticDisplay.cs create mode 100644 osu.Game/Screens/Ranking/Expanded/TotalScoreCounter.cs diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs new file mode 100644 index 0000000000..665b3ad455 --- /dev/null +++ b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs @@ -0,0 +1,80 @@ +// 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.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Screens.Ranking.Expanded; +using osu.Game.Screens.Ranking.Expanded.Accuracy; +using osu.Game.Screens.Ranking.Expanded.Statistics; +using osu.Game.Tests.Beatmaps; +using osu.Game.Users; +using osuTK; + +namespace osu.Game.Tests.Visual.Ranking +{ + public class TestSceneExpandedPanelMiddleContent : OsuTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(ExpandedPanelMiddleContent), + typeof(AccuracyCircle), + typeof(AccuracyStatistic), + typeof(ComboStatistic), + typeof(CounterStatistic), + typeof(StarRatingDisplay), + typeof(StatisticDisplay), + typeof(TotalScoreCounter) + }; + + public TestSceneExpandedPanelMiddleContent() + { + Child = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(500, 700), + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#444"), + }, + new ExpandedPanelMiddleContent(createTestScore()) + } + }; + } + + private ScoreInfo createTestScore() => new ScoreInfo + { + User = new User + { + Id = 2, + Username = "peppy", + }, + Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, + Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }, + TotalScore = 999999, + Accuracy = 0.95, + MaxCombo = 999, + Rank = ScoreRank.S, + Date = DateTimeOffset.Now, + Statistics = + { + { HitResult.Miss, 1 }, + { HitResult.Meh, 50 }, + { HitResult.Good, 100 }, + { HitResult.Great, 300 }, + } + }; + } +} diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index c41829051a..4f45b1c5d7 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -1,15 +1,219 @@ // 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.Extensions; +using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; using osu.Game.Scoring; +using osu.Game.Screens.Play.HUD; +using osu.Game.Screens.Ranking.Expanded.Accuracy; +using osu.Game.Screens.Ranking.Expanded.Statistics; +using osuTK; namespace osu.Game.Screens.Ranking.Expanded { public class ExpandedPanelMiddleContent : CompositeDrawable { + private readonly ScoreInfo score; + + private readonly List statisticDisplays = new List(); + private RollingCounter scoreCounter; + public ExpandedPanelMiddleContent(ScoreInfo score) { + this.score = score; + + RelativeSizeAxes = Axes.Both; + Masking = true; + + Padding = new MarginPadding { Vertical = 10, Horizontal = 10 }; + } + + [BackgroundDependencyLoader] + private void load() + { + var topStatistics = new List + { + new AccuracyStatistic(score.Accuracy), + new ComboStatistic(score.MaxCombo, true), + new CounterStatistic("pp", (int)(score.PP ?? 0)), + }; + + var bottomStatistics = new List(); + foreach (var stat in score.SortedStatistics) + bottomStatistics.Add(new CounterStatistic(stat.Key.GetDescription(), stat.Value)); + + statisticDisplays.AddRange(topStatistics); + statisticDisplays.AddRange(bottomStatistics); + + InternalChild = new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(20), + Children = new Drawable[] + { + new FillFlowContainer + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = new LocalisedString((score.Beatmap.Metadata.Title, score.Beatmap.Metadata.TitleUnicode)), + Font = OsuFont.Torus.With(size: 20, weight: FontWeight.SemiBold), + }, + new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = new LocalisedString((score.Beatmap.Metadata.Artist, score.Beatmap.Metadata.ArtistUnicode)), + Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold) + }, + new Container + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Margin = new MarginPadding { Top = 40 }, + RelativeSizeAxes = Axes.X, + Height = 230, + Child = new AccuracyCircle(score) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fit, + } + }, + scoreCounter = new TotalScoreCounter + { + Margin = new MarginPadding { Top = 0, Bottom = 5 }, + Current = { Value = 0 }, + Alpha = 0, + AlwaysPresent = true + }, + new FillFlowContainer + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new StarRatingDisplay(score.Beatmap) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft + }, + new ModDisplay + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + DisplayUnrankedText = false, + ExpansionMode = ExpansionMode.AlwaysExpanded, + Scale = new Vector2(0.5f), + Current = { Value = score.Mods } + } + } + }, + new FillFlowContainer + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Direction = FillDirection.Vertical, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = score.Beatmap.Version, + Font = OsuFont.Torus.With(size: 16, weight: FontWeight.SemiBold), + }, + new OsuTextFlowContainer(s => s.Font = OsuFont.Torus.With(size: 12)) + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + }.With(t => + { + t.AddText("mapped by "); + t.AddText(score.UserString, s => s.Font = s.Font.With(weight: FontWeight.SemiBold)); + }) + } + }, + } + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 5), + Children = new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Content = new[] { topStatistics.Cast().ToArray() }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + } + }, + new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Content = new[] { bottomStatistics.Cast().ToArray() }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + } + } + } + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + // Score counter value setting must be scheduled so it isn't transferred instantaneously + ScheduleAfterChildren(() => + { + using (BeginDelayedSequence(AccuracyCircle.ACCURACY_TRANSFORM_DELAY, true)) + { + scoreCounter.FadeIn(); + scoreCounter.Current.Value = score.TotalScore; + + double delay = 0; + + foreach (var stat in statisticDisplays) + { + using (BeginDelayedSequence(delay, true)) + stat.Appear(); + + delay += 200; + } + } + }); } } } diff --git a/osu.Game/Screens/Ranking/Expanded/StarRatingDisplay.cs b/osu.Game/Screens/Ranking/Expanded/StarRatingDisplay.cs new file mode 100644 index 0000000000..87d9828707 --- /dev/null +++ b/osu.Game/Screens/Ranking/Expanded/StarRatingDisplay.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 System.Globalization; +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.Beatmaps; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.Ranking.Expanded +{ + public class StarRatingDisplay : CompositeDrawable + { + private readonly BeatmapInfo beatmap; + + public StarRatingDisplay(BeatmapInfo beatmap) + { + this.beatmap = beatmap; + AutoSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + var starRatingParts = beatmap.StarDifficulty.ToString("0.00", CultureInfo.InvariantCulture).Split('.'); + string wholePart = starRatingParts[0]; + string fractionPart = starRatingParts[1]; + string separator = CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator; + + InternalChildren = new Drawable[] + { + new CircularContainer + { + RelativeSizeAxes = Axes.Both, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colours.ForDifficultyRating(beatmap.DifficultyRating) + }, + } + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Padding = new MarginPadding { Horizontal = 8, Vertical = 4 }, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(2, 0), + Children = new Drawable[] + { + new SpriteIcon + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(7), + Icon = FontAwesome.Solid.Star, + Colour = Color4.Black + }, + new OsuTextFlowContainer(s => s.Font = OsuFont.Numeric.With(weight: FontWeight.Black)) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + TextAnchor = Anchor.BottomLeft, + }.With(t => + { + t.AddText($"{wholePart}", s => + { + s.Colour = Color4.Black; + s.Font = s.Font.With(size: 14); + s.UseFullGlyphHeight = false; + }); + + t.AddText($"{separator}{fractionPart}", s => + { + s.Colour = Color4.Black; + s.Font = s.Font.With(size: 7); + s.UseFullGlyphHeight = false; + }); + }) + } + } + }; + } + } +} diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/AccuracyStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/AccuracyStatistic.cs new file mode 100644 index 0000000000..2f7fc3a4fd --- /dev/null +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/AccuracyStatistic.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 osu.Framework.Graphics; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; +using osu.Game.Screens.Ranking.Expanded.Accuracy; +using osu.Game.Utils; +using osuTK; + +namespace osu.Game.Screens.Ranking.Expanded.Statistics +{ + public class AccuracyStatistic : StatisticDisplay + { + private readonly double accuracy; + + private RollingCounter counter; + + public AccuracyStatistic(double accuracy) + : base("accuracy") + { + this.accuracy = accuracy; + } + + public override void Appear() + { + base.Appear(); + counter.Current.Value = accuracy; + } + + protected override Drawable CreateContent() => counter = new Counter(); + + private class Counter : RollingCounter + { + protected override double RollingDuration => AccuracyCircle.ACCURACY_TRANSFORM_DURATION; + + protected override Easing RollingEasing => AccuracyCircle.ACCURACY_TRANSFORM_EASING; + + public Counter() + { + DisplayedCountSpriteText.Font = OsuFont.Torus.With(size: 20, fixedWidth: true); + DisplayedCountSpriteText.Spacing = new Vector2(-2, 0); + } + + protected override string FormatCount(double count) => count.FormatAccuracy(); + + public override void Increment(double amount) + => Current.Value += amount; + } + } +} diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/ComboStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/ComboStatistic.cs new file mode 100644 index 0000000000..ce5a15da01 --- /dev/null +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/ComboStatistic.cs @@ -0,0 +1,66 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Screens.Ranking.Expanded.Accuracy; +using osuTK; + +namespace osu.Game.Screens.Ranking.Expanded.Statistics +{ + public class ComboStatistic : CounterStatistic + { + private readonly bool isPerfect; + + private Drawable perfectText; + + public ComboStatistic(int combo, bool isPerfect) + : base("combo", combo) + { + this.isPerfect = isPerfect; + } + + public override void Appear() + { + base.Appear(); + + if (isPerfect) + { + using (BeginDelayedSequence(AccuracyCircle.ACCURACY_TRANSFORM_DURATION / 2, true)) + perfectText.FadeIn(50); + } + } + + protected override Drawable CreateContent() + { + return new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), + Children = new[] + { + base.CreateContent().With(d => + { + Anchor = Anchor.CentreLeft; + Origin = Anchor.CentreLeft; + }), + perfectText = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Text = "PERFECT", + Font = OsuFont.Torus.With(size: 11, weight: FontWeight.SemiBold), + Colour = ColourInfo.GradientVertical(Color4Extensions.FromHex("#66FFCC"), Color4Extensions.FromHex("#FF9AD7")), + Alpha = 0, + UseFullGlyphHeight = false, + } + } + }; + } + } +} diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs new file mode 100644 index 0000000000..ee07ea326d --- /dev/null +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.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.Graphics; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; +using osu.Game.Screens.Ranking.Expanded.Accuracy; +using osuTK; + +namespace osu.Game.Screens.Ranking.Expanded.Statistics +{ + public class CounterStatistic : StatisticDisplay + { + private readonly int count; + + private RollingCounter counter; + + public CounterStatistic(string header, int count) + : base(header) + { + this.count = count; + } + + public override void Appear() + { + base.Appear(); + counter.Current.Value = count; + } + + protected override Drawable CreateContent() => counter = new Counter(); + + private class Counter : RollingCounter + { + protected override double RollingDuration => AccuracyCircle.ACCURACY_TRANSFORM_DURATION; + + protected override Easing RollingEasing => AccuracyCircle.ACCURACY_TRANSFORM_EASING; + + public Counter() + { + DisplayedCountSpriteText.Font = OsuFont.Torus.With(size: 20, fixedWidth: true); + DisplayedCountSpriteText.Spacing = new Vector2(-2, 0); + } + + public override void Increment(int amount) + => Current.Value += amount; + } + } +} diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/StatisticDisplay.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/StatisticDisplay.cs new file mode 100644 index 0000000000..55015b432b --- /dev/null +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/StatisticDisplay.cs @@ -0,0 +1,82 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; + +namespace osu.Game.Screens.Ranking.Expanded.Statistics +{ + public abstract class StatisticDisplay : CompositeDrawable + { + private readonly string header; + + private Drawable content; + + protected StatisticDisplay(string header) + { + this.header = header; + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Children = new[] + { + new CircularContainer + { + RelativeSizeAxes = Axes.X, + Height = 12, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#222") + }, + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold), + Text = header.ToUpperInvariant(), + } + } + }, + new Container + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + AutoSizeAxes = Axes.Both, + Children = new[] + { + content = CreateContent().With(d => + { + d.Anchor = Anchor.TopCentre; + d.Origin = Anchor.TopCentre; + d.Alpha = 0; + d.AlwaysPresent = true; + }), + } + } + } + }; + } + + public virtual void Appear() => content.FadeIn(100); + + protected abstract Drawable CreateContent(); + } +} diff --git a/osu.Game/Screens/Ranking/Expanded/TotalScoreCounter.cs b/osu.Game/Screens/Ranking/Expanded/TotalScoreCounter.cs new file mode 100644 index 0000000000..d230e56649 --- /dev/null +++ b/osu.Game/Screens/Ranking/Expanded/TotalScoreCounter.cs @@ -0,0 +1,35 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; +using osu.Game.Screens.Ranking.Expanded.Accuracy; +using osuTK; + +namespace osu.Game.Screens.Ranking.Expanded +{ + public class TotalScoreCounter : RollingCounter + { + protected override double RollingDuration => AccuracyCircle.ACCURACY_TRANSFORM_DURATION; + + protected override Easing RollingEasing => AccuracyCircle.ACCURACY_TRANSFORM_EASING; + + public TotalScoreCounter() + { + // Todo: AutoSize X removed here due to https://github.com/ppy/osu-framework/issues/3369 + AutoSizeAxes = Axes.Y; + RelativeSizeAxes = Axes.X; + DisplayedCountSpriteText.Anchor = Anchor.TopCentre; + DisplayedCountSpriteText.Origin = Anchor.TopCentre; + + DisplayedCountSpriteText.Font = OsuFont.Torus.With(size: 60, weight: FontWeight.Light, fixedWidth: true); + DisplayedCountSpriteText.Spacing = new Vector2(-5, 0); + } + + protected override string FormatCount(long count) => count.ToString("N0"); + + public override void Increment(long amount) + => Current.Value += amount; + } +} From 2ee480f1d8aaaec127856a7f0a8234238c9391fa Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 17 Mar 2020 17:34:16 +0900 Subject: [PATCH 0437/2376] Add xmldocs / cleanup --- .../Expanded/ExpandedPanelMiddleContent.cs | 7 +++ .../Ranking/Expanded/StarRatingDisplay.cs | 7 +++ .../Expanded/Statistics/AccuracyStatistic.cs | 7 +++ .../Expanded/Statistics/ComboStatistic.cs | 51 ++++++++++--------- .../Expanded/Statistics/CounterStatistic.cs | 8 +++ .../Expanded/Statistics/StatisticDisplay.cs | 13 +++++ .../Ranking/Expanded/TotalScoreCounter.cs | 3 ++ 7 files changed, 73 insertions(+), 23 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index 4f45b1c5d7..6d5d7e0d95 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -20,6 +20,9 @@ using osuTK; namespace osu.Game.Screens.Ranking.Expanded { + /// + /// The content that appears in the middle section of the . + /// public class ExpandedPanelMiddleContent : CompositeDrawable { private readonly ScoreInfo score; @@ -27,6 +30,10 @@ namespace osu.Game.Screens.Ranking.Expanded private readonly List statisticDisplays = new List(); private RollingCounter scoreCounter; + /// + /// Creates a new . + /// + /// The score to display. public ExpandedPanelMiddleContent(ScoreInfo score) { this.score = score; diff --git a/osu.Game/Screens/Ranking/Expanded/StarRatingDisplay.cs b/osu.Game/Screens/Ranking/Expanded/StarRatingDisplay.cs index 87d9828707..74b58b9f8c 100644 --- a/osu.Game/Screens/Ranking/Expanded/StarRatingDisplay.cs +++ b/osu.Game/Screens/Ranking/Expanded/StarRatingDisplay.cs @@ -15,10 +15,17 @@ using osuTK.Graphics; namespace osu.Game.Screens.Ranking.Expanded { + /// + /// A pill that displays the star rating of a . + /// public class StarRatingDisplay : CompositeDrawable { private readonly BeatmapInfo beatmap; + /// + /// Creates a new . + /// + /// The to display the star difficulty of. public StarRatingDisplay(BeatmapInfo beatmap) { this.beatmap = beatmap; diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/AccuracyStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/AccuracyStatistic.cs index 2f7fc3a4fd..2a0e33aab7 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/AccuracyStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/AccuracyStatistic.cs @@ -10,12 +10,19 @@ using osuTK; namespace osu.Game.Screens.Ranking.Expanded.Statistics { + /// + /// A to display the player's accuracy. + /// public class AccuracyStatistic : StatisticDisplay { private readonly double accuracy; private RollingCounter counter; + /// + /// Creates a new . + /// + /// The accuracy to display. public AccuracyStatistic(double accuracy) : base("accuracy") { diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/ComboStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/ComboStatistic.cs index ce5a15da01..e13138c5a0 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/ComboStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/ComboStatistic.cs @@ -12,12 +12,20 @@ using osuTK; namespace osu.Game.Screens.Ranking.Expanded.Statistics { + /// + /// A to display the player's combo. + /// public class ComboStatistic : CounterStatistic { private readonly bool isPerfect; private Drawable perfectText; + /// + /// Creates a new . + /// + /// The combo to be displayed. + /// Whether this is a perfect combo. public ComboStatistic(int combo, bool isPerfect) : base("combo", combo) { @@ -35,32 +43,29 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics } } - protected override Drawable CreateContent() + protected override Drawable CreateContent() => new FillFlowContainer { - return new FillFlowContainer + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), + Children = new[] { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10, 0), - Children = new[] + base.CreateContent().With(d => { - base.CreateContent().With(d => - { - Anchor = Anchor.CentreLeft; - Origin = Anchor.CentreLeft; - }), - perfectText = new OsuSpriteText - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Text = "PERFECT", - Font = OsuFont.Torus.With(size: 11, weight: FontWeight.SemiBold), - Colour = ColourInfo.GradientVertical(Color4Extensions.FromHex("#66FFCC"), Color4Extensions.FromHex("#FF9AD7")), - Alpha = 0, - UseFullGlyphHeight = false, - } + Anchor = Anchor.CentreLeft; + Origin = Anchor.CentreLeft; + }), + perfectText = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Text = "PERFECT", + Font = OsuFont.Torus.With(size: 11, weight: FontWeight.SemiBold), + Colour = ColourInfo.GradientVertical(Color4Extensions.FromHex("#66FFCC"), Color4Extensions.FromHex("#FF9AD7")), + Alpha = 0, + UseFullGlyphHeight = false, } - }; - } + } + }; } } diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs index ee07ea326d..817cc9b8c2 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs @@ -9,12 +9,20 @@ using osuTK; namespace osu.Game.Screens.Ranking.Expanded.Statistics { + /// + /// A to display general numeric values. + /// public class CounterStatistic : StatisticDisplay { private readonly int count; private RollingCounter counter; + /// + /// Creates a new . + /// + /// The name of the statistic. + /// The value to display. public CounterStatistic(string header, int count) : base(header) { diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/StatisticDisplay.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/StatisticDisplay.cs index 55015b432b..a653cc82d4 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/StatisticDisplay.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/StatisticDisplay.cs @@ -11,12 +11,19 @@ using osu.Game.Graphics.Sprites; namespace osu.Game.Screens.Ranking.Expanded.Statistics { + /// + /// A statistic from the score to be displayed in the . + /// public abstract class StatisticDisplay : CompositeDrawable { private readonly string header; private Drawable content; + /// + /// Creates a new . + /// + /// The name of the statistic. protected StatisticDisplay(string header) { this.header = header; @@ -75,8 +82,14 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics }; } + /// + /// Shows the statistic value. + /// public virtual void Appear() => content.FadeIn(100); + /// + /// Creates the content for this . + /// protected abstract Drawable CreateContent(); } } diff --git a/osu.Game/Screens/Ranking/Expanded/TotalScoreCounter.cs b/osu.Game/Screens/Ranking/Expanded/TotalScoreCounter.cs index d230e56649..cab04edb8b 100644 --- a/osu.Game/Screens/Ranking/Expanded/TotalScoreCounter.cs +++ b/osu.Game/Screens/Ranking/Expanded/TotalScoreCounter.cs @@ -9,6 +9,9 @@ using osuTK; namespace osu.Game.Screens.Ranking.Expanded { + /// + /// A counter for the player's total score to be displayed in the . + /// public class TotalScoreCounter : RollingCounter { protected override double RollingDuration => AccuracyCircle.ACCURACY_TRANSFORM_DURATION; From 6f801e1695c632a9a703ced501f4814f63ffacec Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 17 Mar 2020 17:35:14 +0900 Subject: [PATCH 0438/2376] Add xmldoc --- .../Screens/Ranking/Expanded/ExpandedPanelTopContent.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelTopContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelTopContent.cs index a9853c217c..5dfc43cc29 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelTopContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelTopContent.cs @@ -12,10 +12,17 @@ using osuTK; namespace osu.Game.Screens.Ranking.Expanded { + /// + /// The content that appears in the middle section of the . + /// public class ExpandedPanelTopContent : CompositeDrawable { private readonly User user; + /// + /// Creates a new . + /// + /// The to display. public ExpandedPanelTopContent(User user) { this.user = user; From 1c4296f5e7826689fd6ce4f483e13cafcea21988 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 17 Mar 2020 17:43:16 +0900 Subject: [PATCH 0439/2376] Implement the new results screen --- .../Background/TestSceneUserDimBackgrounds.cs | 5 +- .../Gameplay/TestSceneReplayDownloadButton.cs | 2 +- .../Multiplayer/TestSceneMatchResults.cs | 106 ----- .../TestSceneResultsScreen.cs} | 15 +- .../Screens/Multi/Play/TimeshiftPlayer.cs | 4 - .../Screens/Multi/Ranking/MatchResults.cs | 26 -- .../Ranking/Pages/RoomLeaderboardPage.cs | 135 ------ .../Ranking/Types/RoomLeaderboardPageInfo.cs | 29 -- osu.Game/Screens/Play/Player.cs | 2 +- osu.Game/Screens/Play/SoloResults.cs | 24 - osu.Game/Screens/Ranking/IResultPageInfo.cs | 16 - .../Ranking/Pages/LocalLeaderboardPage.cs | 43 -- .../Screens/Ranking/Pages/ScoreResultsPage.cs | 428 ------------------ .../{Pages => }/ReplayDownloadButton.cs | 2 +- osu.Game/Screens/Ranking/ResultModeButton.cs | 97 ---- .../Screens/Ranking/ResultModeTabControl.cs | 30 -- osu.Game/Screens/Ranking/Results.cs | 291 ------------ osu.Game/Screens/Ranking/ResultsPage.cs | 92 ---- osu.Game/Screens/Ranking/ResultsScreen.cs | 97 ++++ .../Ranking/{Pages => }/RetryButton.cs | 2 +- .../Ranking/Types/LocalLeaderboardPageInfo.cs | 28 -- .../Ranking/Types/ScoreOverviewPageInfo.cs | 30 -- osu.Game/Screens/Select/PlaySongSelect.cs | 3 +- 23 files changed, 113 insertions(+), 1394 deletions(-) delete mode 100644 osu.Game.Tests/Visual/Multiplayer/TestSceneMatchResults.cs rename osu.Game.Tests/Visual/{Gameplay/TestSceneResults.cs => Ranking/TestSceneResultsScreen.cs} (91%) delete mode 100644 osu.Game/Screens/Multi/Ranking/MatchResults.cs delete mode 100644 osu.Game/Screens/Multi/Ranking/Pages/RoomLeaderboardPage.cs delete mode 100644 osu.Game/Screens/Multi/Ranking/Types/RoomLeaderboardPageInfo.cs delete mode 100644 osu.Game/Screens/Play/SoloResults.cs delete mode 100644 osu.Game/Screens/Ranking/IResultPageInfo.cs delete mode 100644 osu.Game/Screens/Ranking/Pages/LocalLeaderboardPage.cs delete mode 100644 osu.Game/Screens/Ranking/Pages/ScoreResultsPage.cs rename osu.Game/Screens/Ranking/{Pages => }/ReplayDownloadButton.cs (98%) delete mode 100644 osu.Game/Screens/Ranking/ResultModeButton.cs delete mode 100644 osu.Game/Screens/Ranking/ResultModeTabControl.cs delete mode 100644 osu.Game/Screens/Ranking/Results.cs delete mode 100644 osu.Game/Screens/Ranking/ResultsPage.cs create mode 100644 osu.Game/Screens/Ranking/ResultsScreen.cs rename osu.Game/Screens/Ranking/{Pages => }/RetryButton.cs (97%) delete mode 100644 osu.Game/Screens/Ranking/Types/LocalLeaderboardPageInfo.cs delete mode 100644 osu.Game/Screens/Ranking/Types/ScoreOverviewPageInfo.cs diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs index 06a155e78b..b51555db3e 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs @@ -27,6 +27,7 @@ using osu.Game.Screens; using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Play; using osu.Game.Screens.Play.PlayerSettings; +using osu.Game.Screens.Ranking; using osu.Game.Screens.Select; using osu.Game.Tests.Resources; using osu.Game.Users; @@ -203,7 +204,7 @@ namespace osu.Game.Tests.Visual.Background } /// - /// Check if the visual settings container removes user dim when suspending for + /// Check if the visual settings container removes user dim when suspending for /// [Test] public void TransitionTest() @@ -335,7 +336,7 @@ namespace osu.Game.Tests.Visual.Background public bool IsBackgroundCurrent() => ((FadeAccessibleBackground)Background).IsCurrentScreen(); } - private class FadeAccessibleResults : SoloResults + private class FadeAccessibleResults : ResultsScreen { public FadeAccessibleResults(ScoreInfo score) : base(score) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs index 8cb44de8cb..c9561a70fa 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs @@ -11,7 +11,7 @@ using System; using System.Collections.Generic; using osu.Framework.Allocation; using osu.Game.Rulesets; -using osu.Game.Screens.Ranking.Pages; +using osu.Game.Screens.Ranking; namespace osu.Game.Tests.Visual.Gameplay { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchResults.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchResults.cs deleted file mode 100644 index 58e9240026..0000000000 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchResults.cs +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Collections.Generic; -using System.Linq; -using osu.Framework.Allocation; -using osu.Game.Beatmaps; -using osu.Game.Online.API; -using osu.Game.Online.API.Requests.Responses; -using osu.Game.Scoring; -using osu.Game.Screens.Multi.Match.Components; -using osu.Game.Screens.Multi.Ranking; -using osu.Game.Screens.Multi.Ranking.Pages; -using osu.Game.Screens.Multi.Ranking.Types; -using osu.Game.Screens.Ranking; -using osu.Game.Users; - -namespace osu.Game.Tests.Visual.Multiplayer -{ - public class TestSceneMatchResults : MultiplayerTestScene - { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(MatchResults), - typeof(RoomLeaderboardPageInfo), - typeof(RoomLeaderboardPage) - }; - - [Resolved] - private BeatmapManager beatmaps { get; set; } - - [BackgroundDependencyLoader] - private void load() - { - var beatmapInfo = beatmaps.QueryBeatmap(b => b.RulesetID == 0); - if (beatmapInfo != null) - Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo); - - Room.RoomID.Value = 1; - Room.Name.Value = "an awesome room"; - - LoadScreen(new TestMatchResults(new ScoreInfo - { - User = new User { Id = 10 }, - })); - } - - private class TestMatchResults : MatchResults - { - public TestMatchResults(ScoreInfo score) - : base(score) - { - } - - protected override IEnumerable CreateResultPages() => new[] { new TestRoomLeaderboardPageInfo(Score, Beatmap.Value) }; - } - - private class TestRoomLeaderboardPageInfo : RoomLeaderboardPageInfo - { - private readonly ScoreInfo score; - private readonly WorkingBeatmap beatmap; - - public TestRoomLeaderboardPageInfo(ScoreInfo score, WorkingBeatmap beatmap) - : base(score, beatmap) - { - this.score = score; - this.beatmap = beatmap; - } - - public override ResultsPage CreatePage() => new TestRoomLeaderboardPage(score, beatmap); - } - - private class TestRoomLeaderboardPage : RoomLeaderboardPage - { - public TestRoomLeaderboardPage(ScoreInfo score, WorkingBeatmap beatmap) - : base(score, beatmap) - { - } - - protected override MatchLeaderboard CreateLeaderboard() => new TestMatchLeaderboard(); - } - - private class TestMatchLeaderboard : RoomLeaderboardPage.ResultsMatchLeaderboard - { - protected override APIRequest FetchScores(Action> scoresCallback) - { - var scores = Enumerable.Range(0, 50).Select(createRoomScore).ToArray(); - - scoresCallback?.Invoke(scores); - ScoresLoaded?.Invoke(scores); - - return null; - } - - private APIUserScoreAggregate createRoomScore(int id) => new APIUserScoreAggregate - { - User = new User { Id = id, Username = $"User {id}" }, - Accuracy = 0.98, - TotalScore = 987654, - TotalAttempts = 13, - CompletedBeatmaps = 5 - }; - } - } -} diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneResults.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs similarity index 91% rename from osu.Game.Tests/Visual/Gameplay/TestSceneResults.cs rename to osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 2b7a32ba17..bd5b039bc1 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneResults.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -10,29 +10,27 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; -using osu.Game.Screens.Ranking.Pages; +using osu.Game.Tests.Beatmaps; using osu.Game.Users; -namespace osu.Game.Tests.Visual.Gameplay +namespace osu.Game.Tests.Visual.Ranking { [TestFixture] - public class TestSceneResults : ScreenTestScene + public class TestSceneResultsScreen : ScreenTestScene { private BeatmapManager beatmaps; public override IReadOnlyList RequiredTypes => new[] { - typeof(Results), - typeof(ResultsPage), - typeof(ScoreResultsPage), + typeof(ResultsScreen), typeof(RetryButton), typeof(ReplayDownloadButton), - typeof(LocalLeaderboardPage), typeof(TestPlayer) }; @@ -65,6 +63,7 @@ namespace osu.Game.Tests.Visual.Gameplay { HitResult.Meh, 50 }, { HitResult.Miss, 1 } }, + Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, User = new User { Username = "peppy", @@ -119,7 +118,7 @@ namespace osu.Game.Tests.Visual.Gameplay } } - private class TestSoloResults : SoloResults + private class TestSoloResults : ResultsScreen { public HotkeyRetryOverlay RetryOverlay; diff --git a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs index 3afacf2f31..7f58de29fb 100644 --- a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs +++ b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs @@ -14,9 +14,7 @@ using osu.Game.Online.API.Requests; using osu.Game.Online.Multiplayer; using osu.Game.Rulesets; using osu.Game.Scoring; -using osu.Game.Screens.Multi.Ranking; using osu.Game.Screens.Play; -using osu.Game.Screens.Ranking; namespace osu.Game.Screens.Multi.Play { @@ -115,7 +113,5 @@ namespace osu.Game.Screens.Multi.Play Exited = null; } - - protected override Results CreateResults(ScoreInfo score) => new MatchResults(score); } } diff --git a/osu.Game/Screens/Multi/Ranking/MatchResults.cs b/osu.Game/Screens/Multi/Ranking/MatchResults.cs deleted file mode 100644 index fe68d7e849..0000000000 --- a/osu.Game/Screens/Multi/Ranking/MatchResults.cs +++ /dev/null @@ -1,26 +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.Game.Scoring; -using osu.Game.Screens.Multi.Ranking.Types; -using osu.Game.Screens.Ranking; -using osu.Game.Screens.Ranking.Types; - -namespace osu.Game.Screens.Multi.Ranking -{ - public class MatchResults : Results - { - public MatchResults(ScoreInfo score) - : base(score) - { - } - - protected override IEnumerable CreateResultPages() => new IResultPageInfo[] - { - new ScoreOverviewPageInfo(Score, Beatmap.Value), - new LocalLeaderboardPageInfo(Score, Beatmap.Value), - new RoomLeaderboardPageInfo(Score, Beatmap.Value), - }; - } -} diff --git a/osu.Game/Screens/Multi/Ranking/Pages/RoomLeaderboardPage.cs b/osu.Game/Screens/Multi/Ranking/Pages/RoomLeaderboardPage.cs deleted file mode 100644 index 0d31805774..0000000000 --- a/osu.Game/Screens/Multi/Ranking/Pages/RoomLeaderboardPage.cs +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Collections.Generic; -using Microsoft.EntityFrameworkCore.Internal; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Lists; -using osu.Game.Beatmaps; -using osu.Game.Graphics; -using osu.Game.Online.API.Requests.Responses; -using osu.Game.Online.Leaderboards; -using osu.Game.Online.Multiplayer; -using osu.Game.Scoring; -using osu.Game.Screens.Multi.Match.Components; -using osu.Game.Screens.Ranking; - -namespace osu.Game.Screens.Multi.Ranking.Pages -{ - public class RoomLeaderboardPage : ResultsPage - { - [Resolved] - private OsuColour colours { get; set; } - - private TextFlowContainer rankText; - - [Resolved(typeof(Room), nameof(Room.Name))] - private Bindable name { get; set; } - - public RoomLeaderboardPage(ScoreInfo score, WorkingBeatmap beatmap) - : base(score, beatmap) - { - } - - [BackgroundDependencyLoader] - private void load() - { - MatchLeaderboard leaderboard; - - Children = new Drawable[] - { - new Box - { - Colour = colours.Gray6, - RelativeSizeAxes = Axes.Both, - }, - new BufferedContainer - { - RelativeSizeAxes = Axes.Both, - BackgroundColour = colours.Gray6, - Child = leaderboard = CreateLeaderboard() - }, - rankText = new TextFlowContainer - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.X, - Width = 0.5f, - AutoSizeAxes = Axes.Y, - Y = 50, - TextAnchor = Anchor.TopCentre - }, - }; - - leaderboard.Origin = Anchor.Centre; - leaderboard.Anchor = Anchor.Centre; - leaderboard.RelativeSizeAxes = Axes.Both; - leaderboard.Height = 0.8f; - leaderboard.Y = 55; - leaderboard.ScoresLoaded = scoresLoaded; - } - - private void scoresLoaded(IEnumerable scores) - { - void gray(SpriteText s) => s.Colour = colours.GrayC; - - void white(SpriteText s) - { - s.Font = s.Font.With(size: s.Font.Size * 1.4f); - s.Colour = colours.GrayF; - } - - rankText.AddText(name + "\n", white); - rankText.AddText("You are placed ", gray); - - int index = scores.IndexOf(new APIUserScoreAggregate { User = Score.User }, new FuncEqualityComparer((s1, s2) => s1.User.Id.Equals(s2.User.Id))); - - rankText.AddText($"#{index + 1} ", s => - { - s.Font = s.Font.With(Typeface.Torus, weight: FontWeight.Bold); - s.Colour = colours.YellowDark; - }); - - rankText.AddText("in the room!", gray); - } - - protected virtual MatchLeaderboard CreateLeaderboard() => new ResultsMatchLeaderboard(); - - public class ResultsMatchLeaderboard : MatchLeaderboard - { - protected override bool FadeTop => true; - - protected override LeaderboardScore CreateDrawableScore(APIUserScoreAggregate model, int index) - => new ResultsMatchLeaderboardScore(model, index); - - protected override FillFlowContainer CreateScoreFlow() - { - var flow = base.CreateScoreFlow(); - flow.Padding = new MarginPadding - { - Top = LeaderboardScore.HEIGHT * 2, - Bottom = LeaderboardScore.HEIGHT * 3, - }; - return flow; - } - - private class ResultsMatchLeaderboardScore : MatchLeaderboardScore - { - public ResultsMatchLeaderboardScore(APIUserScoreAggregate score, int rank) - : base(score, rank) - { - } - - [BackgroundDependencyLoader] - private void load() - { - } - } - } - } -} diff --git a/osu.Game/Screens/Multi/Ranking/Types/RoomLeaderboardPageInfo.cs b/osu.Game/Screens/Multi/Ranking/Types/RoomLeaderboardPageInfo.cs deleted file mode 100644 index dcfad8458f..0000000000 --- a/osu.Game/Screens/Multi/Ranking/Types/RoomLeaderboardPageInfo.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Graphics.Sprites; -using osu.Game.Beatmaps; -using osu.Game.Scoring; -using osu.Game.Screens.Multi.Ranking.Pages; -using osu.Game.Screens.Ranking; - -namespace osu.Game.Screens.Multi.Ranking.Types -{ - public class RoomLeaderboardPageInfo : IResultPageInfo - { - private readonly ScoreInfo score; - private readonly WorkingBeatmap beatmap; - - public RoomLeaderboardPageInfo(ScoreInfo score, WorkingBeatmap beatmap) - { - this.score = score; - this.beatmap = beatmap; - } - - public IconUsage Icon => FontAwesome.Solid.Users; - - public string Name => "Room Leaderboard"; - - public virtual ResultsPage CreatePage() => new RoomLeaderboardPage(score, beatmap); - } -} diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index bcadba14af..7d945f9a31 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -416,7 +416,7 @@ namespace osu.Game.Screens.Play protected override bool OnScroll(ScrollEvent e) => mouseWheelDisabled.Value && !GameplayClockContainer.IsPaused.Value; - protected virtual Results CreateResults(ScoreInfo score) => new SoloResults(score); + protected virtual ResultsScreen CreateResults(ScoreInfo score) => new ResultsScreen(score); #region Fail Logic diff --git a/osu.Game/Screens/Play/SoloResults.cs b/osu.Game/Screens/Play/SoloResults.cs deleted file mode 100644 index 2b9aec257c..0000000000 --- a/osu.Game/Screens/Play/SoloResults.cs +++ /dev/null @@ -1,24 +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.Game.Scoring; -using osu.Game.Screens.Ranking; -using osu.Game.Screens.Ranking.Types; - -namespace osu.Game.Screens.Play -{ - public class SoloResults : Results - { - public SoloResults(ScoreInfo score) - : base(score) - { - } - - protected override IEnumerable CreateResultPages() => new IResultPageInfo[] - { - new ScoreOverviewPageInfo(Score, Beatmap.Value), - new LocalLeaderboardPageInfo(Score, Beatmap.Value) - }; - } -} diff --git a/osu.Game/Screens/Ranking/IResultPageInfo.cs b/osu.Game/Screens/Ranking/IResultPageInfo.cs deleted file mode 100644 index cc86e7441a..0000000000 --- a/osu.Game/Screens/Ranking/IResultPageInfo.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Graphics.Sprites; - -namespace osu.Game.Screens.Ranking -{ - public interface IResultPageInfo - { - IconUsage Icon { get; } - - string Name { get; } - - ResultsPage CreatePage(); - } -} diff --git a/osu.Game/Screens/Ranking/Pages/LocalLeaderboardPage.cs b/osu.Game/Screens/Ranking/Pages/LocalLeaderboardPage.cs deleted file mode 100644 index c997dd6d30..0000000000 --- a/osu.Game/Screens/Ranking/Pages/LocalLeaderboardPage.cs +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Shapes; -using osu.Game.Beatmaps; -using osu.Game.Graphics; -using osu.Game.Scoring; -using osu.Game.Screens.Select.Leaderboards; -using osuTK; - -namespace osu.Game.Screens.Ranking.Pages -{ - public class LocalLeaderboardPage : ResultsPage - { - public LocalLeaderboardPage(ScoreInfo score, WorkingBeatmap beatmap = null) - : base(score, beatmap) - { - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - Children = new Drawable[] - { - new Box - { - Colour = colours.Gray6, - RelativeSizeAxes = Axes.Both, - }, - new BeatmapLeaderboard - { - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Beatmap = Beatmap.BeatmapInfo ?? Score.Beatmap, - Scale = new Vector2(0.7f) - } - }; - } - } -} diff --git a/osu.Game/Screens/Ranking/Pages/ScoreResultsPage.cs b/osu.Game/Screens/Ranking/Pages/ScoreResultsPage.cs deleted file mode 100644 index 0aab067de1..0000000000 --- a/osu.Game/Screens/Ranking/Pages/ScoreResultsPage.cs +++ /dev/null @@ -1,428 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Collections.Generic; -using System.Linq; -using osu.Framework.Allocation; -using osu.Framework.Extensions; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Colour; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -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.Online.Leaderboards; -using osu.Game.Rulesets.Scoring; -using osu.Game.Scoring; -using osu.Game.Screens.Play; -using osu.Game.Users; -using osuTK; -using osuTK.Graphics; - -namespace osu.Game.Screens.Ranking.Pages -{ - public class ScoreResultsPage : ResultsPage - { - private Container scoreContainer; - private ScoreCounter scoreCounter; - - private readonly ScoreInfo score; - - public ScoreResultsPage(ScoreInfo score, WorkingBeatmap beatmap) - : base(score, beatmap) - { - this.score = score; - } - - private FillFlowContainer statisticsContainer; - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - const float user_header_height = 120; - - Children = new Drawable[] - { - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Top = user_header_height }, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.White, - }, - } - }, - new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - new DelayedLoadWrapper(new UserHeader(Score.User) - { - RelativeSizeAxes = Axes.Both, - }) - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.X, - Height = user_header_height, - }, - new UpdateableRank(Score.Rank) - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Size = new Vector2(150, 60), - Margin = new MarginPadding(20), - }, - scoreContainer = new Container - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.X, - Height = 60, - Children = new Drawable[] - { - new SongProgressGraph - { - RelativeSizeAxes = Axes.Both, - Alpha = 0.5f, - Objects = Beatmap.Beatmap.HitObjects, - }, - scoreCounter = new SlowScoreCounter(6) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Colour = colours.PinkDarker, - Y = 10, - TextSize = 56, - }, - } - }, - new OsuSpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Colour = colours.PinkDarker, - Shadow = false, - Font = OsuFont.GetFont(weight: FontWeight.Bold), - Text = "total score", - Margin = new MarginPadding { Bottom = 15 }, - }, - new BeatmapDetails(Beatmap.BeatmapInfo) - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Margin = new MarginPadding { Bottom = 10 }, - }, - new DateTimeDisplay(Score.Date.LocalDateTime) - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - }, - new Container - { - RelativeSizeAxes = Axes.X, - Size = new Vector2(0.75f, 1), - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Margin = new MarginPadding { Top = 10, Bottom = 10 }, - Children = new Drawable[] - { - new Box - { - Colour = ColourInfo.GradientHorizontal( - colours.GrayC.Opacity(0), - colours.GrayC.Opacity(0.9f)), - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.5f, 1), - }, - new Box - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Colour = ColourInfo.GradientHorizontal( - colours.GrayC.Opacity(0.9f), - colours.GrayC.Opacity(0)), - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.5f, 1), - }, - } - }, - statisticsContainer = new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Direction = FillDirection.Horizontal, - LayoutDuration = 200, - LayoutEasing = Easing.OutQuint - }, - }, - }, - new FillFlowContainer - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Margin = new MarginPadding { Bottom = 10 }, - Spacing = new Vector2(5), - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Children = new Drawable[] - { - new ReplayDownloadButton(score), - new RetryButton() - } - }, - }; - - statisticsContainer.ChildrenEnumerable = Score.SortedStatistics.Select(s => new DrawableScoreStatistic(s)); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - Schedule(() => - { - scoreCounter.Increment(Score.TotalScore); - - int delay = 0; - - foreach (var s in statisticsContainer.Children) - { - s.FadeOut() - .Then(delay += 200) - .FadeIn(300 + delay, Easing.Out); - } - }); - } - - protected override void UpdateAfterChildren() - { - base.UpdateAfterChildren(); - - scoreCounter.Scale = new Vector2(Math.Min(1f, (scoreContainer.DrawWidth - 20) / scoreCounter.DrawWidth)); - } - - private class DrawableScoreStatistic : Container - { - private readonly KeyValuePair statistic; - - public DrawableScoreStatistic(KeyValuePair statistic) - { - this.statistic = statistic; - - AutoSizeAxes = Axes.Both; - Margin = new MarginPadding { Left = 5, Right = 5 }; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - Children = new Drawable[] - { - new OsuSpriteText - { - Text = statistic.Value.ToString().PadLeft(4, '0'), - Colour = colours.Gray7, - Font = OsuFont.GetFont(size: 30), - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - }, - new OsuSpriteText - { - Text = statistic.Key.GetDescription(), - Colour = colours.Gray7, - Font = OsuFont.GetFont(weight: FontWeight.Bold), - Y = 26, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - }, - }; - } - } - - private class DateTimeDisplay : Container - { - private readonly DateTime date; - - public DateTimeDisplay(DateTime date) - { - this.date = date; - - AutoSizeAxes = Axes.Both; - - Masking = true; - CornerRadius = 5; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colours.Gray6, - }, - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Padding = new MarginPadding { Horizontal = 10, Vertical = 5 }, - Spacing = new Vector2(10), - Children = new[] - { - new OsuSpriteText - { - Text = date.ToShortDateString(), - Colour = Color4.White, - }, - new OsuSpriteText - { - Text = date.ToShortTimeString(), - Colour = Color4.White, - } - } - }, - }; - } - } - - private class BeatmapDetails : Container - { - private readonly BeatmapInfo beatmap; - - private readonly OsuSpriteText title; - private readonly OsuSpriteText artist; - private readonly OsuSpriteText versionMapper; - - public BeatmapDetails(BeatmapInfo beatmap) - { - this.beatmap = beatmap; - - AutoSizeAxes = Axes.Both; - - Children = new Drawable[] - { - new FillFlowContainer - { - Direction = FillDirection.Vertical, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new Drawable[] - { - title = new OsuSpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Shadow = false, - Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 24, italics: true), - }, - artist = new OsuSpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Shadow = false, - Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 20, italics: true), - }, - versionMapper = new OsuSpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Shadow = false, - Font = OsuFont.GetFont(weight: FontWeight.Bold), - }, - } - } - }; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - title.Colour = artist.Colour = colours.BlueDarker; - versionMapper.Colour = colours.Gray8; - - var creator = beatmap.Metadata.Author?.Username; - - if (!string.IsNullOrEmpty(creator)) - { - versionMapper.Text = $"mapped by {creator}"; - - if (!string.IsNullOrEmpty(beatmap.Version)) - versionMapper.Text = $"{beatmap.Version} - " + versionMapper.Text; - } - - title.Text = new LocalisedString((beatmap.Metadata.TitleUnicode, beatmap.Metadata.Title)); - artist.Text = new LocalisedString((beatmap.Metadata.ArtistUnicode, beatmap.Metadata.Artist)); - } - } - - [LongRunningLoad] - private class UserHeader : Container - { - private readonly User user; - private readonly Sprite cover; - - public UserHeader(User user) - { - this.user = user; - Children = new Drawable[] - { - cover = new Sprite - { - RelativeSizeAxes = Axes.Both, - FillMode = FillMode.Fill, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - new OsuSpriteText - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Text = user.Username, - Font = OsuFont.GetFont(size: 30, weight: FontWeight.Regular, italics: true), - Padding = new MarginPadding { Bottom = 10 }, - } - }; - } - - [BackgroundDependencyLoader] - private void load(LargeTextureStore textures) - { - if (!string.IsNullOrEmpty(user.CoverUrl)) - cover.Texture = textures.Get(user.CoverUrl); - } - } - - private class SlowScoreCounter : ScoreCounter - { - protected override double RollingDuration => 3000; - - protected override Easing RollingEasing => Easing.OutPow10; - - public SlowScoreCounter(uint leading = 0) - : base(leading) - { - DisplayedCountSpriteText.Shadow = false; - DisplayedCountSpriteText.Font = DisplayedCountSpriteText.Font.With(Typeface.Venera, weight: FontWeight.Light); - UseCommaSeparator = true; - } - } - } -} diff --git a/osu.Game/Screens/Ranking/Pages/ReplayDownloadButton.cs b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs similarity index 98% rename from osu.Game/Screens/Ranking/Pages/ReplayDownloadButton.cs rename to osu.Game/Screens/Ranking/ReplayDownloadButton.cs index 62213720aa..a36c86eafc 100644 --- a/osu.Game/Screens/Ranking/Pages/ReplayDownloadButton.cs +++ b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs @@ -9,7 +9,7 @@ using osu.Game.Online; using osu.Game.Scoring; using osuTK; -namespace osu.Game.Screens.Ranking.Pages +namespace osu.Game.Screens.Ranking { public class ReplayDownloadButton : DownloadTrackingComposite { diff --git a/osu.Game/Screens/Ranking/ResultModeButton.cs b/osu.Game/Screens/Ranking/ResultModeButton.cs deleted file mode 100644 index d7eb5db125..0000000000 --- a/osu.Game/Screens/Ranking/ResultModeButton.cs +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Cursor; -using osu.Framework.Graphics.Effects; -using osu.Framework.Graphics.UserInterface; -using osu.Game.Graphics; -using osuTK; -using osuTK.Graphics; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; - -namespace osu.Game.Screens.Ranking -{ - public class ResultModeButton : TabItem, IHasTooltip - { - private readonly IconUsage icon; - private Color4 activeColour; - private Color4 inactiveColour; - private CircularContainer colouredPart; - - public ResultModeButton(IResultPageInfo mode) - : base(mode) - { - icon = mode.Icon; - TooltipText = mode.Name; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - Size = new Vector2(50); - - Masking = true; - - CornerRadius = 25; - CornerExponent = 2; - - activeColour = colours.PinkDarker; - inactiveColour = OsuColour.Gray(0.8f); - - EdgeEffect = new EdgeEffectParameters - { - Colour = Color4.Black.Opacity(0.4f), - Type = EdgeEffectType.Shadow, - Radius = 5, - }; - - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.White, - }, - colouredPart = new CircularContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.8f), - BorderThickness = 4, - BorderColour = Color4.White, - Colour = inactiveColour, - Children = new Drawable[] - { - new Box - { - AlwaysPresent = true, //for border rendering - RelativeSizeAxes = Axes.Both, - Colour = Color4.Transparent, - }, - new SpriteIcon - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Shadow = false, - Colour = OsuColour.Gray(0.95f), - Icon = icon, - Size = new Vector2(20), - } - } - } - }; - } - - protected override void OnActivated() => colouredPart.FadeColour(activeColour, 200, Easing.OutQuint); - - protected override void OnDeactivated() => colouredPart.FadeColour(inactiveColour, 200, Easing.OutQuint); - - public string TooltipText { get; } - } -} diff --git a/osu.Game/Screens/Ranking/ResultModeTabControl.cs b/osu.Game/Screens/Ranking/ResultModeTabControl.cs deleted file mode 100644 index b0d94a4be6..0000000000 --- a/osu.Game/Screens/Ranking/ResultModeTabControl.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Graphics; -using osu.Framework.Graphics.UserInterface; -using osuTK; - -namespace osu.Game.Screens.Ranking -{ - public class ResultModeTabControl : TabControl - { - public ResultModeTabControl() - { - TabContainer.Anchor = Anchor.BottomCentre; - TabContainer.Origin = Anchor.BottomCentre; - TabContainer.Spacing = new Vector2(15); - - TabContainer.Masking = false; - TabContainer.Padding = new MarginPadding(5); - } - - protected override Dropdown CreateDropdown() => null; - - protected override TabItem CreateTabItem(IResultPageInfo value) => new ResultModeButton(value) - { - Anchor = TabContainer.Anchor, - Origin = TabContainer.Origin - }; - } -} diff --git a/osu.Game/Screens/Ranking/Results.cs b/osu.Game/Screens/Ranking/Results.cs deleted file mode 100644 index 05f1872be9..0000000000 --- a/osu.Game/Screens/Ranking/Results.cs +++ /dev/null @@ -1,291 +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 System.Linq; -using osu.Framework.Allocation; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Extensions.IEnumerableExtensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Effects; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Screens; -using osu.Game.Graphics.Containers; -using osu.Game.Screens.Backgrounds; -using osuTK; -using osuTK.Graphics; -using osu.Game.Graphics; -using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics.Sprites; -using osu.Game.Scoring; -using osu.Game.Screens.Play; - -namespace osu.Game.Screens.Ranking -{ - public abstract class Results : OsuScreen - { - protected const float BACKGROUND_BLUR = 20; - - private Container circleOuterBackground; - private Container circleOuter; - private Container circleInner; - - private ParallaxContainer backgroundParallax; - - private ResultModeTabControl modeChangeButtons; - - [Resolved(canBeNull: true)] - private Player player { get; set; } - - public override bool DisallowExternalBeatmapRulesetChanges => true; - - protected readonly ScoreInfo Score; - - private Container currentPage; - - protected override BackgroundScreen CreateBackground() => new BackgroundScreenBeatmap(Beatmap.Value); - - private const float overscan = 1.3f; - - private const float circle_outer_scale = 0.96f; - - protected Results(ScoreInfo score) - { - Score = score; - } - - private const float transition_time = 800; - - private IEnumerable allCircles => new Drawable[] { circleOuterBackground, circleInner, circleOuter }; - - public override void OnEntering(IScreen last) - { - base.OnEntering(last); - ((BackgroundScreenBeatmap)Background).BlurAmount.Value = BACKGROUND_BLUR; - Background.ScaleTo(1.1f, transition_time, Easing.OutQuint); - - allCircles.ForEach(c => - { - c.FadeOut(); - c.ScaleTo(0); - }); - - backgroundParallax.FadeOut(); - modeChangeButtons.FadeOut(); - currentPage?.FadeOut(); - - circleOuterBackground - .FadeIn(transition_time, Easing.OutQuint) - .ScaleTo(1, transition_time, Easing.OutQuint); - - using (BeginDelayedSequence(transition_time * 0.25f, true)) - { - circleOuter - .FadeIn(transition_time, Easing.OutQuint) - .ScaleTo(1, transition_time, Easing.OutQuint); - - using (BeginDelayedSequence(transition_time * 0.3f, true)) - { - backgroundParallax.FadeIn(transition_time, Easing.OutQuint); - - circleInner - .FadeIn(transition_time, Easing.OutQuint) - .ScaleTo(1, transition_time, Easing.OutQuint); - - using (BeginDelayedSequence(transition_time * 0.4f, true)) - { - modeChangeButtons.FadeIn(transition_time, Easing.OutQuint); - currentPage?.FadeIn(transition_time, Easing.OutQuint); - } - } - } - } - - public override bool OnExiting(IScreen next) - { - allCircles.ForEach(c => c.ScaleTo(0, transition_time, Easing.OutSine)); - - Background.ScaleTo(1f, transition_time / 4, Easing.OutQuint); - - this.FadeOut(transition_time / 4); - - return base.OnExiting(next); - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - InternalChild = new AspectContainer - { - RelativeSizeAxes = Axes.Y, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Height = overscan, - Children = new Drawable[] - { - circleOuterBackground = new CircularContainer - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Masking = true, - Children = new Drawable[] - { - new Box - { - Alpha = 0.2f, - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black, - } - } - }, - circleOuter = new CircularContainer - { - Size = new Vector2(circle_outer_scale), - EdgeEffect = new EdgeEffectParameters - { - Colour = Color4.Black.Opacity(0.4f), - Type = EdgeEffectType.Shadow, - Radius = 15, - }, - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Masking = true, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.White, - }, - backgroundParallax = new ParallaxContainer - { - RelativeSizeAxes = Axes.Both, - ParallaxAmount = 0.01f, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = new Drawable[] - { - new Sprite - { - RelativeSizeAxes = Axes.Both, - Alpha = 0.2f, - Texture = Beatmap.Value.Background, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - FillMode = FillMode.Fill - } - } - }, - modeChangeButtons = new ResultModeTabControl - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - RelativeSizeAxes = Axes.X, - Height = 50, - Margin = new MarginPadding { Bottom = 110 }, - }, - new OsuSpriteText - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.BottomCentre, - Text = $"{Score.MaxCombo}x", - RelativePositionAxes = Axes.X, - Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 40), - X = 0.1f, - Colour = colours.BlueDarker, - }, - new OsuSpriteText - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.TopCentre, - Text = "max combo", - Font = OsuFont.GetFont(size: 20), - RelativePositionAxes = Axes.X, - X = 0.1f, - Colour = colours.Gray6, - }, - new OsuSpriteText - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.BottomCentre, - Text = Score.DisplayAccuracy, - Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 40), - RelativePositionAxes = Axes.X, - X = 0.9f, - Colour = colours.BlueDarker, - }, - new OsuSpriteText - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.TopCentre, - Text = "accuracy", - Font = OsuFont.GetFont(size: 20), - RelativePositionAxes = Axes.X, - X = 0.9f, - Colour = colours.Gray6, - }, - } - }, - circleInner = new CircularContainer - { - Size = new Vector2(0.6f), - EdgeEffect = new EdgeEffectParameters - { - Colour = Color4.Black.Opacity(0.4f), - Type = EdgeEffectType.Shadow, - Radius = 15, - }, - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Masking = true, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.White, - }, - } - } - } - }; - - if (player != null) - { - AddInternal(new HotkeyRetryOverlay - { - Action = () => - { - if (!this.IsCurrentScreen()) return; - - player?.Restart(); - }, - }); - } - - var pages = CreateResultPages(); - - foreach (var p in pages) - modeChangeButtons.AddItem(p); - - modeChangeButtons.Current.Value = pages.FirstOrDefault(); - - modeChangeButtons.Current.BindValueChanged(page => - { - currentPage?.FadeOut(); - currentPage?.Expire(); - - currentPage = page.NewValue?.CreatePage(); - - if (currentPage != null) - LoadComponentAsync(currentPage, circleInner.Add); - }, true); - } - - protected abstract IEnumerable CreateResultPages(); - } -} diff --git a/osu.Game/Screens/Ranking/ResultsPage.cs b/osu.Game/Screens/Ranking/ResultsPage.cs deleted file mode 100644 index 8776c599dd..0000000000 --- a/osu.Game/Screens/Ranking/ResultsPage.cs +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Effects; -using osu.Framework.Graphics.Shapes; -using osu.Game.Beatmaps; -using osu.Game.Graphics; -using osu.Game.Scoring; -using osuTK; -using osuTK.Graphics; - -namespace osu.Game.Screens.Ranking -{ - public abstract class ResultsPage : Container - { - protected readonly ScoreInfo Score; - protected readonly WorkingBeatmap Beatmap; - private CircularContainer content; - private Box fill; - - protected override Container Content => content; - - protected ResultsPage(ScoreInfo score, WorkingBeatmap beatmap) - { - Score = score; - Beatmap = beatmap; - RelativeSizeAxes = Axes.Both; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - fill.Delay(400).FadeInFromZero(600); - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - AddRangeInternal(new Drawable[] - { - fill = new Box - { - Alpha = 0, - RelativeSizeAxes = Axes.Both, - Colour = colours.Gray6 - }, - new CircularContainer - { - EdgeEffect = new EdgeEffectParameters - { - Colour = colours.GrayF.Opacity(0.8f), - Type = EdgeEffectType.Shadow, - Radius = 1, - }, - RelativeSizeAxes = Axes.Both, - Masking = true, - BorderThickness = 20, - BorderColour = Color4.White, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - AlwaysPresent = true - }, - } - }, - content = new CircularContainer - { - EdgeEffect = new EdgeEffectParameters - { - Colour = Color4.Black.Opacity(0.2f), - Type = EdgeEffectType.Shadow, - Radius = 15, - }, - RelativeSizeAxes = Axes.Both, - Masking = true, - Size = new Vector2(0.88f), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - } - }); - } - } -} diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs new file mode 100644 index 0000000000..d0e02329fe --- /dev/null +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -0,0 +1,97 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Screens; +using osu.Game.Graphics.UserInterface; +using osu.Game.Scoring; +using osu.Game.Screens.Backgrounds; +using osuTK; + +namespace osu.Game.Screens.Ranking +{ + public class ResultsScreen : OsuScreen + { + protected const float BACKGROUND_BLUR = 20; + + public override bool DisallowExternalBeatmapRulesetChanges => true; + + // Temporary for now to stop dual transitions. Should respect the current toolbar mode, but there's no way to do so currently. + public override bool HideOverlaysOnEnter => true; + + protected override BackgroundScreen CreateBackground() => new BackgroundScreenBeatmap(Beatmap.Value); + + private readonly ScoreInfo score; + + private Drawable bottomPanel; + + public ResultsScreen(ScoreInfo score) + { + this.score = score; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChildren = new[] + { + new ScorePanel(score) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + State = PanelState.Expanded + }, + bottomPanel = new Container + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + Height = TwoLayerButton.SIZE_EXTENDED.Y, + Alpha = 0, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#333") + }, + new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(5), + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + new ReplayDownloadButton(score), + new RetryButton() + } + } + } + } + }; + } + + public override void OnEntering(IScreen last) + { + base.OnEntering(last); + + ((BackgroundScreenBeatmap)Background).BlurAmount.Value = BACKGROUND_BLUR; + + Background.FadeTo(0.5f, 250); + bottomPanel.FadeTo(1, 250); + } + + public override bool OnExiting(IScreen next) + { + Background.FadeTo(1, 250); + + return base.OnExiting(next); + } + } +} diff --git a/osu.Game/Screens/Ranking/Pages/RetryButton.cs b/osu.Game/Screens/Ranking/RetryButton.cs similarity index 97% rename from osu.Game/Screens/Ranking/Pages/RetryButton.cs rename to osu.Game/Screens/Ranking/RetryButton.cs index 06d0440b30..59b69bc949 100644 --- a/osu.Game/Screens/Ranking/Pages/RetryButton.cs +++ b/osu.Game/Screens/Ranking/RetryButton.cs @@ -10,7 +10,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Screens.Play; using osuTK; -namespace osu.Game.Screens.Ranking.Pages +namespace osu.Game.Screens.Ranking { public class RetryButton : OsuAnimatedButton { diff --git a/osu.Game/Screens/Ranking/Types/LocalLeaderboardPageInfo.cs b/osu.Game/Screens/Ranking/Types/LocalLeaderboardPageInfo.cs deleted file mode 100644 index fe183c5f89..0000000000 --- a/osu.Game/Screens/Ranking/Types/LocalLeaderboardPageInfo.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Graphics.Sprites; -using osu.Game.Beatmaps; -using osu.Game.Scoring; -using osu.Game.Screens.Ranking.Pages; - -namespace osu.Game.Screens.Ranking.Types -{ - public class LocalLeaderboardPageInfo : IResultPageInfo - { - private readonly ScoreInfo score; - private readonly WorkingBeatmap beatmap; - - public LocalLeaderboardPageInfo(ScoreInfo score, WorkingBeatmap beatmap) - { - this.score = score; - this.beatmap = beatmap; - } - - public IconUsage Icon => FontAwesome.Solid.User; - - public string Name => @"Local Leaderboard"; - - public ResultsPage CreatePage() => new LocalLeaderboardPage(score, beatmap); - } -} diff --git a/osu.Game/Screens/Ranking/Types/ScoreOverviewPageInfo.cs b/osu.Game/Screens/Ranking/Types/ScoreOverviewPageInfo.cs deleted file mode 100644 index 424dbff6f6..0000000000 --- a/osu.Game/Screens/Ranking/Types/ScoreOverviewPageInfo.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Graphics.Sprites; -using osu.Game.Beatmaps; -using osu.Game.Scoring; -using osu.Game.Screens.Ranking.Pages; - -namespace osu.Game.Screens.Ranking.Types -{ - public class ScoreOverviewPageInfo : IResultPageInfo - { - public IconUsage Icon => FontAwesome.Solid.Asterisk; - - public string Name => "Overview"; - private readonly ScoreInfo score; - private readonly WorkingBeatmap beatmap; - - public ScoreOverviewPageInfo(ScoreInfo score, WorkingBeatmap beatmap) - { - this.score = score; - this.beatmap = beatmap; - } - - public ResultsPage CreatePage() - { - return new ScoreResultsPage(score, beatmap); - } - } -} diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index af113781e5..9e2f5761dd 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -8,6 +8,7 @@ using osu.Framework.Input.Events; using osu.Framework.Screens; using osu.Game.Graphics; using osu.Game.Screens.Play; +using osu.Game.Screens.Ranking; using osu.Game.Users; using osuTK.Input; @@ -31,7 +32,7 @@ namespace osu.Game.Screens.Select Edit(); }, Key.Number4); - ((PlayBeatmapDetailArea)BeatmapDetails).Leaderboard.ScoreSelected += score => this.Push(new SoloResults(score)); + ((PlayBeatmapDetailArea)BeatmapDetails).Leaderboard.ScoreSelected += score => this.Push(new ResultsScreen(score)); } protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); From 86a336d58537c8c3abbe550ea260a127703f3863 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 17 Mar 2020 17:45:25 +0900 Subject: [PATCH 0440/2376] Add back retry overlay --- osu.Game/Screens/Ranking/ResultsScreen.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index d0e02329fe..89547fc5dc 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -10,6 +10,7 @@ using osu.Framework.Screens; using osu.Game.Graphics.UserInterface; using osu.Game.Scoring; using osu.Game.Screens.Backgrounds; +using osu.Game.Screens.Play; using osuTK; namespace osu.Game.Screens.Ranking @@ -25,6 +26,9 @@ namespace osu.Game.Screens.Ranking protected override BackgroundScreen CreateBackground() => new BackgroundScreenBeatmap(Beatmap.Value); + [Resolved(CanBeNull = true)] + private Player player { get; set; } + private readonly ScoreInfo score; private Drawable bottomPanel; @@ -75,6 +79,19 @@ namespace osu.Game.Screens.Ranking } } }; + + if (player != null) + { + AddInternal(new HotkeyRetryOverlay + { + Action = () => + { + if (!this.IsCurrentScreen()) return; + + player?.Restart(); + }, + }); + } } public override void OnEntering(IScreen last) From 6f569d148515b6276c51fabd742bffaa6c7b7cd0 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 17 Mar 2020 18:01:46 +0900 Subject: [PATCH 0441/2376] Fix colour conflicts for expert-plus --- .../Ranking/TestSceneStarRatingDisplay.cs | 32 +++++++++++++++++++ .../Ranking/Expanded/StarRatingDisplay.cs | 8 ++++- 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Tests/Visual/Ranking/TestSceneStarRatingDisplay.cs diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStarRatingDisplay.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStarRatingDisplay.cs new file mode 100644 index 0000000000..d12f32e470 --- /dev/null +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStarRatingDisplay.cs @@ -0,0 +1,32 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Screens.Ranking.Expanded; + +namespace osu.Game.Tests.Visual.Ranking +{ + public class TestSceneStarRatingDisplay : OsuTestScene + { + public TestSceneStarRatingDisplay() + { + Child = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] + { + new StarRatingDisplay(new BeatmapInfo { StarDifficulty = 1.23 }), + new StarRatingDisplay(new BeatmapInfo { StarDifficulty = 2.34 }), + new StarRatingDisplay(new BeatmapInfo { StarDifficulty = 3.45 }), + new StarRatingDisplay(new BeatmapInfo { StarDifficulty = 4.56 }), + new StarRatingDisplay(new BeatmapInfo { StarDifficulty = 5.67 }), + new StarRatingDisplay(new BeatmapInfo { StarDifficulty = 6.78 }), + new StarRatingDisplay(new BeatmapInfo { StarDifficulty = 10.11 }), + } + }; + } + } +} diff --git a/osu.Game/Screens/Ranking/Expanded/StarRatingDisplay.cs b/osu.Game/Screens/Ranking/Expanded/StarRatingDisplay.cs index 74b58b9f8c..4b38b298f1 100644 --- a/osu.Game/Screens/Ranking/Expanded/StarRatingDisplay.cs +++ b/osu.Game/Screens/Ranking/Expanded/StarRatingDisplay.cs @@ -3,7 +3,9 @@ using System.Globalization; using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; @@ -40,6 +42,10 @@ namespace osu.Game.Screens.Ranking.Expanded string fractionPart = starRatingParts[1]; string separator = CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator; + ColourInfo backgroundColour = beatmap.DifficultyRating == DifficultyRating.ExpertPlus + ? ColourInfo.GradientVertical(Color4Extensions.FromHex("#C1C1C1"), Color4Extensions.FromHex("#595959")) + : (ColourInfo)colours.ForDifficultyRating(beatmap.DifficultyRating); + InternalChildren = new Drawable[] { new CircularContainer @@ -51,7 +57,7 @@ namespace osu.Game.Screens.Ranking.Expanded new Box { RelativeSizeAxes = Axes.Both, - Colour = colours.ForDifficultyRating(beatmap.DifficultyRating) + Colour = backgroundColour }, } }, From f06c170d63d5d7b69ed2717eac3fb4e08be787ef Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Mar 2020 18:04:15 +0900 Subject: [PATCH 0442/2376] Display SS badge earlier (when entering virtual ss area) --- osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs index 873c20cc2b..4b6f777283 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs @@ -222,7 +222,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy if (badge.Accuracy > score.Accuracy) continue; - using (BeginDelayedSequence(inverseEasing(ACCURACY_TRANSFORM_EASING, badge.Accuracy / targetAccuracy) * ACCURACY_TRANSFORM_DURATION, true)) + using (BeginDelayedSequence(inverseEasing(ACCURACY_TRANSFORM_EASING, Math.Min(1 - virtual_ss_percentage, badge.Accuracy) / targetAccuracy) * ACCURACY_TRANSFORM_DURATION, true)) badge.Appear(); } From df119eb95a8177093be99e7c039053aa50cd56ca Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Mar 2020 18:04:32 +0900 Subject: [PATCH 0443/2376] Adjust animations --- .../Ranking/Expanded/Accuracy/RankText.cs | 66 +++++++++++++++++-- 1 file changed, 60 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/RankText.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/RankText.cs index b803fe6022..8343716e7e 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/RankText.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/RankText.cs @@ -4,11 +4,13 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Online.Leaderboards; using osu.Game.Scoring; using osuTK; +using osuTK.Graphics; namespace osu.Game.Screens.Ranking.Expanded.Accuracy { @@ -19,7 +21,9 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy { private readonly ScoreRank rank; - private Drawable flash; + private BufferedContainer flash; + private BufferedContainer superFlash; + private GlowingSpriteText rankText; public RankText(ScoreRank rank) { @@ -35,17 +39,38 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy [BackgroundDependencyLoader] private void load() { - InternalChildren = new[] + InternalChildren = new Drawable[] { - new GlowingSpriteText + rankText = new GlowingSpriteText { Anchor = Anchor.Centre, Origin = Anchor.Centre, + GlowColour = OsuColour.ForRank(rank), Spacing = new Vector2(-15, 0), Text = DrawableRank.GetRankName(rank), Font = OsuFont.Numeric.With(size: 76), UseFullGlyphHeight = false }, + superFlash = new BufferedContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + BlurSigma = new Vector2(85), + Size = new Vector2(600), + CacheDrawnFrameBuffer = true, + Blending = BlendingParameters.Additive, + Alpha = 0, + Children = new[] + { + new Box + { + Colour = Color4.White, + Size = new Vector2(150), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + }, + }, flash = new BufferedContainer { Anchor = Anchor.Centre, @@ -53,8 +78,10 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy BlurSigma = new Vector2(35), BypassAutoSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both, + CacheDrawnFrameBuffer = true, Blending = BlendingParameters.Additive, - Size = new Vector2(2f), + Alpha = 0, + Size = new Vector2(2f), // increase buffer size to allow for scale Scale = new Vector2(1.8f), Children = new[] { @@ -75,9 +102,36 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy public void Appear() { - this.FadeIn(0, Easing.In); + this.FadeIn(); - flash.FadeIn(0, Easing.In).Then().FadeOut(800, Easing.OutQuint); + if (rank < ScoreRank.A) + { + this + .MoveToOffset(new Vector2(0, -20)) + .MoveToOffset(new Vector2(0, 20), 200, Easing.OutBounce); + + if (rank <= ScoreRank.D) + { + this.Delay(700) + .RotateTo(5, 150, Easing.In) + .MoveToOffset(new Vector2(0, 3), 150, Easing.In); + } + + this.FadeInFromZero(200, Easing.OutQuint); + return; + } + + flash.Colour = OsuColour.ForRank(rank); + flash.FadeIn().Then().FadeOut(1200, Easing.OutQuint); + + if (rank >= ScoreRank.S) + rankText.ScaleTo(1.05f).ScaleTo(1, 3000, Easing.OutQuint); + + if (rank >= ScoreRank.X) + { + flash.FadeIn().Then().FadeOut(3000); + superFlash.FadeIn().Then().FadeOut(800, Easing.OutQuint); + } } } } From 370ff70dd441aa1d02d82b872bb339eb468048e5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Mar 2020 18:32:30 +0900 Subject: [PATCH 0444/2376] Fix incorrect host name specification --- osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index c1bd73ef05..c6095ae404 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -417,7 +417,7 @@ namespace osu.Game.Tests.Beatmaps.IO [Test] public async Task TestImportWithDuplicateHashes() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportNestedStructure))) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportWithDuplicateHashes))) { try { From 48cbec7a319383a892d1dc12d99c8d33a5748d3e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Mar 2020 22:21:16 +0900 Subject: [PATCH 0445/2376] Add scroll view because --- osu.Game/Screens/Ranking/ResultsScreen.cs | 36 ++++++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 89547fc5dc..6f8b5d19df 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Screens; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Scoring; using osu.Game.Screens.Backgrounds; @@ -43,11 +44,14 @@ namespace osu.Game.Screens.Ranking { InternalChildren = new[] { - new ScorePanel(score) + new ResultsScrollContainer { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - State = PanelState.Expanded + Child = new ScorePanel(score) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + State = PanelState.Expanded + }, }, bottomPanel = new Container { @@ -110,5 +114,29 @@ namespace osu.Game.Screens.Ranking return base.OnExiting(next); } + + private class ResultsScrollContainer : OsuScrollContainer + { + private readonly Container content; + + protected override Container Content => content; + + public ResultsScrollContainer() + { + base.Content.Add(content = new Container + { + RelativeSizeAxes = Axes.X + }); + + RelativeSizeAxes = Axes.Both; + ScrollbarVisible = false; + } + + protected override void Update() + { + base.Update(); + content.Height = DrawHeight; + } + } } } From 24b944fc8e4ba12346052376f7d5ff982544f31d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Mar 2020 22:24:28 +0900 Subject: [PATCH 0446/2376] Make footer buttons wider --- osu.Game/Screens/Ranking/ResultsScreen.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 6f8b5d19df..0952ba1f70 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -76,8 +76,8 @@ namespace osu.Game.Screens.Ranking Direction = FillDirection.Horizontal, Children = new Drawable[] { - new ReplayDownloadButton(score), - new RetryButton() + new ReplayDownloadButton(score) { Width = 300 }, + new RetryButton { Width = 300 }, } } } From 27cc68152d426af6025321414361423437321f91 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Mar 2020 22:54:02 +0900 Subject: [PATCH 0447/2376] Fix potentially missing metadata on local score display --- .../Expanded/ExpandedPanelMiddleContent.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index 6d5d7e0d95..cc376d7dcc 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -8,6 +8,7 @@ using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Localisation; +using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -45,8 +46,16 @@ namespace osu.Game.Screens.Ranking.Expanded } [BackgroundDependencyLoader] - private void load() + private void load(BeatmapManager beatmaps) { + var metadata = score.Beatmap.Metadata; + + if (metadata == null) + { + var beatmap = beatmaps.QueryBeatmap(b => b.ID == score.BeatmapInfoID); + metadata = beatmap.Metadata ?? beatmap.BeatmapSet.Metadata; + } + var topStatistics = new List { new AccuracyStatistic(score.Accuracy), @@ -81,14 +90,14 @@ namespace osu.Game.Screens.Ranking.Expanded { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Text = new LocalisedString((score.Beatmap.Metadata.Title, score.Beatmap.Metadata.TitleUnicode)), + Text = new LocalisedString((metadata.Title, metadata.TitleUnicode)), Font = OsuFont.Torus.With(size: 20, weight: FontWeight.SemiBold), }, new OsuSpriteText { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Text = new LocalisedString((score.Beatmap.Metadata.Artist, score.Beatmap.Metadata.ArtistUnicode)), + Text = new LocalisedString((metadata.Artist, metadata.ArtistUnicode)), Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold) }, new Container From dd3a6c5673fc25ede3838a56c03399a5ceeb0dfe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Mar 2020 23:13:31 +0900 Subject: [PATCH 0448/2376] Use working beatmap to retrieve metadata for now --- .../Expanded/ExpandedPanelMiddleContent.cs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index cc376d7dcc..6b7e4c9cb4 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -46,15 +47,10 @@ namespace osu.Game.Screens.Ranking.Expanded } [BackgroundDependencyLoader] - private void load(BeatmapManager beatmaps) + private void load(Bindable working) { - var metadata = score.Beatmap.Metadata; - - if (metadata == null) - { - var beatmap = beatmaps.QueryBeatmap(b => b.ID == score.BeatmapInfoID); - metadata = beatmap.Metadata ?? beatmap.BeatmapSet.Metadata; - } + var beatmap = working.Value.BeatmapInfo; + var metadata = beatmap.Metadata; var topStatistics = new List { @@ -129,7 +125,7 @@ namespace osu.Game.Screens.Ranking.Expanded AutoSizeAxes = Axes.Both, Children = new Drawable[] { - new StarRatingDisplay(score.Beatmap) + new StarRatingDisplay(beatmap) { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft @@ -157,7 +153,7 @@ namespace osu.Game.Screens.Ranking.Expanded { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Text = score.Beatmap.Version, + Text = beatmap.Version, Font = OsuFont.Torus.With(size: 16, weight: FontWeight.SemiBold), }, new OsuTextFlowContainer(s => s.Font = OsuFont.Torus.With(size: 12)) From 592d8cbd1343d4a38c64c0e47fa5b12edf4b4cb8 Mon Sep 17 00:00:00 2001 From: recapitalverb <41869184+recapitalverb@users.noreply.github.com> Date: Tue, 17 Mar 2020 22:45:28 +0700 Subject: [PATCH 0449/2376] Fix mapper name in score panel --- osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index 6b7e4c9cb4..837467d648 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -163,7 +163,7 @@ namespace osu.Game.Screens.Ranking.Expanded }.With(t => { t.AddText("mapped by "); - t.AddText(score.UserString, s => s.Font = s.Font.With(weight: FontWeight.SemiBold)); + t.AddText(score.Beatmap.Metadata.Author.Username, s => s.Font = s.Font.With(weight: FontWeight.SemiBold)); }) } }, From 99409fb7d1c6596c0aa5d4e8435907b4d92c9b19 Mon Sep 17 00:00:00 2001 From: recapitalverb <41869184+recapitalverb@users.noreply.github.com> Date: Tue, 17 Mar 2020 23:02:39 +0700 Subject: [PATCH 0450/2376] Fix mapper info alignment in score panel --- osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index 6b7e4c9cb4..d542a6f033 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -158,6 +158,8 @@ namespace osu.Game.Screens.Ranking.Expanded }, new OsuTextFlowContainer(s => s.Font = OsuFont.Torus.With(size: 12)) { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, }.With(t => From d18b21ba329f5fe7988605791cf6c01a99ab3ead Mon Sep 17 00:00:00 2001 From: recapitalverb <41869184+recapitalverb@users.noreply.github.com> Date: Tue, 17 Mar 2020 23:23:51 +0700 Subject: [PATCH 0451/2376] Use local variable for metadata instead --- osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index 837467d648..f92c8df012 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -163,7 +163,7 @@ namespace osu.Game.Screens.Ranking.Expanded }.With(t => { t.AddText("mapped by "); - t.AddText(score.Beatmap.Metadata.Author.Username, s => s.Font = s.Font.With(weight: FontWeight.SemiBold)); + t.AddText(metadata.Author.Username, s => s.Font = s.Font.With(weight: FontWeight.SemiBold)); }) } }, From 431571dfa07c8c1b5cb6a932ecbdcf5874f6a874 Mon Sep 17 00:00:00 2001 From: recapitalverb <41869184+recapitalverb@users.noreply.github.com> Date: Wed, 18 Mar 2020 00:15:43 +0700 Subject: [PATCH 0452/2376] Check nulls --- .../Ranking/Expanded/ExpandedPanelMiddleContent.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index f92c8df012..eb5da19303 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -51,6 +51,7 @@ namespace osu.Game.Screens.Ranking.Expanded { var beatmap = working.Value.BeatmapInfo; var metadata = beatmap.Metadata; + var creator = beatmap.Metadata.Author?.Username; var topStatistics = new List { @@ -162,8 +163,11 @@ namespace osu.Game.Screens.Ranking.Expanded Direction = FillDirection.Horizontal, }.With(t => { - t.AddText("mapped by "); - t.AddText(metadata.Author.Username, s => s.Font = s.Font.With(weight: FontWeight.SemiBold)); + if (!string.IsNullOrEmpty(creator)) + { + t.AddText("mapped by "); + t.AddText(creator, s => s.Font = s.Font.With(weight: FontWeight.SemiBold)); + } }) } }, From 100c9d422fbd043943e9743f57a3aeb00f2b23bd Mon Sep 17 00:00:00 2001 From: Tina Date: Tue, 17 Mar 2020 18:55:30 +0100 Subject: [PATCH 0453/2376] Re-use colors defined for each rank in result screen accuracy circle --- .../Ranking/Expanded/Accuracy/AccuracyCircle.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs index 4b6f777283..2d76a7c3b0 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs @@ -119,42 +119,42 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy new SmoothCircularProgress { RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex("#BE0089"), + Colour = OsuColour.ForRank(ScoreRank.X), InnerRadius = RANK_CIRCLE_RADIUS, Current = { Value = 1 } }, new SmoothCircularProgress { RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex("#0096A2"), + Colour = OsuColour.ForRank(ScoreRank.S), InnerRadius = RANK_CIRCLE_RADIUS, Current = { Value = 1 - virtual_ss_percentage } }, new SmoothCircularProgress { RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex("#72C904"), + Colour = OsuColour.ForRank(ScoreRank.A), InnerRadius = RANK_CIRCLE_RADIUS, Current = { Value = 0.95f } }, new SmoothCircularProgress { RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex("#D99D03"), + Colour = OsuColour.ForRank(ScoreRank.B), InnerRadius = RANK_CIRCLE_RADIUS, Current = { Value = 0.9f } }, new SmoothCircularProgress { RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex("#EA7948"), + Colour = OsuColour.ForRank(ScoreRank.C), InnerRadius = RANK_CIRCLE_RADIUS, Current = { Value = 0.8f } }, new SmoothCircularProgress { RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex("#FF5858"), + Colour = OsuColour.ForRank(ScoreRank.D), InnerRadius = RANK_CIRCLE_RADIUS, Current = { Value = 0.7f } }, From 139ae2bc1e3c65157ba228b69b459f688a102ae8 Mon Sep 17 00:00:00 2001 From: recapitalverb <41869184+recapitalverb@users.noreply.github.com> Date: Wed, 18 Mar 2020 01:24:58 +0700 Subject: [PATCH 0454/2376] Use existing variables instead --- osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index eb5da19303..82a2bd87ec 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -51,7 +51,7 @@ namespace osu.Game.Screens.Ranking.Expanded { var beatmap = working.Value.BeatmapInfo; var metadata = beatmap.Metadata; - var creator = beatmap.Metadata.Author?.Username; + var creator = metadata.Author?.Username; var topStatistics = new List { From dc73105a106473d4a98c854cdea4cd93eb72e112 Mon Sep 17 00:00:00 2001 From: recapitalverb <41869184+recapitalverb@users.noreply.github.com> Date: Wed, 18 Mar 2020 01:33:01 +0700 Subject: [PATCH 0455/2376] Add tests for beatmaps with(out) null mappers --- .../TestSceneExpandedPanelMiddleContent.cs | 43 +++++++++++++++++-- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs index 665b3ad455..fb8c438fa4 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs @@ -3,10 +3,13 @@ using System; using System.Collections.Generic; +using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; @@ -23,6 +26,11 @@ namespace osu.Game.Tests.Visual.Ranking { public class TestSceneExpandedPanelMiddleContent : OsuTestScene { + [Resolved] + private BeatmapManager beatmaps { get; set; } + + private User author; + public override IReadOnlyList RequiredTypes => new[] { typeof(ExpandedPanelMiddleContent), @@ -35,8 +43,37 @@ namespace osu.Game.Tests.Visual.Ranking typeof(TotalScoreCounter) }; - public TestSceneExpandedPanelMiddleContent() + protected override void LoadComplete() { + base.LoadComplete(); + + var beatmapInfo = beatmaps.QueryBeatmap(b => b.RulesetID == 0); + if (beatmapInfo != null) + { + Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo); + author = Beatmap.Value.Metadata.Author; + } + } + + [Test] + public void TestExampleScore() + { + addScoreStep(createTestScore()); + } + + [Test] + public void TestScoreWithNullAuthor() + { + AddStep("set author to null", () => { + Beatmap.Value.Metadata.Author = null; + }); + addScoreStep(createTestScore()); + AddStep("set author to not null", () => { + Beatmap.Value.Metadata.Author = author; + }); + } + + private void addScoreStep(ScoreInfo score) => AddStep("add panel", () => { Child = new Container { Anchor = Anchor.Centre, @@ -49,10 +86,10 @@ namespace osu.Game.Tests.Visual.Ranking RelativeSizeAxes = Axes.Both, Colour = Color4Extensions.FromHex("#444"), }, - new ExpandedPanelMiddleContent(createTestScore()) + new ExpandedPanelMiddleContent(score) } }; - } + }); private ScoreInfo createTestScore() => new ScoreInfo { From 7186e3466bd36dce2e1433bab1f3666ad5d9f6a8 Mon Sep 17 00:00:00 2001 From: recapitalverb <41869184+recapitalverb@users.noreply.github.com> Date: Wed, 18 Mar 2020 01:39:19 +0700 Subject: [PATCH 0456/2376] Fix formatting issues --- .../Ranking/TestSceneExpandedPanelMiddleContent.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs index fb8c438fa4..7a20ba6fd0 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs @@ -48,6 +48,7 @@ namespace osu.Game.Tests.Visual.Ranking base.LoadComplete(); var beatmapInfo = beatmaps.QueryBeatmap(b => b.RulesetID == 0); + if (beatmapInfo != null) { Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo); @@ -64,16 +65,19 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestScoreWithNullAuthor() { - AddStep("set author to null", () => { + AddStep("set author to null", () => + { Beatmap.Value.Metadata.Author = null; }); addScoreStep(createTestScore()); - AddStep("set author to not null", () => { + AddStep("set author to not null", () => + { Beatmap.Value.Metadata.Author = author; }); } - private void addScoreStep(ScoreInfo score) => AddStep("add panel", () => { + private void addScoreStep(ScoreInfo score) => AddStep("add panel", () => + { Child = new Container { Anchor = Anchor.Centre, From e951979a12ec05aadce4e4c0b68435d2a24f9f75 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 17 Mar 2020 22:34:46 +0300 Subject: [PATCH 0457/2376] Remove assert from online test --- .../Visual/Online/TestSceneFriendsLayout.cs | 18 +++++------------- .../Dashboard/Friends/FriendsLayout.cs | 10 +++++----- 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs b/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs index c6971a971d..e6b2a41c1c 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs @@ -10,7 +10,6 @@ using osu.Game.Users; using osu.Game.Overlays; using osu.Framework.Allocation; using NUnit.Framework; -using System.Linq; using osu.Game.Online.API; namespace osu.Game.Tests.Visual.Online @@ -32,7 +31,7 @@ namespace osu.Game.Tests.Visual.Online [Resolved] private IAPIProvider api { get; set; } - private TestFriendsLayout layout; + private FriendsLayout layout; [SetUp] public void Setup() => Schedule(() => @@ -40,21 +39,19 @@ namespace osu.Game.Tests.Visual.Online Child = new BasicScrollContainer { RelativeSizeAxes = Axes.Both, - Child = layout = new TestFriendsLayout() + Child = layout = new FriendsLayout() }; }); [Test] - public void TestOnline() + public void TestOffline() { - // Skip online test if user is not logged-in - AddUntilStep("Users loaded", () => !api.IsLoggedIn || (layout?.StatusControl.Items.Any() ?? false)); + AddStep("Populate", () => layout.Users = getUsers()); } [Test] - public void TestPopulate() + public void TestOnline() { - AddStep("Populate", () => layout.Users = getUsers()); } private List getUsers() => new List @@ -89,10 +86,5 @@ namespace osu.Game.Tests.Visual.Online LastVisit = DateTimeOffset.Now } }; - - private class TestFriendsLayout : FriendsLayout - { - public FriendsOnlineStatusControl StatusControl => OnlineStatusControl; - } } } diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs index c02f07fe4a..55a5081435 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs @@ -29,7 +29,7 @@ namespace osu.Game.Overlays.Dashboard.Friends users = value; - OnlineStatusControl.Populate(value); + onlineStatusControl.Populate(value); } } @@ -41,7 +41,7 @@ namespace osu.Game.Overlays.Dashboard.Friends private Drawable currentContent; - protected readonly FriendsOnlineStatusControl OnlineStatusControl; + private readonly FriendsOnlineStatusControl onlineStatusControl; private readonly Box background; private readonly Box controlBackground; private readonly UserListToolbar userListToolbar; @@ -77,7 +77,7 @@ namespace osu.Game.Overlays.Dashboard.Friends Top = 20, Horizontal = 45 }, - Child = OnlineStatusControl = new FriendsOnlineStatusControl(), + Child = onlineStatusControl = new FriendsOnlineStatusControl(), } } }, @@ -149,7 +149,7 @@ namespace osu.Game.Overlays.Dashboard.Friends { base.LoadComplete(); - OnlineStatusControl.Current.BindValueChanged(_ => recreatePanels()); + onlineStatusControl.Current.BindValueChanged(_ => recreatePanels()); userListToolbar.DisplayStyle.BindValueChanged(_ => recreatePanels()); userListToolbar.SortCriteria.BindValueChanged(_ => recreatePanels()); @@ -171,7 +171,7 @@ namespace osu.Game.Overlays.Dashboard.Friends if (itemsPlaceholder.Any()) loading.Show(); - var groupedUsers = OnlineStatusControl.Current.Value?.Users ?? new List(); + var groupedUsers = onlineStatusControl.Current.Value?.Users ?? new List(); var sortedUsers = sortUsers(groupedUsers); From 944f0b0285e51e32f9588d699d1f08c4fbda4f54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 17 Mar 2020 20:45:48 +0100 Subject: [PATCH 0458/2376] Rewrite tests * Use [Cached] injection instead of modifying beatmaps read from store. * Add assertion steps verifying the presence of mapper name (or lack thereof). --- .../TestSceneExpandedPanelMiddleContent.cs | 91 ++++++++++--------- 1 file changed, 50 insertions(+), 41 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs index 7a20ba6fd0..52d8ea0480 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs @@ -3,13 +3,18 @@ using System; using System.Collections.Generic; +using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; @@ -27,9 +32,7 @@ namespace osu.Game.Tests.Visual.Ranking public class TestSceneExpandedPanelMiddleContent : OsuTestScene { [Resolved] - private BeatmapManager beatmaps { get; set; } - - private User author; + private RulesetStore rulesetStore { get; set; } public override IReadOnlyList RequiredTypes => new[] { @@ -43,57 +46,37 @@ namespace osu.Game.Tests.Visual.Ranking typeof(TotalScoreCounter) }; - protected override void LoadComplete() + [Test] + public void TestMapWithKnownMapper() { - base.LoadComplete(); + var author = new User { Username = "mapper_name" }; - var beatmapInfo = beatmaps.QueryBeatmap(b => b.RulesetID == 0); + AddStep("show example score", () => showPanel(createTestBeatmap(author), createTestScore())); - if (beatmapInfo != null) - { - Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo); - author = Beatmap.Value.Metadata.Author; - } + AddAssert("mapper name present", () => this.ChildrenOfType().Any(spriteText => spriteText.Text == "mapper_name")); } [Test] - public void TestExampleScore() + public void TestMapWithUnknownMapper() { - addScoreStep(createTestScore()); + AddStep("show example score", () => showPanel(createTestBeatmap(null), createTestScore())); + + AddAssert("mapped by text not present", () => + this.ChildrenOfType().All(spriteText => !containsAny(spriteText.Text, "mapped", "by"))); } - [Test] - public void TestScoreWithNullAuthor() + private void showPanel(WorkingBeatmap workingBeatmap, ScoreInfo score) { - AddStep("set author to null", () => - { - Beatmap.Value.Metadata.Author = null; - }); - addScoreStep(createTestScore()); - AddStep("set author to not null", () => - { - Beatmap.Value.Metadata.Author = author; - }); + Child = new ExpandedPanelMiddleContentContainer(workingBeatmap, score); } - private void addScoreStep(ScoreInfo score) => AddStep("add panel", () => + private WorkingBeatmap createTestBeatmap(User author) { - Child = new Container - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(500, 700), - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex("#444"), - }, - new ExpandedPanelMiddleContent(score) - } - }; - }); + var beatmap = new TestBeatmap(rulesetStore.GetRuleset(0)); + beatmap.Metadata.Author = author; + + return new TestWorkingBeatmap(beatmap); + } private ScoreInfo createTestScore() => new ScoreInfo { @@ -117,5 +100,31 @@ namespace osu.Game.Tests.Visual.Ranking { HitResult.Great, 300 }, } }; + + private bool containsAny(string text, params string[] stringsToMatch) => stringsToMatch.Any(text.Contains); + + private class ExpandedPanelMiddleContentContainer : Container + { + [Cached] + private Bindable workingBeatmap { get; set; } + + public ExpandedPanelMiddleContentContainer(WorkingBeatmap beatmap, ScoreInfo score) + { + workingBeatmap = new Bindable(beatmap); + + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + Size = new Vector2(500, 700); + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#444"), + }, + new ExpandedPanelMiddleContent(score) + }; + } + } } } From bee8e22d185e39abca1e54886fc3bb3998f3b8a4 Mon Sep 17 00:00:00 2001 From: Fuewburvpoa Date: Tue, 17 Mar 2020 22:27:11 +0200 Subject: [PATCH 0459/2376] Fix BeatDivisorControl allow to select value outside of VALID_DIVISORS --- .../Screens/Edit/Compose/Components/BeatDivisorControl.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs index 8201ec2710..626a12edcf 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs @@ -279,6 +279,10 @@ namespace osu.Game.Screens.Edit.Compose.Components handleMouseInput(e.ScreenSpaceMousePosition); } + protected override void OnDragEnd(DragEndEvent e) + { + } + private void handleMouseInput(Vector2 screenSpaceMousePosition) { // copied from SliderBar so we can do custom spacing logic. From 8c611a981f0fc35ab89fb8012157dc7c62cecb00 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Mon, 16 Mar 2020 21:48:28 +0100 Subject: [PATCH 0460/2376] Update visual tests --- .../Visual/Gameplay/TestSceneHUDOverlay.cs | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs index fc03dc6ed3..579f6ff9b6 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs @@ -103,6 +103,38 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("return value", () => config.Set(OsuSetting.KeyOverlay, keyCounterVisibleValue)); } + [Test] + public void TestChangeHealthValue() + { + void applyToHealthDisplays(double value) + { + if (hudOverlay == null) return; + + hudOverlay.LowHealthDisplay.Current.Value = value; + hudOverlay.HealthDisplay.Current.Value = value; + } + + createNew(); + AddSliderStep("health value", 0, 1, 0.5, applyToHealthDisplays); + + AddStep("enable low health display", () => + { + config.Set(OsuSetting.FadePlayfieldWhenLowHealth, true); + hudOverlay.LowHealthDisplay.FinishTransforms(true); + }); + AddAssert("low health display is visible", () => hudOverlay.LowHealthDisplay.IsPresent); + AddStep("set health to 30%", () => applyToHealthDisplays(0.3)); + AddAssert("hud is not faded to red", () => !hudOverlay.LowHealthDisplay.Child.IsPresent); + AddStep("set health to < 10%", () => applyToHealthDisplays(0.1f)); + AddAssert("hud is faded to red", () => hudOverlay.LowHealthDisplay.Child.IsPresent); + AddStep("disable low health display", () => + { + config.Set(OsuSetting.FadePlayfieldWhenLowHealth, false); + hudOverlay.LowHealthDisplay.FinishTransforms(true); + }); + AddAssert("low health display is not visible", () => !hudOverlay.LowHealthDisplay.IsPresent); + } + private void createNew(Action action = null) { AddStep("create overlay", () => From 6b0c5bc65d1f6aa80fd98abd2261613ca971fbbc Mon Sep 17 00:00:00 2001 From: Lucas A Date: Tue, 17 Mar 2020 22:32:07 +0100 Subject: [PATCH 0461/2376] Rename to LowHealthLayer to FaillingLayer. --- .../Visual/Gameplay/TestSceneHUDOverlay.cs | 32 ------------- osu.Game/Screens/Play/HUD/FaillingLayer.cs | 47 +++++++++++++++++++ osu.Game/Screens/Play/HUD/LowHealthLayer.cs | 47 ------------------- 3 files changed, 47 insertions(+), 79 deletions(-) create mode 100644 osu.Game/Screens/Play/HUD/FaillingLayer.cs delete mode 100644 osu.Game/Screens/Play/HUD/LowHealthLayer.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs index 579f6ff9b6..fc03dc6ed3 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs @@ -103,38 +103,6 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("return value", () => config.Set(OsuSetting.KeyOverlay, keyCounterVisibleValue)); } - [Test] - public void TestChangeHealthValue() - { - void applyToHealthDisplays(double value) - { - if (hudOverlay == null) return; - - hudOverlay.LowHealthDisplay.Current.Value = value; - hudOverlay.HealthDisplay.Current.Value = value; - } - - createNew(); - AddSliderStep("health value", 0, 1, 0.5, applyToHealthDisplays); - - AddStep("enable low health display", () => - { - config.Set(OsuSetting.FadePlayfieldWhenLowHealth, true); - hudOverlay.LowHealthDisplay.FinishTransforms(true); - }); - AddAssert("low health display is visible", () => hudOverlay.LowHealthDisplay.IsPresent); - AddStep("set health to 30%", () => applyToHealthDisplays(0.3)); - AddAssert("hud is not faded to red", () => !hudOverlay.LowHealthDisplay.Child.IsPresent); - AddStep("set health to < 10%", () => applyToHealthDisplays(0.1f)); - AddAssert("hud is faded to red", () => hudOverlay.LowHealthDisplay.Child.IsPresent); - AddStep("disable low health display", () => - { - config.Set(OsuSetting.FadePlayfieldWhenLowHealth, false); - hudOverlay.LowHealthDisplay.FinishTransforms(true); - }); - AddAssert("low health display is not visible", () => !hudOverlay.LowHealthDisplay.IsPresent); - } - private void createNew(Action action = null) { AddStep("create overlay", () => diff --git a/osu.Game/Screens/Play/HUD/FaillingLayer.cs b/osu.Game/Screens/Play/HUD/FaillingLayer.cs new file mode 100644 index 0000000000..3dc18cefec --- /dev/null +++ b/osu.Game/Screens/Play/HUD/FaillingLayer.cs @@ -0,0 +1,47 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; + +namespace osu.Game.Screens.Play.HUD +{ + /// + /// An overlay layer on top of the player HUD which fades to red when the current player health falls a certain threshold defined by . + /// + public class FaillingLayer : HealthDisplay + { + private const float max_alpha = 0.4f; + + private readonly Box box; + + /// + /// The threshold under which the current player life should be considered low and the layer should start fading in. + /// + protected virtual double LowHealthThreshold => 0.20f; + + public FaillingLayer() + { + Child = box = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0 + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour color) + { + box.Colour = color.Red; + } + + protected override void Update() + { + box.Alpha = (float)Math.Clamp(max_alpha * (1 - Current.Value / LowHealthThreshold), 0, max_alpha); + base.Update(); + } + } +} diff --git a/osu.Game/Screens/Play/HUD/LowHealthLayer.cs b/osu.Game/Screens/Play/HUD/LowHealthLayer.cs deleted file mode 100644 index 8f03a95877..0000000000 --- a/osu.Game/Screens/Play/HUD/LowHealthLayer.cs +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Shapes; -using osu.Game.Configuration; -using osu.Game.Graphics; - -namespace osu.Game.Screens.Play.HUD -{ - public class LowHealthLayer : HealthDisplay - { - private const float max_alpha = 0.4f; - - private const double fade_time = 300; - - private readonly Box box; - - private Bindable configFadeRedWhenLowHealth; - - public LowHealthLayer() - { - Child = box = new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0 - }; - } - - [BackgroundDependencyLoader] - private void load(OsuConfigManager config, OsuColour color) - { - configFadeRedWhenLowHealth = config.GetBindable(OsuSetting.FadePlayfieldWhenLowHealth); - box.Colour = color.Red; - - configFadeRedWhenLowHealth.BindValueChanged(value => - { - if (value.NewValue) - this.FadeIn(fade_time, Easing.OutQuint); - else - this.FadeOut(fade_time, Easing.OutQuint); - }, true); - } - } -} From ed4f9f8ba9959c142dc1282ef37e735a4a162b7e Mon Sep 17 00:00:00 2001 From: Lucas A Date: Tue, 17 Mar 2020 22:57:47 +0100 Subject: [PATCH 0462/2376] Bind every HealthDisplay on Player load --- osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs | 8 ++++++++ osu.Game/Screens/Play/HUD/FaillingLayer.cs | 1 + osu.Game/Screens/Play/HUD/HealthDisplay.cs | 6 ++++++ osu.Game/Screens/Play/Player.cs | 4 ++++ 4 files changed, 19 insertions(+) diff --git a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs index a37ef8d9a0..50bff4fe3a 100644 --- a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using osu.Framework.Allocation; using osu.Framework.Input; using osu.Game.Beatmaps; using osu.Game.Input.Handlers; @@ -16,6 +17,7 @@ using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.UI; using osu.Game.Screens.Play; +using osu.Game.Screens.Play.HUD; using osuTK; namespace osu.Game.Rulesets.Osu.UI @@ -29,6 +31,12 @@ namespace osu.Game.Rulesets.Osu.UI { } + [BackgroundDependencyLoader] + private void load() + { + Overlays.Add(new FaillingLayer()); + } + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // always show the gameplay cursor protected override Playfield CreatePlayfield() => new OsuPlayfield(); diff --git a/osu.Game/Screens/Play/HUD/FaillingLayer.cs b/osu.Game/Screens/Play/HUD/FaillingLayer.cs index 3dc18cefec..6651ad6c88 100644 --- a/osu.Game/Screens/Play/HUD/FaillingLayer.cs +++ b/osu.Game/Screens/Play/HUD/FaillingLayer.cs @@ -25,6 +25,7 @@ namespace osu.Game.Screens.Play.HUD public FaillingLayer() { + RelativeSizeAxes = Axes.Both; Child = box = new Box { RelativeSizeAxes = Axes.Both, diff --git a/osu.Game/Screens/Play/HUD/HealthDisplay.cs b/osu.Game/Screens/Play/HUD/HealthDisplay.cs index 37038ad58c..6a5b77a64b 100644 --- a/osu.Game/Screens/Play/HUD/HealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/HealthDisplay.cs @@ -3,6 +3,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Screens.Play.HUD { @@ -13,5 +14,10 @@ namespace osu.Game.Screens.Play.HUD MinValue = 0, MaxValue = 1 }; + + public virtual void BindHealthProcessor(HealthProcessor processor) + { + Current.BindTo(processor.Health); + } } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index bcadba14af..0df4aacb7a 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -24,6 +24,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Scoring; +using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Ranking; using osu.Game.Skinning; using osu.Game.Users; @@ -184,6 +185,9 @@ namespace osu.Game.Screens.Play foreach (var mod in Mods.Value.OfType()) mod.ApplyToHealthProcessor(HealthProcessor); + foreach (var overlay in DrawableRuleset.Overlays.OfType()) + overlay.BindHealthProcessor(HealthProcessor); + BreakOverlay.IsBreakTime.BindValueChanged(onBreakTimeChanged, true); } From 44c13b081c4167685ace193a5a6fadae95072fcf Mon Sep 17 00:00:00 2001 From: Lucas A Date: Tue, 17 Mar 2020 22:58:20 +0100 Subject: [PATCH 0463/2376] Remove old configuration variants. --- osu.Game/Configuration/OsuConfigManager.cs | 2 -- .../Overlays/Settings/Sections/Gameplay/GeneralSettings.cs | 6 ------ 2 files changed, 8 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 895bacafc4..21de654670 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -88,7 +88,6 @@ namespace osu.Game.Configuration Set(OsuSetting.ShowInterface, true); Set(OsuSetting.ShowProgressGraph, true); Set(OsuSetting.ShowHealthDisplayWhenCantFail, true); - Set(OsuSetting.FadePlayfieldWhenLowHealth, true); Set(OsuSetting.KeyOverlay, false); Set(OsuSetting.ScoreMeter, ScoreMeterType.HitErrorBoth); @@ -184,7 +183,6 @@ namespace osu.Game.Configuration ShowInterface, ShowProgressGraph, ShowHealthDisplayWhenCantFail, - FadePlayfieldWhenLowHealth, MouseDisableButtons, MouseDisableWheel, AudioOffset, diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs index 6b6b3e8fa4..2d2cd42213 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs @@ -53,12 +53,6 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay Keywords = new[] { "hp", "bar" } }, new SettingsCheckbox - { - LabelText = "Fade playfield to red when health is low", - Bindable = config.GetBindable(OsuSetting.FadePlayfieldWhenLowHealth), - Keywords = new[] { "hp", "playfield", "health" } - }, - new SettingsCheckbox { LabelText = "Always show key overlay", Bindable = config.GetBindable(OsuSetting.KeyOverlay) From 6c825eb74499799fe5dd7c07f56fbd0359862c9f Mon Sep 17 00:00:00 2001 From: Fuewburvpoa <44163588+Fuewburvpoa@users.noreply.github.com> Date: Wed, 18 Mar 2020 00:04:03 +0200 Subject: [PATCH 0464/2376] Update osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Bartłomiej Dach --- osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs index 626a12edcf..2dec3fd22e 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs @@ -281,6 +281,7 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override void OnDragEnd(DragEndEvent e) { + handleMouseInput(e.ScreenSpaceMousePosition); } private void handleMouseInput(Vector2 screenSpaceMousePosition) From 17bae532bd91e782ac9be727843b4e8f57456df9 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Tue, 17 Mar 2020 23:09:50 +0100 Subject: [PATCH 0465/2376] Add failling layer to others rulesets. --- osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs | 8 ++++++++ osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs | 3 +++ 2 files changed, 11 insertions(+) diff --git a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs index fd8a1d175d..705c2d756c 100644 --- a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using osu.Framework.Allocation; using osu.Framework.Input; using osu.Game.Beatmaps; using osu.Game.Configuration; @@ -14,6 +15,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Screens.Play.HUD; namespace osu.Game.Rulesets.Catch.UI { @@ -30,6 +32,12 @@ namespace osu.Game.Rulesets.Catch.UI TimeRange.Value = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450); } + [BackgroundDependencyLoader] + private void load() + { + Overlays.Add(new FaillingLayer()); + } + protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new CatchFramedReplayInputHandler(replay); protected override Playfield CreatePlayfield() => new CatchPlayfield(Beatmap.BeatmapInfo.BaseDifficulty, CreateDrawableRepresentation); diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index 2c497541a8..b8b6ff3c3c 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -19,6 +19,7 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Screens.Play.HUD; using osuTK; namespace osu.Game.Rulesets.Mania.UI @@ -52,6 +53,8 @@ namespace osu.Game.Rulesets.Mania.UI configDirection.BindValueChanged(direction => Direction.Value = (ScrollingDirection)direction.NewValue, true); Config.BindWith(ManiaRulesetSetting.ScrollTime, TimeRange); + + Overlays.Add(new FaillingLayer()); } /// From a5c0da392edd913928371f4e2e7ea072518cb80a Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Tue, 17 Mar 2020 18:22:29 -0400 Subject: [PATCH 0466/2376] fix 'good first issue' link in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 77c7eb9d2d..b44a529f2b 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ JetBrains ReSharper InspectCode is also used for wider rule sets. You can run it We welcome all contributions, but keep in mind that we already have a lot of the UI designed. If you wish to work on something with the intention of having it included in the official distribution, please open an issue for discussion and we will give you what you need from a design perspective to proceed. If you want to make *changes* to the design, we recommend you open an issue with your intentions before spending too much time to ensure no effort is wasted. -If you're unsure of what you can help with, check out the [list of open issues](https://github.com/ppy/osu/issues) (especially those with the ["good first issue"](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22good+first+issue%22) label). +If you're unsure of what you can help with, check out the [list of open issues](https://github.com/ppy/osu/issues) (especially those with the ["good first issue"](https://github.com/ppy/osu/labels/good-first-issue) label). Before starting, please make sure you are familiar with the [development and testing](https://github.com/ppy/osu-framework/wiki/Development-and-Testing) procedure we have set up. New component development, and where possible, bug fixing and debugging existing components **should always be done under VisualTests**. From 23e698c8a5d025221dc6fad9d183b2633ef14840 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Tue, 17 Mar 2020 18:26:41 -0400 Subject: [PATCH 0467/2376] sort by most recently updated --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b44a529f2b..59d72247f5 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ JetBrains ReSharper InspectCode is also used for wider rule sets. You can run it We welcome all contributions, but keep in mind that we already have a lot of the UI designed. If you wish to work on something with the intention of having it included in the official distribution, please open an issue for discussion and we will give you what you need from a design perspective to proceed. If you want to make *changes* to the design, we recommend you open an issue with your intentions before spending too much time to ensure no effort is wasted. -If you're unsure of what you can help with, check out the [list of open issues](https://github.com/ppy/osu/issues) (especially those with the ["good first issue"](https://github.com/ppy/osu/labels/good-first-issue) label). +If you're unsure of what you can help with, check out the [list of open issues](https://github.com/ppy/osu/issues) (especially those with the ["good first issue"](https://github.com/ppy/osu/issues?q=is%3Aopen+label%3Agood-first-issue+sort%3Aupdated-desc) label). Before starting, please make sure you are familiar with the [development and testing](https://github.com/ppy/osu-framework/wiki/Development-and-Testing) procedure we have set up. New component development, and where possible, bug fixing and debugging existing components **should always be done under VisualTests**. From 3c07f73c7b1b559e848419d3af6fc0b047db56ec Mon Sep 17 00:00:00 2001 From: Joehu Date: Tue, 17 Mar 2020 17:32:58 -0700 Subject: [PATCH 0468/2376] Fix results' beatmap title and artist language setting being swapped --- .../Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index 82a2bd87ec..d64008e6db 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -87,14 +87,14 @@ namespace osu.Game.Screens.Ranking.Expanded { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Text = new LocalisedString((metadata.Title, metadata.TitleUnicode)), + Text = new LocalisedString((metadata.TitleUnicode, metadata.Title)), Font = OsuFont.Torus.With(size: 20, weight: FontWeight.SemiBold), }, new OsuSpriteText { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Text = new LocalisedString((metadata.Artist, metadata.ArtistUnicode)), + Text = new LocalisedString((metadata.ArtistUnicode, metadata.Artist)), Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold) }, new Container From 044707adb7d1f1d17625e46bcfd242f728b3b699 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Mar 2020 12:10:51 +0900 Subject: [PATCH 0469/2376] Update incorrect file path causing error on rider solution load --- .idea/.idea.osu.Desktop/.idea/modules.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/.idea/.idea.osu.Desktop/.idea/modules.xml b/.idea/.idea.osu.Desktop/.idea/modules.xml index fe63f5faf3..366f172c30 100644 --- a/.idea/.idea.osu.Desktop/.idea/modules.xml +++ b/.idea/.idea.osu.Desktop/.idea/modules.xml @@ -2,6 +2,7 @@ + From 44cfed8af1cb7a577f67c5c25c895eb2fb6c2a51 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Mar 2020 15:03:01 +0900 Subject: [PATCH 0470/2376] Fix perfect display showing when misses are present --- .../Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index 7a50a96fe7..cd2534bd31 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -14,6 +14,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Ranking.Expanded.Accuracy; @@ -56,7 +57,7 @@ namespace osu.Game.Screens.Ranking.Expanded var topStatistics = new List { new AccuracyStatistic(score.Accuracy), - new ComboStatistic(score.MaxCombo, true), + new ComboStatistic(score.MaxCombo, score.Statistics[HitResult.Miss] == 0), new CounterStatistic("pp", (int)(score.PP ?? 0)), }; From 336d92715755320e33ab6c2f17095935a42b99c3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Mar 2020 15:38:19 +0900 Subject: [PATCH 0471/2376] Update tests to not use positional data (nunit runs at an incompatible window size) --- .../Screens/TestSceneMapPoolScreen.cs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs index cc5f66761e..a4538be384 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs @@ -4,6 +4,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Game.Tournament.Components; using osu.Game.Tournament.Models; @@ -38,7 +39,7 @@ namespace osu.Game.Tournament.Tests.Screens Ladder.CurrentMatch.Value = Ladder.Matches.First(); }); - AddAssert("ensure layout width is 2", () => screen.ChildrenOfType().ElementAt(2).Y > 0); + assertTwoWide(); } [Test] @@ -58,7 +59,7 @@ namespace osu.Game.Tournament.Tests.Screens Ladder.CurrentMatch.Value = Ladder.Matches.First(); }); - AddAssert("ensure layout width is 2", () => screen.ChildrenOfType().ElementAt(2).Y > 0); + assertTwoWide(); } [Test] @@ -78,7 +79,7 @@ namespace osu.Game.Tournament.Tests.Screens Ladder.CurrentMatch.Value = Ladder.Matches.First(); }); - AddAssert("ensure layout width is 3", () => screen.ChildrenOfType().ElementAt(2).Y == 0); + assertThreeWide(); } [Test] @@ -98,9 +99,15 @@ namespace osu.Game.Tournament.Tests.Screens Ladder.CurrentMatch.Value = Ladder.Matches.First(); }); - AddAssert("ensure layout width is 2", () => screen.ChildrenOfType().ElementAt(2).Y > 0); + assertTwoWide(); } + private void assertTwoWide() => + AddAssert("ensure layout width is 2", () => screen.ChildrenOfType>>().First().Padding.Left > 0); + + private void assertThreeWide() => + AddAssert("ensure layout width is 3", () => screen.ChildrenOfType>>().First().Padding.Left == 0); + [Test] public void TestManyMods() { @@ -118,7 +125,7 @@ namespace osu.Game.Tournament.Tests.Screens Ladder.CurrentMatch.Value = Ladder.Matches.First(); }); - AddAssert("ensure layout width is 3", () => screen.ChildrenOfType().ElementAt(2).Y == 0); + assertThreeWide(); } private void addBeatmap(string mods = "nm") From fdcb60706b286ea8c4c8479206642d4fe8ad9a7f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Mar 2020 15:49:24 +0900 Subject: [PATCH 0472/2376] Use TryGetValue to make tests happy --- osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index cd2534bd31..4ae9bb05cc 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -57,7 +57,7 @@ namespace osu.Game.Screens.Ranking.Expanded var topStatistics = new List { new AccuracyStatistic(score.Accuracy), - new ComboStatistic(score.MaxCombo, score.Statistics[HitResult.Miss] == 0), + new ComboStatistic(score.MaxCombo, !score.Statistics.TryGetValue(HitResult.Miss, out var missCount) || missCount == 0), new CounterStatistic("pp", (int)(score.PP ?? 0)), }; From a1274a9eb0ca927c4d07cb94aaba0fa101745a4a Mon Sep 17 00:00:00 2001 From: Lucas A Date: Wed, 18 Mar 2020 08:17:41 +0100 Subject: [PATCH 0473/2376] Fix and add missing XMLDoc --- osu.Game/Screens/Play/HUD/FaillingLayer.cs | 2 +- osu.Game/Screens/Play/HUD/HealthDisplay.cs | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/FaillingLayer.cs b/osu.Game/Screens/Play/HUD/FaillingLayer.cs index 6651ad6c88..55cc4476b0 100644 --- a/osu.Game/Screens/Play/HUD/FaillingLayer.cs +++ b/osu.Game/Screens/Play/HUD/FaillingLayer.cs @@ -10,7 +10,7 @@ using osu.Game.Graphics; namespace osu.Game.Screens.Play.HUD { /// - /// An overlay layer on top of the player HUD which fades to red when the current player health falls a certain threshold defined by . + /// An overlay layer on top of the playfield which fades to red when the current player health falls a certain threshold defined by . /// public class FaillingLayer : HealthDisplay { diff --git a/osu.Game/Screens/Play/HUD/HealthDisplay.cs b/osu.Game/Screens/Play/HUD/HealthDisplay.cs index 6a5b77a64b..4094b3de69 100644 --- a/osu.Game/Screens/Play/HUD/HealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/HealthDisplay.cs @@ -4,9 +4,14 @@ using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI; namespace osu.Game.Screens.Play.HUD { + /// + /// A container for components displaying the current player health. + /// Gets bound automatically to the when inserted to hierarchy. + /// public abstract class HealthDisplay : Container { public readonly BindableDouble Current = new BindableDouble @@ -14,7 +19,11 @@ namespace osu.Game.Screens.Play.HUD MinValue = 0, MaxValue = 1 }; - + + /// + /// Bind the tracked fields of to this health display. + /// + /// public virtual void BindHealthProcessor(HealthProcessor processor) { Current.BindTo(processor.Health); From 80a86102b65b5a2421ef75e1899ff609ae463cb8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 18 Mar 2020 17:00:48 +0900 Subject: [PATCH 0474/2376] Add test --- .../TestSceneNoteLock.cs | 180 ++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs new file mode 100644 index 0000000000..a7416671f6 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs @@ -0,0 +1,180 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Screens; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Replays; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Replays; +using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Screens.Play; +using osu.Game.Tests.Visual; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests +{ + public class TestSceneNoteLock : RateAdjustedBeatmapTestScene + { + private const double time_first_circle = 1500; + private const double time_second_circle = 1600; + + private static readonly Vector2 position_first_circle = Vector2.Zero; + private static readonly Vector2 position_second_circle = new Vector2(80); + + /// + /// Tests clicking the second circle before the first hitobject's start time, while the first hitobject HAS NOT been judged. + /// + [Test] + public void TestClickSecondCircleBeforeFirstCircleTime() + { + performTest(new List + { + new OsuReplayFrame { Time = time_first_circle - 100, Position = position_second_circle, Actions = { OsuAction.LeftButton } } + }); + + addJudgementAssert(HitResult.Miss, HitResult.Miss); + } + + /// + /// Tests clicking the second circle at the first hitobject's start time, while the first hitobject HAS NOT been judged. + /// + [Test] + public void TestClickSecondCircleAtFirstCircleTime() + { + performTest(new List + { + new OsuReplayFrame { Time = time_first_circle, Position = position_second_circle, Actions = { OsuAction.LeftButton } } + }); + + addJudgementAssert(HitResult.Miss, HitResult.Miss); + } + + /// + /// Tests clicking the second circle after the first hitobject's start time, while the first hitobject HAS NOT been judged. + /// + [Test] + public void TestClickSecondCircleAfterFirstCircleTime() + { + performTest(new List + { + new OsuReplayFrame { Time = time_first_circle + 100, Position = position_second_circle, Actions = { OsuAction.LeftButton } } + }); + + addJudgementAssert(HitResult.Miss, HitResult.Great); + } + + /// + /// Tests clicking the second circle before the first hitobject's start time, while the first hitobject HAS been judged. + /// + [Test] + public void TestClickSecondCircleBeforeFirstCircleTimeWithFirstCircleJudged() + { + performTest(new List + { + new OsuReplayFrame { Time = time_first_circle - 200, Position = position_first_circle, Actions = { OsuAction.LeftButton } }, + new OsuReplayFrame { Time = time_first_circle - 100, Position = position_second_circle, Actions = { OsuAction.RightButton } } + }); + + addJudgementAssert(HitResult.Great, HitResult.Great); + } + + private void addJudgementAssert(HitResult firstCircle, HitResult secondCircle) + { + AddAssert($"first circle judgement is {firstCircle}", () => judgementResults.Single(r => r.HitObject.StartTime == time_first_circle).Type == firstCircle); + AddAssert($"second circle judgement is {secondCircle}", () => judgementResults.Single(r => r.HitObject.StartTime == time_second_circle).Type == secondCircle); + } + + private ScoreAccessibleReplayPlayer currentPlayer; + private List judgementResults; + private bool allJudgedFired; + + private void performTest(List frames) + { + AddStep("load player", () => + { + Beatmap.Value = CreateWorkingBeatmap(new Beatmap + { + HitObjects = + { + new TestHitCircle + { + StartTime = time_first_circle, + Position = position_first_circle + }, + new TestHitCircle + { + StartTime = time_second_circle, + Position = position_second_circle + } + }, + BeatmapInfo = + { + BaseDifficulty = new BeatmapDifficulty { SliderTickRate = 3 }, + Ruleset = new OsuRuleset().RulesetInfo + }, + }); + + Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f }); + + var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } }); + + p.OnLoadComplete += _ => + { + p.ScoreProcessor.NewJudgement += result => + { + if (currentPlayer == p) judgementResults.Add(result); + }; + p.ScoreProcessor.AllJudged += () => + { + if (currentPlayer == p) allJudgedFired = true; + }; + }; + + LoadScreen(currentPlayer = p); + allJudgedFired = false; + judgementResults = new List(); + }); + + AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0); + AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); + AddUntilStep("Wait for all judged", () => allJudgedFired); + } + + private class TestHitCircle : HitCircle + { + protected override HitWindows CreateHitWindows() => new TestHitWindows(); + } + + private class TestHitWindows : HitWindows + { + private static readonly DifficultyRange[] ranges = + { + new DifficultyRange(HitResult.Great, 500, 500, 500), + new DifficultyRange(HitResult.Miss, 1000, 1000, 1000), + }; + + public override bool IsHitResultAllowed(HitResult result) => result == HitResult.Great || result == HitResult.Miss; + + protected override DifficultyRange[] GetRanges() => ranges; + } + + private class ScoreAccessibleReplayPlayer : ReplayPlayer + { + public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; + + protected override bool PauseOnFocusLost => false; + + public ScoreAccessibleReplayPlayer(Score score) + : base(score, false, false) + { + } + } + } +} From 5f09c70f756f6548577a8a28d21335a7ce4df43f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 18 Mar 2020 17:21:36 +0900 Subject: [PATCH 0475/2376] Move judgement colours to OsuColour --- osu.Game/Graphics/OsuColour.cs | 27 +++++++++++++++++++ .../Rulesets/Judgements/DrawableJudgement.cs | 26 +----------------- 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs index f7ed55410c..caf09a7df6 100644 --- a/osu.Game/Graphics/OsuColour.cs +++ b/osu.Game/Graphics/OsuColour.cs @@ -3,6 +3,7 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osuTK.Graphics; @@ -67,6 +68,32 @@ namespace osu.Game.Graphics } } + /// + /// Retrieves the colour for a . + /// + public Color4 ForHitResult(HitResult judgement) + { + switch (judgement) + { + case HitResult.Perfect: + case HitResult.Great: + return Blue; + + case HitResult.Ok: + case HitResult.Good: + return Green; + + case HitResult.Meh: + return Yellow; + + case HitResult.Miss: + return Red; + + default: + return Color4.White; + } + } + // See https://github.com/ppy/osu-web/blob/master/resources/assets/less/colors.less public readonly Color4 PurpleLighter = Color4Extensions.FromHex(@"eeeeff"); public readonly Color4 PurpleLight = Color4Extensions.FromHex(@"aa88ff"); diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs index 960585b968..7113acbbfb 100644 --- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs @@ -12,7 +12,6 @@ using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; -using osuTK.Graphics; namespace osu.Game.Rulesets.Judgements { @@ -68,7 +67,7 @@ namespace osu.Game.Rulesets.Judgements { Text = Result.Type.GetDescription().ToUpperInvariant(), Font = OsuFont.Numeric.With(size: 20), - Colour = judgementColour(Result.Type), + Colour = colours.ForHitResult(Result.Type), Scale = new Vector2(0.85f, 1), }, confineMode: ConfineMode.NoScaling) }; @@ -110,28 +109,5 @@ namespace osu.Game.Rulesets.Judgements Expire(true); } - - private Color4 judgementColour(HitResult judgement) - { - switch (judgement) - { - case HitResult.Perfect: - case HitResult.Great: - return colours.Blue; - - case HitResult.Ok: - case HitResult.Good: - return colours.Green; - - case HitResult.Meh: - return colours.Yellow; - - case HitResult.Miss: - return colours.Red; - - default: - return Color4.White; - } - } } } From 66558ca8c527e44a5739389cf7dd0a716fe65b1b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 18 Mar 2020 17:21:57 +0900 Subject: [PATCH 0476/2376] Colourise hit result statistics --- .../Expanded/ExpandedPanelMiddleContent.cs | 3 +-- .../Expanded/Statistics/HitResultStatistic.cs | 27 +++++++++++++++++++ .../Expanded/Statistics/StatisticDisplay.cs | 6 +++-- 3 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 osu.Game/Screens/Ranking/Expanded/Statistics/HitResultStatistic.cs diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index 7a50a96fe7..1de071a228 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Localisation; @@ -62,7 +61,7 @@ namespace osu.Game.Screens.Ranking.Expanded var bottomStatistics = new List(); foreach (var stat in score.SortedStatistics) - bottomStatistics.Add(new CounterStatistic(stat.Key.GetDescription(), stat.Value)); + bottomStatistics.Add(new HitResultStatistic(stat.Key, stat.Value)); statisticDisplays.AddRange(topStatistics); statisticDisplays.AddRange(bottomStatistics); diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/HitResultStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/HitResultStatistic.cs new file mode 100644 index 0000000000..faa4a6a96c --- /dev/null +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/HitResultStatistic.cs @@ -0,0 +1,27 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Extensions; +using osu.Game.Graphics; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Screens.Ranking.Expanded.Statistics +{ + public class HitResultStatistic : CounterStatistic + { + private readonly HitResult result; + + public HitResultStatistic(HitResult result, int count) + : base(result.GetDescription(), count) + { + this.result = result; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + HeaderText.Colour = colours.ForHitResult(result); + } + } +} diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/StatisticDisplay.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/StatisticDisplay.cs index a653cc82d4..9206c58bc9 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/StatisticDisplay.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/StatisticDisplay.cs @@ -6,6 +6,7 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; @@ -16,8 +17,9 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics /// public abstract class StatisticDisplay : CompositeDrawable { - private readonly string header; + protected SpriteText HeaderText { get; private set; } + private readonly string header; private Drawable content; /// @@ -53,7 +55,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics RelativeSizeAxes = Axes.Both, Colour = Color4Extensions.FromHex("#222") }, - new OsuSpriteText + HeaderText = new OsuSpriteText { Anchor = Anchor.Centre, Origin = Anchor.Centre, From b91dc15dbf2b39fb4f8c5c51bafaaec4d8bc8cb0 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 18 Mar 2020 17:37:03 +0900 Subject: [PATCH 0477/2376] Update rank badge colours --- osu.Game/Graphics/OsuColour.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs index f7ed55410c..aa5c424599 100644 --- a/osu.Game/Graphics/OsuColour.cs +++ b/osu.Game/Graphics/OsuColour.cs @@ -47,23 +47,23 @@ namespace osu.Game.Graphics { case ScoreRank.XH: case ScoreRank.X: - return Color4Extensions.FromHex(@"ce1c9d"); + return Color4Extensions.FromHex(@"de31ae"); case ScoreRank.SH: case ScoreRank.S: - return Color4Extensions.FromHex(@"00a8b5"); + return Color4Extensions.FromHex(@"02b5c3"); case ScoreRank.A: - return Color4Extensions.FromHex(@"7cce14"); + return Color4Extensions.FromHex(@"88da20"); case ScoreRank.B: return Color4Extensions.FromHex(@"e3b130"); case ScoreRank.C: - return Color4Extensions.FromHex(@"f18252"); + return Color4Extensions.FromHex(@"ff8e5d"); default: - return Color4Extensions.FromHex(@"e95353"); + return Color4Extensions.FromHex(@"ff5a5a"); } } From 63531a8564c9bd03cacb53c9038c791fc18caa11 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 18 Mar 2020 17:59:44 +0900 Subject: [PATCH 0478/2376] Add date played to score panel --- .../Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index 7a50a96fe7..dcd988a247 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -204,6 +204,13 @@ namespace osu.Game.Screens.Ranking.Expanded } } } + }, + new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Font = OsuFont.GetFont(size: 10, weight: FontWeight.SemiBold), + Text = $"Played on {score.Date.ToLocalTime():g}" } } }; From 1d211cb56354056b4068d82ce252b8b0d74525bc Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 18 Mar 2020 18:28:42 +0900 Subject: [PATCH 0479/2376] Make score panel scroll if off-screen --- osu.Game/Screens/Ranking/ResultsScreen.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 0952ba1f70..803b33a998 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -135,7 +136,7 @@ namespace osu.Game.Screens.Ranking protected override void Update() { base.Update(); - content.Height = DrawHeight; + content.Height = Math.Max(768, DrawHeight); } } } From 1d680b7a0073b783cee638e64c31d90c966f9deb Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 18 Mar 2020 19:13:25 +0900 Subject: [PATCH 0480/2376] Better english Co-Authored-By: Dean Herbert --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs | 2 +- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index 82a81040e4..3e66549ca0 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables /// /// Whether this can be hit. - /// If not-null, this will not receive a judgement until this function returns true. + /// If non-null, judgements will be ignored (resulting in a shake) whilst the function returns false. /// public Func CheckHittable; diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 9eb2786951..643253b1af 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Osu.UI { var lastObject = HitObjectContainer.AliveObjects.GetPrevious(osuHitObject); - // Ensure the last object is not alive anymore, in which case always allow the hit. + // If there is no previous object alive, allow the hit. if (lastObject == null) return true; From b342e25c9d3fd36fa5c6cc0e9db7b1e28c82eade Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Mar 2020 00:26:49 +0900 Subject: [PATCH 0481/2376] Add testflight distribution step automation --- fastlane/Fastfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 510b53054b..f895c465d2 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -99,6 +99,8 @@ platform :ios do pilot( wait_processing_interval: 1800, changelog: changelog, + groups: ['osu! supporters', 'public'], + distribute_external: true, ipa: './osu.iOS/bin/iPhone/Release/osu.iOS.ipa' ) end From 1c0c26985280e634d5dab81bf8b0cc5361d8d0c3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Mar 2020 00:34:24 +0900 Subject: [PATCH 0482/2376] Reduce allocations of followpoints by reusing existing --- .../Connections/FollowPointConnection.cs | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs index 3e9c0f341b..d0935e46f7 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs @@ -88,8 +88,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections private void refresh() { - ClearInternal(); - OsuHitObject osuStart = Start.HitObject; double startTime = osuStart.GetEndTime(); @@ -116,6 +114,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections double? firstTransformStartTime = null; double finalTransformEndTime = startTime; + int point = 0; + for (int d = (int)(spacing * 1.5); d < distance - spacing; d += spacing) { float fraction = (float)d / distance; @@ -126,13 +126,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections FollowPoint fp; - AddInternal(fp = new FollowPoint + if (InternalChildren.Count > point) { - Position = pointStartPosition, - Rotation = rotation, - Alpha = 0, - Scale = new Vector2(1.5f * osuEnd.Scale), - }); + fp = (FollowPoint)InternalChildren[point]; + fp.ClearTransforms(); + } + else + AddInternal(fp = new FollowPoint()); + + fp.Position = pointStartPosition; + fp.Rotation = rotation; + fp.Alpha = 0; + fp.Scale = new Vector2(1.5f * osuEnd.Scale); if (firstTransformStartTime == null) firstTransformStartTime = fadeInTime; @@ -146,8 +151,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections finalTransformEndTime = fadeOutTime + osuEnd.TimeFadeIn; } + + point++; } + int excessPoints = InternalChildren.Count - point; + for (int i = 0; i < excessPoints; i++) + RemoveInternal(InternalChildren[^1]); + // todo: use Expire() on FollowPoints and take lifetime from them when https://github.com/ppy/osu-framework/issues/3300 is fixed. LifetimeStart = firstTransformStartTime ?? startTime; LifetimeEnd = finalTransformEndTime; From 463dde1fc40206a8e7ee3d59ebecdb6186a2f1e4 Mon Sep 17 00:00:00 2001 From: Fuewburvpoa Date: Wed, 18 Mar 2020 21:04:38 +0200 Subject: [PATCH 0483/2376] Tests for BeatDivisorControl --- .../Editor/TestSceneBeatDivisorControl.cs | 52 ++++++++++++++++++- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs b/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs index 7531a7be2c..2ee5e649a8 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs +++ b/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Screens.Edit; @@ -11,19 +12,66 @@ using osuTK; namespace osu.Game.Tests.Visual.Editor { - public class TestSceneBeatDivisorControl : OsuTestScene + public class TestSceneBeatDivisorControl : ManualInputManagerTestScene { public override IReadOnlyList RequiredTypes => new[] { typeof(BindableBeatDivisor) }; + private BeatDivisorControl beatDivisorControl; + private BindableBeatDivisor bindableBeatDivisor; [BackgroundDependencyLoader] private void load() { - Child = new BeatDivisorControl(new BindableBeatDivisor()) + Child = beatDivisorControl = new BeatDivisorControl(bindableBeatDivisor = new BindableBeatDivisor()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(90, 90) }; } + + [Test] + public void TestBindableBeatDivisor() + { + AddStep("Reset", () => reset()); + AddRepeatStep("Move previous", () => bindableBeatDivisor.Previous(), 4); + AddAssert("Position at 4", () => bindableBeatDivisor.Value == 4); + AddRepeatStep("Move next", () => bindableBeatDivisor.Next(), 3); + AddAssert("Position at 12", () => bindableBeatDivisor.Value == 12); + } + + [Test] + public void TestMouseInput() + { + AddStep("Reset", () => reset()); + AddStep("Move to marker", () => + { + InputManager.MoveMouseTo(beatDivisorControl, new Vector2(38, -18)); + InputManager.PressButton(osuTK.Input.MouseButton.Left); + }); + AddStep("Mote to divisor 8", () => + { + InputManager.MoveMouseTo(beatDivisorControl, new Vector2(0, -18)); + InputManager.ReleaseButton(osuTK.Input.MouseButton.Left); + }); + AddAssert("Position at 8", () => bindableBeatDivisor.Value == 8); + AddStep("Prepare to move marker", () => { InputManager.PressButton(osuTK.Input.MouseButton.Left); }); + AddStep("Trigger marker jump", () => + { + InputManager.MoveMouseTo(beatDivisorControl, new Vector2(30, -18)); + }); + AddStep("Move to divisor ~10", () => + { + InputManager.MoveMouseTo(beatDivisorControl, new Vector2(10, -18)); + InputManager.ReleaseButton(osuTK.Input.MouseButton.Left); + }); + AddAssert("Position clamped to 8", () => bindableBeatDivisor.Value == 8); + + } + + private void reset() + { + bindableBeatDivisor.Value = 16; + InputManager.MoveMouseTo(beatDivisorControl, new Vector2(beatDivisorControl.Width, beatDivisorControl.Height)); + } } } From d5541dfc651635eecd650b4619000760f8f2fa97 Mon Sep 17 00:00:00 2001 From: Fuewburvpoa Date: Wed, 18 Mar 2020 21:06:14 +0200 Subject: [PATCH 0484/2376] Codefactor fix --- osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs b/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs index 2ee5e649a8..b8cefdb841 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs +++ b/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs @@ -65,7 +65,6 @@ namespace osu.Game.Tests.Visual.Editor InputManager.ReleaseButton(osuTK.Input.MouseButton.Left); }); AddAssert("Position clamped to 8", () => bindableBeatDivisor.Value == 8); - } private void reset() From e9f224b5e8c3ae93098848ee3ee2146d47e7146e Mon Sep 17 00:00:00 2001 From: Lucas A Date: Wed, 18 Mar 2020 21:16:54 +0100 Subject: [PATCH 0485/2376] Apply review suggestions --- osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs | 2 +- osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs | 2 +- osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs | 2 +- .../Play/HUD/{FaillingLayer.cs => FailingLayer.cs} | 8 ++++---- osu.Game/Screens/Play/HUD/HealthDisplay.cs | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) rename osu.Game/Screens/Play/HUD/{FaillingLayer.cs => FailingLayer.cs} (82%) diff --git a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs index 705c2d756c..50c4154c61 100644 --- a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Catch.UI [BackgroundDependencyLoader] private void load() { - Overlays.Add(new FaillingLayer()); + Overlays.Add(new FailingLayer()); } protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new CatchFramedReplayInputHandler(replay); diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index b8b6ff3c3c..8e56144752 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -54,7 +54,7 @@ namespace osu.Game.Rulesets.Mania.UI Config.BindWith(ManiaRulesetSetting.ScrollTime, TimeRange); - Overlays.Add(new FaillingLayer()); + Overlays.Add(new FailingLayer()); } /// diff --git a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs index 50bff4fe3a..ed75d47bbe 100644 --- a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.UI [BackgroundDependencyLoader] private void load() { - Overlays.Add(new FaillingLayer()); + Overlays.Add(new FailingLayer()); } public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // always show the gameplay cursor diff --git a/osu.Game/Screens/Play/HUD/FaillingLayer.cs b/osu.Game/Screens/Play/HUD/FailingLayer.cs similarity index 82% rename from osu.Game/Screens/Play/HUD/FaillingLayer.cs rename to osu.Game/Screens/Play/HUD/FailingLayer.cs index 55cc4476b0..5f7dc77928 100644 --- a/osu.Game/Screens/Play/HUD/FaillingLayer.cs +++ b/osu.Game/Screens/Play/HUD/FailingLayer.cs @@ -10,9 +10,9 @@ using osu.Game.Graphics; namespace osu.Game.Screens.Play.HUD { /// - /// An overlay layer on top of the playfield which fades to red when the current player health falls a certain threshold defined by . + /// An overlay layer on top of the playfield which fades to red when the current player health falls below a certain threshold defined by . /// - public class FaillingLayer : HealthDisplay + public class FailingLayer : HealthDisplay { private const float max_alpha = 0.4f; @@ -21,9 +21,9 @@ namespace osu.Game.Screens.Play.HUD /// /// The threshold under which the current player life should be considered low and the layer should start fading in. /// - protected virtual double LowHealthThreshold => 0.20f; + protected double LowHealthThreshold { get; set; } = 0.20f; - public FaillingLayer() + public FailingLayer() { RelativeSizeAxes = Axes.Both; Child = box = new Box diff --git a/osu.Game/Screens/Play/HUD/HealthDisplay.cs b/osu.Game/Screens/Play/HUD/HealthDisplay.cs index 4094b3de69..4ea08626ad 100644 --- a/osu.Game/Screens/Play/HUD/HealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/HealthDisplay.cs @@ -24,7 +24,7 @@ namespace osu.Game.Screens.Play.HUD /// Bind the tracked fields of to this health display. /// /// - public virtual void BindHealthProcessor(HealthProcessor processor) + public void BindHealthProcessor(HealthProcessor processor) { Current.BindTo(processor.Health); } From a4171253a38f2d09eedea9f38eb7d3eca0afebff Mon Sep 17 00:00:00 2001 From: Lucas A Date: Wed, 18 Mar 2020 21:41:43 +0100 Subject: [PATCH 0486/2376] Make LowHealthThreshold a field. --- osu.Game/Screens/Play/HUD/FailingLayer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/FailingLayer.cs b/osu.Game/Screens/Play/HUD/FailingLayer.cs index 5f7dc77928..5f4037c14d 100644 --- a/osu.Game/Screens/Play/HUD/FailingLayer.cs +++ b/osu.Game/Screens/Play/HUD/FailingLayer.cs @@ -21,7 +21,7 @@ namespace osu.Game.Screens.Play.HUD /// /// The threshold under which the current player life should be considered low and the layer should start fading in. /// - protected double LowHealthThreshold { get; set; } = 0.20f; + public double LowHealthThreshold = 0.20f; public FailingLayer() { From 4bda520695e37a5753a667289e6ac7f2c31147d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 18 Mar 2020 21:54:17 +0100 Subject: [PATCH 0487/2376] Use [SetUp] instead of reset method --- .../Editor/TestSceneBeatDivisorControl.cs | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs b/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs index b8cefdb841..dc41f7d92b 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs +++ b/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using NUnit.Framework; -using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Compose.Components; @@ -18,21 +17,20 @@ namespace osu.Game.Tests.Visual.Editor private BeatDivisorControl beatDivisorControl; private BindableBeatDivisor bindableBeatDivisor; - [BackgroundDependencyLoader] - private void load() + [SetUp] + public void SetUp() => Schedule(() => { - Child = beatDivisorControl = new BeatDivisorControl(bindableBeatDivisor = new BindableBeatDivisor()) + Child = beatDivisorControl = new BeatDivisorControl(bindableBeatDivisor = new BindableBeatDivisor(16)) { Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(90, 90) }; - } + }); [Test] public void TestBindableBeatDivisor() { - AddStep("Reset", () => reset()); AddRepeatStep("Move previous", () => bindableBeatDivisor.Previous(), 4); AddAssert("Position at 4", () => bindableBeatDivisor.Value == 4); AddRepeatStep("Move next", () => bindableBeatDivisor.Next(), 3); @@ -42,7 +40,6 @@ namespace osu.Game.Tests.Visual.Editor [Test] public void TestMouseInput() { - AddStep("Reset", () => reset()); AddStep("Move to marker", () => { InputManager.MoveMouseTo(beatDivisorControl, new Vector2(38, -18)); @@ -66,11 +63,5 @@ namespace osu.Game.Tests.Visual.Editor }); AddAssert("Position clamped to 8", () => bindableBeatDivisor.Value == 8); } - - private void reset() - { - bindableBeatDivisor.Value = 16; - InputManager.MoveMouseTo(beatDivisorControl, new Vector2(beatDivisorControl.Width, beatDivisorControl.Height)); - } } } From 1d3cac4cdc5e61e9148c69f5c567ad135c04d6ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 18 Mar 2020 21:55:35 +0100 Subject: [PATCH 0488/2376] Eliminate osuTK.Input namespace qualifications --- .../Visual/Editor/TestSceneBeatDivisorControl.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs b/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs index dc41f7d92b..5a441f568a 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs +++ b/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Compose.Components; using osuTK; +using osuTK.Input; namespace osu.Game.Tests.Visual.Editor { @@ -43,15 +44,15 @@ namespace osu.Game.Tests.Visual.Editor AddStep("Move to marker", () => { InputManager.MoveMouseTo(beatDivisorControl, new Vector2(38, -18)); - InputManager.PressButton(osuTK.Input.MouseButton.Left); + InputManager.PressButton(MouseButton.Left); }); AddStep("Mote to divisor 8", () => { InputManager.MoveMouseTo(beatDivisorControl, new Vector2(0, -18)); - InputManager.ReleaseButton(osuTK.Input.MouseButton.Left); + InputManager.ReleaseButton(MouseButton.Left); }); AddAssert("Position at 8", () => bindableBeatDivisor.Value == 8); - AddStep("Prepare to move marker", () => { InputManager.PressButton(osuTK.Input.MouseButton.Left); }); + AddStep("Prepare to move marker", () => { InputManager.PressButton(MouseButton.Left); }); AddStep("Trigger marker jump", () => { InputManager.MoveMouseTo(beatDivisorControl, new Vector2(30, -18)); @@ -59,7 +60,7 @@ namespace osu.Game.Tests.Visual.Editor AddStep("Move to divisor ~10", () => { InputManager.MoveMouseTo(beatDivisorControl, new Vector2(10, -18)); - InputManager.ReleaseButton(osuTK.Input.MouseButton.Left); + InputManager.ReleaseButton(MouseButton.Left); }); AddAssert("Position clamped to 8", () => bindableBeatDivisor.Value == 8); } From 78bdf5cf9150c851c74433113ca433fa069830b2 Mon Sep 17 00:00:00 2001 From: Fuewburvpoa Date: Wed, 18 Mar 2020 23:03:58 +0200 Subject: [PATCH 0489/2376] InspectCode fixes --- osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs b/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs index b8cefdb841..38b33acc32 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs +++ b/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs @@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.Editor [Test] public void TestBindableBeatDivisor() { - AddStep("Reset", () => reset()); + AddStep("Reset", reset); AddRepeatStep("Move previous", () => bindableBeatDivisor.Previous(), 4); AddAssert("Position at 4", () => bindableBeatDivisor.Value == 4); AddRepeatStep("Move next", () => bindableBeatDivisor.Next(), 3); @@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.Editor [Test] public void TestMouseInput() { - AddStep("Reset", () => reset()); + AddStep("Reset", reset); AddStep("Move to marker", () => { InputManager.MoveMouseTo(beatDivisorControl, new Vector2(38, -18)); @@ -54,7 +54,7 @@ namespace osu.Game.Tests.Visual.Editor InputManager.ReleaseButton(osuTK.Input.MouseButton.Left); }); AddAssert("Position at 8", () => bindableBeatDivisor.Value == 8); - AddStep("Prepare to move marker", () => { InputManager.PressButton(osuTK.Input.MouseButton.Left); }); + AddStep("Prepare to move marker", () => InputManager.PressButton(osuTK.Input.MouseButton.Left)); AddStep("Trigger marker jump", () => { InputManager.MoveMouseTo(beatDivisorControl, new Vector2(30, -18)); From 23338a6c82ed4f14b5e8e4cab0dafd96fb371deb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 18 Mar 2020 22:23:06 +0100 Subject: [PATCH 0490/2376] Adjust test implementation * Use slider bar and slider marker coordinates in manual tests instead of hard-coded offsets. * Reword test steps slightly for greater clarity. --- .../Editor/TestSceneBeatDivisorControl.cs | 51 ++++++++++++------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs b/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs index 5a441f568a..746b2c99aa 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs +++ b/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs @@ -3,8 +3,12 @@ using System; using System.Collections.Generic; +using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Testing; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Compose.Components; using osuTK; @@ -18,6 +22,9 @@ namespace osu.Game.Tests.Visual.Editor private BeatDivisorControl beatDivisorControl; private BindableBeatDivisor bindableBeatDivisor; + private SliderBar tickSliderBar; + private EquilateralTriangle tickMarkerHead; + [SetUp] public void SetUp() => Schedule(() => { @@ -27,42 +34,52 @@ namespace osu.Game.Tests.Visual.Editor Origin = Anchor.Centre, Size = new Vector2(90, 90) }; + + tickSliderBar = beatDivisorControl.ChildrenOfType>().Single(); + tickMarkerHead = tickSliderBar.ChildrenOfType().Single(); }); [Test] public void TestBindableBeatDivisor() { - AddRepeatStep("Move previous", () => bindableBeatDivisor.Previous(), 4); - AddAssert("Position at 4", () => bindableBeatDivisor.Value == 4); - AddRepeatStep("Move next", () => bindableBeatDivisor.Next(), 3); - AddAssert("Position at 12", () => bindableBeatDivisor.Value == 12); + AddRepeatStep("move previous", () => bindableBeatDivisor.Previous(), 4); + AddAssert("divisor is 4", () => bindableBeatDivisor.Value == 4); + AddRepeatStep("move next", () => bindableBeatDivisor.Next(), 3); + AddAssert("divisor is 12", () => bindableBeatDivisor.Value == 12); } [Test] public void TestMouseInput() { - AddStep("Move to marker", () => + AddStep("hold marker", () => { - InputManager.MoveMouseTo(beatDivisorControl, new Vector2(38, -18)); + InputManager.MoveMouseTo(tickMarkerHead.ScreenSpaceDrawQuad.Centre); InputManager.PressButton(MouseButton.Left); }); - AddStep("Mote to divisor 8", () => + AddStep("move to 8 and release", () => { - InputManager.MoveMouseTo(beatDivisorControl, new Vector2(0, -18)); + InputManager.MoveMouseTo(tickSliderBar.ScreenSpaceDrawQuad.Centre); InputManager.ReleaseButton(MouseButton.Left); }); - AddAssert("Position at 8", () => bindableBeatDivisor.Value == 8); - AddStep("Prepare to move marker", () => { InputManager.PressButton(MouseButton.Left); }); - AddStep("Trigger marker jump", () => + AddAssert("divisor is 8", () => bindableBeatDivisor.Value == 8); + AddStep("hold marker", () => InputManager.PressButton(MouseButton.Left)); + AddStep("move to 16", () => InputManager.MoveMouseTo(getPositionForDivisor(16))); + AddStep("move to ~10 and release", () => { - InputManager.MoveMouseTo(beatDivisorControl, new Vector2(30, -18)); - }); - AddStep("Move to divisor ~10", () => - { - InputManager.MoveMouseTo(beatDivisorControl, new Vector2(10, -18)); + InputManager.MoveMouseTo(getPositionForDivisor(10)); InputManager.ReleaseButton(MouseButton.Left); }); - AddAssert("Position clamped to 8", () => bindableBeatDivisor.Value == 8); + AddAssert("divisor clamped to 8", () => bindableBeatDivisor.Value == 8); + } + + private Vector2 getPositionForDivisor(int divisor) + { + var relativePosition = (float)Math.Clamp(divisor, 0, 16) / 16; + var sliderDrawQuad = tickSliderBar.ScreenSpaceDrawQuad; + return new Vector2( + sliderDrawQuad.TopLeft.X + sliderDrawQuad.Width * relativePosition, + sliderDrawQuad.Centre.Y + ); } } } From c50784da934163e38991d837a7d0c670274c6b7f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Mar 2020 11:58:52 +0900 Subject: [PATCH 0491/2376] Show 'D' rank badge on accuracy circle --- .../Visual/Ranking/TestSceneAccuracyCircle.cs | 10 ++++++++++ .../Ranking/Expanded/Accuracy/AccuracyCircle.cs | 1 + 2 files changed, 11 insertions(+) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs index d0b9d43f51..0781cba924 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs @@ -32,6 +32,16 @@ namespace osu.Game.Tests.Visual.Ranking typeof(SmoothCircularProgress) }; + [Test] + public void TestLowDRank() + { + var score = createScore(); + score.Accuracy = 0.2; + score.Rank = ScoreRank.D; + + addCircleStep(score); + } + [Test] public void TestDRank() { diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs index 2d76a7c3b0..ee53ee9879 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs @@ -196,6 +196,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy new RankBadge(0.9f, ScoreRank.A), new RankBadge(0.8f, ScoreRank.B), new RankBadge(0.7f, ScoreRank.C), + new RankBadge(0.35f, ScoreRank.D), } }, rankText = new RankText(score.Rank) From e59d7fee26728e6c281d03bf67ee004c127973de Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Wed, 18 Mar 2020 23:42:14 -0400 Subject: [PATCH 0492/2376] fix comment grammar --- osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index 2083671072..c3d1e4c857 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -79,7 +79,7 @@ namespace osu.Game.Rulesets.Mods /// /// Transfer a setting from to a configuration bindable. - /// Only performs the transfer if the user it not currently overriding.. + /// Only performs the transfer if the user is not currently overriding. /// protected void TransferSetting(BindableNumber bindable, T beatmapDefault) where T : struct, IComparable, IConvertible, IEquatable From 18bf7c913b1b73f444f09ca16d2f43334d696404 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Wed, 18 Mar 2020 23:43:26 -0400 Subject: [PATCH 0493/2376] show mod settings in ModIcon tooltip --- .../Mods/CatchModDifficultyAdjust.cs | 5 +++++ osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs | 5 +++++ osu.Game/Rulesets/Mods/Mod.cs | 11 +++++++++++ osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs | 3 +++ osu.Game/Rulesets/Mods/ModDoubleTime.cs | 2 ++ osu.Game/Rulesets/Mods/ModEasy.cs | 2 ++ osu.Game/Rulesets/Mods/ModHalfTime.cs | 2 ++ osu.Game/Rulesets/Mods/ModTimeRamp.cs | 2 ++ osu.Game/Rulesets/UI/ModIcon.cs | 2 +- 9 files changed, 33 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs index e2465d727e..8ea39c8676 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs @@ -30,6 +30,11 @@ namespace osu.Game.Rulesets.Catch.Mods Value = 5, }; + public override string IconTooltip => ($"{Name} ({(CircleSize.IsDefault ? "" : $"CS {CircleSize.Value.ToString()}, ")}" + + $"{(DrainRate.IsDefault ? "" : $"HP {DrainRate.Value.ToString()}, ")}" + + $"{(OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value.ToString()}, ")}" + + $"{(ApproachRate.IsDefault ? "" : $"AR {ApproachRate.Value.ToString()}")}").TrimEnd(new char[] { ',', ' ' }) + ")"; + protected override void TransferSettings(BeatmapDifficulty difficulty) { base.TransferSettings(difficulty); diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs index 75de6896a3..c3e1321dac 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs @@ -30,6 +30,11 @@ namespace osu.Game.Rulesets.Osu.Mods Value = 5, }; + public override string IconTooltip => ($"{Name} ({(CircleSize.IsDefault ? "" : $"CS {CircleSize.Value.ToString()}, ")}" + + $"{(DrainRate.IsDefault ? "" : $"HP {DrainRate.Value.ToString()}, ")}" + + $"{(OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value.ToString()}, ")}" + + $"{(ApproachRate.IsDefault ? "" : $"AR {ApproachRate.Value.ToString()}")}").TrimEnd(new char[] { ',', ' ' }) + ")"; + protected override void TransferSettings(BeatmapDifficulty difficulty) { base.TransferSettings(difficulty); diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 46c0c1da07..b70ddc6d46 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -5,6 +5,7 @@ using System; using Newtonsoft.Json; using osu.Framework.Graphics.Sprites; using osu.Game.IO.Serialization; +using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Mods { @@ -42,6 +43,16 @@ namespace osu.Game.Rulesets.Mods [JsonIgnore] public virtual string Description => string.Empty; + /// + /// The tooltip to display for this mod when used in a . + /// + /// + /// Differs from , as the value of attributes (AR, CS, etc) changeable via the mod + /// are displayed in the tooltip. + /// + [JsonIgnore] + public virtual string IconTooltip => Name; + /// /// The score multiplier of this mod. /// diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index c3d1e4c857..4072e6a6af 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -52,6 +52,9 @@ namespace osu.Game.Rulesets.Mods Value = 5, }; + public override string IconTooltip => $"{Name} ({(DrainRate.IsDefault ? $"HP {DrainRate.Value.ToString()}, " : "")}" + + $"{(OverallDifficulty.IsDefault ? $"OD {OverallDifficulty.Value.ToString()}, " : "")})"; + private BeatmapDifficulty difficulty; public void ReadFromDifficulty(BeatmapDifficulty difficulty) diff --git a/osu.Game/Rulesets/Mods/ModDoubleTime.cs b/osu.Game/Rulesets/Mods/ModDoubleTime.cs index 152657da33..4f7d82418d 100644 --- a/osu.Game/Rulesets/Mods/ModDoubleTime.cs +++ b/osu.Game/Rulesets/Mods/ModDoubleTime.cs @@ -30,5 +30,7 @@ namespace osu.Game.Rulesets.Mods Value = 1.5, Precision = 0.01, }; + + public override string IconTooltip => $"{Name}{(SpeedChange.IsDefault ? "" : $" ({SpeedChange.Value}x)")}"; } } diff --git a/osu.Game/Rulesets/Mods/ModEasy.cs b/osu.Game/Rulesets/Mods/ModEasy.cs index b56be95dfe..2ec4e9610b 100644 --- a/osu.Game/Rulesets/Mods/ModEasy.cs +++ b/osu.Game/Rulesets/Mods/ModEasy.cs @@ -28,6 +28,8 @@ namespace osu.Game.Rulesets.Mods MaxValue = 10 }; + public override string IconTooltip => $"{Name}{(Retries.IsDefault ? "" : $" ({Retries.Value} lives)")}"; + private int retries; private BindableNumber health; diff --git a/osu.Game/Rulesets/Mods/ModHalfTime.cs b/osu.Game/Rulesets/Mods/ModHalfTime.cs index 203b88951c..14133bddcd 100644 --- a/osu.Game/Rulesets/Mods/ModHalfTime.cs +++ b/osu.Game/Rulesets/Mods/ModHalfTime.cs @@ -30,5 +30,7 @@ namespace osu.Game.Rulesets.Mods Value = 0.75, Precision = 0.01, }; + + public override string IconTooltip => $"{Name}{(SpeedChange.IsDefault ? "" : $" ({SpeedChange.Value}x)")}"; } } diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index 9e63142b42..9cb97dfc35 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -26,6 +26,8 @@ namespace osu.Game.Rulesets.Mods [SettingSource("Final rate", "The final speed to ramp to")] public abstract BindableNumber FinalRate { get; } + public override string IconTooltip => $"{Name} ({InitialRate.Value}x to {FinalRate.Value}x)"; + private double finalRateTime; private double beginRampTime; diff --git a/osu.Game/Rulesets/UI/ModIcon.cs b/osu.Game/Rulesets/UI/ModIcon.cs index 3edab0745d..3cd1b0820d 100644 --- a/osu.Game/Rulesets/UI/ModIcon.cs +++ b/osu.Game/Rulesets/UI/ModIcon.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.UI type = mod.Type; - TooltipText = mod.Name; + TooltipText = mod.IconTooltip; Size = new Vector2(size); From 7a0a633ef9320fbd2c519f7630999306fcce6ce5 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Thu, 19 Mar 2020 00:06:55 -0400 Subject: [PATCH 0494/2376] don't use ToString, proper indent level --- osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs | 8 ++++---- osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs index 8ea39c8676..661c59332f 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs @@ -30,10 +30,10 @@ namespace osu.Game.Rulesets.Catch.Mods Value = 5, }; - public override string IconTooltip => ($"{Name} ({(CircleSize.IsDefault ? "" : $"CS {CircleSize.Value.ToString()}, ")}" + - $"{(DrainRate.IsDefault ? "" : $"HP {DrainRate.Value.ToString()}, ")}" + - $"{(OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value.ToString()}, ")}" + - $"{(ApproachRate.IsDefault ? "" : $"AR {ApproachRate.Value.ToString()}")}").TrimEnd(new char[] { ',', ' ' }) + ")"; + public override string IconTooltip => ($"{Name} ({(CircleSize.IsDefault ? "" : $"CS {CircleSize.Value}, ")}" + + $"{(DrainRate.IsDefault ? "" : $"HP {DrainRate.Value}, ")}" + + $"{(OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value}, ")}" + + $"{(ApproachRate.IsDefault ? "" : $"AR {ApproachRate.Value}")}").TrimEnd(new char[] { ',', ' ' }) + ")"; protected override void TransferSettings(BeatmapDifficulty difficulty) { diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs index c3e1321dac..477028dbbe 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs @@ -30,10 +30,10 @@ namespace osu.Game.Rulesets.Osu.Mods Value = 5, }; - public override string IconTooltip => ($"{Name} ({(CircleSize.IsDefault ? "" : $"CS {CircleSize.Value.ToString()}, ")}" + - $"{(DrainRate.IsDefault ? "" : $"HP {DrainRate.Value.ToString()}, ")}" + - $"{(OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value.ToString()}, ")}" + - $"{(ApproachRate.IsDefault ? "" : $"AR {ApproachRate.Value.ToString()}")}").TrimEnd(new char[] { ',', ' ' }) + ")"; + public override string IconTooltip => ($"{Name} ({(CircleSize.IsDefault ? "" : $"CS {CircleSize.Value}, ")}" + + $"{(DrainRate.IsDefault ? "" : $"HP {DrainRate.Value}, ")}" + + $"{(OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value}, ")}" + + $"{(ApproachRate.IsDefault ? "" : $"AR {ApproachRate.Value}")}").TrimEnd(new char[] { ',', ' ' }) + ")"; protected override void TransferSettings(BeatmapDifficulty difficulty) { From 17c3455b36b9ee38a2116556d62de503cf2232fd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Mar 2020 14:10:54 +0900 Subject: [PATCH 0495/2376] Fix potentially invalid push in player while already exiting --- osu.Game/Screens/Play/Player.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 32261efd4e..efce2e05ce 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -387,6 +387,10 @@ namespace osu.Game.Screens.Play private void onCompletion() { + // screen may be in the exiting transition phase. + if (!this.IsCurrentScreen()) + return; + // Only show the completion screen if the player hasn't failed if (HealthProcessor.HasFailed || completionProgressDelegate != null) return; @@ -581,7 +585,7 @@ namespace osu.Game.Screens.Play if (completionProgressDelegate != null && !completionProgressDelegate.Cancelled && !completionProgressDelegate.Completed) { // proceed to result screen if beatmap already finished playing - scheduleGotoRanking(); + completionProgressDelegate.RunTask(); return true; } @@ -623,7 +627,12 @@ namespace osu.Game.Screens.Play { var score = CreateScore(); if (DrawableRuleset.ReplayScore == null) - scoreManager.Import(score).ContinueWith(_ => Schedule(() => this.Push(CreateResults(score)))); + scoreManager.Import(score).ContinueWith(_ => Schedule(() => + { + // screen may be in the exiting transition phase. + if (this.IsCurrentScreen()) + this.Push(CreateResults(score)); + })); else this.Push(CreateResults(score)); }); From 94c3ffb6e508226da53b1f6cc786b7dcbea928fe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Mar 2020 14:26:24 +0900 Subject: [PATCH 0496/2376] Fix slider ticks contributing to accuracy --- osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs | 6 +++--- .../TestSceneSliderInput.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs | 2 +- .../Objects/Drawables/DrawableSlider.cs | 10 +++++----- ...bleRepeatPoint.cs => DrawableSliderRepeat.cs} | 16 ++++++++-------- osu.Game.Rulesets.Osu/Objects/Slider.cs | 4 ++-- .../{RepeatPoint.cs => SliderRepeatPoint.cs} | 13 ++++++++++--- osu.Game.Rulesets.Osu/Objects/SliderTick.cs | 11 +++++++++-- 10 files changed, 41 insertions(+), 27 deletions(-) rename osu.Game.Rulesets.Osu/Objects/Drawables/{DrawableRepeatPoint.cs => DrawableSliderRepeat.cs} (88%) rename osu.Game.Rulesets.Osu/Objects/{RepeatPoint.cs => SliderRepeatPoint.cs} (76%) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs index defd3a6f22..1c0dd27e69 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Tests typeof(DrawableSliderTick), typeof(DrawableSliderTail), typeof(DrawableSliderHead), - typeof(DrawableRepeatPoint), + typeof(DrawableSliderRepeat), typeof(DrawableOsuHitObject) }; @@ -146,7 +146,7 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("head samples updated", () => assertSamples(((Slider)slider.HitObject).HeadCircle)); AddAssert("tick samples not updated", () => ((Slider)slider.HitObject).NestedHitObjects.OfType().All(assertTickSamples)); - AddAssert("repeat samples updated", () => ((Slider)slider.HitObject).NestedHitObjects.OfType().All(assertSamples)); + AddAssert("repeat samples updated", () => ((Slider)slider.HitObject).NestedHitObjects.OfType().All(assertSamples)); AddAssert("tail has no samples", () => ((Slider)slider.HitObject).TailCircle.Samples.Count == 0); static bool assertTickSamples(SliderTick tick) => tick.Samples.Single().Name == "slidertick"; @@ -181,7 +181,7 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("head samples not updated", () => assertSamples(((Slider)slider.HitObject).HeadCircle)); AddAssert("tick samples not updated", () => ((Slider)slider.HitObject).NestedHitObjects.OfType().All(assertTickSamples)); - AddAssert("repeat samples not updated", () => ((Slider)slider.HitObject).NestedHitObjects.OfType().All(assertSamples)); + AddAssert("repeat samples not updated", () => ((Slider)slider.HitObject).NestedHitObjects.OfType().All(assertSamples)); AddAssert("tail has no samples", () => ((Slider)slider.HitObject).TailCircle.Samples.Count == 0); static bool assertTickSamples(SliderTick tick) => tick.Samples.Single().Name == "slidertick"; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs index 94df239267..21244f0e9c 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Tests typeof(SliderBall), typeof(DrawableSlider), typeof(DrawableSliderTick), - typeof(DrawableRepeatPoint), + typeof(DrawableSliderRepeat), typeof(DrawableOsuHitObject), typeof(DrawableSliderHead), typeof(DrawableSliderTail), diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs index bc5f79331f..c1fc589798 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Mods return; slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y)); - slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y)); + slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y)); foreach (var point in slider.Path.ControlPoints) point.Position.Value = new Vector2(point.Position.Value.X, -point.Position.Value.Y); diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs index 41daef1f38..44dba7715a 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.Mods case DrawableSliderHead _: case DrawableSliderTail _: case DrawableSliderTick _: - case DrawableRepeatPoint _: + case DrawableSliderRepeat _: return; default: diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs b/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs index cc2f4c3f70..fe7c70c52c 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Osu.Mods // Wiggle the repeat points with the slider instead of independently. // Also fixes an issue with repeat points being positioned incorrectly. - if (osuObject is RepeatPoint) + if (osuObject is SliderRepeatPoint) return; Random objRand = new Random((int)osuObject.StartTime); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 7403649184..cb7005cb17 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private readonly Container headContainer; private readonly Container tailContainer; private readonly Container tickContainer; - private readonly Container repeatContainer; + private readonly Container repeatContainer; private readonly Slider slider; @@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { Body = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderBody), _ => new DefaultSliderBody(), confineMode: ConfineMode.NoScaling), tickContainer = new Container { RelativeSizeAxes = Axes.Both }, - repeatContainer = new Container { RelativeSizeAxes = Axes.Both }, + repeatContainer = new Container { RelativeSizeAxes = Axes.Both }, Ball = new SliderBall(s, this) { GetInitialHitAction = () => HeadCircle.HitAction, @@ -100,7 +100,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables tickContainer.Add(tick); break; - case DrawableRepeatPoint repeat: + case DrawableSliderRepeat repeat: repeatContainer.Add(repeat); break; } @@ -129,8 +129,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables case SliderTick tick: return new DrawableSliderTick(tick) { Position = tick.Position - slider.Position }; - case RepeatPoint repeat: - return new DrawableRepeatPoint(repeat, this) { Position = repeat.Position - slider.Position }; + case SliderRepeatPoint repeat: + return new DrawableSliderRepeat(repeat, this) { Position = repeat.Position - slider.Position }; } return base.CreateNestedHitObject(hitObject); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs similarity index 88% rename from osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs rename to osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs index 8fdcd060e7..3336188068 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs @@ -14,19 +14,19 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables { - public class DrawableRepeatPoint : DrawableOsuHitObject, ITrackSnaking + public class DrawableSliderRepeat : DrawableOsuHitObject, ITrackSnaking { - private readonly RepeatPoint repeatPoint; + private readonly SliderRepeatPoint sliderRepeatPoint; private readonly DrawableSlider drawableSlider; private double animDuration; private readonly Drawable scaleContainer; - public DrawableRepeatPoint(RepeatPoint repeatPoint, DrawableSlider drawableSlider) - : base(repeatPoint) + public DrawableSliderRepeat(SliderRepeatPoint sliderRepeatPoint, DrawableSlider drawableSlider) + : base(sliderRepeatPoint) { - this.repeatPoint = repeatPoint; + this.sliderRepeatPoint = sliderRepeatPoint; this.drawableSlider = drawableSlider; Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); @@ -48,13 +48,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void CheckForResult(bool userTriggered, double timeOffset) { - if (repeatPoint.StartTime <= Time.Current) + if (sliderRepeatPoint.StartTime <= Time.Current) ApplyResult(r => r.Type = drawableSlider.Tracking.Value ? HitResult.Great : HitResult.Miss); } protected override void UpdateInitialTransforms() { - animDuration = Math.Min(300, repeatPoint.SpanDuration); + animDuration = Math.Min(300, sliderRepeatPoint.SpanDuration); this.Animate( d => d.FadeIn(animDuration), @@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public void UpdateSnakingPosition(Vector2 start, Vector2 end) { - bool isRepeatAtEnd = repeatPoint.RepeatIndex % 2 == 0; + bool isRepeatAtEnd = sliderRepeatPoint.RepeatIndex % 2 == 0; List curve = ((PlaySliderBody)drawableSlider.Body.Drawable).CurrentCurve; Position = isRepeatAtEnd ? end : start; diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 77f8ec6cc8..04546b2216 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -177,7 +177,7 @@ namespace osu.Game.Rulesets.Osu.Objects break; case SliderEventType.Repeat: - AddNested(new RepeatPoint + AddNested(new SliderRepeatPoint { RepeatIndex = e.SpanIndex, SpanDuration = SpanDuration, @@ -223,7 +223,7 @@ namespace osu.Game.Rulesets.Osu.Objects foreach (var tick in NestedHitObjects.OfType()) tick.Samples = sampleList; - foreach (var repeat in NestedHitObjects.OfType()) + foreach (var repeat in NestedHitObjects.OfType()) repeat.Samples = getNodeSamples(repeat.RepeatIndex + 1); if (HeadCircle != null) diff --git a/osu.Game.Rulesets.Osu/Objects/RepeatPoint.cs b/osu.Game.Rulesets.Osu/Objects/SliderRepeatPoint.cs similarity index 76% rename from osu.Game.Rulesets.Osu/Objects/RepeatPoint.cs rename to osu.Game.Rulesets.Osu/Objects/SliderRepeatPoint.cs index a277517f9f..797383910f 100644 --- a/osu.Game.Rulesets.Osu/Objects/RepeatPoint.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderRepeatPoint.cs @@ -10,7 +10,7 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects { - public class RepeatPoint : OsuHitObject + public class SliderRepeatPoint : OsuHitObject { public int RepeatIndex { get; set; } public double SpanDuration { get; set; } @@ -28,8 +28,15 @@ namespace osu.Game.Rulesets.Osu.Objects TimePreempt = Math.Min(SpanDuration * 2, TimePreempt); } - public override Judgement CreateJudgement() => new OsuJudgement(); - protected override HitWindows CreateHitWindows() => HitWindows.Empty; + + public override Judgement CreateJudgement() => new SliderRepeatPointJudgement(); + + public class SliderRepeatPointJudgement : OsuJudgement + { + public override bool IsBonus => true; + + protected override int NumericResultFor(HitResult result) => result == MaxResult ? 30 : 0; + } } } diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs index a49f4cef8b..212a84c04a 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs @@ -30,8 +30,15 @@ namespace osu.Game.Rulesets.Osu.Objects TimePreempt = (StartTime - SpanStartTime) / 2 + offset; } - public override Judgement CreateJudgement() => new OsuJudgement(); - protected override HitWindows CreateHitWindows() => HitWindows.Empty; + + public override Judgement CreateJudgement() => new SliderTickJudgement(); + + public class SliderTickJudgement : OsuJudgement + { + public override bool IsBonus => true; + + protected override int NumericResultFor(HitResult result) => result == MaxResult ? 10 : 0; + } } } From 855f0a42530635fad333ed4962aed98079c07850 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Mar 2020 14:38:49 +0900 Subject: [PATCH 0497/2376] Fix bracket style --- osu.Game/Screens/Play/Player.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index efce2e05ce..a120963abd 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -626,13 +626,16 @@ namespace osu.Game.Screens.Play completionProgressDelegate = Schedule(delegate { var score = CreateScore(); + if (DrawableRuleset.ReplayScore == null) + { scoreManager.Import(score).ContinueWith(_ => Schedule(() => { // screen may be in the exiting transition phase. if (this.IsCurrentScreen()) this.Push(CreateResults(score)); })); + } else this.Push(CreateResults(score)); }); From 08b5ab8ec43c01eace7d1639fbb92dd2332bea2c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Mar 2020 14:42:02 +0900 Subject: [PATCH 0498/2376] SliderRepeatPoint -> SliderRepeat --- osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs | 4 ++-- osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs | 2 +- .../Objects/Drawables/DrawableSlider.cs | 2 +- .../Objects/Drawables/DrawableSliderRepeat.cs | 14 +++++++------- osu.Game.Rulesets.Osu/Objects/Slider.cs | 4 ++-- .../{SliderRepeatPoint.cs => SliderRepeat.cs} | 6 +++--- 7 files changed, 17 insertions(+), 17 deletions(-) rename osu.Game.Rulesets.Osu/Objects/{SliderRepeatPoint.cs => SliderRepeat.cs} (91%) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs index 1c0dd27e69..a201364de4 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs @@ -146,7 +146,7 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("head samples updated", () => assertSamples(((Slider)slider.HitObject).HeadCircle)); AddAssert("tick samples not updated", () => ((Slider)slider.HitObject).NestedHitObjects.OfType().All(assertTickSamples)); - AddAssert("repeat samples updated", () => ((Slider)slider.HitObject).NestedHitObjects.OfType().All(assertSamples)); + AddAssert("repeat samples updated", () => ((Slider)slider.HitObject).NestedHitObjects.OfType().All(assertSamples)); AddAssert("tail has no samples", () => ((Slider)slider.HitObject).TailCircle.Samples.Count == 0); static bool assertTickSamples(SliderTick tick) => tick.Samples.Single().Name == "slidertick"; @@ -181,7 +181,7 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("head samples not updated", () => assertSamples(((Slider)slider.HitObject).HeadCircle)); AddAssert("tick samples not updated", () => ((Slider)slider.HitObject).NestedHitObjects.OfType().All(assertTickSamples)); - AddAssert("repeat samples not updated", () => ((Slider)slider.HitObject).NestedHitObjects.OfType().All(assertSamples)); + AddAssert("repeat samples not updated", () => ((Slider)slider.HitObject).NestedHitObjects.OfType().All(assertSamples)); AddAssert("tail has no samples", () => ((Slider)slider.HitObject).TailCircle.Samples.Count == 0); static bool assertTickSamples(SliderTick tick) => tick.Samples.Single().Name == "slidertick"; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs index c1fc589798..cf6677a55d 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Mods return; slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y)); - slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y)); + slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y)); foreach (var point in slider.Path.ControlPoints) point.Position.Value = new Vector2(point.Position.Value.X, -point.Position.Value.Y); diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs b/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs index fe7c70c52c..297a0fea79 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Osu.Mods // Wiggle the repeat points with the slider instead of independently. // Also fixes an issue with repeat points being positioned incorrectly. - if (osuObject is SliderRepeatPoint) + if (osuObject is SliderRepeat) return; Random objRand = new Random((int)osuObject.StartTime); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index cb7005cb17..8b8a0ff22a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -129,7 +129,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables case SliderTick tick: return new DrawableSliderTick(tick) { Position = tick.Position - slider.Position }; - case SliderRepeatPoint repeat: + case SliderRepeat repeat: return new DrawableSliderRepeat(repeat, this) { Position = repeat.Position - slider.Position }; } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs index 3336188068..b9cee71ca1 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs @@ -16,17 +16,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public class DrawableSliderRepeat : DrawableOsuHitObject, ITrackSnaking { - private readonly SliderRepeatPoint sliderRepeatPoint; + private readonly SliderRepeat sliderRepeat; private readonly DrawableSlider drawableSlider; private double animDuration; private readonly Drawable scaleContainer; - public DrawableSliderRepeat(SliderRepeatPoint sliderRepeatPoint, DrawableSlider drawableSlider) - : base(sliderRepeatPoint) + public DrawableSliderRepeat(SliderRepeat sliderRepeat, DrawableSlider drawableSlider) + : base(sliderRepeat) { - this.sliderRepeatPoint = sliderRepeatPoint; + this.sliderRepeat = sliderRepeat; this.drawableSlider = drawableSlider; Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); @@ -48,13 +48,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void CheckForResult(bool userTriggered, double timeOffset) { - if (sliderRepeatPoint.StartTime <= Time.Current) + if (sliderRepeat.StartTime <= Time.Current) ApplyResult(r => r.Type = drawableSlider.Tracking.Value ? HitResult.Great : HitResult.Miss); } protected override void UpdateInitialTransforms() { - animDuration = Math.Min(300, sliderRepeatPoint.SpanDuration); + animDuration = Math.Min(300, sliderRepeat.SpanDuration); this.Animate( d => d.FadeIn(animDuration), @@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public void UpdateSnakingPosition(Vector2 start, Vector2 end) { - bool isRepeatAtEnd = sliderRepeatPoint.RepeatIndex % 2 == 0; + bool isRepeatAtEnd = sliderRepeat.RepeatIndex % 2 == 0; List curve = ((PlaySliderBody)drawableSlider.Body.Drawable).CurrentCurve; Position = isRepeatAtEnd ? end : start; diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 04546b2216..28706b07f3 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -177,7 +177,7 @@ namespace osu.Game.Rulesets.Osu.Objects break; case SliderEventType.Repeat: - AddNested(new SliderRepeatPoint + AddNested(new SliderRepeat { RepeatIndex = e.SpanIndex, SpanDuration = SpanDuration, @@ -223,7 +223,7 @@ namespace osu.Game.Rulesets.Osu.Objects foreach (var tick in NestedHitObjects.OfType()) tick.Samples = sampleList; - foreach (var repeat in NestedHitObjects.OfType()) + foreach (var repeat in NestedHitObjects.OfType()) repeat.Samples = getNodeSamples(repeat.RepeatIndex + 1); if (HeadCircle != null) diff --git a/osu.Game.Rulesets.Osu/Objects/SliderRepeatPoint.cs b/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs similarity index 91% rename from osu.Game.Rulesets.Osu/Objects/SliderRepeatPoint.cs rename to osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs index 797383910f..a8fd3764c5 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderRepeatPoint.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs @@ -10,7 +10,7 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects { - public class SliderRepeatPoint : OsuHitObject + public class SliderRepeat : OsuHitObject { public int RepeatIndex { get; set; } public double SpanDuration { get; set; } @@ -30,9 +30,9 @@ namespace osu.Game.Rulesets.Osu.Objects protected override HitWindows CreateHitWindows() => HitWindows.Empty; - public override Judgement CreateJudgement() => new SliderRepeatPointJudgement(); + public override Judgement CreateJudgement() => new SliderRepeatJudgement(); - public class SliderRepeatPointJudgement : OsuJudgement + public class SliderRepeatJudgement : OsuJudgement { public override bool IsBonus => true; From 114b46c4f02a9a8d92c9e614722a70a9877971d4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Mar 2020 14:44:48 +0900 Subject: [PATCH 0499/2376] Change slider tail to give repeat judgement; slider itself to give none (managed by head already) --- .../Objects/Drawables/DrawableSlider.cs | 21 +++---------------- osu.Game.Rulesets.Osu/Objects/Slider.cs | 3 +-- .../Objects/SliderTailCircle.cs | 4 ++-- 3 files changed, 6 insertions(+), 22 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 8b8a0ff22a..2d5b9d874c 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -6,13 +6,11 @@ using osuTK; using osu.Framework.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Skinning; -using osu.Game.Rulesets.Scoring; using osuTK.Graphics; using osu.Game.Skinning; @@ -26,6 +24,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public readonly SliderBall Ball; public readonly SkinnableDrawable Body; + public override bool DisplayResult => false; + private PlaySliderBody sliderBody => Body.Drawable as PlaySliderBody; private readonly Container headContainer; @@ -193,22 +193,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (userTriggered || Time.Current < slider.EndTime) return; - ApplyResult(r => - { - var judgementsCount = NestedHitObjects.Count; - var judgementsHit = NestedHitObjects.Count(h => h.IsHit); - - var hitFraction = (double)judgementsHit / judgementsCount; - - if (hitFraction == 1 && HeadCircle.Result.Type == HitResult.Great) - r.Type = HitResult.Great; - else if (hitFraction >= 0.5 && HeadCircle.Result.Type >= HitResult.Good) - r.Type = HitResult.Good; - else if (hitFraction > 0) - r.Type = HitResult.Meh; - else - r.Type = HitResult.Miss; - }); + ApplyResult(r => r.Type = r.Judgement.MaxResult); } protected override void UpdateStateTransforms(ArmedState state) diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 28706b07f3..3812a62a25 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -11,7 +11,6 @@ using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects @@ -233,7 +232,7 @@ namespace osu.Game.Rulesets.Osu.Objects private IList getNodeSamples(int nodeIndex) => nodeIndex < NodeSamples.Count ? NodeSamples[nodeIndex] : Samples; - public override Judgement CreateJudgement() => new OsuJudgement(); + public override Judgement CreateJudgement() => new IgnoreJudgement(); protected override HitWindows CreateHitWindows() => HitWindows.Empty; } diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs index 127c36fcc0..c11e20c9e7 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs @@ -22,8 +22,8 @@ namespace osu.Game.Rulesets.Osu.Objects pathVersion.BindValueChanged(_ => Position = slider.EndPosition); } - public override Judgement CreateJudgement() => new IgnoreJudgement(); - protected override HitWindows CreateHitWindows() => HitWindows.Empty; + + public override Judgement CreateJudgement() => new SliderRepeat.SliderRepeatJudgement(); } } From 3489514b65f6462d27f80dcf9db6b5039d8d527e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Mar 2020 17:15:53 +0900 Subject: [PATCH 0500/2376] Fix tests asserting incorrectly --- osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs index 21244f0e9c..67e1b77770 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs @@ -327,7 +327,7 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("Tracking dropped", assertMidSliderJudgementFail); } - private bool assertGreatJudge() => judgementResults.Last().Type == HitResult.Great; + private bool assertGreatJudge() => judgementResults.Any() && judgementResults.All(t => t.Type == HitResult.Great); private bool assertHeadMissTailTracked() => judgementResults[^2].Type == HitResult.Great && judgementResults.First().Type == HitResult.Miss; From f285b43a74afd66c6c2ec1dcbe63e4f66f007314 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 19 Mar 2020 17:44:32 +0900 Subject: [PATCH 0501/2376] Allow simultaneous hitobjects --- osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs | 2 +- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs index a7416671f6..59d8727ae1 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs @@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Osu.Tests new OsuReplayFrame { Time = time_first_circle, Position = position_second_circle, Actions = { OsuAction.LeftButton } } }); - addJudgementAssert(HitResult.Miss, HitResult.Miss); + addJudgementAssert(HitResult.Miss, HitResult.Great); } /// diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 643253b1af..bf91504b00 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -94,8 +94,9 @@ namespace osu.Game.Rulesets.Osu.UI if (lastObject == null) return true; - // Ensure that either the last object has received a judgement or the hit time occurs after the last object's start time. - if (lastObject.Judged || Time.Current > lastObject.HitObject.StartTime) + // Ensure that either the last object has received a judgement or the hit time occurs at or after the last object's start time. + // Simultaneous hitobjects are allowed to be hit at the same time value to account for edge-cases such as Centipede. + if (lastObject.Judged || Time.Current >= lastObject.HitObject.StartTime) return true; return false; From 8f9e97b4ccbec6e9de72dba2fca7bfe29e1b7b88 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Mar 2020 18:07:39 +0900 Subject: [PATCH 0502/2376] Fix carousel not remembering last selection correctly --- osu.Game/Screens/Select/Carousel/CarouselGroupEagerSelect.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/Carousel/CarouselGroupEagerSelect.cs b/osu.Game/Screens/Select/Carousel/CarouselGroupEagerSelect.cs index 045c682dc3..6ce12f7b89 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselGroupEagerSelect.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselGroupEagerSelect.cs @@ -104,7 +104,8 @@ namespace osu.Game.Screens.Select.Carousel private void updateSelected(CarouselItem newSelection) { - LastSelected = newSelection; + if (newSelection != null) + LastSelected = newSelection; updateSelectedIndex(); } From 0c1f385d5aae66ed9f4735fd154487481efc397f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Mar 2020 18:19:10 +0900 Subject: [PATCH 0503/2376] Add OsuIgnoreJudgement to get correct result type --- .../Judgements/OsuIgnoreJudgement.cs | 16 ++++++++++++++++ .../Objects/Drawables/DrawableSliderTail.cs | 2 +- .../Objects/Drawables/DrawableSliderTick.cs | 2 +- osu.Game.Rulesets.Osu/Objects/Slider.cs | 3 ++- 4 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Judgements/OsuIgnoreJudgement.cs diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuIgnoreJudgement.cs b/osu.Game.Rulesets.Osu/Judgements/OsuIgnoreJudgement.cs new file mode 100644 index 0000000000..e528f65dca --- /dev/null +++ b/osu.Game.Rulesets.Osu/Judgements/OsuIgnoreJudgement.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 osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Osu.Judgements +{ + public class OsuIgnoreJudgement : OsuJudgement + { + public override bool AffectsCombo => false; + + protected override int NumericResultFor(HitResult result) => 0; + + protected override double HealthIncreaseFor(HitResult result) => 0; + } +} diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs index 21a3a0d236..29a4929c1b 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs @@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void CheckForResult(bool userTriggered, double timeOffset) { if (!userTriggered && timeOffset >= 0) - ApplyResult(r => r.Type = Tracking ? HitResult.Great : HitResult.Miss); + ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : HitResult.Miss); } private void updatePosition() => Position = HitObject.Position - slider.Position; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs index 60b5c335d6..66eb60aa28 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs @@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void CheckForResult(bool userTriggered, double timeOffset) { if (timeOffset >= 0) - ApplyResult(r => r.Type = Tracking ? HitResult.Great : HitResult.Miss); + ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : HitResult.Miss); } protected override void UpdateInitialTransforms() diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 3812a62a25..db1f46d8e2 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -11,6 +11,7 @@ using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects @@ -232,7 +233,7 @@ namespace osu.Game.Rulesets.Osu.Objects private IList getNodeSamples(int nodeIndex) => nodeIndex < NodeSamples.Count ? NodeSamples[nodeIndex] : Samples; - public override Judgement CreateJudgement() => new IgnoreJudgement(); + public override Judgement CreateJudgement() => new OsuIgnoreJudgement(); protected override HitWindows CreateHitWindows() => HitWindows.Empty; } From 3a50c4bb51bf0377ad51e830656ef2a24c32417f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Mar 2020 18:58:22 +0900 Subject: [PATCH 0504/2376] Update tests --- .../SongSelect/TestSceneBeatmapCarousel.cs | 86 +++++++++++++++---- 1 file changed, 69 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 8df75c78f5..c2534e2cc7 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -419,7 +419,7 @@ namespace osu.Game.Tests.Visual.SongSelect } [Test] - public void TestCarouselRootIsRandom() + public void TestCarouselRemembersSelection() { List manySets = new List(); @@ -429,12 +429,74 @@ namespace osu.Game.Tests.Visual.SongSelect loadBeatmaps(manySets); advanceSelection(direction: 1, diff: false); - checkNonmatchingFilter(); - checkNonmatchingFilter(); - checkNonmatchingFilter(); - checkNonmatchingFilter(); - checkNonmatchingFilter(); - AddAssert("Selection was random", () => eagerSelectedIDs.Count > 1); + + for (int i = 0; i < 5; i++) + { + AddStep("Toggle non-matching filter", () => + { + carousel.Filter(new FilterCriteria { SearchText = Guid.NewGuid().ToString() }, false); + }); + + AddStep("Restore no filter", () => + { + carousel.Filter(new FilterCriteria(), false); + eagerSelectedIDs.Add(carousel.SelectedBeatmapSet.ID); + }); + } + + // always returns to same selection as long as it's available. + AddAssert("Selection was remembered", () => eagerSelectedIDs.Count == 1); + } + + [Test] + public void TestRandomFallbackOnNonMatchingPrevious() + { + List manySets = new List(); + + AddStep("populate maps", () => + { + for (int i = 0; i < 10; i++) + { + var set = createTestBeatmapSet(i); + + foreach (var b in set.Beatmaps) + { + // all taiko except for first + int ruleset = i > 0 ? 1 : 0; + + b.Ruleset = rulesets.GetRuleset(ruleset); + b.RulesetID = ruleset; + } + + manySets.Add(set); + } + }); + + loadBeatmaps(manySets); + + for (int i = 0; i < 10; i++) + { + AddStep("Reset filter", () => carousel.Filter(new FilterCriteria(), false)); + + AddStep("select first beatmap", () => carousel.SelectBeatmap(manySets.First().Beatmaps.First())); + + AddStep("Toggle non-matching filter", () => + { + carousel.Filter(new FilterCriteria { SearchText = Guid.NewGuid().ToString() }, false); + }); + + AddAssert("selection lost", () => carousel.SelectedBeatmap == null); + + AddStep("Restore different ruleset filter", () => + { + carousel.Filter(new FilterCriteria { Ruleset = rulesets.GetRuleset(1) }, false); + eagerSelectedIDs.Add(carousel.SelectedBeatmapSet.ID); + }); + + AddAssert("selection changed", () => carousel.SelectedBeatmap != manySets.First().Beatmaps.First()); + } + + AddAssert("Selection was random", () => eagerSelectedIDs.Count > 2); } [Test] @@ -593,16 +655,6 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert("Selection is visible", selectedBeatmapVisible); } - private void checkNonmatchingFilter() - { - AddStep("Toggle non-matching filter", () => - { - carousel.Filter(new FilterCriteria { SearchText = "Dingo" }, false); - carousel.Filter(new FilterCriteria(), false); - eagerSelectedIDs.Add(carousel.SelectedBeatmapSet.ID); - }); - } - private BeatmapSetInfo createTestBeatmapSet(int id) { return new BeatmapSetInfo From 12a48d2774dd0e4aa19cdd989b34c7022343ff1e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 19 Mar 2020 19:16:24 +0900 Subject: [PATCH 0505/2376] Cause all earlier hitobjects to get missed --- .../TestSceneNoteLock.cs | 13 ++++- .../Objects/Drawables/DrawableOsuHitObject.cs | 6 +++ osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 52 +++++++++++++++++++ 3 files changed, 70 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs index 59d8727ae1..e2b8364f3e 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Screens; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Replays; @@ -24,6 +25,8 @@ namespace osu.Game.Rulesets.Osu.Tests { private const double time_first_circle = 1500; private const double time_second_circle = 1600; + private const double early_miss_window = 1000; // time after -1000 to -500 is considered a miss + private const double late_miss_window = 500; // time after +500 is considered a miss private static readonly Vector2 position_first_circle = Vector2.Zero; private static readonly Vector2 position_second_circle = new Vector2(80); @@ -40,6 +43,7 @@ namespace osu.Game.Rulesets.Osu.Tests }); addJudgementAssert(HitResult.Miss, HitResult.Miss); + addJudgementOffsetAssert(late_miss_window); } /// @@ -54,6 +58,7 @@ namespace osu.Game.Rulesets.Osu.Tests }); addJudgementAssert(HitResult.Miss, HitResult.Great); + addJudgementOffsetAssert(0); } /// @@ -68,6 +73,7 @@ namespace osu.Game.Rulesets.Osu.Tests }); addJudgementAssert(HitResult.Miss, HitResult.Great); + addJudgementOffsetAssert(100); } /// @@ -91,6 +97,11 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert($"second circle judgement is {secondCircle}", () => judgementResults.Single(r => r.HitObject.StartTime == time_second_circle).Type == secondCircle); } + private void addJudgementOffsetAssert(double offset) + { + AddAssert($"first circle judged at {offset}", () => Precision.AlmostEquals(judgementResults.Single(r => r.HitObject.StartTime == time_first_circle).TimeOffset, offset, 100)); + } + private ScoreAccessibleReplayPlayer currentPlayer; private List judgementResults; private bool allJudgedFired; @@ -157,7 +168,7 @@ namespace osu.Game.Rulesets.Osu.Tests private static readonly DifficultyRange[] ranges = { new DifficultyRange(HitResult.Great, 500, 500, 500), - new DifficultyRange(HitResult.Miss, 1000, 1000, 1000), + new DifficultyRange(HitResult.Miss, early_miss_window, early_miss_window, early_miss_window), }; public override bool IsHitResultAllowed(HitResult result) => result == HitResult.Great || result == HitResult.Miss; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index 3e66549ca0..13829dc2f7 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Graphics.Containers; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects.Drawables { @@ -61,6 +62,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } } + /// + /// Causes this to get missed, disregarding all conditions in implementations of . + /// + public void MissForcefully() => ApplyResult(r => r.Type = HitResult.Miss); + protected override JudgementResult CreateResult(Judgement judgement) => new OsuJudgementResult(HitObject, judgement); } } diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index bf91504b00..e36d32d01a 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.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.Extensions.IEnumerableExtensions; using osuTK; using osu.Framework.Graphics; @@ -104,6 +105,8 @@ namespace osu.Game.Rulesets.Osu.UI private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) { + missAllEarlier(result); + if (!judgedObject.DisplayResult || !DisplayJudgements.Value) return; @@ -117,6 +120,55 @@ namespace osu.Game.Rulesets.Osu.UI judgementLayer.Add(explosion); } + /// + /// Misses all s occurring earlier than the start time of a judged . + /// + /// The of the judged . + private void missAllEarlier(JudgementResult result) + { + // Hitobjects that count as bonus should not cause other hitobjects to get missed. + // E.g. For the sequence slider-head -> circle -> slider-tick, hitting the tick before the circle should not cause the circle to be missed. + // E.g. For the sequence spinner -> circle -> spinner-bonus, hitting the bonus before the circle should not cause the circle to be missed. + if (result.Judgement.IsBonus) + return; + + // The minimum start time required for hitobjects so that they aren't missed. + double minimumTime = result.HitObject.StartTime; + + foreach (var obj in HitObjectContainer.AliveObjects) + { + if (obj.HitObject.StartTime >= minimumTime) + break; + + attemptMiss(obj); + + foreach (var n in obj.NestedHitObjects) + { + if (n.HitObject.StartTime >= minimumTime) + break; + + attemptMiss(n); + } + } + + static void attemptMiss(DrawableHitObject obj) + { + if (!(obj is DrawableOsuHitObject osuObject)) + throw new InvalidOperationException($"{obj.GetType()} is not a {nameof(DrawableOsuHitObject)}."); + + // Hitobjects that have already been judged cannot be missed. + if (osuObject.Judged) + return; + + // Hitobjects that count as bonus should not be missed. + // For the sequence slider-head -> slider-tick -> circle, hitting the circle before the tick should not cause the tick to be missed. + if (osuObject.Result.Judgement.IsBonus) + return; + + osuObject.MissForcefully(); + } + } + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => HitObjectContainer.ReceivePositionalInputAt(screenSpacePos); private class ApproachCircleProxyContainer : LifetimeManagementContainer From 43c1f27f2490ee304e2347473f6cbc2bd3b5de36 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Mar 2020 20:19:50 +0900 Subject: [PATCH 0506/2376] 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 942970c890..7e17f9da16 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 54f1ad2845..46d17bcf05 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -23,7 +23,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 816a430b52..9cc9792ecf 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + @@ -79,7 +79,7 @@ - + From 549cec80d6d62ecd55d7a8dbfc55363c6ea7f273 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Mar 2020 02:47:45 +0900 Subject: [PATCH 0507/2376] Reduce app store processing wait interval in line with faster processing time --- fastlane/Fastfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index f895c465d2..4fd0e5e8c7 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -97,7 +97,7 @@ platform :ios do changelog.gsub!('$BUILD_ID', options[:build]) pilot( - wait_processing_interval: 1800, + wait_processing_interval: 900, changelog: changelog, groups: ['osu! supporters', 'public'], distribute_external: true, From be4a97c2894282c794c763f269dcdb991aacf512 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Mar 2020 13:01:24 +0900 Subject: [PATCH 0508/2376] Correctly bypass last selected when it is filtered --- osu.Game/Screens/Select/BeatmapCarousel.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 9f8b201eff..389ae918b9 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -333,8 +333,7 @@ namespace osu.Game.Screens.Select else set = visibleSets.ElementAt(RNG.Next(visibleSets.Count)); - var visibleBeatmaps = set.Beatmaps.Where(s => !s.Filtered.Value).ToList(); - select(visibleBeatmaps[RNG.Next(visibleBeatmaps.Count)]); + select(set); return true; } @@ -756,7 +755,7 @@ namespace osu.Game.Screens.Select protected override void PerformSelection() { - if (LastSelected == null) + if (LastSelected == null || LastSelected.Filtered.Value) carousel.SelectNextRandom(); else base.PerformSelection(); From 9b60b535e596c051c353400f5feeb68e4562fa5c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Mar 2020 15:01:26 +0900 Subject: [PATCH 0509/2376] Fix selection not occurring when switching from empty ruleset on first load --- .../SongSelect/TestSceneBeatmapCarousel.cs | 32 +++++++++++++++++-- osu.Game/Screens/Select/BeatmapCarousel.cs | 6 +++- osu.Game/Screens/Select/SongSelect.cs | 3 ++ 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 8df75c78f5..bd26120da9 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -227,6 +227,32 @@ namespace osu.Game.Tests.Visual.SongSelect waitForSelection(set_count); } + [Test] + public void TestSelectionEnteringFromEmptyRuleset() + { + var sets = new List(); + + AddStep("Create beatmaps for taiko only", () => + { + var rulesetBeatmapSet = createTestBeatmapSet(1); + var taikoRuleset = rulesets.AvailableRulesets.ElementAt(1); + rulesetBeatmapSet.Beatmaps.ForEach(b => + { + b.Ruleset = taikoRuleset; + b.RulesetID = 1; + }); + + sets.Add(rulesetBeatmapSet); + }); + + loadBeatmaps(sets, () => new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(0) }); + + AddStep("Set non-empty mode filter", () => + carousel.Filter(new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(1) }, false)); + + AddAssert("Something is selected", () => carousel.SelectedBeatmap != null); + } + /// /// Test sorting /// @@ -399,7 +425,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 == null); + AddAssert("unfiltered beatmap not selected", () => carousel.SelectedBeatmap.RulesetID == 0); AddStep("remove mixed set", () => { @@ -484,7 +510,7 @@ namespace osu.Game.Tests.Visual.SongSelect checkVisibleItemCount(true, 15); } - private void loadBeatmaps(List beatmapSets = null) + private void loadBeatmaps(List beatmapSets = null, Func initialCriteria = null) { createCarousel(); @@ -499,7 +525,7 @@ namespace osu.Game.Tests.Visual.SongSelect bool changed = false; AddStep($"Load {(beatmapSets.Count > 0 ? beatmapSets.Count.ToString() : "some")} beatmaps", () => { - carousel.Filter(new FilterCriteria()); + carousel.Filter(initialCriteria?.Invoke() ?? new FilterCriteria()); carousel.BeatmapSetsChanged = () => changed = true; carousel.BeatmapSets = beatmapSets; }); diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 9f8b201eff..6c6f9a0e79 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -751,13 +751,17 @@ namespace osu.Game.Screens.Select public CarouselRoot(BeatmapCarousel carousel) { + // root should always remaing selected. if not, PerformSelection will not be called. + State.Value = CarouselItemState.Selected; + State.ValueChanged += state => State.Value = CarouselItemState.Selected; + this.carousel = carousel; } protected override void PerformSelection() { if (LastSelected == null) - carousel.SelectNextRandom(); + carousel?.SelectNextRandom(); else base.PerformSelection(); } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 11c680bdb0..b6ec40ab88 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -150,6 +150,7 @@ namespace osu.Game.Screens.Select }, Child = Carousel = new BeatmapCarousel { + AllowSelection = false, // delay any selection until our bindables are ready to make a good choice. Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, RelativeSizeAxes = Axes.Both, @@ -655,6 +656,8 @@ namespace osu.Game.Screens.Select { bindBindables(); + Carousel.AllowSelection = true; + // If a selection was already obtained, do not attempt to update the selected beatmap. if (Carousel.SelectedBeatmapSet != null) return; From 8136ea561e689786be2b5938fec47bdffc495d8c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Mar 2020 15:02:13 +0900 Subject: [PATCH 0510/2376] Fix a couple of broken tests --- .../Background/TestSceneUserDimBackgrounds.cs | 1 + .../Visual/SongSelect/TestSceneBeatmapCarousel.cs | 15 ++++++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs index b51555db3e..1ddc1326d5 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs @@ -278,6 +278,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); AddStep("Set default user settings", () => { diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index bd26120da9..5b4c57e28f 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -432,13 +432,18 @@ namespace osu.Game.Tests.Visual.SongSelect carousel.RemoveBeatmapSet(testMixed); testMixed = null; }); - var testSingle = createTestBeatmapSet(set_count + 2); - testSingle.Beatmaps.ForEach(b => + BeatmapSetInfo testSingle = null; + AddStep("add single ruleset beatmapset", () => { - b.Ruleset = rulesets.AvailableRulesets.ElementAt(1); - b.RulesetID = b.Ruleset.ID ?? 1; + testSingle = createTestBeatmapSet(set_count + 2); + testSingle.Beatmaps.ForEach(b => + { + b.Ruleset = rulesets.AvailableRulesets.ElementAt(1); + b.RulesetID = b.Ruleset.ID ?? 1; + }); + + carousel.UpdateBeatmapSet(testSingle); }); - AddStep("add single ruleset beatmapset", () => carousel.UpdateBeatmapSet(testSingle)); AddStep("select filtered map skipping filtered", () => carousel.SelectBeatmap(testSingle.Beatmaps[0], false)); checkNoSelection(); AddStep("remove single ruleset set", () => carousel.RemoveBeatmapSet(testSingle)); From b813c0aff1bff795bb5247caf7dfeac26a1d19a4 Mon Sep 17 00:00:00 2001 From: OctopuSSX Date: Fri, 20 Mar 2020 14:26:26 +0300 Subject: [PATCH 0511/2376] Don't open profile if it's Autoplay --- osu.Game/Users/Drawables/DrawableAvatar.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Users/Drawables/DrawableAvatar.cs b/osu.Game/Users/Drawables/DrawableAvatar.cs index 93136e88a0..fe44c56ae0 100644 --- a/osu.Game/Users/Drawables/DrawableAvatar.cs +++ b/osu.Game/Users/Drawables/DrawableAvatar.cs @@ -68,7 +68,7 @@ namespace osu.Game.Users.Drawables if (!OpenOnClick.Value) return; - if (user != null) + if (user != null && user.Id != 1) game?.ShowUser(user.Id); } From 157f05c3e508a228a9f13e7e454a937faab0b7c9 Mon Sep 17 00:00:00 2001 From: OctopuSSX Date: Fri, 20 Mar 2020 14:35:27 +0300 Subject: [PATCH 0512/2376] Update osu.Game/Users/Drawables/DrawableAvatar.cs Co-Authored-By: Dean Herbert --- osu.Game/Users/Drawables/DrawableAvatar.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Users/Drawables/DrawableAvatar.cs b/osu.Game/Users/Drawables/DrawableAvatar.cs index fe44c56ae0..09750c5bfe 100644 --- a/osu.Game/Users/Drawables/DrawableAvatar.cs +++ b/osu.Game/Users/Drawables/DrawableAvatar.cs @@ -68,7 +68,7 @@ namespace osu.Game.Users.Drawables if (!OpenOnClick.Value) return; - if (user != null && user.Id != 1) + if (user?.Id > 1) game?.ShowUser(user.Id); } From 5a6d8f1932715d9fc7479d8cb5614e0b2efe4025 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Fri, 20 Mar 2020 12:47:17 -0400 Subject: [PATCH 0513/2376] use SettingSource to define IconTooltip format --- .../Mods/CatchModDifficultyAdjust.cs | 9 ++--- .../Mods/OsuModDifficultyAdjust.cs | 9 ++--- .../Configuration/SettingSourceAttribute.cs | 11 +++++- osu.Game/Rulesets/Mods/Mod.cs | 34 ++++++++++++++++++- osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs | 7 ++-- osu.Game/Rulesets/Mods/ModDoubleTime.cs | 4 +-- osu.Game/Rulesets/Mods/ModEasy.cs | 4 +-- osu.Game/Rulesets/Mods/ModHalfTime.cs | 4 +-- 8 files changed, 52 insertions(+), 30 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs index 661c59332f..e4298dc008 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs @@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Catch.Mods { public class CatchModDifficultyAdjust : ModDifficultyAdjust { - [SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1)] + [SettingSource("Circle Size", "Override a beatmap's set CS.", "CS {0}", FIRST_SETTING_ORDER - 1)] public BindableNumber CircleSize { get; } = new BindableFloat { Precision = 0.1f, @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Catch.Mods Value = 5, }; - [SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1)] + [SettingSource("Approach Rate", "Override a beatmap's set AR.", "AR {0}", LAST_SETTING_ORDER + 1)] public BindableNumber ApproachRate { get; } = new BindableFloat { Precision = 0.1f, @@ -30,11 +30,6 @@ namespace osu.Game.Rulesets.Catch.Mods Value = 5, }; - public override string IconTooltip => ($"{Name} ({(CircleSize.IsDefault ? "" : $"CS {CircleSize.Value}, ")}" + - $"{(DrainRate.IsDefault ? "" : $"HP {DrainRate.Value}, ")}" + - $"{(OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value}, ")}" + - $"{(ApproachRate.IsDefault ? "" : $"AR {ApproachRate.Value}")}").TrimEnd(new char[] { ',', ' ' }) + ")"; - protected override void TransferSettings(BeatmapDifficulty difficulty) { base.TransferSettings(difficulty); diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs index 477028dbbe..91707ea328 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs @@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Osu.Mods { public class OsuModDifficultyAdjust : ModDifficultyAdjust { - [SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1)] + [SettingSource("Circle Size", "Override a beatmap's set CS.", "CS {0}", FIRST_SETTING_ORDER - 1)] public BindableNumber CircleSize { get; } = new BindableFloat { Precision = 0.1f, @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods Value = 5, }; - [SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1)] + [SettingSource("Approach Rate", "Override a beatmap's set AR.", "AR {0}", LAST_SETTING_ORDER + 1)] public BindableNumber ApproachRate { get; } = new BindableFloat { Precision = 0.1f, @@ -30,11 +30,6 @@ namespace osu.Game.Rulesets.Osu.Mods Value = 5, }; - public override string IconTooltip => ($"{Name} ({(CircleSize.IsDefault ? "" : $"CS {CircleSize.Value}, ")}" + - $"{(DrainRate.IsDefault ? "" : $"HP {DrainRate.Value}, ")}" + - $"{(OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value}, ")}" + - $"{(ApproachRate.IsDefault ? "" : $"AR {ApproachRate.Value}")}").TrimEnd(new char[] { ',', ' ' }) + ")"; - protected override void TransferSettings(BeatmapDifficulty difficulty) { base.TransferSettings(difficulty); diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs index fe487cb1d0..1a79dc7335 100644 --- a/osu.Game/Configuration/SettingSourceAttribute.cs +++ b/osu.Game/Configuration/SettingSourceAttribute.cs @@ -30,17 +30,26 @@ namespace osu.Game.Configuration public int? OrderPosition { get; } + public string TooltipText { get; } + public SettingSourceAttribute(string label, string description = null) { Label = label ?? string.Empty; Description = description ?? string.Empty; } - public SettingSourceAttribute(string label, string description, int orderPosition) + public SettingSourceAttribute(string label, string description, string tooltipText, int orderPosition) : this(label, description) { OrderPosition = orderPosition; + TooltipText = tooltipText; } + + public SettingSourceAttribute(string label, string description, string tooltipText) : this(label, description) + { + TooltipText = tooltipText; + } + } public static class SettingSourceExtensions diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index b70ddc6d46..860e768350 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -2,8 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using System.Linq; using Newtonsoft.Json; +using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; +using osu.Game.Configuration; using osu.Game.IO.Serialization; using osu.Game.Rulesets.UI; @@ -51,7 +55,35 @@ namespace osu.Game.Rulesets.Mods /// are displayed in the tooltip. /// [JsonIgnore] - public virtual string IconTooltip => Name; + public virtual string IconTooltip + { + get + { + List attributes = new List(); + foreach ((SettingSourceAttribute attr, System.Reflection.PropertyInfo property) in this.GetOrderedSettingsSourceProperties()) + { + // use TooltipText from SettingSource if available, but fall back to Label, which has to be provided + string tooltipText = attr.TooltipText ?? attr.Label + " {0}"; + object bindableObj = property.GetValue(this); + if (bindableObj is BindableInt bindableInt && !bindableInt.IsDefault) + { + attributes.Add(string.Format(tooltipText, bindableInt.Value)); + continue; + } + if (bindableObj is BindableFloat bindableFloat && !bindableFloat.IsDefault) + { + attributes.Add(string.Format(tooltipText, bindableFloat.Value)); + continue; + } + if (bindableObj is BindableDouble bindableDouble && !bindableDouble.IsDefault) + { + attributes.Add(string.Format(tooltipText, bindableDouble.Value)); + continue; + } + } + return $"{Name}{(attributes.Any() ? $" ({string.Join(", ", attributes)})" : "")}"; + } + } /// /// The score multiplier of this mod. diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index 4072e6a6af..8188e36b64 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Mods protected const int LAST_SETTING_ORDER = 2; - [SettingSource("HP Drain", "Override a beatmap's set HP.", FIRST_SETTING_ORDER)] + [SettingSource("HP Drain", "Override a beatmap's set HP.", "HP {0}", FIRST_SETTING_ORDER)] public BindableNumber DrainRate { get; } = new BindableFloat { Precision = 0.1f, @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Mods Value = 5, }; - [SettingSource("Accuracy", "Override a beatmap's set OD.", LAST_SETTING_ORDER)] + [SettingSource("Accuracy", "Override a beatmap's set OD.", "OD {0}", LAST_SETTING_ORDER)] public BindableNumber OverallDifficulty { get; } = new BindableFloat { Precision = 0.1f, @@ -52,9 +52,6 @@ namespace osu.Game.Rulesets.Mods Value = 5, }; - public override string IconTooltip => $"{Name} ({(DrainRate.IsDefault ? $"HP {DrainRate.Value.ToString()}, " : "")}" + - $"{(OverallDifficulty.IsDefault ? $"OD {OverallDifficulty.Value.ToString()}, " : "")})"; - private BeatmapDifficulty difficulty; public void ReadFromDifficulty(BeatmapDifficulty difficulty) diff --git a/osu.Game/Rulesets/Mods/ModDoubleTime.cs b/osu.Game/Rulesets/Mods/ModDoubleTime.cs index 4f7d82418d..fe027b9da0 100644 --- a/osu.Game/Rulesets/Mods/ModDoubleTime.cs +++ b/osu.Game/Rulesets/Mods/ModDoubleTime.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Mods public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModHalfTime)).ToArray(); - [SettingSource("Speed increase", "The actual increase to apply")] + [SettingSource("Speed increase", "The actual increase to apply", "{0}x")] public override BindableNumber SpeedChange { get; } = new BindableDouble { MinValue = 1.01, @@ -30,7 +30,5 @@ namespace osu.Game.Rulesets.Mods Value = 1.5, Precision = 0.01, }; - - public override string IconTooltip => $"{Name}{(SpeedChange.IsDefault ? "" : $" ({SpeedChange.Value}x)")}"; } } diff --git a/osu.Game/Rulesets/Mods/ModEasy.cs b/osu.Game/Rulesets/Mods/ModEasy.cs index 2ec4e9610b..c92c7297c3 100644 --- a/osu.Game/Rulesets/Mods/ModEasy.cs +++ b/osu.Game/Rulesets/Mods/ModEasy.cs @@ -21,15 +21,13 @@ namespace osu.Game.Rulesets.Mods public override bool Ranked => true; public override Type[] IncompatibleMods => new[] { typeof(ModHardRock), typeof(ModDifficultyAdjust) }; - [SettingSource("Extra Lives", "Number of extra lives")] + [SettingSource("Extra Lives", "Number of extra lives", "{0} lives")] public Bindable Retries { get; } = new BindableInt(2) { MinValue = 0, MaxValue = 10 }; - public override string IconTooltip => $"{Name}{(Retries.IsDefault ? "" : $" ({Retries.Value} lives)")}"; - private int retries; private BindableNumber health; diff --git a/osu.Game/Rulesets/Mods/ModHalfTime.cs b/osu.Game/Rulesets/Mods/ModHalfTime.cs index 14133bddcd..7c1f4b8e12 100644 --- a/osu.Game/Rulesets/Mods/ModHalfTime.cs +++ b/osu.Game/Rulesets/Mods/ModHalfTime.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Mods public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModDoubleTime)).ToArray(); - [SettingSource("Speed decrease", "The actual decrease to apply")] + [SettingSource("Speed decrease", "The actual decrease to apply", "{0}x")] public override BindableNumber SpeedChange { get; } = new BindableDouble { MinValue = 0.5, @@ -30,7 +30,5 @@ namespace osu.Game.Rulesets.Mods Value = 0.75, Precision = 0.01, }; - - public override string IconTooltip => $"{Name}{(SpeedChange.IsDefault ? "" : $" ({SpeedChange.Value}x)")}"; } } From 9dc814681195f864192436ba0a9f8197a726108b Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Fri, 20 Mar 2020 13:21:44 -0400 Subject: [PATCH 0514/2376] fix style issues --- osu.Game/Configuration/SettingSourceAttribute.cs | 4 ++-- osu.Game/Rulesets/Mods/Mod.cs | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs index 1a79dc7335..fb0daf9217 100644 --- a/osu.Game/Configuration/SettingSourceAttribute.cs +++ b/osu.Game/Configuration/SettingSourceAttribute.cs @@ -45,11 +45,11 @@ namespace osu.Game.Configuration TooltipText = tooltipText; } - public SettingSourceAttribute(string label, string description, string tooltipText) : this(label, description) + public SettingSourceAttribute(string label, string description, string tooltipText) + : this(label, description) { TooltipText = tooltipText; } - } public static class SettingSourceExtensions diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 860e768350..69fd45767b 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -60,25 +60,28 @@ namespace osu.Game.Rulesets.Mods get { List attributes = new List(); + foreach ((SettingSourceAttribute attr, System.Reflection.PropertyInfo property) in this.GetOrderedSettingsSourceProperties()) { // use TooltipText from SettingSource if available, but fall back to Label, which has to be provided string tooltipText = attr.TooltipText ?? attr.Label + " {0}"; object bindableObj = property.GetValue(this); + if (bindableObj is BindableInt bindableInt && !bindableInt.IsDefault) { attributes.Add(string.Format(tooltipText, bindableInt.Value)); continue; } + if (bindableObj is BindableFloat bindableFloat && !bindableFloat.IsDefault) { attributes.Add(string.Format(tooltipText, bindableFloat.Value)); continue; } + if (bindableObj is BindableDouble bindableDouble && !bindableDouble.IsDefault) { attributes.Add(string.Format(tooltipText, bindableDouble.Value)); - continue; } } return $"{Name}{(attributes.Any() ? $" ({string.Join(", ", attributes)})" : "")}"; From 9e3bff3b97503c6e4f2a89fa5ea1c8a01f3767bf Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Fri, 20 Mar 2020 13:36:16 -0400 Subject: [PATCH 0515/2376] oops, missed a newline --- osu.Game/Rulesets/Mods/Mod.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 69fd45767b..f3b7fed96a 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -84,6 +84,7 @@ namespace osu.Game.Rulesets.Mods attributes.Add(string.Format(tooltipText, bindableDouble.Value)); } } + return $"{Name}{(attributes.Any() ? $" ({string.Join(", ", attributes)})" : "")}"; } } From 3d955921302cbe8e449640bdb0488b528654fd7c Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Fri, 20 Mar 2020 14:37:31 -0400 Subject: [PATCH 0516/2376] use var for list declaration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Bartłomiej Dach --- osu.Game/Rulesets/Mods/Mod.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index f3b7fed96a..b4faf55734 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Mods { get { - List attributes = new List(); + var attributes = new List(); foreach ((SettingSourceAttribute attr, System.Reflection.PropertyInfo property) in this.GetOrderedSettingsSourceProperties()) { From 7bdbdd25f8b3955b9c0a8f2dcb897a722073f958 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Fri, 20 Mar 2020 16:05:12 -0400 Subject: [PATCH 0517/2376] Revert "use SettingSource to define IconTooltip format" This reverts commit 5a6d8f1932715d9fc7479d8cb5614e0b2efe4025. --- .../Mods/CatchModDifficultyAdjust.cs | 9 ++++- .../Mods/OsuModDifficultyAdjust.cs | 9 ++++- .../Configuration/SettingSourceAttribute.cs | 11 +----- osu.Game/Rulesets/Mods/Mod.cs | 38 +------------------ osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs | 7 +++- osu.Game/Rulesets/Mods/ModDoubleTime.cs | 4 +- osu.Game/Rulesets/Mods/ModEasy.cs | 4 +- osu.Game/Rulesets/Mods/ModHalfTime.cs | 4 +- 8 files changed, 30 insertions(+), 56 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs index e4298dc008..661c59332f 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs @@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Catch.Mods { public class CatchModDifficultyAdjust : ModDifficultyAdjust { - [SettingSource("Circle Size", "Override a beatmap's set CS.", "CS {0}", FIRST_SETTING_ORDER - 1)] + [SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1)] public BindableNumber CircleSize { get; } = new BindableFloat { Precision = 0.1f, @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Catch.Mods Value = 5, }; - [SettingSource("Approach Rate", "Override a beatmap's set AR.", "AR {0}", LAST_SETTING_ORDER + 1)] + [SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1)] public BindableNumber ApproachRate { get; } = new BindableFloat { Precision = 0.1f, @@ -30,6 +30,11 @@ namespace osu.Game.Rulesets.Catch.Mods Value = 5, }; + public override string IconTooltip => ($"{Name} ({(CircleSize.IsDefault ? "" : $"CS {CircleSize.Value}, ")}" + + $"{(DrainRate.IsDefault ? "" : $"HP {DrainRate.Value}, ")}" + + $"{(OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value}, ")}" + + $"{(ApproachRate.IsDefault ? "" : $"AR {ApproachRate.Value}")}").TrimEnd(new char[] { ',', ' ' }) + ")"; + protected override void TransferSettings(BeatmapDifficulty difficulty) { base.TransferSettings(difficulty); diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs index 91707ea328..477028dbbe 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs @@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Osu.Mods { public class OsuModDifficultyAdjust : ModDifficultyAdjust { - [SettingSource("Circle Size", "Override a beatmap's set CS.", "CS {0}", FIRST_SETTING_ORDER - 1)] + [SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1)] public BindableNumber CircleSize { get; } = new BindableFloat { Precision = 0.1f, @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods Value = 5, }; - [SettingSource("Approach Rate", "Override a beatmap's set AR.", "AR {0}", LAST_SETTING_ORDER + 1)] + [SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1)] public BindableNumber ApproachRate { get; } = new BindableFloat { Precision = 0.1f, @@ -30,6 +30,11 @@ namespace osu.Game.Rulesets.Osu.Mods Value = 5, }; + public override string IconTooltip => ($"{Name} ({(CircleSize.IsDefault ? "" : $"CS {CircleSize.Value}, ")}" + + $"{(DrainRate.IsDefault ? "" : $"HP {DrainRate.Value}, ")}" + + $"{(OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value}, ")}" + + $"{(ApproachRate.IsDefault ? "" : $"AR {ApproachRate.Value}")}").TrimEnd(new char[] { ',', ' ' }) + ")"; + protected override void TransferSettings(BeatmapDifficulty difficulty) { base.TransferSettings(difficulty); diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs index fb0daf9217..fe487cb1d0 100644 --- a/osu.Game/Configuration/SettingSourceAttribute.cs +++ b/osu.Game/Configuration/SettingSourceAttribute.cs @@ -30,25 +30,16 @@ namespace osu.Game.Configuration public int? OrderPosition { get; } - public string TooltipText { get; } - public SettingSourceAttribute(string label, string description = null) { Label = label ?? string.Empty; Description = description ?? string.Empty; } - public SettingSourceAttribute(string label, string description, string tooltipText, int orderPosition) + public SettingSourceAttribute(string label, string description, int orderPosition) : this(label, description) { OrderPosition = orderPosition; - TooltipText = tooltipText; - } - - public SettingSourceAttribute(string label, string description, string tooltipText) - : this(label, description) - { - TooltipText = tooltipText; } } diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index b4faf55734..b70ddc6d46 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -2,12 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; -using System.Linq; using Newtonsoft.Json; -using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; -using osu.Game.Configuration; using osu.Game.IO.Serialization; using osu.Game.Rulesets.UI; @@ -55,39 +51,7 @@ namespace osu.Game.Rulesets.Mods /// are displayed in the tooltip. /// [JsonIgnore] - public virtual string IconTooltip - { - get - { - var attributes = new List(); - - foreach ((SettingSourceAttribute attr, System.Reflection.PropertyInfo property) in this.GetOrderedSettingsSourceProperties()) - { - // use TooltipText from SettingSource if available, but fall back to Label, which has to be provided - string tooltipText = attr.TooltipText ?? attr.Label + " {0}"; - object bindableObj = property.GetValue(this); - - if (bindableObj is BindableInt bindableInt && !bindableInt.IsDefault) - { - attributes.Add(string.Format(tooltipText, bindableInt.Value)); - continue; - } - - if (bindableObj is BindableFloat bindableFloat && !bindableFloat.IsDefault) - { - attributes.Add(string.Format(tooltipText, bindableFloat.Value)); - continue; - } - - if (bindableObj is BindableDouble bindableDouble && !bindableDouble.IsDefault) - { - attributes.Add(string.Format(tooltipText, bindableDouble.Value)); - } - } - - return $"{Name}{(attributes.Any() ? $" ({string.Join(", ", attributes)})" : "")}"; - } - } + public virtual string IconTooltip => Name; /// /// The score multiplier of this mod. diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index 8188e36b64..4072e6a6af 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Mods protected const int LAST_SETTING_ORDER = 2; - [SettingSource("HP Drain", "Override a beatmap's set HP.", "HP {0}", FIRST_SETTING_ORDER)] + [SettingSource("HP Drain", "Override a beatmap's set HP.", FIRST_SETTING_ORDER)] public BindableNumber DrainRate { get; } = new BindableFloat { Precision = 0.1f, @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Mods Value = 5, }; - [SettingSource("Accuracy", "Override a beatmap's set OD.", "OD {0}", LAST_SETTING_ORDER)] + [SettingSource("Accuracy", "Override a beatmap's set OD.", LAST_SETTING_ORDER)] public BindableNumber OverallDifficulty { get; } = new BindableFloat { Precision = 0.1f, @@ -52,6 +52,9 @@ namespace osu.Game.Rulesets.Mods Value = 5, }; + public override string IconTooltip => $"{Name} ({(DrainRate.IsDefault ? $"HP {DrainRate.Value.ToString()}, " : "")}" + + $"{(OverallDifficulty.IsDefault ? $"OD {OverallDifficulty.Value.ToString()}, " : "")})"; + private BeatmapDifficulty difficulty; public void ReadFromDifficulty(BeatmapDifficulty difficulty) diff --git a/osu.Game/Rulesets/Mods/ModDoubleTime.cs b/osu.Game/Rulesets/Mods/ModDoubleTime.cs index fe027b9da0..4f7d82418d 100644 --- a/osu.Game/Rulesets/Mods/ModDoubleTime.cs +++ b/osu.Game/Rulesets/Mods/ModDoubleTime.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Mods public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModHalfTime)).ToArray(); - [SettingSource("Speed increase", "The actual increase to apply", "{0}x")] + [SettingSource("Speed increase", "The actual increase to apply")] public override BindableNumber SpeedChange { get; } = new BindableDouble { MinValue = 1.01, @@ -30,5 +30,7 @@ namespace osu.Game.Rulesets.Mods Value = 1.5, Precision = 0.01, }; + + public override string IconTooltip => $"{Name}{(SpeedChange.IsDefault ? "" : $" ({SpeedChange.Value}x)")}"; } } diff --git a/osu.Game/Rulesets/Mods/ModEasy.cs b/osu.Game/Rulesets/Mods/ModEasy.cs index c92c7297c3..2ec4e9610b 100644 --- a/osu.Game/Rulesets/Mods/ModEasy.cs +++ b/osu.Game/Rulesets/Mods/ModEasy.cs @@ -21,13 +21,15 @@ namespace osu.Game.Rulesets.Mods public override bool Ranked => true; public override Type[] IncompatibleMods => new[] { typeof(ModHardRock), typeof(ModDifficultyAdjust) }; - [SettingSource("Extra Lives", "Number of extra lives", "{0} lives")] + [SettingSource("Extra Lives", "Number of extra lives")] public Bindable Retries { get; } = new BindableInt(2) { MinValue = 0, MaxValue = 10 }; + public override string IconTooltip => $"{Name}{(Retries.IsDefault ? "" : $" ({Retries.Value} lives)")}"; + private int retries; private BindableNumber health; diff --git a/osu.Game/Rulesets/Mods/ModHalfTime.cs b/osu.Game/Rulesets/Mods/ModHalfTime.cs index 7c1f4b8e12..14133bddcd 100644 --- a/osu.Game/Rulesets/Mods/ModHalfTime.cs +++ b/osu.Game/Rulesets/Mods/ModHalfTime.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Mods public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModDoubleTime)).ToArray(); - [SettingSource("Speed decrease", "The actual decrease to apply", "{0}x")] + [SettingSource("Speed decrease", "The actual decrease to apply")] public override BindableNumber SpeedChange { get; } = new BindableDouble { MinValue = 0.5, @@ -30,5 +30,7 @@ namespace osu.Game.Rulesets.Mods Value = 0.75, Precision = 0.01, }; + + public override string IconTooltip => $"{Name}{(SpeedChange.IsDefault ? "" : $" ({SpeedChange.Value}x)")}"; } } From cda1efef0bde4d9ee692cb8d7747aea63b502e58 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Fri, 20 Mar 2020 16:34:36 -0400 Subject: [PATCH 0518/2376] move overridability to SettingDescription method --- .../Mods/CatchModDifficultyAdjust.cs | 20 +++++++++++++++---- .../Mods/OsuModDifficultyAdjust.cs | 20 +++++++++++++++---- osu.Game/Rulesets/Mods/Mod.cs | 18 ++++++++++++++++- osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs | 16 +++++++++++++-- osu.Game/Rulesets/Mods/ModDoubleTime.cs | 2 +- osu.Game/Rulesets/Mods/ModEasy.cs | 2 +- osu.Game/Rulesets/Mods/ModHalfTime.cs | 2 +- osu.Game/Rulesets/Mods/ModTimeRamp.cs | 2 +- 8 files changed, 67 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs index 661c59332f..ee05dd1560 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.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.Game.Beatmaps; using osu.Game.Configuration; @@ -30,10 +31,21 @@ namespace osu.Game.Rulesets.Catch.Mods Value = 5, }; - public override string IconTooltip => ($"{Name} ({(CircleSize.IsDefault ? "" : $"CS {CircleSize.Value}, ")}" + - $"{(DrainRate.IsDefault ? "" : $"HP {DrainRate.Value}, ")}" + - $"{(OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value}, ")}" + - $"{(ApproachRate.IsDefault ? "" : $"AR {ApproachRate.Value}")}").TrimEnd(new char[] { ',', ' ' }) + ")"; + public override string SettingDescription + { + get + { + string circleSize = CircleSize.IsDefault ? "" : $"CS {CircleSize.Value.ToString()}"; + string drainRate = DrainRate.IsDefault ? "" : $"HP {DrainRate.Value.ToString()}"; + string overallDifficulty = OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value.ToString()}"; + string approachRate = ApproachRate.IsDefault ? "" : $"AR {ApproachRate.Value.ToString()}"; + + string[] settings = new string[] { circleSize, drainRate, overallDifficulty, approachRate }; + // filter out empty strings so we don't have orphaned commas + settings = Array.FindAll(settings, s => !string.IsNullOrEmpty(s)); + return string.Join(", ", settings); + } + } protected override void TransferSettings(BeatmapDifficulty difficulty) { diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs index 477028dbbe..520e5a6726 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.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.Game.Beatmaps; using osu.Game.Configuration; @@ -30,10 +31,21 @@ namespace osu.Game.Rulesets.Osu.Mods Value = 5, }; - public override string IconTooltip => ($"{Name} ({(CircleSize.IsDefault ? "" : $"CS {CircleSize.Value}, ")}" + - $"{(DrainRate.IsDefault ? "" : $"HP {DrainRate.Value}, ")}" + - $"{(OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value}, ")}" + - $"{(ApproachRate.IsDefault ? "" : $"AR {ApproachRate.Value}")}").TrimEnd(new char[] { ',', ' ' }) + ")"; + public override string SettingDescription + { + get + { + string circleSize = CircleSize.IsDefault ? "" : $"CS {CircleSize.Value.ToString()}"; + string drainRate = DrainRate.IsDefault ? "" : $"HP {DrainRate.Value.ToString()}"; + string overallDifficulty = OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value.ToString()}"; + string approachRate = ApproachRate.IsDefault ? "" : $"AR {ApproachRate.Value.ToString()}"; + + string[] settings = new string[] { circleSize, drainRate, overallDifficulty, approachRate }; + // filter out empty strings so we don't have orphaned commas + settings = Array.FindAll(settings, s => !string.IsNullOrEmpty(s)); + return string.Join(", ", settings); + } + } protected override void TransferSettings(BeatmapDifficulty difficulty) { diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index b70ddc6d46..231b95f974 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -51,7 +51,23 @@ namespace osu.Game.Rulesets.Mods /// are displayed in the tooltip. /// [JsonIgnore] - public virtual string IconTooltip => Name; + public string IconTooltip + { + get + { + string settingDescription = string.IsNullOrEmpty(SettingDescription) ? "" : $" ({SettingDescription})"; + return $"{Name}{settingDescription}"; + } + } + + /// + /// The description of editable settings of a mod to use in the . + /// + /// + /// Parentheses are added to the tooltip, surrounding the value of this property. If this property is string.Empty, + /// the tooltip will not have parentheses. + /// + public virtual string SettingDescription => string.Empty; /// /// The score multiplier of this mod. diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index 4072e6a6af..a5024d6988 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics.Sprites; using System; using System.Collections.Generic; using osu.Game.Configuration; +using System.Linq; namespace osu.Game.Rulesets.Mods { @@ -52,8 +53,19 @@ namespace osu.Game.Rulesets.Mods Value = 5, }; - public override string IconTooltip => $"{Name} ({(DrainRate.IsDefault ? $"HP {DrainRate.Value.ToString()}, " : "")}" + - $"{(OverallDifficulty.IsDefault ? $"OD {OverallDifficulty.Value.ToString()}, " : "")})"; + public override string SettingDescription + { + get + { + string drainRate = DrainRate.IsDefault ? "" : $"HP {DrainRate.Value.ToString()}"; + string overallDifficulty = OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value.ToString()}"; + + string[] settings = new string[] { drainRate, overallDifficulty }; + // filter out empty strings so we don't have orphaned commas + settings = Array.FindAll(settings, s => !string.IsNullOrEmpty(s)); + return string.Join(", ", settings); + } + } private BeatmapDifficulty difficulty; diff --git a/osu.Game/Rulesets/Mods/ModDoubleTime.cs b/osu.Game/Rulesets/Mods/ModDoubleTime.cs index 4f7d82418d..1730c98c7f 100644 --- a/osu.Game/Rulesets/Mods/ModDoubleTime.cs +++ b/osu.Game/Rulesets/Mods/ModDoubleTime.cs @@ -31,6 +31,6 @@ namespace osu.Game.Rulesets.Mods Precision = 0.01, }; - public override string IconTooltip => $"{Name}{(SpeedChange.IsDefault ? "" : $" ({SpeedChange.Value}x)")}"; + public override string SettingDescription => SpeedChange.IsDefault ? "" : $" ({SpeedChange.Value}x)"; } } diff --git a/osu.Game/Rulesets/Mods/ModEasy.cs b/osu.Game/Rulesets/Mods/ModEasy.cs index 2ec4e9610b..4433a28a95 100644 --- a/osu.Game/Rulesets/Mods/ModEasy.cs +++ b/osu.Game/Rulesets/Mods/ModEasy.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Mods MaxValue = 10 }; - public override string IconTooltip => $"{Name}{(Retries.IsDefault ? "" : $" ({Retries.Value} lives)")}"; + public override string SettingDescription => Retries.IsDefault ? "" : $" ({Retries.Value} lives)"; private int retries; diff --git a/osu.Game/Rulesets/Mods/ModHalfTime.cs b/osu.Game/Rulesets/Mods/ModHalfTime.cs index 14133bddcd..5f99748a87 100644 --- a/osu.Game/Rulesets/Mods/ModHalfTime.cs +++ b/osu.Game/Rulesets/Mods/ModHalfTime.cs @@ -31,6 +31,6 @@ namespace osu.Game.Rulesets.Mods Precision = 0.01, }; - public override string IconTooltip => $"{Name}{(SpeedChange.IsDefault ? "" : $" ({SpeedChange.Value}x)")}"; + public override string SettingDescription => SpeedChange.IsDefault ? "" : $" ({SpeedChange.Value}x)"; } } diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index 9cb97dfc35..7b4c1370ac 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Mods [SettingSource("Final rate", "The final speed to ramp to")] public abstract BindableNumber FinalRate { get; } - public override string IconTooltip => $"{Name} ({InitialRate.Value}x to {FinalRate.Value}x)"; + public override string SettingDescription => $"{InitialRate.Value} to {FinalRate.Value}"; private double finalRateTime; private double beginRampTime; From 6a63ba1bb826235cdca903ccdc855188d4e3d584 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Fri, 20 Mar 2020 16:42:35 -0400 Subject: [PATCH 0519/2376] use humanizer for ModEasy lives setting --- osu.Game/Rulesets/Mods/ModEasy.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/ModEasy.cs b/osu.Game/Rulesets/Mods/ModEasy.cs index 4433a28a95..a1dd6c088d 100644 --- a/osu.Game/Rulesets/Mods/ModEasy.cs +++ b/osu.Game/Rulesets/Mods/ModEasy.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using Humanizer; using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; using osu.Game.Beatmaps; @@ -28,7 +29,7 @@ namespace osu.Game.Rulesets.Mods MaxValue = 10 }; - public override string SettingDescription => Retries.IsDefault ? "" : $" ({Retries.Value} lives)"; + public override string SettingDescription => Retries.IsDefault ? "" : $"{"lives".ToQuantity(Retries.Value)}"; private int retries; From 55568ee6a5cae8ce8494eae518f8fc14562f64bf Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Fri, 20 Mar 2020 16:44:38 -0400 Subject: [PATCH 0520/2376] remove extra parentheses --- osu.Game/Rulesets/Mods/ModDoubleTime.cs | 2 +- osu.Game/Rulesets/Mods/ModHalfTime.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModDoubleTime.cs b/osu.Game/Rulesets/Mods/ModDoubleTime.cs index 1730c98c7f..05a8dbfa56 100644 --- a/osu.Game/Rulesets/Mods/ModDoubleTime.cs +++ b/osu.Game/Rulesets/Mods/ModDoubleTime.cs @@ -31,6 +31,6 @@ namespace osu.Game.Rulesets.Mods Precision = 0.01, }; - public override string SettingDescription => SpeedChange.IsDefault ? "" : $" ({SpeedChange.Value}x)"; + public override string SettingDescription => SpeedChange.IsDefault ? "" : $"{SpeedChange.Value}x"; } } diff --git a/osu.Game/Rulesets/Mods/ModHalfTime.cs b/osu.Game/Rulesets/Mods/ModHalfTime.cs index 5f99748a87..5252ce8d89 100644 --- a/osu.Game/Rulesets/Mods/ModHalfTime.cs +++ b/osu.Game/Rulesets/Mods/ModHalfTime.cs @@ -31,6 +31,6 @@ namespace osu.Game.Rulesets.Mods Precision = 0.01, }; - public override string SettingDescription => SpeedChange.IsDefault ? "" : $" ({SpeedChange.Value}x)"; + public override string SettingDescription => SpeedChange.IsDefault ? "" : $"{SpeedChange.Value}x"; } } From e84b40f8ed86507a16a162d5b1ce6ee1e66821ff Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Fri, 20 Mar 2020 16:53:40 -0400 Subject: [PATCH 0521/2376] remove unnecessary ToString calls --- osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs | 8 ++++---- osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs index ee05dd1560..3ebe8c6f6f 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs @@ -35,10 +35,10 @@ namespace osu.Game.Rulesets.Catch.Mods { get { - string circleSize = CircleSize.IsDefault ? "" : $"CS {CircleSize.Value.ToString()}"; - string drainRate = DrainRate.IsDefault ? "" : $"HP {DrainRate.Value.ToString()}"; - string overallDifficulty = OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value.ToString()}"; - string approachRate = ApproachRate.IsDefault ? "" : $"AR {ApproachRate.Value.ToString()}"; + string circleSize = CircleSize.IsDefault ? "" : $"CS {CircleSize.Value}"; + string drainRate = DrainRate.IsDefault ? "" : $"HP {DrainRate.Value}"; + string overallDifficulty = OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value}"; + string approachRate = ApproachRate.IsDefault ? "" : $"AR {ApproachRate.Value}"; string[] settings = new string[] { circleSize, drainRate, overallDifficulty, approachRate }; // filter out empty strings so we don't have orphaned commas diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs index 520e5a6726..d63239755b 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs @@ -35,10 +35,10 @@ namespace osu.Game.Rulesets.Osu.Mods { get { - string circleSize = CircleSize.IsDefault ? "" : $"CS {CircleSize.Value.ToString()}"; - string drainRate = DrainRate.IsDefault ? "" : $"HP {DrainRate.Value.ToString()}"; - string overallDifficulty = OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value.ToString()}"; - string approachRate = ApproachRate.IsDefault ? "" : $"AR {ApproachRate.Value.ToString()}"; + string circleSize = CircleSize.IsDefault ? "" : $"CS {CircleSize.Value}"; + string drainRate = DrainRate.IsDefault ? "" : $"HP {DrainRate.Value}"; + string overallDifficulty = OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value}"; + string approachRate = ApproachRate.IsDefault ? "" : $"AR {ApproachRate.Value}"; string[] settings = new string[] { circleSize, drainRate, overallDifficulty, approachRate }; // filter out empty strings so we don't have orphaned commas From ac202ba7ea963d43b2e5a7054dbfd285edf2a48b Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Fri, 20 Mar 2020 16:57:37 -0400 Subject: [PATCH 0522/2376] remove unused using directive --- osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index a5024d6988..f50c2cf001 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -7,7 +7,6 @@ using osu.Framework.Graphics.Sprites; using System; using System.Collections.Generic; using osu.Game.Configuration; -using System.Linq; namespace osu.Game.Rulesets.Mods { @@ -60,7 +59,7 @@ namespace osu.Game.Rulesets.Mods string drainRate = DrainRate.IsDefault ? "" : $"HP {DrainRate.Value.ToString()}"; string overallDifficulty = OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value.ToString()}"; - string[] settings = new string[] { drainRate, overallDifficulty }; + string[] settings = { drainRate, overallDifficulty }; // filter out empty strings so we don't have orphaned commas settings = Array.FindAll(settings, s => !string.IsNullOrEmpty(s)); return string.Join(", ", settings); From a440d156205ea6503cfed552bb782dad9e6cba41 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Fri, 20 Mar 2020 16:58:02 -0400 Subject: [PATCH 0523/2376] simplify array initializationstatement --- osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs index 3ebe8c6f6f..6d0025b236 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Catch.Mods string overallDifficulty = OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value}"; string approachRate = ApproachRate.IsDefault ? "" : $"AR {ApproachRate.Value}"; - string[] settings = new string[] { circleSize, drainRate, overallDifficulty, approachRate }; + string[] settings = { circleSize, drainRate, overallDifficulty, approachRate }; // filter out empty strings so we don't have orphaned commas settings = Array.FindAll(settings, s => !string.IsNullOrEmpty(s)); return string.Join(", ", settings); diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs index d63239755b..307fe3da4a 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Mods string overallDifficulty = OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value}"; string approachRate = ApproachRate.IsDefault ? "" : $"AR {ApproachRate.Value}"; - string[] settings = new string[] { circleSize, drainRate, overallDifficulty, approachRate }; + string[] settings = { circleSize, drainRate, overallDifficulty, approachRate }; // filter out empty strings so we don't have orphaned commas settings = Array.FindAll(settings, s => !string.IsNullOrEmpty(s)); return string.Join(", ", settings); From eab705a9b690e9bdac340edae616895aef3465f1 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Fri, 20 Mar 2020 17:00:36 -0400 Subject: [PATCH 0524/2376] remove another ToString statement --- osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index f50c2cf001..4a8313b1f1 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Mods get { string drainRate = DrainRate.IsDefault ? "" : $"HP {DrainRate.Value.ToString()}"; - string overallDifficulty = OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value.ToString()}"; + string overallDifficulty = OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value}"; string[] settings = { drainRate, overallDifficulty }; // filter out empty strings so we don't have orphaned commas From 4907fb8fd1e966c1940936b74712c9bf672b3184 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Fri, 20 Mar 2020 17:04:22 -0400 Subject: [PATCH 0525/2376] remove another ToString statement --- osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index 4a8313b1f1..f8341e6cdb 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Mods { get { - string drainRate = DrainRate.IsDefault ? "" : $"HP {DrainRate.Value.ToString()}"; + string drainRate = DrainRate.IsDefault ? "" : $"HP {DrainRate.Value}"; string overallDifficulty = OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value}"; string[] settings = { drainRate, overallDifficulty }; From 29009c85c01ad1729d54f25137e8ae6eb18b6d5b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 22 Mar 2020 00:32:53 +0900 Subject: [PATCH 0526/2376] Fix typo in comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Bartłomiej Dach --- osu.Game/Screens/Select/BeatmapCarousel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 6c6f9a0e79..739f99d72d 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -751,7 +751,7 @@ namespace osu.Game.Screens.Select public CarouselRoot(BeatmapCarousel carousel) { - // root should always remaing selected. if not, PerformSelection will not be called. + // root should always remain selected. if not, PerformSelection will not be called. State.Value = CarouselItemState.Selected; State.ValueChanged += state => State.Value = CarouselItemState.Selected; From d8041a0dcbfc5929c3244b686c959fa78a25857a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 22 Mar 2020 02:16:28 +0900 Subject: [PATCH 0527/2376] Increase sample concurrency to better match stable --- osu.Game/OsuGameBase.cs | 4 ++++ osu.Game/Rulesets/UI/DrawableRuleset.cs | 1 + osu.Game/Skinning/LegacySkin.cs | 6 +++++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 3c7ab27651..5487bd9320 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -47,6 +47,8 @@ namespace osu.Game { public const string CLIENT_STREAM_NAME = "lazer"; + public const int SAMPLE_CONCURRENCY = 6; + protected OsuConfigManager LocalConfig; protected BeatmapManager BeatmapManager; @@ -153,6 +155,8 @@ namespace osu.Game AddFont(Resources, @"Fonts/Venera-Bold"); AddFont(Resources, @"Fonts/Venera-Black"); + Audio.Samples.PlaybackConcurrency = SAMPLE_CONCURRENCY; + runMigrations(); dependencies.Cache(SkinManager = new SkinManager(Storage, contextFactory, Host, Audio, new NamespacedResourceStore(Resources, "Skins/Legacy"))); diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index e624fb80fa..d0a2722f58 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -158,6 +158,7 @@ namespace osu.Game.Rulesets.UI dependencies.Cache(textureStore); localSampleStore = dependencies.Get().GetSampleStore(new NamespacedResourceStore(resources, "Samples")); + localSampleStore.PlaybackConcurrency = OsuGameBase.SAMPLE_CONCURRENCY; dependencies.CacheAs(new FallbackSampleStore(localSampleStore, dependencies.Get())); } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 29bcd2e210..c71a321e74 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -52,7 +52,11 @@ namespace osu.Game.Skinning if (storage != null) { - Samples = audioManager?.GetSampleStore(storage); + var samples = audioManager?.GetSampleStore(storage); + if (samples != null) + samples.PlaybackConcurrency = OsuGameBase.SAMPLE_CONCURRENCY; + + Samples = samples; Textures = new TextureStore(new TextureLoaderStore(storage)); (storage as ResourceStore)?.AddExtension("ogg"); From d241f7c55fece40263c582e0dd6f89ceb3a60d2e Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 21 Mar 2020 20:32:55 +0300 Subject: [PATCH 0528/2376] Better variable naming --- osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs index 55a5081435..c2f0917e29 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs @@ -171,9 +171,9 @@ namespace osu.Game.Overlays.Dashboard.Friends if (itemsPlaceholder.Any()) loading.Show(); - var groupedUsers = onlineStatusControl.Current.Value?.Users ?? new List(); + var usersInCurrentGroup = onlineStatusControl.Current.Value?.Users ?? new List(); - var sortedUsers = sortUsers(groupedUsers); + var sortedUsers = sortUsers(usersInCurrentGroup); LoadComponentAsync(createTable(sortedUsers), addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token); } From 2b0c267cb902a8c9667a7e6afba57fb351fb1deb Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 21 Mar 2020 20:37:21 +0300 Subject: [PATCH 0529/2376] Expose Fetch method --- osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs | 5 +---- osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs | 5 +++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs b/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs index e6b2a41c1c..1d8238dd40 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs @@ -10,7 +10,6 @@ using osu.Game.Users; using osu.Game.Overlays; using osu.Framework.Allocation; using NUnit.Framework; -using osu.Game.Online.API; namespace osu.Game.Tests.Visual.Online { @@ -28,9 +27,6 @@ namespace osu.Game.Tests.Visual.Online [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); - [Resolved] - private IAPIProvider api { get; set; } - private FriendsLayout layout; [SetUp] @@ -52,6 +48,7 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestOnline() { + AddStep("Fetch online", () => layout?.Fetch()); } private List getUsers() => new List diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs index c2f0917e29..94c8230d8e 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs @@ -25,8 +25,6 @@ namespace osu.Game.Overlays.Dashboard.Friends get => users; set { - request?.Cancel(); - users = value; onlineStatusControl.Populate(value); @@ -152,7 +150,10 @@ namespace osu.Game.Overlays.Dashboard.Friends onlineStatusControl.Current.BindValueChanged(_ => recreatePanels()); userListToolbar.DisplayStyle.BindValueChanged(_ => recreatePanels()); userListToolbar.SortCriteria.BindValueChanged(_ => recreatePanels()); + } + public void Fetch() + { if (!api.IsLoggedIn) return; From 19b6e496efbc0dddc8abb4af66a002e98a130512 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 22 Mar 2020 04:17:04 +0900 Subject: [PATCH 0530/2376] Fix (very) long spinners degrading in performance due to high transform count --- osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs index e3dd2b1b4f..3de30d51d9 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs @@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces .FadeTo(tracking_alpha, 250, Easing.OutQuint); } - this.RotateTo(currentRotation / 2, validAndTracking ? 500 : 1500, Easing.OutExpo); + Rotation = (float)Interpolation.Lerp(Rotation, currentRotation / 2, Math.Clamp(Math.Abs(Time.Elapsed) / 40, 0, 1)); } } } From 9482fc5b9990af7a53611539332e81cc1d5d79d5 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 22 Mar 2020 20:13:54 +0300 Subject: [PATCH 0531/2376] Refactor grouping logic --- .../Dashboard/Friends/FriendsBundle.cs | 11 +++------- .../Dashboard/Friends/FriendsLayout.cs | 20 ++++++++++++++++--- .../Friends/FriendsOnlineStatusControl.cs | 9 ++++++--- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsBundle.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsBundle.cs index 772d9c67a0..d5fad1ffd3 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendsBundle.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsBundle.cs @@ -1,23 +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.Users; - namespace osu.Game.Overlays.Dashboard.Friends { public class FriendsBundle { public FriendsOnlineStatus Status { get; } - public int Count => Users.Count; + public int Count { get; } - public List Users { get; } - - public FriendsBundle(FriendsOnlineStatus status, List users) + public FriendsBundle(FriendsOnlineStatus status, int count) { Status = status; - Users = users; + Count = count; } } diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs index 94c8230d8e..3514bf7ff7 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs @@ -172,13 +172,27 @@ namespace osu.Game.Overlays.Dashboard.Friends if (itemsPlaceholder.Any()) loading.Show(); - var usersInCurrentGroup = onlineStatusControl.Current.Value?.Users ?? new List(); - - var sortedUsers = sortUsers(usersInCurrentGroup); + var sortedUsers = sortUsers(getUsersInCurrentGroup()); LoadComponentAsync(createTable(sortedUsers), addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token); } + private List getUsersInCurrentGroup() + { + switch (onlineStatusControl.Current.Value?.Status) + { + default: + case FriendsOnlineStatus.All: + return users; + + case FriendsOnlineStatus.Offline: + return users.Where(u => !u.IsOnline).ToList(); + + case FriendsOnlineStatus.Online: + return users.Where(u => u.IsOnline).ToList(); + } + } + private void addContentToPlaceholder(Drawable content) { loading.Hide(); diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusControl.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusControl.cs index 88035e0a34..c54e9e2a06 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusControl.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusControl.cs @@ -15,9 +15,12 @@ namespace osu.Game.Overlays.Dashboard.Friends { Clear(); - AddItem(new FriendsBundle(FriendsOnlineStatus.All, users)); - AddItem(new FriendsBundle(FriendsOnlineStatus.Online, users.Where(u => u.IsOnline).ToList())); - AddItem(new FriendsBundle(FriendsOnlineStatus.Offline, users.Where(u => !u.IsOnline).ToList())); + var userCount = users.Count; + var onlineUsersCount = users.Count(user => user.IsOnline); + + AddItem(new FriendsBundle(FriendsOnlineStatus.All, userCount)); + AddItem(new FriendsBundle(FriendsOnlineStatus.Online, onlineUsersCount)); + AddItem(new FriendsBundle(FriendsOnlineStatus.Offline, userCount - onlineUsersCount)); Current.Value = Items.FirstOrDefault(); } From 97076325c46f8f734da5fa50cceaab7ea3747cf0 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 23 Mar 2020 01:45:13 +0300 Subject: [PATCH 0532/2376] Fix test scenes using framework-testing-specifc test scene --- .../TestSceneHitCircleArea.cs | 35 ++++++++----------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs index 67b6dac787..394d959d0a 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs @@ -4,13 +4,13 @@ using System; using NUnit.Framework; using osu.Framework.Graphics; -using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; +using osu.Game.Tests.Visual; using osuTK; namespace osu.Game.Rulesets.Osu.Tests @@ -22,30 +22,25 @@ namespace osu.Game.Rulesets.Osu.Tests private DrawableHitCircle.HitReceptor hitAreaReceptor => drawableHitCircle.HitArea; [SetUp] - public new void SetUp() + public void SetUp() => Schedule(() => { - base.SetUp(); - - Schedule(() => + hitCircle = new HitCircle { - hitCircle = new HitCircle - { - Position = new Vector2(100, 100), - StartTime = Time.Current + 500 - }; + Position = new Vector2(100, 100), + StartTime = Time.Current + 500 + }; - hitCircle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + hitCircle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - Child = new SkinProvidingContainer(new DefaultSkin()) + Child = new SkinProvidingContainer(new DefaultSkin()) + { + RelativeSizeAxes = Axes.Both, + Child = drawableHitCircle = new DrawableHitCircle(hitCircle) { - RelativeSizeAxes = Axes.Both, - Child = drawableHitCircle = new DrawableHitCircle(hitCircle) - { - Size = new Vector2(100) - } - }; - }); - } + Size = new Vector2(100) + } + }; + }); [Test] public void TestCircleHitCentre() From 63e9b2a299cf6496ec535315a4787ad9327cb044 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sun, 22 Mar 2020 18:49:45 -0400 Subject: [PATCH 0533/2376] use string.Empty, use base SettingDescription for [Osu/Catch]ModDifficultyAdjust --- .../Mods/CatchModDifficultyAdjust.cs | 17 ++++++------ .../Mods/OsuModDifficultyAdjust.cs | 17 ++++++------ osu.Game/Rulesets/Mods/Mod.cs | 26 ++++++++++++++++++- osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs | 14 +++++----- osu.Game/Rulesets/Mods/ModDoubleTime.cs | 2 +- osu.Game/Rulesets/Mods/ModEasy.cs | 2 +- osu.Game/Rulesets/Mods/ModHalfTime.cs | 2 +- 7 files changed, 54 insertions(+), 26 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs index 6d0025b236..a60b35739e 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using osu.Framework.Bindables; using osu.Game.Beatmaps; using osu.Game.Configuration; @@ -35,15 +36,15 @@ namespace osu.Game.Rulesets.Catch.Mods { get { - string circleSize = CircleSize.IsDefault ? "" : $"CS {CircleSize.Value}"; - string drainRate = DrainRate.IsDefault ? "" : $"HP {DrainRate.Value}"; - string overallDifficulty = OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value}"; - string approachRate = ApproachRate.IsDefault ? "" : $"AR {ApproachRate.Value}"; + string circleSize = CircleSize.IsDefault ? string.Empty : $"CS {CircleSize.Value}"; + string approachRate = ApproachRate.IsDefault ? string.Empty : $"AR {ApproachRate.Value}"; - string[] settings = { circleSize, drainRate, overallDifficulty, approachRate }; - // filter out empty strings so we don't have orphaned commas - settings = Array.FindAll(settings, s => !string.IsNullOrEmpty(s)); - return string.Join(", ", settings); + return string.Join(", ", new[] + { + circleSize, + base.SettingDescription, + approachRate + }.Where(s => !string.IsNullOrEmpty(s))); } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs index 307fe3da4a..18492828f0 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using osu.Framework.Bindables; using osu.Game.Beatmaps; using osu.Game.Configuration; @@ -35,15 +36,15 @@ namespace osu.Game.Rulesets.Osu.Mods { get { - string circleSize = CircleSize.IsDefault ? "" : $"CS {CircleSize.Value}"; - string drainRate = DrainRate.IsDefault ? "" : $"HP {DrainRate.Value}"; - string overallDifficulty = OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value}"; - string approachRate = ApproachRate.IsDefault ? "" : $"AR {ApproachRate.Value}"; + string circleSize = CircleSize.IsDefault ? string.Empty : $"CS {CircleSize.Value}"; + string approachRate = ApproachRate.IsDefault ? string.Empty : $"AR {ApproachRate.Value}"; - string[] settings = { circleSize, drainRate, overallDifficulty, approachRate }; - // filter out empty strings so we don't have orphaned commas - settings = Array.FindAll(settings, s => !string.IsNullOrEmpty(s)); - return string.Join(", ", settings); + return string.Join(", ", new[] + { + circleSize, + base.SettingDescription, + approachRate + }.Where(s => !string.IsNullOrEmpty(s))); } } diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 231b95f974..95e8ff86eb 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -2,8 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; using Newtonsoft.Json; +using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; +using osu.Game.Configuration; using osu.Game.IO.Serialization; using osu.Game.Rulesets.UI; @@ -67,7 +72,26 @@ namespace osu.Game.Rulesets.Mods /// Parentheses are added to the tooltip, surrounding the value of this property. If this property is string.Empty, /// the tooltip will not have parentheses. /// - public virtual string SettingDescription => string.Empty; + public virtual string SettingDescription + { + get + { + var tooltipTexts = new List(); + + foreach ((SettingSourceAttribute attr, PropertyInfo property) in this.GetOrderedSettingsSourceProperties()) + { + object bindableObj = property.GetValue(this); + bool? settingIsDefault = (bindableObj as IHasDefaultValue)?.IsDefault; + string tooltipText = settingIsDefault == true ? string.Empty : attr.Label + " " + bindableObj.ToString(); + tooltipTexts.Add(tooltipText); + } + + // filter out empty strings so we don't have orphaned commas + //tooltipTexts = tooltipTexts.Where(s => !string.IsNullOrEmpty(s)).ToList(); + string joinedTooltipText = string.Join(", ", tooltipTexts); + return $"{Name}{joinedTooltipText}"; + } + } /// /// The score multiplier of this mod. diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index f8341e6cdb..1baf9f7057 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics.Sprites; using System; using System.Collections.Generic; using osu.Game.Configuration; +using System.Linq; namespace osu.Game.Rulesets.Mods { @@ -56,13 +57,14 @@ namespace osu.Game.Rulesets.Mods { get { - string drainRate = DrainRate.IsDefault ? "" : $"HP {DrainRate.Value}"; - string overallDifficulty = OverallDifficulty.IsDefault ? "" : $"OD {OverallDifficulty.Value}"; + string drainRate = DrainRate.IsDefault ? string.Empty : $"HP {DrainRate.Value}"; + string overallDifficulty = OverallDifficulty.IsDefault ? string.Empty : $"OD {OverallDifficulty.Value}"; - string[] settings = { drainRate, overallDifficulty }; - // filter out empty strings so we don't have orphaned commas - settings = Array.FindAll(settings, s => !string.IsNullOrEmpty(s)); - return string.Join(", ", settings); + return string.Join(", ", new[] + { + drainRate, + overallDifficulty + }.Where(s => !string.IsNullOrEmpty(s))); } } diff --git a/osu.Game/Rulesets/Mods/ModDoubleTime.cs b/osu.Game/Rulesets/Mods/ModDoubleTime.cs index 05a8dbfa56..7d86190134 100644 --- a/osu.Game/Rulesets/Mods/ModDoubleTime.cs +++ b/osu.Game/Rulesets/Mods/ModDoubleTime.cs @@ -31,6 +31,6 @@ namespace osu.Game.Rulesets.Mods Precision = 0.01, }; - public override string SettingDescription => SpeedChange.IsDefault ? "" : $"{SpeedChange.Value}x"; + public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value}x"; } } diff --git a/osu.Game/Rulesets/Mods/ModEasy.cs b/osu.Game/Rulesets/Mods/ModEasy.cs index a1dd6c088d..c1c4124b98 100644 --- a/osu.Game/Rulesets/Mods/ModEasy.cs +++ b/osu.Game/Rulesets/Mods/ModEasy.cs @@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Mods MaxValue = 10 }; - public override string SettingDescription => Retries.IsDefault ? "" : $"{"lives".ToQuantity(Retries.Value)}"; + public override string SettingDescription => Retries.IsDefault ? string.Empty : $"{"lives".ToQuantity(Retries.Value)}"; private int retries; diff --git a/osu.Game/Rulesets/Mods/ModHalfTime.cs b/osu.Game/Rulesets/Mods/ModHalfTime.cs index 5252ce8d89..ec215369a3 100644 --- a/osu.Game/Rulesets/Mods/ModHalfTime.cs +++ b/osu.Game/Rulesets/Mods/ModHalfTime.cs @@ -31,6 +31,6 @@ namespace osu.Game.Rulesets.Mods Precision = 0.01, }; - public override string SettingDescription => SpeedChange.IsDefault ? "" : $"{SpeedChange.Value}x"; + public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value}x"; } } From 0b728f483fb1d2934a629387fe936ff9fba84d8f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 23 Mar 2020 10:01:33 +0900 Subject: [PATCH 0534/2376] Rename base test class to help avoid incorrect reference --- osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs | 2 +- osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs | 2 +- osu.Game.Rulesets.Osu.Tests/TestSceneResumeOverlay.cs | 2 +- .../Visual/Background/TestSceneUserDimBackgrounds.cs | 2 +- osu.Game.Tests/Visual/Components/TestSceneIdleTracker.cs | 2 +- osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs | 2 +- .../Visual/Editor/TestSceneZoomableScrollContainer.cs | 2 +- .../Visual/Gameplay/TestSceneGameplayMenuOverlay.cs | 2 +- osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs | 2 +- osu.Game.Tests/Visual/Gameplay/TestSceneHoldForMenuButton.cs | 2 +- osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs | 2 +- osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs | 2 +- osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs | 2 +- .../Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs | 2 +- osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs | 2 +- osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs | 2 +- osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs | 2 +- osu.Game.Tests/Visual/UserInterface/TestSceneCursors.cs | 2 +- .../Visual/UserInterface/TestSceneDeleteLocalScore.cs | 2 +- .../Visual/UserInterface/TestSceneOsuHoverContainer.cs | 2 +- .../Visual/UserInterface/TestSceneStatefulMenuItem.cs | 2 +- osu.Game.Tests/Visual/UserInterface/TestSceneSwitchButton.cs | 2 +- ...tManagerTestScene.cs => OsuManualInputManagerTestScene.cs} | 4 ++-- osu.Game/Tests/Visual/ScreenTestScene.cs | 2 +- osu.Game/Tests/Visual/SelectionBlueprintTestScene.cs | 2 +- 25 files changed, 26 insertions(+), 26 deletions(-) rename osu.Game/Tests/Visual/{ManualInputManagerTestScene.cs => OsuManualInputManagerTestScene.cs} (97%) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs index 394d959d0a..0649989dc0 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs @@ -15,7 +15,7 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Tests { - public class TestSceneHitCircleArea : ManualInputManagerTestScene + public class TestSceneHitCircleArea : OsuManualInputManagerTestScene { private HitCircle hitCircle; private DrawableHitCircle drawableHitCircle; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs index 4af4d5f966..0ae49790cd 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs @@ -23,7 +23,7 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Tests { - public class TestSceneOsuDistanceSnapGrid : ManualInputManagerTestScene + public class TestSceneOsuDistanceSnapGrid : OsuManualInputManagerTestScene { private const double beat_length = 100; private static readonly Vector2 grid_position = new Vector2(512, 384); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneResumeOverlay.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneResumeOverlay.cs index 8e73d6152f..f4809b0c9b 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneResumeOverlay.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneResumeOverlay.cs @@ -12,7 +12,7 @@ using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Osu.Tests { - public class TestSceneResumeOverlay : ManualInputManagerTestScene + public class TestSceneResumeOverlay : OsuManualInputManagerTestScene { public override IReadOnlyList RequiredTypes => new[] { diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs index b51555db3e..c906f21e22 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs @@ -37,7 +37,7 @@ using osuTK.Graphics; namespace osu.Game.Tests.Visual.Background { [TestFixture] - public class TestSceneUserDimBackgrounds : ManualInputManagerTestScene + public class TestSceneUserDimBackgrounds : OsuManualInputManagerTestScene { public override IReadOnlyList RequiredTypes => new[] { diff --git a/osu.Game.Tests/Visual/Components/TestSceneIdleTracker.cs b/osu.Game.Tests/Visual/Components/TestSceneIdleTracker.cs index 55aaeed8bf..4d64c7d35d 100644 --- a/osu.Game.Tests/Visual/Components/TestSceneIdleTracker.cs +++ b/osu.Game.Tests/Visual/Components/TestSceneIdleTracker.cs @@ -12,7 +12,7 @@ using osuTK.Graphics; namespace osu.Game.Tests.Visual.Components { [TestFixture] - public class TestSceneIdleTracker : ManualInputManagerTestScene + public class TestSceneIdleTracker : OsuManualInputManagerTestScene { private IdleTrackingBox box1; private IdleTrackingBox box2; diff --git a/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs b/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs index 746b2c99aa..fd7a5980f3 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs +++ b/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs @@ -16,7 +16,7 @@ using osuTK.Input; namespace osu.Game.Tests.Visual.Editor { - public class TestSceneBeatDivisorControl : ManualInputManagerTestScene + public class TestSceneBeatDivisorControl : OsuManualInputManagerTestScene { public override IReadOnlyList RequiredTypes => new[] { typeof(BindableBeatDivisor) }; private BeatDivisorControl beatDivisorControl; diff --git a/osu.Game.Tests/Visual/Editor/TestSceneZoomableScrollContainer.cs b/osu.Game.Tests/Visual/Editor/TestSceneZoomableScrollContainer.cs index fd248abbc9..19d19c2759 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneZoomableScrollContainer.cs +++ b/osu.Game.Tests/Visual/Editor/TestSceneZoomableScrollContainer.cs @@ -17,7 +17,7 @@ using osuTK.Graphics; namespace osu.Game.Tests.Visual.Editor { - public class TestSceneZoomableScrollContainer : ManualInputManagerTestScene + public class TestSceneZoomableScrollContainer : OsuManualInputManagerTestScene { private ZoomableScrollContainer scrollContainer; private Drawable innerBox; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayMenuOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayMenuOverlay.cs index c1635ffc83..ea3e0c2293 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayMenuOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayMenuOverlay.cs @@ -18,7 +18,7 @@ using osuTK.Input; namespace osu.Game.Tests.Visual.Gameplay { [Description("player pause/fail screens")] - public class TestSceneGameplayMenuOverlay : ManualInputManagerTestScene + public class TestSceneGameplayMenuOverlay : OsuManualInputManagerTestScene { public override IReadOnlyList RequiredTypes => new[] { typeof(FailOverlay), typeof(PauseOverlay) }; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs index fc03dc6ed3..c192a7b0e0 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs @@ -15,7 +15,7 @@ using osuTK.Input; namespace osu.Game.Tests.Visual.Gameplay { - public class TestSceneHUDOverlay : ManualInputManagerTestScene + public class TestSceneHUDOverlay : OsuManualInputManagerTestScene { private HUDOverlay hudOverlay; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHoldForMenuButton.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHoldForMenuButton.cs index 0c5ead10cf..235842acc9 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHoldForMenuButton.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHoldForMenuButton.cs @@ -13,7 +13,7 @@ using osuTK.Input; namespace osu.Game.Tests.Visual.Gameplay { [Description("'Hold to Quit' UI element")] - public class TestSceneHoldForMenuButton : ManualInputManagerTestScene + public class TestSceneHoldForMenuButton : OsuManualInputManagerTestScene { private bool exitAction; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs index 227ada70fe..593dcd245c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs @@ -13,7 +13,7 @@ using osuTK.Input; namespace osu.Game.Tests.Visual.Gameplay { [TestFixture] - public class TestSceneKeyCounter : ManualInputManagerTestScene + public class TestSceneKeyCounter : OsuManualInputManagerTestScene { public override IReadOnlyList RequiredTypes => new[] { diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index 175f909a5a..4c73065087 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -29,7 +29,7 @@ using osuTK.Input; namespace osu.Game.Tests.Visual.Gameplay { - public class TestScenePlayerLoader : ManualInputManagerTestScene + public class TestScenePlayerLoader : OsuManualInputManagerTestScene { private TestPlayerLoader loader; private TestPlayerLoaderContainer container; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs index 4c5c18f38a..6a0f86fe53 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs @@ -14,7 +14,7 @@ using osuTK.Input; namespace osu.Game.Tests.Visual.Gameplay { [TestFixture] - public class TestSceneSkipOverlay : ManualInputManagerTestScene + public class TestSceneSkipOverlay : OsuManualInputManagerTestScene { private SkipOverlay skip; private int requestCount; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index 9fbe8f7ffe..713ba13439 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -20,7 +20,7 @@ using osuTK.Input; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneDrawableRoomPlaylist : ManualInputManagerTestScene + public class TestSceneDrawableRoomPlaylist : OsuManualInputManagerTestScene { public override IReadOnlyList RequiredTypes => new[] { diff --git a/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs b/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs index 0d64eb651f..31afce86ae 100644 --- a/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs +++ b/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs @@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.Navigation /// /// A scene which tests full game flow. /// - public abstract class OsuGameTestScene : ManualInputManagerTestScene + public abstract class OsuGameTestScene : OsuManualInputManagerTestScene { private GameHost host; diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index 6665452d94..14924dda21 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -22,7 +22,7 @@ using osuTK.Input; namespace osu.Game.Tests.Visual.Online { - public class TestSceneChatOverlay : ManualInputManagerTestScene + public class TestSceneChatOverlay : OsuManualInputManagerTestScene { public override IReadOnlyList RequiredTypes => new[] { diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs index 7b0b644dab..cef04a4c18 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs @@ -15,7 +15,7 @@ using osuTK.Input; namespace osu.Game.Tests.Visual.UserInterface { - public class TestSceneCommentEditor : ManualInputManagerTestScene + public class TestSceneCommentEditor : OsuManualInputManagerTestScene { public override IReadOnlyList RequiredTypes => new[] { diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneCursors.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneCursors.cs index d1dde4664a..5b74852259 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneCursors.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneCursors.cs @@ -17,7 +17,7 @@ using osuTK.Graphics; namespace osu.Game.Tests.Visual.UserInterface { [TestFixture] - public class TestSceneCursors : ManualInputManagerTestScene + public class TestSceneCursors : OsuManualInputManagerTestScene { private readonly MenuCursorContainer menuCursorContainer; private readonly CustomCursorBox[] cursorBoxes = new CustomCursorBox[6]; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index 1e5e26e4c5..a812b4dc79 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -27,7 +27,7 @@ using osuTK.Input; namespace osu.Game.Tests.Visual.UserInterface { - public class TestSceneDeleteLocalScore : ManualInputManagerTestScene + public class TestSceneDeleteLocalScore : OsuManualInputManagerTestScene { public override IReadOnlyList RequiredTypes => new[] { diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuHoverContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuHoverContainer.cs index dbef7d1686..396bec51b6 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuHoverContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuHoverContainer.cs @@ -12,7 +12,7 @@ using osuTK.Graphics; namespace osu.Game.Tests.Visual.UserInterface { [TestFixture] - public class TestSceneOsuHoverContainer : ManualInputManagerTestScene + public class TestSceneOsuHoverContainer : OsuManualInputManagerTestScene { private OsuHoverTestContainer hoverContainer; private Box colourContainer; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneStatefulMenuItem.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneStatefulMenuItem.cs index 2ada5b927b..85fea73bf5 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneStatefulMenuItem.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneStatefulMenuItem.cs @@ -12,7 +12,7 @@ using osuTK.Input; namespace osu.Game.Tests.Visual.UserInterface { - public class TestSceneStatefulMenuItem : ManualInputManagerTestScene + public class TestSceneStatefulMenuItem : OsuManualInputManagerTestScene { public override IReadOnlyList RequiredTypes => new[] { diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneSwitchButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneSwitchButton.cs index 4a104b4a41..37fab75aee 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneSwitchButton.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneSwitchButton.cs @@ -9,7 +9,7 @@ using osuTK.Input; namespace osu.Game.Tests.Visual.UserInterface { - public class TestSceneSwitchButton : ManualInputManagerTestScene + public class TestSceneSwitchButton : OsuManualInputManagerTestScene { private SwitchButton switchButton; diff --git a/osu.Game/Tests/Visual/ManualInputManagerTestScene.cs b/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs similarity index 97% rename from osu.Game/Tests/Visual/ManualInputManagerTestScene.cs rename to osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs index a0af07013c..0da3ae7f87 100644 --- a/osu.Game/Tests/Visual/ManualInputManagerTestScene.cs +++ b/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs @@ -14,7 +14,7 @@ using osuTK.Graphics; namespace osu.Game.Tests.Visual { - public abstract class ManualInputManagerTestScene : OsuTestScene + public abstract class OsuManualInputManagerTestScene : OsuTestScene { protected override Container Content => content; private readonly Container content; @@ -24,7 +24,7 @@ namespace osu.Game.Tests.Visual private readonly TriangleButton buttonTest; private readonly TriangleButton buttonLocal; - protected ManualInputManagerTestScene() + protected OsuManualInputManagerTestScene() { base.Content.AddRange(new Drawable[] { diff --git a/osu.Game/Tests/Visual/ScreenTestScene.cs b/osu.Game/Tests/Visual/ScreenTestScene.cs index d26aacf2bc..33cc00e748 100644 --- a/osu.Game/Tests/Visual/ScreenTestScene.cs +++ b/osu.Game/Tests/Visual/ScreenTestScene.cs @@ -11,7 +11,7 @@ namespace osu.Game.Tests.Visual /// /// A test case which can be used to test a screen (that relies on OnEntering being called to execute startup instructions). /// - public abstract class ScreenTestScene : ManualInputManagerTestScene + public abstract class ScreenTestScene : OsuManualInputManagerTestScene { protected readonly OsuScreenStack Stack; diff --git a/osu.Game/Tests/Visual/SelectionBlueprintTestScene.cs b/osu.Game/Tests/Visual/SelectionBlueprintTestScene.cs index 6565f98666..1176361679 100644 --- a/osu.Game/Tests/Visual/SelectionBlueprintTestScene.cs +++ b/osu.Game/Tests/Visual/SelectionBlueprintTestScene.cs @@ -8,7 +8,7 @@ using osu.Game.Rulesets.Edit; namespace osu.Game.Tests.Visual { - public abstract class SelectionBlueprintTestScene : ManualInputManagerTestScene + public abstract class SelectionBlueprintTestScene : OsuManualInputManagerTestScene { protected override Container Content => content ?? base.Content; private readonly Container content; From 67667b3d22c0f0e389cd0717c1ed717704dfb11c Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sun, 22 Mar 2020 21:22:46 -0400 Subject: [PATCH 0535/2376] enforce precision for ModDifficultyAdjust and derived classes --- osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs | 4 ++-- osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs | 4 ++-- osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs index a60b35739e..6288d498bd 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs @@ -36,8 +36,8 @@ namespace osu.Game.Rulesets.Catch.Mods { get { - string circleSize = CircleSize.IsDefault ? string.Empty : $"CS {CircleSize.Value}"; - string approachRate = ApproachRate.IsDefault ? string.Empty : $"AR {ApproachRate.Value}"; + string circleSize = CircleSize.IsDefault ? string.Empty : $"CS {CircleSize.Value:0.#}"; + string approachRate = ApproachRate.IsDefault ? string.Empty : $"AR {ApproachRate.Value:0.#}"; return string.Join(", ", new[] { diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs index 18492828f0..4830b29c4e 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs @@ -36,8 +36,8 @@ namespace osu.Game.Rulesets.Osu.Mods { get { - string circleSize = CircleSize.IsDefault ? string.Empty : $"CS {CircleSize.Value}"; - string approachRate = ApproachRate.IsDefault ? string.Empty : $"AR {ApproachRate.Value}"; + string circleSize = CircleSize.IsDefault ? string.Empty : $"CS {CircleSize.Value:0.#}"; + string approachRate = ApproachRate.IsDefault ? string.Empty : $"AR {ApproachRate.Value:0.#}"; return string.Join(", ", new[] { diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index 1baf9f7057..06616c7b24 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -57,8 +57,8 @@ namespace osu.Game.Rulesets.Mods { get { - string drainRate = DrainRate.IsDefault ? string.Empty : $"HP {DrainRate.Value}"; - string overallDifficulty = OverallDifficulty.IsDefault ? string.Empty : $"OD {OverallDifficulty.Value}"; + string drainRate = DrainRate.IsDefault ? string.Empty : $"HP {DrainRate.Value:0.#}"; + string overallDifficulty = OverallDifficulty.IsDefault ? string.Empty : $"OD {OverallDifficulty.Value:0.#}"; return string.Join(", ", new[] { From 754d0ca14d43037c745c9a74a5ce8321bb0f195a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 23 Mar 2020 10:28:17 +0900 Subject: [PATCH 0536/2376] Fix crashes due to ladder re-use --- osu.Game.Tournament.Tests/LadderTestScene.cs | 21 ++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tournament.Tests/LadderTestScene.cs b/osu.Game.Tournament.Tests/LadderTestScene.cs index 4477ca8338..b962d035ab 100644 --- a/osu.Game.Tournament.Tests/LadderTestScene.cs +++ b/osu.Game.Tournament.Tests/LadderTestScene.cs @@ -1,10 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Rulesets; using osu.Game.Tournament.Models; using osu.Game.Users; @@ -13,8 +15,20 @@ namespace osu.Game.Tournament.Tests [TestFixture] public abstract class LadderTestScene : TournamentTestScene { + [Cached] + protected LadderInfo Ladder { get; private set; } = new LadderInfo(); + [Resolved] - protected LadderInfo Ladder { get; private set; } + private RulesetStore rulesetStore { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + if (Ladder.Ruleset.Value == null) + Ladder.Ruleset.Value = rulesetStore.AvailableRulesets.First(); + + Ruleset.BindTo(Ladder.Ruleset); + } protected override void LoadComplete() { @@ -22,13 +36,8 @@ namespace osu.Game.Tournament.Tests TournamentMatch match = CreateSampleMatch(); - Ladder.Rounds.Clear(); Ladder.Rounds.Add(match.Round.Value); - - Ladder.Matches.Clear(); Ladder.Matches.Add(match); - - Ladder.Teams.Clear(); Ladder.Teams.Add(match.Team1.Value); Ladder.Teams.Add(match.Team2.Value); From bfd643dd1659179186a5d26982b58bcbfcd3d5e9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 23 Mar 2020 10:47:27 +0900 Subject: [PATCH 0537/2376] Rename classes --- .../Visual/Online/TestSceneFriendsLayout.cs | 12 +++++------ .../TestSceneFriendsOnlineStatusControl.cs | 14 ++++++------- .../{FriendsLayout.cs => FriendDisplay.cs} | 20 +++++++++---------- ...ontrol.cs => FriendOnlineStreamControl.cs} | 10 +++++----- .../{FriendsBundle.cs => FriendStream.cs} | 13 +++--------- .../Friends/FriendsOnlineStatusItem.cs | 10 +++++----- .../Dashboard/Friends/OnlineStatus.cs | 12 +++++++++++ 7 files changed, 48 insertions(+), 43 deletions(-) rename osu.Game/Overlays/Dashboard/Friends/{FriendsLayout.cs => FriendDisplay.cs} (94%) rename osu.Game/Overlays/Dashboard/Friends/{FriendsOnlineStatusControl.cs => FriendOnlineStreamControl.cs} (53%) rename osu.Game/Overlays/Dashboard/Friends/{FriendsBundle.cs => FriendStream.cs} (57%) create mode 100644 osu.Game/Overlays/Dashboard/Friends/OnlineStatus.cs diff --git a/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs b/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs index 1d8238dd40..c0a617fe57 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs @@ -17,8 +17,8 @@ namespace osu.Game.Tests.Visual.Online { public override IReadOnlyList RequiredTypes => new[] { - typeof(FriendsLayout), - typeof(FriendsOnlineStatusControl), + typeof(FriendDisplay), + typeof(FriendOnlineStreamControl), typeof(UserListToolbar) }; @@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.Online [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); - private FriendsLayout layout; + private FriendDisplay display; [SetUp] public void Setup() => Schedule(() => @@ -35,20 +35,20 @@ namespace osu.Game.Tests.Visual.Online Child = new BasicScrollContainer { RelativeSizeAxes = Axes.Both, - Child = layout = new FriendsLayout() + Child = display = new FriendDisplay() }; }); [Test] public void TestOffline() { - AddStep("Populate", () => layout.Users = getUsers()); + AddStep("Populate", () => display.Users = getUsers()); } [Test] public void TestOnline() { - AddStep("Fetch online", () => layout?.Fetch()); + AddStep("Fetch online", () => display?.Fetch()); } private List getUsers() => new List diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs index d72818ed89..f6dcf78d55 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs @@ -17,20 +17,20 @@ namespace osu.Game.Tests.Visual.UserInterface { public override IReadOnlyList RequiredTypes => new[] { - typeof(FriendsOnlineStatusControl), + typeof(FriendOnlineStreamControl), typeof(FriendsOnlineStatusItem), typeof(OverlayStreamControl<>), typeof(OverlayStreamItem<>), - typeof(FriendsBundle) + typeof(FriendStream) }; [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); - private FriendsOnlineStatusControl control; + private FriendOnlineStreamControl control; [SetUp] - public void SetUp() => Schedule(() => Child = control = new FriendsOnlineStatusControl + public void SetUp() => Schedule(() => Child = control = new FriendOnlineStreamControl { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -55,9 +55,9 @@ namespace osu.Game.Tests.Visual.UserInterface } })); - AddAssert("3 users", () => control.Items.FirstOrDefault(item => item.Status == FriendsOnlineStatus.All)?.Count == 3); - AddAssert("1 online user", () => control.Items.FirstOrDefault(item => item.Status == FriendsOnlineStatus.Online)?.Count == 1); - AddAssert("2 offline users", () => control.Items.FirstOrDefault(item => item.Status == FriendsOnlineStatus.Offline)?.Count == 2); + AddAssert("3 users", () => control.Items.FirstOrDefault(item => item.Status == OnlineStatus.All)?.Count == 3); + AddAssert("1 online user", () => control.Items.FirstOrDefault(item => item.Status == OnlineStatus.Online)?.Count == 1); + AddAssert("2 offline users", () => control.Items.FirstOrDefault(item => item.Status == OnlineStatus.Offline)?.Count == 2); } } } diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs similarity index 94% rename from osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs rename to osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs index 3514bf7ff7..3c9b31daae 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendsLayout.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs @@ -16,7 +16,7 @@ using osuTK; namespace osu.Game.Overlays.Dashboard.Friends { - public class FriendsLayout : CompositeDrawable + public class FriendDisplay : CompositeDrawable { private List users = new List(); @@ -27,7 +27,7 @@ namespace osu.Game.Overlays.Dashboard.Friends { users = value; - onlineStatusControl.Populate(value); + onlineStreamControl.Populate(value); } } @@ -39,14 +39,14 @@ namespace osu.Game.Overlays.Dashboard.Friends private Drawable currentContent; - private readonly FriendsOnlineStatusControl onlineStatusControl; + private readonly FriendOnlineStreamControl onlineStreamControl; private readonly Box background; private readonly Box controlBackground; private readonly UserListToolbar userListToolbar; private readonly Container itemsPlaceholder; private readonly LoadingLayer loading; - public FriendsLayout() + public FriendDisplay() { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; @@ -75,7 +75,7 @@ namespace osu.Game.Overlays.Dashboard.Friends Top = 20, Horizontal = 45 }, - Child = onlineStatusControl = new FriendsOnlineStatusControl(), + Child = onlineStreamControl = new FriendOnlineStreamControl(), } } }, @@ -147,7 +147,7 @@ namespace osu.Game.Overlays.Dashboard.Friends { base.LoadComplete(); - onlineStatusControl.Current.BindValueChanged(_ => recreatePanels()); + onlineStreamControl.Current.BindValueChanged(_ => recreatePanels()); userListToolbar.DisplayStyle.BindValueChanged(_ => recreatePanels()); userListToolbar.SortCriteria.BindValueChanged(_ => recreatePanels()); } @@ -179,16 +179,16 @@ namespace osu.Game.Overlays.Dashboard.Friends private List getUsersInCurrentGroup() { - switch (onlineStatusControl.Current.Value?.Status) + switch (onlineStreamControl.Current.Value?.Status) { default: - case FriendsOnlineStatus.All: + case OnlineStatus.All: return users; - case FriendsOnlineStatus.Offline: + case OnlineStatus.Offline: return users.Where(u => !u.IsOnline).ToList(); - case FriendsOnlineStatus.Online: + case OnlineStatus.Online: return users.Where(u => u.IsOnline).ToList(); } } diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusControl.cs b/osu.Game/Overlays/Dashboard/Friends/FriendOnlineStreamControl.cs similarity index 53% rename from osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusControl.cs rename to osu.Game/Overlays/Dashboard/Friends/FriendOnlineStreamControl.cs index c54e9e2a06..28546ceab8 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusControl.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendOnlineStreamControl.cs @@ -7,9 +7,9 @@ using osu.Game.Users; namespace osu.Game.Overlays.Dashboard.Friends { - public class FriendsOnlineStatusControl : OverlayStreamControl + public class FriendOnlineStreamControl : OverlayStreamControl { - protected override OverlayStreamItem CreateStreamItem(FriendsBundle value) => new FriendsOnlineStatusItem(value); + protected override OverlayStreamItem CreateStreamItem(FriendStream value) => new FriendsOnlineStatusItem(value); public void Populate(List users) { @@ -18,9 +18,9 @@ namespace osu.Game.Overlays.Dashboard.Friends var userCount = users.Count; var onlineUsersCount = users.Count(user => user.IsOnline); - AddItem(new FriendsBundle(FriendsOnlineStatus.All, userCount)); - AddItem(new FriendsBundle(FriendsOnlineStatus.Online, onlineUsersCount)); - AddItem(new FriendsBundle(FriendsOnlineStatus.Offline, userCount - onlineUsersCount)); + AddItem(new FriendStream(OnlineStatus.All, userCount)); + AddItem(new FriendStream(OnlineStatus.Online, onlineUsersCount)); + AddItem(new FriendStream(OnlineStatus.Offline, userCount - onlineUsersCount)); Current.Value = Items.FirstOrDefault(); } diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsBundle.cs b/osu.Game/Overlays/Dashboard/Friends/FriendStream.cs similarity index 57% rename from osu.Game/Overlays/Dashboard/Friends/FriendsBundle.cs rename to osu.Game/Overlays/Dashboard/Friends/FriendStream.cs index d5fad1ffd3..4abece9a8d 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendsBundle.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendStream.cs @@ -3,23 +3,16 @@ namespace osu.Game.Overlays.Dashboard.Friends { - public class FriendsBundle + public class FriendStream { - public FriendsOnlineStatus Status { get; } + public OnlineStatus Status { get; } public int Count { get; } - public FriendsBundle(FriendsOnlineStatus status, int count) + public FriendStream(OnlineStatus status, int count) { Status = status; Count = count; } } - - public enum FriendsOnlineStatus - { - All, - Online, - Offline - } } diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusItem.cs b/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusItem.cs index eada9420ea..7e902203f8 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusItem.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendsOnlineStatusItem.cs @@ -7,9 +7,9 @@ using osuTK.Graphics; namespace osu.Game.Overlays.Dashboard.Friends { - public class FriendsOnlineStatusItem : OverlayStreamItem + public class FriendsOnlineStatusItem : OverlayStreamItem { - public FriendsOnlineStatusItem(FriendsBundle value) + public FriendsOnlineStatusItem(FriendStream value) : base(value) { } @@ -22,13 +22,13 @@ namespace osu.Game.Overlays.Dashboard.Friends { switch (Value.Status) { - case FriendsOnlineStatus.All: + case OnlineStatus.All: return Color4.White; - case FriendsOnlineStatus.Online: + case OnlineStatus.Online: return colours.GreenLight; - case FriendsOnlineStatus.Offline: + case OnlineStatus.Offline: return Color4.Black; default: diff --git a/osu.Game/Overlays/Dashboard/Friends/OnlineStatus.cs b/osu.Game/Overlays/Dashboard/Friends/OnlineStatus.cs new file mode 100644 index 0000000000..6f2f55a6ed --- /dev/null +++ b/osu.Game/Overlays/Dashboard/Friends/OnlineStatus.cs @@ -0,0 +1,12 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Overlays.Dashboard.Friends +{ + public enum OnlineStatus + { + All, + Online, + Offline + } +} From cb6e6025567179280b52d2e58aa099af6b2ef5d9 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sun, 22 Mar 2020 22:06:35 -0400 Subject: [PATCH 0538/2376] enforce single signficiant digit precision for other mods --- osu.Game/Rulesets/Mods/ModDoubleTime.cs | 2 +- osu.Game/Rulesets/Mods/ModHalfTime.cs | 2 +- osu.Game/Rulesets/Mods/ModTimeRamp.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModDoubleTime.cs b/osu.Game/Rulesets/Mods/ModDoubleTime.cs index 7d86190134..105c19dec7 100644 --- a/osu.Game/Rulesets/Mods/ModDoubleTime.cs +++ b/osu.Game/Rulesets/Mods/ModDoubleTime.cs @@ -31,6 +31,6 @@ namespace osu.Game.Rulesets.Mods Precision = 0.01, }; - public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value}x"; + public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value:0.#}x"; } } diff --git a/osu.Game/Rulesets/Mods/ModHalfTime.cs b/osu.Game/Rulesets/Mods/ModHalfTime.cs index ec215369a3..32e16e0914 100644 --- a/osu.Game/Rulesets/Mods/ModHalfTime.cs +++ b/osu.Game/Rulesets/Mods/ModHalfTime.cs @@ -31,6 +31,6 @@ namespace osu.Game.Rulesets.Mods Precision = 0.01, }; - public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value}x"; + public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value:0.#}x"; } } diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index 7b4c1370ac..01b49faa75 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Mods [SettingSource("Final rate", "The final speed to ramp to")] public abstract BindableNumber FinalRate { get; } - public override string SettingDescription => $"{InitialRate.Value} to {FinalRate.Value}"; + public override string SettingDescription => $"{InitialRate.Value:0.#} to {FinalRate.Value:0.#}"; private double finalRateTime; private double beginRampTime; From ea87afd5775c8f4220d642d093f0fe664b1a0d3e Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sun, 22 Mar 2020 22:06:54 -0400 Subject: [PATCH 0539/2376] use string.Empty in IconTooltip --- osu.Game/Rulesets/Mods/Mod.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 95e8ff86eb..23ad48ac5a 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; @@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Mods { get { - string settingDescription = string.IsNullOrEmpty(SettingDescription) ? "" : $" ({SettingDescription})"; + string settingDescription = string.IsNullOrEmpty(SettingDescription) ? string.Empty : $" ({SettingDescription})"; return $"{Name}{settingDescription}"; } } From 98b8f828103c037ae54ecbeb07c9a398289ae335 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sun, 22 Mar 2020 22:07:09 -0400 Subject: [PATCH 0540/2376] simplify SettingDescription default definition --- osu.Game/Rulesets/Mods/Mod.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 23ad48ac5a..5944717c13 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; @@ -86,10 +86,8 @@ namespace osu.Game.Rulesets.Mods tooltipTexts.Add(tooltipText); } - // filter out empty strings so we don't have orphaned commas - //tooltipTexts = tooltipTexts.Where(s => !string.IsNullOrEmpty(s)).ToList(); - string joinedTooltipText = string.Join(", ", tooltipTexts); - return $"{Name}{joinedTooltipText}"; + string joinedTooltipText = string.Join(", ", tooltipTexts.Where(s => !string.IsNullOrEmpty(s))); + return $"{joinedTooltipText}"; } } From bf70276496b072dad7009cb4b1c27848b7710edd Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 23 Mar 2020 11:12:36 +0900 Subject: [PATCH 0541/2376] Fix test re-using the same beatmap sets --- osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 5b4c57e28f..6b4090ea58 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -234,6 +234,8 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("Create beatmaps for taiko only", () => { + sets.Clear(); + var rulesetBeatmapSet = createTestBeatmapSet(1); var taikoRuleset = rulesets.AvailableRulesets.ElementAt(1); rulesetBeatmapSet.Beatmaps.ForEach(b => From 27ae2d29aac6d10f13a696713d5a9de78b0056ef Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 23 Mar 2020 11:47:24 +0900 Subject: [PATCH 0542/2376] Add ability to adjust (and save) chroma-key area width --- osu.Game.Tournament/Models/LadderInfo.cs | 8 +++++++- .../Screens/Gameplay/GameplayScreen.cs | 14 ++++++++++++-- osu.Game.Tournament/Screens/SetupScreen.cs | 2 +- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tournament/Models/LadderInfo.cs b/osu.Game.Tournament/Models/LadderInfo.cs index 5db0b01547..c2e6da9ca5 100644 --- a/osu.Game.Tournament/Models/LadderInfo.cs +++ b/osu.Game.Tournament/Models/LadderInfo.cs @@ -24,7 +24,13 @@ namespace osu.Game.Tournament.Models // only used for serialisation public List Progressions = new List(); - [JsonIgnore] + [JsonIgnore] // updated manually in TournamentGameBase public Bindable CurrentMatch = new Bindable(); + + public Bindable ChromaKeyWidth = new BindableInt(1024) + { + MinValue = 640, + MaxValue = 1366, + }; } } diff --git a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs index 8920990d1b..64a5cd6dec 100644 --- a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs +++ b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Platform; using osu.Framework.Threading; using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays.Settings; using osu.Game.Tournament.Components; using osu.Game.Tournament.IPC; using osu.Game.Tournament.Models; @@ -35,6 +36,8 @@ namespace osu.Game.Tournament.Screens.Gameplay [Resolved] private TournamentMatchChatDisplay chat { get; set; } + private Box chroma; + [BackgroundDependencyLoader] private void load(LadderInfo ladder, MatchIPCInfo ipc, Storage storage) { @@ -60,11 +63,10 @@ namespace osu.Game.Tournament.Screens.Gameplay Origin = Anchor.TopCentre, Children = new Drawable[] { - new Box + chroma = new Box { // chroma key area for stable gameplay Name = "chroma", - RelativeSizeAxes = Axes.X, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Height = 512, @@ -93,6 +95,12 @@ namespace osu.Game.Tournament.Screens.Gameplay RelativeSizeAxes = Axes.X, Text = "Toggle chat", Action = () => { State.Value = State.Value == TourneyState.Idle ? TourneyState.Playing : TourneyState.Idle; } + }, + new SettingsSlider + { + LabelText = "Chroma Width", + Bindable = LadderInfo.ChromaKeyWidth, + KeyboardStep = 1, } } } @@ -101,6 +109,8 @@ namespace osu.Game.Tournament.Screens.Gameplay State.BindTo(ipc.State); State.BindValueChanged(stateChanged, true); + ladder.ChromaKeyWidth.BindValueChanged(width => chroma.Width = width.NewValue, true); + currentMatch.BindValueChanged(m => { warmup.Value = m.NewValue.Team1Score.Value + m.NewValue.Team2Score.Value == 0; diff --git a/osu.Game.Tournament/Screens/SetupScreen.cs b/osu.Game.Tournament/Screens/SetupScreen.cs index b7f8b2bfd6..c91379b2d6 100644 --- a/osu.Game.Tournament/Screens/SetupScreen.cs +++ b/osu.Game.Tournament/Screens/SetupScreen.cs @@ -116,7 +116,7 @@ namespace osu.Game.Tournament.Screens { windowSize.Value = new Size((int)(1920 / TournamentSceneManager.STREAM_AREA_WIDTH * TournamentSceneManager.REQUIRED_WIDTH), 1080); } - } + }, }; } From afe7397d891237271902caa4b3f31f8b189b770b Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sun, 22 Mar 2020 22:50:52 -0400 Subject: [PATCH 0543/2376] remove unnecessary using statements --- osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs | 1 - osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs index 6288d498bd..c465048da3 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Linq; using osu.Framework.Bindables; using osu.Game.Beatmaps; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs index 4830b29c4e..fab4638fb7 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Linq; using osu.Framework.Bindables; using osu.Game.Beatmaps; From 889608a408e854e30ffd2177aa9d3f079cc71717 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sun, 22 Mar 2020 22:51:29 -0400 Subject: [PATCH 0544/2376] remove redundant ToString call --- osu.Game/Rulesets/Mods/Mod.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 5944717c13..f712fdc3be 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -82,7 +82,7 @@ namespace osu.Game.Rulesets.Mods { object bindableObj = property.GetValue(this); bool? settingIsDefault = (bindableObj as IHasDefaultValue)?.IsDefault; - string tooltipText = settingIsDefault == true ? string.Empty : attr.Label + " " + bindableObj.ToString(); + string tooltipText = settingIsDefault == true ? string.Empty : attr.Label + " " + bindableObj; tooltipTexts.Add(tooltipText); } From 1da590c63f6b77b68471e564e1210b2109f304aa Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sun, 22 Mar 2020 22:54:21 -0400 Subject: [PATCH 0545/2376] use N1 format instead of 0.# --- osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs | 4 ++-- osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs | 4 ++-- osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs | 4 ++-- osu.Game/Rulesets/Mods/ModDoubleTime.cs | 2 +- osu.Game/Rulesets/Mods/ModHalfTime.cs | 2 +- osu.Game/Rulesets/Mods/ModTimeRamp.cs | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs index c465048da3..acdd0a420c 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs @@ -35,8 +35,8 @@ namespace osu.Game.Rulesets.Catch.Mods { get { - string circleSize = CircleSize.IsDefault ? string.Empty : $"CS {CircleSize.Value:0.#}"; - string approachRate = ApproachRate.IsDefault ? string.Empty : $"AR {ApproachRate.Value:0.#}"; + string circleSize = CircleSize.IsDefault ? string.Empty : $"CS {CircleSize.Value:N1}"; + string approachRate = ApproachRate.IsDefault ? string.Empty : $"AR {ApproachRate.Value:N1}"; return string.Join(", ", new[] { diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs index fab4638fb7..8228161008 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs @@ -35,8 +35,8 @@ namespace osu.Game.Rulesets.Osu.Mods { get { - string circleSize = CircleSize.IsDefault ? string.Empty : $"CS {CircleSize.Value:0.#}"; - string approachRate = ApproachRate.IsDefault ? string.Empty : $"AR {ApproachRate.Value:0.#}"; + string circleSize = CircleSize.IsDefault ? string.Empty : $"CS {CircleSize.Value:N1}"; + string approachRate = ApproachRate.IsDefault ? string.Empty : $"AR {ApproachRate.Value:N1}"; return string.Join(", ", new[] { diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index 06616c7b24..c3a8efdd66 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -57,8 +57,8 @@ namespace osu.Game.Rulesets.Mods { get { - string drainRate = DrainRate.IsDefault ? string.Empty : $"HP {DrainRate.Value:0.#}"; - string overallDifficulty = OverallDifficulty.IsDefault ? string.Empty : $"OD {OverallDifficulty.Value:0.#}"; + string drainRate = DrainRate.IsDefault ? string.Empty : $"HP {DrainRate.Value:N1}"; + string overallDifficulty = OverallDifficulty.IsDefault ? string.Empty : $"OD {OverallDifficulty.Value:N1}"; return string.Join(", ", new[] { diff --git a/osu.Game/Rulesets/Mods/ModDoubleTime.cs b/osu.Game/Rulesets/Mods/ModDoubleTime.cs index 105c19dec7..3f01bfb11e 100644 --- a/osu.Game/Rulesets/Mods/ModDoubleTime.cs +++ b/osu.Game/Rulesets/Mods/ModDoubleTime.cs @@ -31,6 +31,6 @@ namespace osu.Game.Rulesets.Mods Precision = 0.01, }; - public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value:0.#}x"; + public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value:N1}x"; } } diff --git a/osu.Game/Rulesets/Mods/ModHalfTime.cs b/osu.Game/Rulesets/Mods/ModHalfTime.cs index 32e16e0914..c555692ed9 100644 --- a/osu.Game/Rulesets/Mods/ModHalfTime.cs +++ b/osu.Game/Rulesets/Mods/ModHalfTime.cs @@ -31,6 +31,6 @@ namespace osu.Game.Rulesets.Mods Precision = 0.01, }; - public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value:0.#}x"; + public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value:N1}x"; } } diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index 01b49faa75..f21ba684b4 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Mods [SettingSource("Final rate", "The final speed to ramp to")] public abstract BindableNumber FinalRate { get; } - public override string SettingDescription => $"{InitialRate.Value:0.#} to {FinalRate.Value:0.#}"; + public override string SettingDescription => $"{InitialRate.Value:0.#} to {FinalRate.Value:N1}"; private double finalRateTime; private double beginRampTime; From 5cc626d37b746c69d5979e9c465933c8fe6c1b8a Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sun, 22 Mar 2020 22:57:46 -0400 Subject: [PATCH 0546/2376] move SettingDescription override to ModRateAdjust --- osu.Game/Rulesets/Mods/ModDoubleTime.cs | 2 -- osu.Game/Rulesets/Mods/ModHalfTime.cs | 2 -- osu.Game/Rulesets/Mods/ModRateAdjust.cs | 2 ++ 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModDoubleTime.cs b/osu.Game/Rulesets/Mods/ModDoubleTime.cs index 3f01bfb11e..152657da33 100644 --- a/osu.Game/Rulesets/Mods/ModDoubleTime.cs +++ b/osu.Game/Rulesets/Mods/ModDoubleTime.cs @@ -30,7 +30,5 @@ namespace osu.Game.Rulesets.Mods Value = 1.5, Precision = 0.01, }; - - public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value:N1}x"; } } diff --git a/osu.Game/Rulesets/Mods/ModHalfTime.cs b/osu.Game/Rulesets/Mods/ModHalfTime.cs index c555692ed9..203b88951c 100644 --- a/osu.Game/Rulesets/Mods/ModHalfTime.cs +++ b/osu.Game/Rulesets/Mods/ModHalfTime.cs @@ -30,7 +30,5 @@ namespace osu.Game.Rulesets.Mods Value = 0.75, Precision = 0.01, }; - - public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value:N1}x"; } } diff --git a/osu.Game/Rulesets/Mods/ModRateAdjust.cs b/osu.Game/Rulesets/Mods/ModRateAdjust.cs index 1739524bcd..9059d54035 100644 --- a/osu.Game/Rulesets/Mods/ModRateAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModRateAdjust.cs @@ -15,5 +15,7 @@ namespace osu.Game.Rulesets.Mods { track.AddAdjustment(AdjustableProperty.Tempo, SpeedChange); } + + public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value:N1}x"; } } From 1b6342438f8cc54ff14dad811d54512c7e8af29b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 23 Mar 2020 12:03:33 +0900 Subject: [PATCH 0547/2376] Hide scrollbars in tournament chat display --- .../Components/TournamentMatchChatDisplay.cs | 11 +++++++++++ osu.Game/Online/Chat/StandAloneChatDisplay.cs | 17 ++++++++++------- osu.Game/Overlays/Chat/DrawableChannel.cs | 15 +++++++++++++++ 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs index 2a183d0d45..fe22d1e76d 100644 --- a/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs +++ b/osu.Game.Tournament/Components/TournamentMatchChatDisplay.cs @@ -70,6 +70,17 @@ namespace osu.Game.Tournament.Components protected override ChatLine CreateMessage(Message message) => new MatchMessage(message); + protected override StandAloneDrawableChannel CreateDrawableChannel(Channel channel) => new MatchChannel(channel); + + public class MatchChannel : StandAloneDrawableChannel + { + public MatchChannel(Channel channel) + : base(channel) + { + ScrollbarVisible = false; + } + } + protected class MatchMessage : StandAloneMessage { public MatchMessage(Message message) diff --git a/osu.Game/Online/Chat/StandAloneChatDisplay.cs b/osu.Game/Online/Chat/StandAloneChatDisplay.cs index 0914f688e9..4fbeac1db9 100644 --- a/osu.Game/Online/Chat/StandAloneChatDisplay.cs +++ b/osu.Game/Online/Chat/StandAloneChatDisplay.cs @@ -26,7 +26,7 @@ namespace osu.Game.Online.Chat protected ChannelManager ChannelManager; - private DrawableChannel drawableChannel; + private StandAloneDrawableChannel drawableChannel; private readonly bool postingTextbox; @@ -77,6 +77,9 @@ namespace osu.Game.Online.Chat ChannelManager = manager; } + protected virtual StandAloneDrawableChannel CreateDrawableChannel(Channel channel) => + new StandAloneDrawableChannel(channel); + private void postMessage(TextBox sender, bool newtext) { var text = textbox.Text.Trim(); @@ -100,14 +103,14 @@ namespace osu.Game.Online.Chat if (e.NewValue == null) return; - AddInternal(drawableChannel = new StandAloneDrawableChannel(e.NewValue) - { - CreateChatLineAction = CreateMessage, - Padding = new MarginPadding { Bottom = postingTextbox ? textbox_height : 0 } - }); + drawableChannel = CreateDrawableChannel(e.NewValue); + drawableChannel.CreateChatLineAction = CreateMessage; + drawableChannel.Padding = new MarginPadding { Bottom = postingTextbox ? textbox_height : 0 }; + + AddInternal(drawableChannel); } - protected class StandAloneDrawableChannel : DrawableChannel + public class StandAloneDrawableChannel : DrawableChannel { public Func CreateChatLineAction; diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index 443f2b7bf7..6019657cf0 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -26,6 +26,20 @@ namespace osu.Game.Overlays.Chat protected FillFlowContainer ChatLineFlow; private OsuScrollContainer scroll; + private bool scrollbarVisible = true; + + public bool ScrollbarVisible + { + set + { + if (scrollbarVisible == value) return; + + scrollbarVisible = value; + if (scroll != null) + scroll.ScrollbarVisible = value; + } + } + [Resolved] private OsuColour colours { get; set; } @@ -44,6 +58,7 @@ namespace osu.Game.Overlays.Chat Masking = true, Child = scroll = new OsuScrollContainer { + ScrollbarVisible = scrollbarVisible, RelativeSizeAxes = Axes.Both, // Some chat lines have effects that slightly protrude to the bottom, // which we do not want to mask away, hence the padding. From 64fc116d673500e6b4e55639e7e7daf7d7fad640 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sun, 22 Mar 2020 23:08:00 -0400 Subject: [PATCH 0548/2376] use two decimal points for ModRateAdjust format --- osu.Game/Rulesets/Mods/ModRateAdjust.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/ModRateAdjust.cs b/osu.Game/Rulesets/Mods/ModRateAdjust.cs index 9059d54035..cb2ff149f1 100644 --- a/osu.Game/Rulesets/Mods/ModRateAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModRateAdjust.cs @@ -16,6 +16,6 @@ namespace osu.Game.Rulesets.Mods track.AddAdjustment(AdjustableProperty.Tempo, SpeedChange); } - public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value:N1}x"; + public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value:N2}x"; } } From 5106d275ca1cf58f26897f1f962762fa8d6119c5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 23 Mar 2020 12:08:15 +0900 Subject: [PATCH 0549/2376] Remove CentreHit/RimHit hitobject abstraction --- .../Mods/TestSceneTaikoModPerfect.cs | 2 +- .../TaikoBeatmapConversionTest.cs | 4 +- .../TestSceneTaikoPlayfield.cs | 2 +- .../Beatmaps/TaikoBeatmapConverter.cs | 46 +++++-------------- .../Preprocessing/TaikoDifficultyHitObject.cs | 2 +- osu.Game.Rulesets.Taiko/Objects/CentreHit.cs | 9 ---- osu.Game.Rulesets.Taiko/Objects/Hit.cs | 4 ++ osu.Game.Rulesets.Taiko/Objects/HitType.cs | 21 +++++++++ osu.Game.Rulesets.Taiko/Objects/RimHit.cs | 9 ---- .../Replays/TaikoAutoGenerator.cs | 2 +- .../UI/DrawableTaikoRuleset.cs | 10 ++-- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 4 +- 12 files changed, 50 insertions(+), 65 deletions(-) delete mode 100644 osu.Game.Rulesets.Taiko/Objects/CentreHit.cs create mode 100644 osu.Game.Rulesets.Taiko/Objects/HitType.cs delete mode 100644 osu.Game.Rulesets.Taiko/Objects/RimHit.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModPerfect.cs b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModPerfect.cs index d3be2cdf0d..26c90ad295 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModPerfect.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModPerfect.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Mods [TestCase(false)] [TestCase(true)] - public void TestHit(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new CentreHit { StartTime = 1000 }), shouldMiss); + public void TestHit(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new Hit { StartTime = 1000, Type = HitType.Centre }), shouldMiss); [TestCase(false)] [TestCase(true)] diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs index f23fd6d3f9..8c26ca70ac 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs @@ -27,8 +27,8 @@ namespace osu.Game.Rulesets.Taiko.Tests { StartTime = hitObject.StartTime, EndTime = hitObject.GetEndTime(), - IsRim = hitObject is RimHit, - IsCentre = hitObject is CentreHit, + IsRim = (hitObject as Hit)?.Type == HitType.Rim, + IsCentre = (hitObject as Hit)?.Type == HitType.Centre, IsDrumRoll = hitObject is DrumRoll, IsSwell = hitObject is Swell, IsStrong = ((TaikoHitObject)hitObject).IsStrong diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayfield.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayfield.cs index c01eef5252..0d9e813c60 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayfield.cs @@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Taiko.Tests WorkingBeatmap beatmap = CreateWorkingBeatmap(new Beatmap { - HitObjects = new List { new CentreHit() }, + HitObjects = new List { new Hit { Type = HitType.Centre } }, BeatmapInfo = new BeatmapInfo { BaseDifficulty = new BeatmapDifficulty(), diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index cc9d6e4470..695ada3a00 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs @@ -124,24 +124,13 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps bool isRim = currentSamples.Any(s => s.Name == HitSampleInfo.HIT_CLAP || s.Name == HitSampleInfo.HIT_WHISTLE); strong = currentSamples.Any(s => s.Name == HitSampleInfo.HIT_FINISH); - if (isRim) + yield return new Hit { - yield return new RimHit - { - StartTime = j, - Samples = currentSamples, - IsStrong = strong - }; - } - else - { - yield return new CentreHit - { - StartTime = j, - Samples = currentSamples, - IsStrong = strong - }; - } + StartTime = j, + Type = isRim ? HitType.Rim : HitType.Centre, + Samples = currentSamples, + IsStrong = strong + }; i = (i + 1) % allSamples.Count; } @@ -180,24 +169,13 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps { bool isRim = samples.Any(s => s.Name == HitSampleInfo.HIT_CLAP || s.Name == HitSampleInfo.HIT_WHISTLE); - if (isRim) + yield return new Hit { - yield return new RimHit - { - StartTime = obj.StartTime, - Samples = obj.Samples, - IsStrong = strong - }; - } - else - { - yield return new CentreHit - { - StartTime = obj.StartTime, - Samples = obj.Samples, - IsStrong = strong - }; - } + StartTime = obj.StartTime, + Type = isRim ? HitType.Rim : HitType.Centre, + Samples = obj.Samples, + IsStrong = strong + }; break; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index 24345275c1..6807142327 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate) : base(hitObject, lastObject, clockRate) { - HasTypeChange = lastObject is RimHit != hitObject is RimHit; + HasTypeChange = (lastObject as Hit)?.Type != (hitObject as Hit)?.Type; } } } diff --git a/osu.Game.Rulesets.Taiko/Objects/CentreHit.cs b/osu.Game.Rulesets.Taiko/Objects/CentreHit.cs deleted file mode 100644 index a6354b16ed..0000000000 --- a/osu.Game.Rulesets.Taiko/Objects/CentreHit.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -namespace osu.Game.Rulesets.Taiko.Objects -{ - public class CentreHit : Hit - { - } -} diff --git a/osu.Game.Rulesets.Taiko/Objects/Hit.cs b/osu.Game.Rulesets.Taiko/Objects/Hit.cs index 6cc9357580..2aca701515 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Hit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Hit.cs @@ -5,5 +5,9 @@ namespace osu.Game.Rulesets.Taiko.Objects { public class Hit : TaikoHitObject { + /// + /// The that actuates this . + /// + public HitType Type { get; set; } } } diff --git a/osu.Game.Rulesets.Taiko/Objects/HitType.cs b/osu.Game.Rulesets.Taiko/Objects/HitType.cs new file mode 100644 index 0000000000..17b3fdbd04 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Objects/HitType.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. + +namespace osu.Game.Rulesets.Taiko.Objects +{ + /// + /// The type of a . + /// + public enum HitType + { + /// + /// A that can be hit by the centre portion of the drum. + /// + Centre, + + /// + /// A that can be hit by the rim portion of the drum. + /// + Rim + } +} diff --git a/osu.Game.Rulesets.Taiko/Objects/RimHit.cs b/osu.Game.Rulesets.Taiko/Objects/RimHit.cs deleted file mode 100644 index 6f6b089e03..0000000000 --- a/osu.Game.Rulesets.Taiko/Objects/RimHit.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -namespace osu.Game.Rulesets.Taiko.Objects -{ - public class RimHit : Hit - { - } -} diff --git a/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs b/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs index 48eb33976e..273f4e4105 100644 --- a/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs +++ b/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs @@ -97,7 +97,7 @@ namespace osu.Game.Rulesets.Taiko.Replays { TaikoAction[] actions; - if (hit is CentreHit) + if (hit.Type == HitType.Centre) { actions = h.IsStrong ? new[] { TaikoAction.LeftCentre, TaikoAction.RightCentre } diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs index 0c7495aa52..9196bbf13e 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs @@ -48,11 +48,11 @@ namespace osu.Game.Rulesets.Taiko.UI { switch (h) { - case CentreHit centreHit: - return new DrawableCentreHit(centreHit); - - case RimHit rimHit: - return new DrawableRimHit(rimHit); + case Hit hit: + if (hit.Type == HitType.Centre) + return new DrawableCentreHit(hit); + else + return new DrawableRimHit(hit); case DrumRoll drumRoll: return new DrawableDrumRoll(drumRoll); diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index a10f70a344..bde9085c23 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -14,9 +14,9 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; -using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.Taiko.Judgements; +using osu.Game.Rulesets.Taiko.Objects; using osuTK; using osuTK.Graphics; @@ -245,7 +245,7 @@ namespace osu.Game.Rulesets.Taiko.UI if (!result.IsHit) break; - bool isRim = judgedObject.HitObject is RimHit; + bool isRim = (judgedObject.HitObject as Hit)?.Type == HitType.Rim; hitExplosionContainer.Add(new HitExplosion(judgedObject, isRim)); From e3a5be71cce01b6e42311a89772cc6fedbbb9c8f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 23 Mar 2020 12:09:30 +0900 Subject: [PATCH 0550/2376] Implement random mod for taiko --- .../Mods/ManiaModRandom.cs | 9 +------ .../Mods/TaikoModRandom.cs | 27 +++++++++++++++++++ osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 1 + osu.Game/Rulesets/Mods/ModRandom.cs | 17 ++++++++++++ 4 files changed, 46 insertions(+), 8 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/Mods/TaikoModRandom.cs create mode 100644 osu.Game/Rulesets/Mods/ModRandom.cs diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModRandom.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModRandom.cs index 14b36fb765..699c58c373 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModRandom.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModRandom.cs @@ -3,24 +3,17 @@ using System.Linq; using osu.Framework.Extensions.IEnumerableExtensions; -using osu.Framework.Graphics.Sprites; using osu.Framework.Utils; using osu.Game.Beatmaps; -using osu.Game.Graphics; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Mania.Mods { - public class ManiaModRandom : Mod, IApplicableToBeatmap + public class ManiaModRandom : ModRandom, IApplicableToBeatmap { - public override string Name => "Random"; - public override string Acronym => "RD"; - public override ModType Type => ModType.Conversion; - public override IconUsage? Icon => OsuIcon.Dice; public override string Description => @"Shuffle around the keys!"; - public override double ScoreMultiplier => 1; public void ApplyToBeatmap(IBeatmap beatmap) { diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModRandom.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModRandom.cs new file mode 100644 index 0000000000..1cf19ac18e --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModRandom.cs @@ -0,0 +1,27 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Taiko.Beatmaps; +using osu.Game.Rulesets.Taiko.Objects; + +namespace osu.Game.Rulesets.Taiko.Mods +{ + public class TaikoModRandom : ModRandom, IApplicableToBeatmap + { + public override string Description => @"Shuffle around the colours!"; + + public void ApplyToBeatmap(IBeatmap beatmap) + { + var taikoBeatmap = (TaikoBeatmap)beatmap; + + foreach (var obj in taikoBeatmap.HitObjects) + { + if (obj is Hit hit) + hit.Type = RNG.Next(2) == 0 ? HitType.Centre : HitType.Rim; + } + } + } +} diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index fc79e59864..4a841bf8c3 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -114,6 +114,7 @@ namespace osu.Game.Rulesets.Taiko case ModType.Conversion: return new Mod[] { + new TaikoModRandom(), new TaikoModDifficultyAdjust(), }; diff --git a/osu.Game/Rulesets/Mods/ModRandom.cs b/osu.Game/Rulesets/Mods/ModRandom.cs new file mode 100644 index 0000000000..da55ab3fbf --- /dev/null +++ b/osu.Game/Rulesets/Mods/ModRandom.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.Framework.Graphics.Sprites; +using osu.Game.Graphics; + +namespace osu.Game.Rulesets.Mods +{ + public abstract class ModRandom : Mod + { + public override string Name => "Random"; + public override string Acronym => "RD"; + public override ModType Type => ModType.Conversion; + public override IconUsage? Icon => OsuIcon.Dice; + public override double ScoreMultiplier => 1; + } +} From 997ce397efa6c64e6218fdd342cc0ea738a47b40 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 23 Mar 2020 12:48:05 +0900 Subject: [PATCH 0551/2376] Disable raw input toggle on all but windows --- .../Settings/Sections/Input/MouseSettings.cs | 39 ++++++++++++------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs index 59d39a1c3c..e7f2f21465 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Configuration; @@ -56,24 +57,32 @@ namespace osu.Game.Overlays.Settings.Sections.Input }, }; - rawInputToggle.ValueChanged += enabled => + if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows) { - // this is temporary until we support per-handler settings. - const string raw_mouse_handler = @"OsuTKRawMouseHandler"; - const string standard_mouse_handler = @"OsuTKMouseHandler"; - - ignoredInputHandler.Value = enabled.NewValue ? standard_mouse_handler : raw_mouse_handler; - }; - - ignoredInputHandler = config.GetBindable(FrameworkSetting.IgnoredInputHandlers); - ignoredInputHandler.ValueChanged += handler => + rawInputToggle.Disabled = true; + sensitivity.Bindable.Disabled = true; + } + else { - bool raw = !handler.NewValue.Contains("Raw"); - rawInputToggle.Value = raw; - sensitivity.Bindable.Disabled = !raw; - }; + rawInputToggle.ValueChanged += enabled => + { + // this is temporary until we support per-handler settings. + const string raw_mouse_handler = @"OsuTKRawMouseHandler"; + const string standard_mouse_handler = @"OsuTKMouseHandler"; - ignoredInputHandler.TriggerChange(); + ignoredInputHandler.Value = enabled.NewValue ? standard_mouse_handler : raw_mouse_handler; + }; + + ignoredInputHandler = config.GetBindable(FrameworkSetting.IgnoredInputHandlers); + ignoredInputHandler.ValueChanged += handler => + { + bool raw = !handler.NewValue.Contains("Raw"); + rawInputToggle.Value = raw; + sensitivity.Bindable.Disabled = !raw; + }; + + ignoredInputHandler.TriggerChange(); + } } private class SensitivitySetting : SettingsSlider From 3a3df06e0b2e0a38ab2c1591d012a78859b164cc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 23 Mar 2020 13:11:40 +0900 Subject: [PATCH 0552/2376] Fix some pieces of SettingsItem getting dimmed twice when disabled --- .../Overlays/Settings/SettingsCheckbox.cs | 6 ++---- osu.Game/Overlays/Settings/SettingsItem.cs | 20 +++++++++++++------ 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/osu.Game/Overlays/Settings/SettingsCheckbox.cs b/osu.Game/Overlays/Settings/SettingsCheckbox.cs index a554159fd7..437b2e45b3 100644 --- a/osu.Game/Overlays/Settings/SettingsCheckbox.cs +++ b/osu.Game/Overlays/Settings/SettingsCheckbox.cs @@ -8,16 +8,14 @@ namespace osu.Game.Overlays.Settings { public class SettingsCheckbox : SettingsItem { - private OsuCheckbox checkbox; - private string labelText; - protected override Drawable CreateControl() => checkbox = new OsuCheckbox(); + protected override Drawable CreateControl() => new OsuCheckbox(); public override string LabelText { get => labelText; - set => checkbox.LabelText = labelText = value; + set => ((OsuCheckbox)Control).LabelText = labelText = value; } } } diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index e89f2adf0b..c2dd40d2a6 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.cs @@ -33,22 +33,24 @@ namespace osu.Game.Overlays.Settings protected readonly FillFlowContainer FlowContent; - private SpriteText text; + private SpriteText labelText; public bool ShowsDefaultIndicator = true; public virtual string LabelText { - get => text?.Text ?? string.Empty; + get => labelText?.Text ?? string.Empty; set { - if (text == null) + if (labelText == null) { // construct lazily for cases where the label is not needed (may be provided by the Control). - FlowContent.Insert(-1, text = new OsuSpriteText()); + FlowContent.Insert(-1, labelText = new OsuSpriteText()); + + updateDisabled(); } - text.Text = value; + labelText.Text = value; } } @@ -96,13 +98,19 @@ namespace osu.Game.Overlays.Settings if (controlWithCurrent != null) { controlWithCurrent.Current.ValueChanged += _ => SettingChanged?.Invoke(); - controlWithCurrent.Current.DisabledChanged += disabled => { Colour = disabled ? Color4.Gray : Color4.White; }; + controlWithCurrent.Current.DisabledChanged += _ => updateDisabled(); if (ShowsDefaultIndicator) restoreDefaultButton.Bindable = controlWithCurrent.Current; } } + private void updateDisabled() + { + if (labelText != null) + labelText.Alpha = controlWithCurrent.Current.Disabled ? 0.3f : 1; + } + private class RestoreDefaultValueButton : Container, IHasTooltip { private Bindable bindable; From a6b153673e2fa61dc5c7c674a5fb6b964008063d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 23 Mar 2020 14:58:02 +0900 Subject: [PATCH 0553/2376] Fix icons not updating tooltip text correctly --- osu.Game/Rulesets/UI/ModIcon.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/UI/ModIcon.cs b/osu.Game/Rulesets/UI/ModIcon.cs index 3cd1b0820d..8ea6c74349 100644 --- a/osu.Game/Rulesets/UI/ModIcon.cs +++ b/osu.Game/Rulesets/UI/ModIcon.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.UI private readonly ModType type; - public virtual string TooltipText { get; } + public virtual string TooltipText => mod.IconTooltip; private Mod mod; @@ -48,8 +48,6 @@ namespace osu.Game.Rulesets.UI type = mod.Type; - TooltipText = mod.IconTooltip; - Size = new Vector2(size); Children = new Drawable[] From 205f4dcb54f854fc36fcf8e322e20329e4e3144f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 23 Mar 2020 15:20:56 +0900 Subject: [PATCH 0554/2376] Simplify string construction logic --- osu.Game/Rulesets/Mods/Mod.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index f712fdc3be..0e5fe3fc9c 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -60,8 +60,9 @@ namespace osu.Game.Rulesets.Mods { get { - string settingDescription = string.IsNullOrEmpty(SettingDescription) ? string.Empty : $" ({SettingDescription})"; - return $"{Name}{settingDescription}"; + string description = SettingDescription; + + return string.IsNullOrEmpty(description) ? Name : $"{Name} ({description})"; } } @@ -81,13 +82,14 @@ namespace osu.Game.Rulesets.Mods foreach ((SettingSourceAttribute attr, PropertyInfo property) in this.GetOrderedSettingsSourceProperties()) { object bindableObj = property.GetValue(this); - bool? settingIsDefault = (bindableObj as IHasDefaultValue)?.IsDefault; - string tooltipText = settingIsDefault == true ? string.Empty : attr.Label + " " + bindableObj; - tooltipTexts.Add(tooltipText); + + if ((bindableObj as IHasDefaultValue)?.IsDefault == true) + continue; + + tooltipTexts.Add($"{attr.Label} {bindableObj}"); } - string joinedTooltipText = string.Join(", ", tooltipTexts.Where(s => !string.IsNullOrEmpty(s))); - return $"{joinedTooltipText}"; + return string.Join(", ", tooltipTexts.Where(s => !string.IsNullOrEmpty(s))); } } From 98e6896e934d5aa0f88a3977c7edc0a6e8005d92 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 23 Mar 2020 15:37:34 +0900 Subject: [PATCH 0555/2376] Rename test class --- .../{TestSceneFriendsLayout.cs => TestSceneFriendDisplay.cs} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename osu.Game.Tests/Visual/Online/{TestSceneFriendsLayout.cs => TestSceneFriendDisplay.cs} (97%) diff --git a/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs similarity index 97% rename from osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs rename to osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs index c0a617fe57..cf365a7614 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneFriendsLayout.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs @@ -13,7 +13,7 @@ using NUnit.Framework; namespace osu.Game.Tests.Visual.Online { - public class TestSceneFriendsLayout : OsuTestScene + public class TestSceneFriendDisplay : OsuTestScene { public override IReadOnlyList RequiredTypes => new[] { From 47c7673c9e7a855e02dd7d090830a7677657f64b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 23 Mar 2020 17:04:54 +0900 Subject: [PATCH 0556/2376] Fix crash when holding a key down while entering player --- osu.Game/Screens/Select/FilterControl.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index 6a03cfb68e..cad7672c71 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -136,6 +136,8 @@ namespace osu.Game.Screens.Select public void Deactivate() { + searchTextBox.ReadOnly = true; + searchTextBox.HoldFocus = false; if (searchTextBox.HasFocus) GetContainingInputManager().ChangeFocus(searchTextBox); @@ -143,6 +145,7 @@ namespace osu.Game.Screens.Select public void Activate() { + searchTextBox.ReadOnly = false; searchTextBox.HoldFocus = true; } From 232c2559867ceebfb8b61e0a979096c07c67c406 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 23 Mar 2020 17:33:02 +0900 Subject: [PATCH 0557/2376] Basic test scene setup --- .../ManiaInputTestScene.cs | 2 +- osu.Game.Rulesets.Osu/OsuInputManager.cs | 2 +- .../Gameplay/TestSceneReplayRecording.cs | 105 ++++++++++++++++++ osu.Game/Rulesets/UI/RulesetInputManager.cs | 2 +- 4 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 osu.Game.Tests/Gameplay/TestSceneReplayRecording.cs diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaInputTestScene.cs b/osu.Game.Rulesets.Mania.Tests/ManiaInputTestScene.cs index 909d0d45c6..9049bb3a82 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaInputTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaInputTestScene.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Mania.Tests { } - protected override RulesetKeyBindingContainer CreateKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) + protected override KeyBindingContainer CreateKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) => new LocalKeyBindingContainer(ruleset, variant, unique); private class LocalKeyBindingContainer : RulesetKeyBindingContainer diff --git a/osu.Game.Rulesets.Osu/OsuInputManager.cs b/osu.Game.Rulesets.Osu/OsuInputManager.cs index cdea7276f3..c8fe4f41ca 100644 --- a/osu.Game.Rulesets.Osu/OsuInputManager.cs +++ b/osu.Game.Rulesets.Osu/OsuInputManager.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu /// public bool AllowUserCursorMovement { get; set; } = true; - protected override RulesetKeyBindingContainer CreateKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) + protected override KeyBindingContainer CreateKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) => new OsuKeyBindingContainer(ruleset, variant, unique); public OsuInputManager(RulesetInfo ruleset) diff --git a/osu.Game.Tests/Gameplay/TestSceneReplayRecording.cs b/osu.Game.Tests/Gameplay/TestSceneReplayRecording.cs new file mode 100644 index 0000000000..0dc19cc3f2 --- /dev/null +++ b/osu.Game.Tests/Gameplay/TestSceneReplayRecording.cs @@ -0,0 +1,105 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; +using osu.Game.Rulesets; +using osu.Game.Rulesets.UI; +using osu.Game.Tests.Visual; +using osu.Game.Tests.Visual.UserInterface; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Tests.Gameplay +{ + public class TestSceneReplayRecording : OsuTestScene + { + public TestSceneReplayRecording() + { + Add(new TestRulesetInputManager(new TestSceneModSettings.TestRulesetInfo(), 0, SimultaneousBindingMode.Unique) + { + Child = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + Colour = Color4.Brown, + RelativeSizeAxes = Axes.Both, + }, + new TestConsumer() + } + }, + }); + } + + public class TestConsumer : CompositeDrawable, IKeyBindingHandler + { + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; + + private readonly Box box; + + public TestConsumer() + { + Size = new Vector2(30); + + Origin = Anchor.Centre; + + InternalChildren = new Drawable[] + { + box = new Box + { + Colour = Color4.Black, + RelativeSizeAxes = Axes.Both, + }, + }; + } + + protected override bool OnMouseMove(MouseMoveEvent e) + { + this.Position = e.MousePosition; + return base.OnMouseMove(e); + } + + public bool OnPressed(TestAction action) + { + box.Colour = Color4.White; + return true; + } + + public void OnReleased(TestAction action) + { + box.Colour = Color4.Black; + } + } + + private class TestRulesetInputManager : RulesetInputManager + { + public TestRulesetInputManager(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) + : base(ruleset, variant, unique) + { + } + + protected override KeyBindingContainer CreateKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) + => new TestKeyBindingContainer(); + + internal class TestKeyBindingContainer : KeyBindingContainer + { + public override IEnumerable DefaultKeyBindings => new[] + { + new KeyBinding(InputKey.MouseLeft, TestAction.Down), + }; + } + } + + public enum TestAction + { + Down, + } + } +} diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index 41b2739fc5..7f85c10b56 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -148,7 +148,7 @@ namespace osu.Game.Rulesets.UI #endregion - protected virtual RulesetKeyBindingContainer CreateKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) + protected virtual KeyBindingContainer CreateKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) => new RulesetKeyBindingContainer(ruleset, variant, unique); public class RulesetKeyBindingContainer : DatabasedKeyBindingContainer From 66f2a52dd286693aa0aecd6b3e0f343acef23f12 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2020 08:59:47 +0000 Subject: [PATCH 0558/2376] Bump Sentry from 2.1.0 to 2.1.1 Bumps [Sentry](https://github.com/getsentry/sentry-dotnet) from 2.1.0 to 2.1.1. - [Release notes](https://github.com/getsentry/sentry-dotnet/releases) - [Commits](https://github.com/getsentry/sentry-dotnet/compare/2.1.0...2.1.1) Signed-off-by: dependabot-preview[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 46d17bcf05..3894c06994 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -24,7 +24,7 @@ - + From 467066112f3845393623fa9dc0b2d470a7260935 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 23 Mar 2020 18:50:16 +0900 Subject: [PATCH 0559/2376] Initial record/playback implementation --- .../Gameplay/TestSceneReplayRecording.cs | 317 ++++++++++++++---- 1 file changed, 247 insertions(+), 70 deletions(-) diff --git a/osu.Game.Tests/Gameplay/TestSceneReplayRecording.cs b/osu.Game.Tests/Gameplay/TestSceneReplayRecording.cs index 0dc19cc3f2..3ff11bbd4e 100644 --- a/osu.Game.Tests/Gameplay/TestSceneReplayRecording.cs +++ b/osu.Game.Tests/Gameplay/TestSceneReplayRecording.cs @@ -1,13 +1,20 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Framework.Input.StateChanges; +using osu.Framework.Input.States; +using osu.Framework.Logging; +using osu.Game.Graphics.Sprites; +using osu.Game.Replays; using osu.Game.Rulesets; +using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.UI; using osu.Game.Tests.Visual; using osu.Game.Tests.Visual.UserInterface; @@ -18,88 +25,258 @@ namespace osu.Game.Tests.Gameplay { public class TestSceneReplayRecording : OsuTestScene { + private readonly TestRulesetInputManager playbackManager; + public TestSceneReplayRecording() { - Add(new TestRulesetInputManager(new TestSceneModSettings.TestRulesetInfo(), 0, SimultaneousBindingMode.Unique) + Replay replay = new Replay(); + + Add(new GridContainer { - Child = new Container + RelativeSizeAxes = Axes.Both, + Content = new[] { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + new Drawable[] { - new Box + new TestRulesetInputManager(new TestSceneModSettings.TestRulesetInfo(), 0, SimultaneousBindingMode.Unique) { - Colour = Color4.Brown, - RelativeSizeAxes = Axes.Both, - }, - new TestConsumer() + RecordTarget = replay, + Child = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + Colour = Color4.Brown, + RelativeSizeAxes = Axes.Both, + }, + new OsuSpriteText + { + Text = "Recording", + Scale = new Vector2(3), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + new TestConsumer() + } + }, + } + }, + new Drawable[] + { + playbackManager = new TestRulesetInputManager(new TestSceneModSettings.TestRulesetInfo(), 0, SimultaneousBindingMode.Unique) + { + ReplayInputHandler = new TestFramedReplayInputHandler(replay) + { + GamefieldToScreenSpace = pos => playbackManager.ToScreenSpace(pos), + }, + Child = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + Colour = Color4.DarkBlue, + RelativeSizeAxes = Axes.Both, + }, + new OsuSpriteText + { + Text = "Playback", + Scale = new Vector2(3), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + new TestConsumer() + } + }, + } } - }, + } }); } - public class TestConsumer : CompositeDrawable, IKeyBindingHandler + protected override void Update() { - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; + base.Update(); - private readonly Box box; - - public TestConsumer() - { - Size = new Vector2(30); - - Origin = Anchor.Centre; - - InternalChildren = new Drawable[] - { - box = new Box - { - Colour = Color4.Black, - RelativeSizeAxes = Axes.Both, - }, - }; - } - - protected override bool OnMouseMove(MouseMoveEvent e) - { - this.Position = e.MousePosition; - return base.OnMouseMove(e); - } - - public bool OnPressed(TestAction action) - { - box.Colour = Color4.White; - return true; - } - - public void OnReleased(TestAction action) - { - box.Colour = Color4.Black; - } - } - - private class TestRulesetInputManager : RulesetInputManager - { - public TestRulesetInputManager(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) - : base(ruleset, variant, unique) - { - } - - protected override KeyBindingContainer CreateKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) - => new TestKeyBindingContainer(); - - internal class TestKeyBindingContainer : KeyBindingContainer - { - public override IEnumerable DefaultKeyBindings => new[] - { - new KeyBinding(InputKey.MouseLeft, TestAction.Down), - }; - } - } - - public enum TestAction - { - Down, + playbackManager.ReplayInputHandler.SetFrameFromTime(Time.Current - 500); } } + + public class TestFramedReplayInputHandler : FramedReplayInputHandler + { + public TestFramedReplayInputHandler(Replay replay) + : base(replay) + { + } + + public override List GetPendingInputs() + { + return new List + { + new MousePositionAbsoluteInput + { + Position = GamefieldToScreenSpace(CurrentFrame?.Position ?? Vector2.Zero) + }, + new ReplayState + { + PressedActions = CurrentFrame?.Actions ?? new List() + } + }; + } + } + + public class TestConsumer : CompositeDrawable, IKeyBindingHandler + { + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Parent.ReceivePositionalInputAt(screenSpacePos); + + private readonly Box box; + + public TestConsumer() + { + Size = new Vector2(30); + + Origin = Anchor.Centre; + + InternalChildren = new Drawable[] + { + box = new Box + { + Colour = Color4.Black, + RelativeSizeAxes = Axes.Both, + }, + }; + } + + protected override bool OnMouseMove(MouseMoveEvent e) + { + Position = e.MousePosition; + return base.OnMouseMove(e); + } + + public bool OnPressed(TestAction action) + { + box.Colour = Color4.White; + return true; + } + + public void OnReleased(TestAction action) + { + box.Colour = Color4.Black; + } + } + + public class TestRulesetInputManager : RulesetInputManager + { + private ReplayRecorder recorder; + + public Replay RecordTarget + { + set + { + if (recorder != null) + throw new InvalidOperationException("Cannot attach more than one recorder"); + + KeyBindingContainer.Add(recorder = new TestReplayRecorder(value)); + } + } + + public TestRulesetInputManager(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) + : base(ruleset, variant, unique) + { + } + + protected override KeyBindingContainer CreateKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) + => new TestKeyBindingContainer(); + + internal class TestKeyBindingContainer : KeyBindingContainer + { + public override IEnumerable DefaultKeyBindings => new[] + { + new KeyBinding(InputKey.MouseLeft, TestAction.Down), + }; + } + } + + public class TestReplayFrame : ReplayFrame + { + public Vector2 Position; + + public List Actions = new List(); + + public TestReplayFrame() + { + } + + public TestReplayFrame(double time, Vector2 position, params TestAction[] actions) + : base(time) + { + Position = position; + Actions.AddRange(actions); + } + } + + public enum TestAction + { + Down, + } + + internal class TestReplayRecorder : ReplayRecorder + { + public TestReplayRecorder(Replay target) + : base(target) + { + } + + protected override ReplayFrame HandleFrame(InputState state, List pressedActions) => + new TestReplayFrame(Time.Current, ToLocalSpace(state.Mouse.Position), pressedActions.ToArray()); + } + + internal abstract class ReplayRecorder : Component, IKeyBindingHandler + where T : struct + { + private readonly Replay target; + + private readonly List pressedActions = new List(); + + protected ReplayRecorder(Replay target) + { + this.target = target; + + RelativeSizeAxes = Axes.Both; + + Depth = float.MinValue; + } + + protected override bool OnMouseMove(MouseMoveEvent e) + { + recordFrame(); + return base.OnMouseMove(e); + } + + public bool OnPressed(T action) + { + pressedActions.Add(action); + recordFrame(); + return false; + } + + public void OnReleased(T action) + { + pressedActions.Remove(action); + recordFrame(); + } + + private void recordFrame() + { + var frame = HandleFrame(GetContainingInputManager().CurrentState, pressedActions); + + if (frame != null) + target.Frames.Add(frame); + } + + protected abstract ReplayFrame HandleFrame(InputState state, List pressedActions); + } } From d5bc4915e6fd3961ae4a4c6b62059cddc5740817 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 23 Mar 2020 19:02:45 +0900 Subject: [PATCH 0560/2376] Add "important" frames and record rate options --- .../Gameplay/TestSceneReplayRecording.cs | 37 +++++++++++++------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Gameplay/TestSceneReplayRecording.cs b/osu.Game.Tests/Gameplay/TestSceneReplayRecording.cs index 3ff11bbd4e..c2b2ebdb98 100644 --- a/osu.Game.Tests/Gameplay/TestSceneReplayRecording.cs +++ b/osu.Game.Tests/Gameplay/TestSceneReplayRecording.cs @@ -3,14 +3,15 @@ using System; using System.Collections.Generic; +using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Input.StateChanges; using osu.Framework.Input.States; -using osu.Framework.Logging; using osu.Game.Graphics.Sprites; using osu.Game.Replays; using osu.Game.Rulesets; @@ -206,10 +207,6 @@ namespace osu.Game.Tests.Gameplay public List Actions = new List(); - public TestReplayFrame() - { - } - public TestReplayFrame(double time, Vector2 position, params TestAction[] actions) : base(time) { @@ -230,7 +227,7 @@ namespace osu.Game.Tests.Gameplay { } - protected override ReplayFrame HandleFrame(InputState state, List pressedActions) => + protected override ReplayFrame HandleFrame(InputState state, List pressedActions, ReplayFrame previousFrame) => new TestReplayFrame(Time.Current, ToLocalSpace(state.Mouse.Position), pressedActions.ToArray()); } @@ -241,6 +238,10 @@ namespace osu.Game.Tests.Gameplay private readonly List pressedActions = new List(); + private InputManager inputManager; + + public int RecordFrameRate = 60; + protected ReplayRecorder(Replay target) { this.target = target; @@ -250,33 +251,45 @@ namespace osu.Game.Tests.Gameplay Depth = float.MinValue; } + protected override void LoadComplete() + { + base.LoadComplete(); + + inputManager = GetContainingInputManager(); + } + protected override bool OnMouseMove(MouseMoveEvent e) { - recordFrame(); + recordFrame(false); return base.OnMouseMove(e); } public bool OnPressed(T action) { pressedActions.Add(action); - recordFrame(); + recordFrame(true); return false; } public void OnReleased(T action) { pressedActions.Remove(action); - recordFrame(); + recordFrame(true); } - private void recordFrame() + private void recordFrame(bool important) { - var frame = HandleFrame(GetContainingInputManager().CurrentState, pressedActions); + var last = target.Frames.LastOrDefault(); + + if (!important && last != null && Time.Current - last.Time < (1000d / RecordFrameRate)) + return; + + var frame = HandleFrame(inputManager.CurrentState, pressedActions, last); if (frame != null) target.Frames.Add(frame); } - protected abstract ReplayFrame HandleFrame(InputState state, List pressedActions); + protected abstract ReplayFrame HandleFrame(InputState state, List testActions, ReplayFrame previousFrame); } } From 6d480680612b2bf0f875e36fa7d86ddc16464013 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 23 Mar 2020 19:03:42 +0900 Subject: [PATCH 0561/2376] Move replay recorder to final location --- osu.Game.Rulesets.Osu/UI/OsuReplayRecorder.cs | 23 ++++++ .../Gameplay/TestSceneReplayRecording.cs | 80 +----------------- osu.Game/Rulesets/UI/DrawableRuleset.cs | 2 + osu.Game/Rulesets/UI/ReplayRecorder.cs | 81 +++++++++++++++++++ osu.Game/Rulesets/UI/RulesetInputManager.cs | 16 ++++ 5 files changed, 123 insertions(+), 79 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/UI/OsuReplayRecorder.cs create mode 100644 osu.Game/Rulesets/UI/ReplayRecorder.cs diff --git a/osu.Game.Rulesets.Osu/UI/OsuReplayRecorder.cs b/osu.Game.Rulesets.Osu/UI/OsuReplayRecorder.cs new file mode 100644 index 0000000000..898212ee6b --- /dev/null +++ b/osu.Game.Rulesets.Osu/UI/OsuReplayRecorder.cs @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Game.Replays; +using osu.Game.Rulesets.Osu.Replays; +using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.UI; +using osuTK; + +namespace osu.Game.Rulesets.Osu.UI +{ + public class OsuReplayRecorder : ReplayRecorder + { + public OsuReplayRecorder(Replay replay) + : base(replay) + { + } + + protected override ReplayFrame HandleFrame(Vector2 position, List actions, ReplayFrame previousFrame) + => new OsuReplayFrame(Time.Current, position, actions.ToArray()); + } +} diff --git a/osu.Game.Tests/Gameplay/TestSceneReplayRecording.cs b/osu.Game.Tests/Gameplay/TestSceneReplayRecording.cs index c2b2ebdb98..ab1998a650 100644 --- a/osu.Game.Tests/Gameplay/TestSceneReplayRecording.cs +++ b/osu.Game.Tests/Gameplay/TestSceneReplayRecording.cs @@ -1,13 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; -using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Input.StateChanges; @@ -41,7 +38,7 @@ namespace osu.Game.Tests.Gameplay { new TestRulesetInputManager(new TestSceneModSettings.TestRulesetInfo(), 0, SimultaneousBindingMode.Unique) { - RecordTarget = replay, + Recorder = new TestReplayRecorder(replay), Child = new Container { RelativeSizeAxes = Axes.Both, @@ -171,19 +168,6 @@ namespace osu.Game.Tests.Gameplay public class TestRulesetInputManager : RulesetInputManager { - private ReplayRecorder recorder; - - public Replay RecordTarget - { - set - { - if (recorder != null) - throw new InvalidOperationException("Cannot attach more than one recorder"); - - KeyBindingContainer.Add(recorder = new TestReplayRecorder(value)); - } - } - public TestRulesetInputManager(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) : base(ruleset, variant, unique) { @@ -230,66 +214,4 @@ namespace osu.Game.Tests.Gameplay protected override ReplayFrame HandleFrame(InputState state, List pressedActions, ReplayFrame previousFrame) => new TestReplayFrame(Time.Current, ToLocalSpace(state.Mouse.Position), pressedActions.ToArray()); } - - internal abstract class ReplayRecorder : Component, IKeyBindingHandler - where T : struct - { - private readonly Replay target; - - private readonly List pressedActions = new List(); - - private InputManager inputManager; - - public int RecordFrameRate = 60; - - protected ReplayRecorder(Replay target) - { - this.target = target; - - RelativeSizeAxes = Axes.Both; - - Depth = float.MinValue; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - inputManager = GetContainingInputManager(); - } - - protected override bool OnMouseMove(MouseMoveEvent e) - { - recordFrame(false); - return base.OnMouseMove(e); - } - - public bool OnPressed(T action) - { - pressedActions.Add(action); - recordFrame(true); - return false; - } - - public void OnReleased(T action) - { - pressedActions.Remove(action); - recordFrame(true); - } - - private void recordFrame(bool important) - { - var last = target.Frames.LastOrDefault(); - - if (!important && last != null && Time.Current - last.Time < (1000d / RecordFrameRate)) - return; - - var frame = HandleFrame(inputManager.CurrentState, pressedActions, last); - - if (frame != null) - target.Frames.Add(frame); - } - - protected abstract ReplayFrame HandleFrame(InputState state, List testActions, ReplayFrame previousFrame); - } } diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index d0a2722f58..c8af3be980 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -302,6 +302,8 @@ namespace osu.Game.Rulesets.UI protected virtual ReplayInputHandler CreateReplayInputHandler(Replay replay) => null; + protected virtual ReplayRecorder CreateReplayRecorder(Replay replay) => null; + /// /// Creates a Playfield. /// diff --git a/osu.Game/Rulesets/UI/ReplayRecorder.cs b/osu.Game/Rulesets/UI/ReplayRecorder.cs new file mode 100644 index 0000000000..9e2f898206 --- /dev/null +++ b/osu.Game/Rulesets/UI/ReplayRecorder.cs @@ -0,0 +1,81 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Graphics; +using osu.Framework.Input; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; +using osu.Framework.Input.States; +using osu.Game.Replays; +using osu.Game.Rulesets.Replays; + +namespace osu.Game.Rulesets.UI +{ + public abstract class ReplayRecorder : ReplayRecorder, IKeyBindingHandler + where T : struct + { + private readonly Replay target; + + private readonly List pressedActions = new List(); + + private InputManager inputManager; + + public int RecordFrameRate = 60; + + protected ReplayRecorder(Replay target) + { + this.target = target; + + RelativeSizeAxes = Axes.Both; + + Depth = float.MinValue; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + inputManager = GetContainingInputManager(); + } + + protected override bool OnMouseMove(MouseMoveEvent e) + { + recordFrame(false); + return base.OnMouseMove(e); + } + + public bool OnPressed(T action) + { + pressedActions.Add(action); + recordFrame(true); + return false; + } + + public void OnReleased(T action) + { + pressedActions.Remove(action); + recordFrame(true); + } + + private void recordFrame(bool important) + { + var last = target.Frames.LastOrDefault(); + + if (!important && last != null && Time.Current - last.Time < (1000d / RecordFrameRate)) + return; + + var frame = HandleFrame(inputManager.CurrentState, pressedActions, last); + + if (frame != null) + target.Frames.Add(frame); + } + + protected abstract ReplayFrame HandleFrame(InputState state, List testActions, ReplayFrame previousFrame); + } + + public abstract class ReplayRecorder : Component + { + } +} diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index 7f85c10b56..043e0f56cc 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -26,6 +27,21 @@ namespace osu.Game.Rulesets.UI public abstract class RulesetInputManager : PassThroughInputManager, ICanAttachKeyCounter, IHasReplayHandler where T : struct { + private ReplayRecorder recorder; + + public ReplayRecorder Recorder + { + set + { + if (recorder != null) + throw new InvalidOperationException("Cannot attach more than one recorder"); + + recorder = value; + + KeyBindingContainer.Add(recorder); + } + } + protected override InputState CreateInitialState() { var state = base.CreateInitialState(); From 14a85a84bf6001336b82702b7a9e5ba815bab0a1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 23 Mar 2020 19:18:56 +0900 Subject: [PATCH 0562/2376] Add proper screen space - gamefield mapping --- osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs | 2 ++ .../Gameplay/TestSceneReplayRecording.cs | 14 +++++++++----- osu.Game/Rulesets/UI/Playfield.cs | 5 +++++ osu.Game/Rulesets/UI/ReplayRecorder.cs | 10 +++++++--- 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs index a37ef8d9a0..b4d51d11c9 100644 --- a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs @@ -58,6 +58,8 @@ namespace osu.Game.Rulesets.Osu.UI protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new OsuFramedReplayInputHandler(replay); + protected override ReplayRecorder CreateReplayRecorder(Replay replay) => new OsuReplayRecorder(replay); + public override double GameplayStartTime { get diff --git a/osu.Game.Tests/Gameplay/TestSceneReplayRecording.cs b/osu.Game.Tests/Gameplay/TestSceneReplayRecording.cs index ab1998a650..fe87ca675b 100644 --- a/osu.Game.Tests/Gameplay/TestSceneReplayRecording.cs +++ b/osu.Game.Tests/Gameplay/TestSceneReplayRecording.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Input.StateChanges; -using osu.Framework.Input.States; using osu.Game.Graphics.Sprites; using osu.Game.Replays; using osu.Game.Rulesets; @@ -25,6 +24,8 @@ namespace osu.Game.Tests.Gameplay { private readonly TestRulesetInputManager playbackManager; + private readonly TestRulesetInputManager recordingManager; + public TestSceneReplayRecording() { Replay replay = new Replay(); @@ -36,9 +37,12 @@ namespace osu.Game.Tests.Gameplay { new Drawable[] { - new TestRulesetInputManager(new TestSceneModSettings.TestRulesetInfo(), 0, SimultaneousBindingMode.Unique) + recordingManager = new TestRulesetInputManager(new TestSceneModSettings.TestRulesetInfo(), 0, SimultaneousBindingMode.Unique) { - Recorder = new TestReplayRecorder(replay), + Recorder = new TestReplayRecorder(replay) + { + ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos) + }, Child = new Container { RelativeSizeAxes = Axes.Both, @@ -211,7 +215,7 @@ namespace osu.Game.Tests.Gameplay { } - protected override ReplayFrame HandleFrame(InputState state, List pressedActions, ReplayFrame previousFrame) => - new TestReplayFrame(Time.Current, ToLocalSpace(state.Mouse.Position), pressedActions.ToArray()); + protected override ReplayFrame HandleFrame(Vector2 position, List actions, ReplayFrame previousFrame) => + new TestReplayFrame(Time.Current, position, actions.ToArray()); } } diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 047047ccfd..8141108aef 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -30,6 +30,11 @@ namespace osu.Game.Rulesets.UI /// public Func GamefieldToScreenSpace => HitObjectContainer.ToScreenSpace; + /// + /// A function that converts screen space coordinates to gamefield. + /// + public Func ScreenSpaceToGamefield => HitObjectContainer.ToLocalSpace; + /// /// All the s contained in this and all . /// diff --git a/osu.Game/Rulesets/UI/ReplayRecorder.cs b/osu.Game/Rulesets/UI/ReplayRecorder.cs index 9e2f898206..74e8109d52 100644 --- a/osu.Game/Rulesets/UI/ReplayRecorder.cs +++ b/osu.Game/Rulesets/UI/ReplayRecorder.cs @@ -1,15 +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.Collections.Generic; using System.Linq; using osu.Framework.Graphics; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; -using osu.Framework.Input.States; using osu.Game.Replays; using osu.Game.Rulesets.Replays; +using osuTK; namespace osu.Game.Rulesets.UI { @@ -66,16 +67,19 @@ namespace osu.Game.Rulesets.UI if (!important && last != null && Time.Current - last.Time < (1000d / RecordFrameRate)) return; - var frame = HandleFrame(inputManager.CurrentState, pressedActions, last); + var position = ScreenSpaceToGamefield?.Invoke(inputManager.CurrentState.Mouse.Position) ?? inputManager.CurrentState.Mouse.Position; + + var frame = HandleFrame(position, pressedActions, last); if (frame != null) target.Frames.Add(frame); } - protected abstract ReplayFrame HandleFrame(InputState state, List testActions, ReplayFrame previousFrame); + protected abstract ReplayFrame HandleFrame(Vector2 position, List actions, ReplayFrame previousFrame); } public abstract class ReplayRecorder : Component { + public Func ScreenSpaceToGamefield; } } From 617149fb2702a4686ee9ce69200ea7c84f250d35 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 23 Mar 2020 19:31:43 +0900 Subject: [PATCH 0563/2376] Implement in player --- osu.Game/Rulesets/UI/DrawableRuleset.cs | 17 +++++++++++++++++ osu.Game/Rulesets/UI/RulesetInputManager.cs | 7 ++++++- osu.Game/Screens/Play/Player.cs | 18 ++++++++++++++++++ osu.Game/Screens/Play/ReplayPlayer.cs | 3 +-- 4 files changed, 42 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index c8af3be980..5c57a92cd1 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -262,6 +262,17 @@ namespace osu.Game.Rulesets.UI Playfield.Add(drawableObject); } + public override void SetRecordTarget(Replay recordingReplay) + { + if (!(KeyBindingInputManager is IHasRecordingHandler recordingInputHandler)) + throw new InvalidOperationException($"A {nameof(KeyBindingInputManager)} which supports recording is not available"); + + var recorder = CreateReplayRecorder(recordingReplay); + recorder.ScreenSpaceToGamefield = Playfield.ScreenSpaceToGamefield; + + recordingInputHandler.Recorder = recorder; + } + public override void SetReplayScore(Score replayScore) { if (!(KeyBindingInputManager is IHasReplayHandler replayInputManager)) @@ -472,6 +483,12 @@ namespace osu.Game.Rulesets.UI /// The replay, null for local input. public abstract void SetReplayScore(Score replayScore); + /// + /// Sets a replay to be used to record gameplay. + /// + /// The target to be recorded to. + public abstract void SetRecordTarget(Replay recordingReplay); + /// /// Invoked when the interactive user requests resuming from a paused state. /// Allows potentially delaying the resume process until an interaction is performed. diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index 043e0f56cc..ba30fe28d5 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -24,7 +24,7 @@ using MouseState = osu.Framework.Input.States.MouseState; namespace osu.Game.Rulesets.UI { - public abstract class RulesetInputManager : PassThroughInputManager, ICanAttachKeyCounter, IHasReplayHandler + public abstract class RulesetInputManager : PassThroughInputManager, ICanAttachKeyCounter, IHasReplayHandler, IHasRecordingHandler where T : struct { private ReplayRecorder recorder; @@ -184,6 +184,11 @@ namespace osu.Game.Rulesets.UI ReplayInputHandler ReplayInputHandler { get; set; } } + public interface IHasRecordingHandler + { + public ReplayRecorder Recorder { set; } + } + /// /// Supports attaching a . /// Keys will be populated automatically and a receptor will be injected inside. diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index a120963abd..8fee516f2b 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -19,6 +19,7 @@ using osu.Game.Configuration; using osu.Game.Graphics.Containers; using osu.Game.Online.API; using osu.Game.Overlays; +using osu.Game.Replays; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; @@ -118,6 +119,23 @@ namespace osu.Game.Screens.Play protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + protected override void LoadComplete() + { + base.LoadComplete(); + + PrepareReplay(); + } + + private Replay recordingReplay; + + /// + /// Run any recording / playback setup for replays. + /// + protected virtual void PrepareReplay() + { + DrawableRuleset.SetRecordTarget(recordingReplay = new Replay()); + } + [BackgroundDependencyLoader] private void load(AudioManager audio, OsuConfigManager config) { diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index b040549efc..8708b5f634 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -18,9 +18,8 @@ namespace osu.Game.Screens.Play this.score = score; } - protected override void LoadComplete() + protected override void PrepareReplay() { - base.LoadComplete(); DrawableRuleset?.SetReplayScore(score); } From 2fa42ed644d4dede960ec3e1bb4c424551b22ec6 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Mon, 23 Mar 2020 12:54:08 -0400 Subject: [PATCH 0564/2376] use N2 for ModTimeRamp, add x text --- osu.Game/Rulesets/Mods/ModTimeRamp.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index f21ba684b4..c1f3e357a1 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Mods [SettingSource("Final rate", "The final speed to ramp to")] public abstract BindableNumber FinalRate { get; } - public override string SettingDescription => $"{InitialRate.Value:0.#} to {FinalRate.Value:N1}"; + public override string SettingDescription => $"{InitialRate.Value:N2}x to {FinalRate.Value:N2}x"; private double finalRateTime; private double beginRampTime; From 96848405fd8df958850d4421c5eb31625416586f Mon Sep 17 00:00:00 2001 From: Joehu Date: Mon, 23 Mar 2020 10:54:45 -0700 Subject: [PATCH 0565/2376] Fix song select filter not absorbing input from carousel --- osu.Game/Screens/Select/FilterControl.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index 6a03cfb68e..c831e1dcc4 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -16,6 +16,7 @@ using Container = osu.Framework.Graphics.Containers.Container; using osu.Framework.Graphics.Shapes; using osu.Game.Configuration; using osu.Game.Rulesets; +using osu.Framework.Input.Events; namespace osu.Game.Screens.Select { @@ -184,5 +185,7 @@ namespace osu.Game.Screens.Select } private void updateCriteria() => FilterChanged?.Invoke(CreateCriteria()); + + protected override bool OnClick(ClickEvent e) => true; } } From 96d962ab307dfd6c6f7e5213ae5bead1ef241403 Mon Sep 17 00:00:00 2001 From: Joehu Date: Mon, 23 Mar 2020 11:25:40 -0700 Subject: [PATCH 0566/2376] Fix autoplay keyboard shortcut not working with keypad enter key --- osu.Game/Screens/Select/PlaySongSelect.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index 9e2f5761dd..8b0547376d 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -56,6 +56,7 @@ namespace osu.Game.Screens.Select switch (e.Key) { case Key.Enter: + case Key.KeypadEnter: // this is a special hard-coded case; we can't rely on OnPressed (of SongSelect) as GlobalActionContainer is // matching with exact modifier consideration (so Ctrl+Enter would be ignored). FinaliseSelection(); From 5bc51193891766d39cd564a06d52383bacad3f7a Mon Sep 17 00:00:00 2001 From: Joehu Date: Mon, 23 Mar 2020 16:03:33 -0700 Subject: [PATCH 0567/2376] Handle OnHover on song select filter and footer --- osu.Game/Screens/Select/FilterControl.cs | 2 ++ osu.Game/Screens/Select/Footer.cs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index c831e1dcc4..a4a7ac5c9d 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -187,5 +187,7 @@ namespace osu.Game.Screens.Select private void updateCriteria() => FilterChanged?.Invoke(CreateCriteria()); protected override bool OnClick(ClickEvent e) => true; + + protected override bool OnHover(HoverEvent e) => true; } } diff --git a/osu.Game/Screens/Select/Footer.cs b/osu.Game/Screens/Select/Footer.cs index 1dc7081c1c..689a11166a 100644 --- a/osu.Game/Screens/Select/Footer.cs +++ b/osu.Game/Screens/Select/Footer.cs @@ -107,5 +107,7 @@ namespace osu.Game.Screens.Select protected override bool OnMouseDown(MouseDownEvent e) => true; protected override bool OnClick(ClickEvent e) => true; + + protected override bool OnHover(HoverEvent e) => true; } } From e5f4d8686e04b610ac560b3e62cd60c3f7425445 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Mar 2020 10:38:24 +0900 Subject: [PATCH 0568/2376] Rename decoder --- ...dLegacyScoreParser.cs => DatabasedLegacyScoreDecoder.cs} | 6 +++--- .../Legacy/{LegacyScoreParser.cs => LegacyScoreDecoder.cs} | 2 +- osu.Game/Scoring/LegacyDatabasedScore.cs | 2 +- osu.Game/Scoring/ScoreManager.cs | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) rename osu.Game/Scoring/Legacy/{DatabasedLegacyScoreParser.cs => DatabasedLegacyScoreDecoder.cs} (74%) rename osu.Game/Scoring/Legacy/{LegacyScoreParser.cs => LegacyScoreDecoder.cs} (99%) diff --git a/osu.Game/Scoring/Legacy/DatabasedLegacyScoreParser.cs b/osu.Game/Scoring/Legacy/DatabasedLegacyScoreDecoder.cs similarity index 74% rename from osu.Game/Scoring/Legacy/DatabasedLegacyScoreParser.cs rename to osu.Game/Scoring/Legacy/DatabasedLegacyScoreDecoder.cs index 2115d784a0..9b590f56dd 100644 --- a/osu.Game/Scoring/Legacy/DatabasedLegacyScoreParser.cs +++ b/osu.Game/Scoring/Legacy/DatabasedLegacyScoreDecoder.cs @@ -7,15 +7,15 @@ using osu.Game.Rulesets; namespace osu.Game.Scoring.Legacy { /// - /// A which retrieves the applicable and + /// A which retrieves the applicable and /// for the score from the database. /// - public class DatabasedLegacyScoreParser : LegacyScoreParser + public class DatabasedLegacyScoreDecoder : LegacyScoreDecoder { private readonly RulesetStore rulesets; private readonly BeatmapManager beatmaps; - public DatabasedLegacyScoreParser(RulesetStore rulesets, BeatmapManager beatmaps) + public DatabasedLegacyScoreDecoder(RulesetStore rulesets, BeatmapManager beatmaps) { this.rulesets = rulesets; this.beatmaps = beatmaps; diff --git a/osu.Game/Scoring/Legacy/LegacyScoreParser.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs similarity index 99% rename from osu.Game/Scoring/Legacy/LegacyScoreParser.cs rename to osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index 19d8410cc2..f29e98b0b4 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreParser.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -19,7 +19,7 @@ using SharpCompress.Compressors.LZMA; namespace osu.Game.Scoring.Legacy { - public abstract class LegacyScoreParser + public abstract class LegacyScoreDecoder { private IBeatmap currentBeatmap; private Ruleset currentRuleset; diff --git a/osu.Game/Scoring/LegacyDatabasedScore.cs b/osu.Game/Scoring/LegacyDatabasedScore.cs index 172e08e2d0..bd673eaa29 100644 --- a/osu.Game/Scoring/LegacyDatabasedScore.cs +++ b/osu.Game/Scoring/LegacyDatabasedScore.cs @@ -19,7 +19,7 @@ namespace osu.Game.Scoring var replayFilename = score.Files.First(f => f.Filename.EndsWith(".osr", StringComparison.InvariantCultureIgnoreCase)).FileInfo.StoragePath; using (var stream = store.GetStream(replayFilename)) - Replay = new DatabasedLegacyScoreParser(rulesets, beatmaps).Parse(stream).Replay; + Replay = new DatabasedLegacyScoreDecoder(rulesets, beatmaps).Parse(stream).Replay; } } } diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 249f0a932b..d5bd486e43 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -46,9 +46,9 @@ namespace osu.Game.Scoring { try { - return new DatabasedLegacyScoreParser(rulesets, beatmaps()).Parse(stream).ScoreInfo; + return new DatabasedLegacyScoreDecoder(rulesets, beatmaps()).Parse(stream).ScoreInfo; } - catch (LegacyScoreParser.BeatmapNotFoundException e) + catch (LegacyScoreDecoder.BeatmapNotFoundException e) { Logger.Log(e.Message, LoggingTarget.Information, LogLevel.Error); return null; From 546772192ce30e0afd20e9830864de65ac11680d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Mar 2020 12:06:24 +0900 Subject: [PATCH 0569/2376] Add helper method to convert to legacy mods enums --- osu.Game.Rulesets.Catch/CatchRuleset.cs | 2 +- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 55 ++++++++++++++++- osu.Game.Rulesets.Osu/OsuRuleset.cs | 2 +- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 2 +- osu.Game/Rulesets/Ruleset.cs | 60 ++++++++++++++++++- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 2 +- osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs | 35 +++++++++++ .../Tests/Beatmaps/LegacyModConversionTest.cs | 2 +- 8 files changed, 151 insertions(+), 9 deletions(-) create mode 100644 osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index b9d791fdb1..212365caad 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Catch new KeyBinding(InputKey.Shift, CatchAction.Dash), }; - public override IEnumerable ConvertLegacyMods(LegacyMods mods) + public override IEnumerable ConvertFromLegacyMods(LegacyMods mods) { if (mods.HasFlag(LegacyMods.Nightcore)) yield return new CatchModNightcore(); diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index b7b523a94d..9d06bd7c25 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Mania public override ISkin CreateLegacySkinProvider(ISkinSource source) => new ManiaLegacySkinTransformer(source); - public override IEnumerable ConvertLegacyMods(LegacyMods mods) + public override IEnumerable ConvertFromLegacyMods(LegacyMods mods) { if (mods.HasFlag(LegacyMods.Nightcore)) yield return new ManiaModNightcore(); @@ -118,6 +118,59 @@ namespace osu.Game.Rulesets.Mania yield return new ManiaModRandom(); } + public override LegacyMods ConvertToLegacyMods(Mod[] mods) + { + var value = base.ConvertToLegacyMods(mods); + + foreach (var mod in mods) + { + switch (mod) + { + case ManiaModKey1 _: + value |= LegacyMods.Key1; + break; + + case ManiaModKey2 _: + value |= LegacyMods.Key2; + break; + + case ManiaModKey3 _: + value |= LegacyMods.Key3; + break; + + case ManiaModKey4 _: + value |= LegacyMods.Key4; + break; + + case ManiaModKey5 _: + value |= LegacyMods.Key5; + break; + + case ManiaModKey6 _: + value |= LegacyMods.Key6; + break; + + case ManiaModKey7 _: + value |= LegacyMods.Key7; + break; + + case ManiaModKey8 _: + value |= LegacyMods.Key8; + break; + + case ManiaModKey9 _: + value |= LegacyMods.Key9; + break; + + case ManiaModFadeIn _: + value |= LegacyMods.FadeIn; + break; + } + } + + return value; + } + public override IEnumerable GetModsFor(ModType type) { switch (type) diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 148869f5e8..a2c0e051d0 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Osu new KeyBinding(InputKey.MouseRight, OsuAction.RightButton), }; - public override IEnumerable ConvertLegacyMods(LegacyMods mods) + public override IEnumerable ConvertFromLegacyMods(LegacyMods mods) { if (mods.HasFlag(LegacyMods.Nightcore)) yield return new OsuModNightcore(); diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index fc79e59864..dfcc886940 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Taiko new KeyBinding(InputKey.K, TaikoAction.RightRim), }; - public override IEnumerable ConvertLegacyMods(LegacyMods mods) + public override IEnumerable ConvertFromLegacyMods(LegacyMods mods) { if (mods.HasFlag(LegacyMods.Nightcore)) yield return new TaikoModNightcore(); diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index c38a5c6af7..58f598a203 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -42,9 +42,63 @@ namespace osu.Game.Rulesets /// /// Converts mods from legacy enum values. Do not override if you're not a legacy ruleset. /// - /// The legacy enum which will be converted - /// An enumerable of constructed s - public virtual IEnumerable ConvertLegacyMods(LegacyMods mods) => Array.Empty(); + /// The legacy enum which will be converted. + /// An enumerable of constructed s. + public virtual IEnumerable ConvertFromLegacyMods(LegacyMods mods) => Array.Empty(); + + /// + /// Converts mods to legacy enum values. Do not override if you're not a legacy ruleset. + /// + /// The mods which will be converted. + /// A single bitwise enumerable value representing (to the best of our ability) the mods. + public virtual LegacyMods ConvertToLegacyMods(Mod[] mods) + { + var value = LegacyMods.None; + + foreach (var mod in mods) + { + switch (mod) + { + case ModNoFail _: + value |= LegacyMods.NoFail; + break; + + case ModEasy _: + value |= LegacyMods.Easy; + break; + + case ModHidden _: + value |= LegacyMods.Hidden; + break; + + case ModHardRock _: + value |= LegacyMods.HardRock; + break; + + case ModSuddenDeath _: + value |= LegacyMods.SuddenDeath; + break; + + case ModDoubleTime _: + value |= LegacyMods.DoubleTime; + break; + + case ModRelax _: + value |= LegacyMods.Relax; + break; + + case ModHalfTime _: + value |= LegacyMods.HalfTime; + break; + + case ModFlashlight _: + value |= LegacyMods.Flashlight; + break; + } + } + + return value; + } public ModAutoplay GetAutoplayMod() => GetAllMods().OfType().First(); diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index f29e98b0b4..495d8c8cc0 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -66,7 +66,7 @@ namespace osu.Game.Scoring.Legacy /* score.Perfect = */ sr.ReadBoolean(); - scoreInfo.Mods = currentRuleset.ConvertLegacyMods((LegacyMods)sr.ReadInt32()).ToArray(); + scoreInfo.Mods = currentRuleset.ConvertFromLegacyMods((LegacyMods)sr.ReadInt32()).ToArray(); /* score.HpGraphString = */ sr.ReadString(); diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs new file mode 100644 index 0000000000..927ab3fe07 --- /dev/null +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -0,0 +1,35 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.IO; + +namespace osu.Game.Scoring.Legacy +{ + public class LegacyScoreEncoder + { + public const int LATEST_VERSION = 128; + + private readonly Score score; + + public LegacyScoreEncoder(Score score) + { + this.score = score; + + if (score.ScoreInfo.Beatmap.RulesetID < 0 || score.ScoreInfo.Beatmap.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)); + } + + public void Encode(TextWriter writer) + { + writer.WriteLine($"osu file format v{LATEST_VERSION}"); + + writer.WriteLine(); + handleGeneral(writer); + } + + private void handleGeneral(TextWriter writer) + { + } + } +} diff --git a/osu.Game/Tests/Beatmaps/LegacyModConversionTest.cs b/osu.Game/Tests/Beatmaps/LegacyModConversionTest.cs index e9251f8011..e93bf916c7 100644 --- a/osu.Game/Tests/Beatmaps/LegacyModConversionTest.cs +++ b/osu.Game/Tests/Beatmaps/LegacyModConversionTest.cs @@ -23,7 +23,7 @@ namespace osu.Game.Tests.Beatmaps protected void Test(LegacyMods legacyMods, Type[] expectedMods) { var ruleset = CreateRuleset(); - var mods = ruleset.ConvertLegacyMods(legacyMods).ToList(); + var mods = ruleset.ConvertFromLegacyMods(legacyMods).ToList(); Assert.AreEqual(expectedMods.Length, mods.Count); foreach (var modType in expectedMods) From 68ebe98fdee98368a276e07275c0d27fd10bc51c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Mar 2020 14:08:25 +0900 Subject: [PATCH 0570/2376] Remove unused GetUnderlyingStream method --- osu.Game.Tests/Scores/IO/ImportScoreTest.cs | 2 -- osu.Game/IO/Archives/ArchiveReader.cs | 2 -- osu.Game/IO/Archives/LegacyDirectoryArchiveReader.cs | 2 -- osu.Game/IO/Archives/LegacyFileArchiveReader.cs | 2 -- osu.Game/IO/Archives/ZipArchiveReader.cs | 2 -- 5 files changed, 10 deletions(-) diff --git a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs index a139c3a8c2..90bf419644 100644 --- a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs +++ b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs @@ -236,8 +236,6 @@ namespace osu.Game.Tests.Scores.IO } public override IEnumerable Filenames => new[] { "test_file.osr" }; - - public override Stream GetUnderlyingStream() => new MemoryStream(); } } } diff --git a/osu.Game/IO/Archives/ArchiveReader.cs b/osu.Game/IO/Archives/ArchiveReader.cs index 4ee7a19ebc..a30f961daf 100644 --- a/osu.Game/IO/Archives/ArchiveReader.cs +++ b/osu.Game/IO/Archives/ArchiveReader.cs @@ -45,7 +45,5 @@ namespace osu.Game.IO.Archives return buffer; } } - - public abstract Stream GetUnderlyingStream(); } } diff --git a/osu.Game/IO/Archives/LegacyDirectoryArchiveReader.cs b/osu.Game/IO/Archives/LegacyDirectoryArchiveReader.cs index eff02ae7a5..dfae58aed7 100644 --- a/osu.Game/IO/Archives/LegacyDirectoryArchiveReader.cs +++ b/osu.Game/IO/Archives/LegacyDirectoryArchiveReader.cs @@ -28,7 +28,5 @@ namespace osu.Game.IO.Archives } public override IEnumerable Filenames => Directory.GetFiles(path, "*", SearchOption.AllDirectories).Select(f => f.Replace(path, string.Empty).Trim(Path.DirectorySeparatorChar)).ToArray(); - - public override Stream GetUnderlyingStream() => null; } } diff --git a/osu.Game/IO/Archives/LegacyFileArchiveReader.cs b/osu.Game/IO/Archives/LegacyFileArchiveReader.cs index bd5f9cbd07..72e5a21079 100644 --- a/osu.Game/IO/Archives/LegacyFileArchiveReader.cs +++ b/osu.Game/IO/Archives/LegacyFileArchiveReader.cs @@ -28,7 +28,5 @@ namespace osu.Game.IO.Archives } public override IEnumerable Filenames => new[] { Name }; - - public override Stream GetUnderlyingStream() => null; } } diff --git a/osu.Game/IO/Archives/ZipArchiveReader.cs b/osu.Game/IO/Archives/ZipArchiveReader.cs index 35f38ea7e8..80dfa104f3 100644 --- a/osu.Game/IO/Archives/ZipArchiveReader.cs +++ b/osu.Game/IO/Archives/ZipArchiveReader.cs @@ -45,7 +45,5 @@ namespace osu.Game.IO.Archives } public override IEnumerable Filenames => archive.Entries.Select(e => e.Key).ExcludeSystemFileNames(); - - public override Stream GetUnderlyingStream() => archiveStream; } } From 4bb15a8b93e1290c6a1052005bf3c2bd47623f6c Mon Sep 17 00:00:00 2001 From: voidedWarranties Date: Mon, 23 Mar 2020 22:51:37 -0700 Subject: [PATCH 0571/2376] Allow individual storyboard layers to disable masking --- osu.Game/Screens/Play/DimmableStoryboard.cs | 1 - osu.Game/Storyboards/Drawables/DrawableStoryboardLayer.cs | 1 + osu.Game/Storyboards/StoryboardLayer.cs | 4 +++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/DimmableStoryboard.cs b/osu.Game/Screens/Play/DimmableStoryboard.cs index 0fe315fbab..eabdee95fb 100644 --- a/osu.Game/Screens/Play/DimmableStoryboard.cs +++ b/osu.Game/Screens/Play/DimmableStoryboard.cs @@ -44,7 +44,6 @@ namespace osu.Game.Screens.Play return; drawableStoryboard = storyboard.CreateDrawable(); - drawableStoryboard.Masking = true; if (async) LoadComponentAsync(drawableStoryboard, Add); diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardLayer.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardLayer.cs index 39f5418902..69219fb038 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardLayer.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardLayer.cs @@ -22,6 +22,7 @@ namespace osu.Game.Storyboards.Drawables Anchor = Anchor.Centre; Origin = Anchor.Centre; Enabled = layer.EnabledWhenPassing; + Masking = layer.Masking; } [BackgroundDependencyLoader] diff --git a/osu.Game/Storyboards/StoryboardLayer.cs b/osu.Game/Storyboards/StoryboardLayer.cs index d15f771534..e3b74ca609 100644 --- a/osu.Game/Storyboards/StoryboardLayer.cs +++ b/osu.Game/Storyboards/StoryboardLayer.cs @@ -10,15 +10,17 @@ namespace osu.Game.Storyboards { public string Name; public int Depth; + public bool Masking; public bool EnabledWhenPassing = true; public bool EnabledWhenFailing = true; public List Elements = new List(); - public StoryboardLayer(string name, int depth) + public StoryboardLayer(string name, int depth, bool masking = true) { Name = name; Depth = depth; + Masking = masking; } public void Add(IStoryboardElement element) From 022465f546ba5cfb93f21f9792365cf6010d3a2f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Mar 2020 14:13:46 +0900 Subject: [PATCH 0572/2376] Add encoding and import support --- .../Replays/CatchReplayFrame.cs | 9 ++ .../Replays/ManiaReplayFrame.cs | 37 ++++++++ .../Replays/OsuReplayFrame.cs | 12 +++ .../Replays/TaikoReplayFrame.cs | 12 +++ osu.Game/IO/Archives/LegacyByteArrayReader.cs | 30 ++++++ .../Replays/Types/IConvertibleReplayFrame.cs | 6 ++ osu.Game/Rulesets/UI/DrawableRuleset.cs | 4 + osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs | 91 +++++++++++++++++-- osu.Game/Screens/Play/Player.cs | 37 +++++--- 9 files changed, 220 insertions(+), 18 deletions(-) create mode 100644 osu.Game/IO/Archives/LegacyByteArrayReader.cs diff --git a/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs b/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs index b41a5e0612..bc60f16ae8 100644 --- a/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs +++ b/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs @@ -56,5 +56,14 @@ namespace osu.Game.Rulesets.Catch.Replays Actions.Add(CatchAction.MoveLeft); } } + + public LegacyReplayFrame ConvertTo(IBeatmap beatmap) + { + ReplayButtonState state = ReplayButtonState.None; + + if (Actions.Contains(CatchAction.Dash)) state |= ReplayButtonState.Left1; + + return new LegacyReplayFrame(Time, Position * CatchPlayfield.BASE_WIDTH, null, state); + } } } diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs b/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs index 877a9ee410..4987aa8e4c 100644 --- a/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs +++ b/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs @@ -56,5 +56,42 @@ namespace osu.Game.Rulesets.Mania.Replays activeColumns >>= 1; } } + + public LegacyReplayFrame ConvertTo(IBeatmap beatmap) + { + int keys = 0; + + var converter = new ManiaBeatmapConverter(beatmap, new ManiaRuleset()); + + var stage = new StageDefinition { Columns = converter.TargetColumns }; + + var specialColumns = new List(); + + for (int i = 0; i < converter.TargetColumns; i++) + { + if (stage.IsSpecialColumn(i)) + specialColumns.Add(i); + } + + foreach (var action in Actions) + { + switch (action) + { + case ManiaAction.Special1: + keys |= 1 << specialColumns[0]; + break; + + case ManiaAction.Special2: + keys |= 1 << specialColumns[1]; + break; + + default: + keys |= 1 << (action - ManiaAction.Key1); + break; + } + } + + return new LegacyReplayFrame(Time, keys, null, ReplayButtonState.None); + } } } diff --git a/osu.Game.Rulesets.Osu/Replays/OsuReplayFrame.cs b/osu.Game.Rulesets.Osu/Replays/OsuReplayFrame.cs index e6c6db5e61..93cf4db5b1 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuReplayFrame.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuReplayFrame.cs @@ -32,5 +32,17 @@ namespace osu.Game.Rulesets.Osu.Replays if (currentFrame.MouseLeft) Actions.Add(OsuAction.LeftButton); if (currentFrame.MouseRight) Actions.Add(OsuAction.RightButton); } + + public LegacyReplayFrame ConvertTo(IBeatmap beatmap) + { + ReplayButtonState state = ReplayButtonState.None; + + if (Actions.Contains(OsuAction.LeftButton)) + state |= ReplayButtonState.Left1; + if (Actions.Contains(OsuAction.RightButton)) + state |= ReplayButtonState.Right1; + + return new LegacyReplayFrame(Time, Position.X, Position.Y, state); + } } } diff --git a/osu.Game.Rulesets.Taiko/Replays/TaikoReplayFrame.cs b/osu.Game.Rulesets.Taiko/Replays/TaikoReplayFrame.cs index c5ebefc397..cb4ca35c2b 100644 --- a/osu.Game.Rulesets.Taiko/Replays/TaikoReplayFrame.cs +++ b/osu.Game.Rulesets.Taiko/Replays/TaikoReplayFrame.cs @@ -30,5 +30,17 @@ namespace osu.Game.Rulesets.Taiko.Replays if (currentFrame.MouseLeft1) Actions.Add(TaikoAction.LeftCentre); if (currentFrame.MouseLeft2) Actions.Add(TaikoAction.RightCentre); } + + public LegacyReplayFrame ConvertTo(IBeatmap beatmap) + { + ReplayButtonState state = ReplayButtonState.None; + + if (Actions.Contains(TaikoAction.LeftRim)) state |= ReplayButtonState.Right1; + if (Actions.Contains(TaikoAction.RightRim)) state |= ReplayButtonState.Right2; + if (Actions.Contains(TaikoAction.LeftCentre)) state |= ReplayButtonState.Left1; + if (Actions.Contains(TaikoAction.RightCentre)) state |= ReplayButtonState.Left2; + + return new LegacyReplayFrame(Time, null, null, state); + } } } diff --git a/osu.Game/IO/Archives/LegacyByteArrayReader.cs b/osu.Game/IO/Archives/LegacyByteArrayReader.cs new file mode 100644 index 0000000000..0c3620403f --- /dev/null +++ b/osu.Game/IO/Archives/LegacyByteArrayReader.cs @@ -0,0 +1,30 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.IO; + +namespace osu.Game.IO.Archives +{ + /// + /// Allows reading a single file from the provided stream. + /// + public class LegacyByteArrayReader : ArchiveReader + { + private readonly byte[] content; + + public LegacyByteArrayReader(byte[] content, string filename) + : base(filename) + { + this.content = content; + } + + public override Stream GetStream(string name) => new MemoryStream(content); + + public override void Dispose() + { + } + + public override IEnumerable Filenames => new[] { Name }; + } +} diff --git a/osu.Game/Rulesets/Replays/Types/IConvertibleReplayFrame.cs b/osu.Game/Rulesets/Replays/Types/IConvertibleReplayFrame.cs index c2947c0aca..a240e7aa0e 100644 --- a/osu.Game/Rulesets/Replays/Types/IConvertibleReplayFrame.cs +++ b/osu.Game/Rulesets/Replays/Types/IConvertibleReplayFrame.cs @@ -18,5 +18,11 @@ namespace osu.Game.Rulesets.Replays.Types /// The beatmap. /// The last post-conversion , used to fill in missing delta information. May be null. void ConvertFrom(LegacyReplayFrame currentFrame, IBeatmap beatmap, ReplayFrame lastFrame = null); + + /// + /// Populates this using values from a . + /// + /// The beatmap. + LegacyReplayFrame ConvertTo(IBeatmap beatmap); } } diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 5c57a92cd1..e4e2f5d569 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -268,6 +268,10 @@ namespace osu.Game.Rulesets.UI throw new InvalidOperationException($"A {nameof(KeyBindingInputManager)} which supports recording is not available"); var recorder = CreateReplayRecorder(recordingReplay); + + if (recorder == null) + return; + recorder.ScreenSpaceToGamefield = Playfield.ScreenSpaceToGamefield; recordingInputHandler.Recorder = recorder; diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index 927ab3fe07..3e3120d99f 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -3,6 +3,14 @@ using System; using System.IO; +using System.Linq; +using System.Text; +using osu.Framework.Extensions; +using osu.Game.Beatmaps; +using osu.Game.IO.Legacy; +using osu.Game.Replays.Legacy; +using osu.Game.Rulesets.Replays.Types; +using SharpCompress.Compressors.LZMA; namespace osu.Game.Scoring.Legacy { @@ -11,25 +19,96 @@ namespace osu.Game.Scoring.Legacy public const int LATEST_VERSION = 128; private readonly Score score; + private readonly IBeatmap beatmap; - public LegacyScoreEncoder(Score score) + public LegacyScoreEncoder(Score score, IBeatmap beatmap) { this.score = score; + this.beatmap = beatmap; if (score.ScoreInfo.Beatmap.RulesetID < 0 || score.ScoreInfo.Beatmap.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)); } - public void Encode(TextWriter writer) + public void Encode(Stream stream) { - writer.WriteLine($"osu file format v{LATEST_VERSION}"); + using (SerializationWriter sw = new SerializationWriter(stream)) + { + sw.Write((byte)score.ScoreInfo.RulesetID); + sw.Write(LATEST_VERSION); + sw.Write(score.ScoreInfo.Beatmap.MD5Hash); + sw.Write(score.ScoreInfo.UserString); + sw.Write($"lazer-{score.ScoreInfo.UserString}-{score.ScoreInfo.Date}".ComputeMD5Hash()); + sw.Write((ushort)(score.ScoreInfo.GetCount300() ?? 0)); + sw.Write((ushort)(score.ScoreInfo.GetCount100() ?? 0)); + sw.Write((ushort)(score.ScoreInfo.GetCount50() ?? 0)); + sw.Write((ushort)(score.ScoreInfo.GetCountGeki() ?? 0)); + sw.Write((ushort)(score.ScoreInfo.GetCountKatu() ?? 0)); + sw.Write((ushort)(score.ScoreInfo.GetCountMiss() ?? 0)); + sw.Write((int)(score.ScoreInfo.TotalScore)); + sw.Write((ushort)score.ScoreInfo.MaxCombo); + sw.Write(score.ScoreInfo.Combo == score.ScoreInfo.MaxCombo); + sw.Write((int)score.ScoreInfo.Ruleset.CreateInstance().ConvertToLegacyMods(score.ScoreInfo.Mods)); - writer.WriteLine(); - handleGeneral(writer); + sw.Write(getHpGraphFormatted()); + sw.Write(score.ScoreInfo.Date.DateTime); + sw.WriteByteArray(createReplayData()); + sw.Write((long)0); + writeModSpecificData(score.ScoreInfo, sw); + } } - private void handleGeneral(TextWriter writer) + private void writeModSpecificData(ScoreInfo score, SerializationWriter sw) { } + + private byte[] createReplayData() + { + var content = new ASCIIEncoding().GetBytes(replayStringContent); + + using (var outStream = new MemoryStream()) + { + using (var lzma = new LzmaStream(new LzmaEncoderProperties(false, 1 << 21, 255), false, outStream)) + { + outStream.Write(lzma.Properties); + + long fileSize = content.Length; + for (int i = 0; i < 8; i++) + outStream.WriteByte((byte)(fileSize >> (8 * i))); + + lzma.Write(content); + } + + return outStream.ToArray(); + } + } + + private string replayStringContent + { + get + { + StringBuilder replayData = new StringBuilder(); + + if (score.Replay != null) + { + LegacyReplayFrame lastF = new LegacyReplayFrame(0, 0, 0, ReplayButtonState.None); + + foreach (var f in score.Replay.Frames.OfType().Select(f => f.ConvertTo(beatmap))) + { + replayData.Append(FormattableString.Invariant($"{f.Time - lastF.Time}|{f.MouseX}|{f.MouseY}|{(int)f.ButtonState},")); + lastF = f; + } + } + + replayData.AppendFormat(@"{0}|{1}|{2}|{3},", -12345, 0, 0, 0); + return replayData.ToString(); + } + } + + private string getHpGraphFormatted() + { + // todo: implement, maybe? + return string.Empty; + } } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 8fee516f2b..f0f36db490 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -17,6 +18,7 @@ using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics.Containers; +using osu.Game.IO.Archives; using osu.Game.Online.API; using osu.Game.Overlays; using osu.Game.Replays; @@ -25,6 +27,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Scoring; +using osu.Game.Scoring.Legacy; using osu.Game.Screens.Ranking; using osu.Game.Skinning; using osu.Game.Users; @@ -643,19 +646,29 @@ namespace osu.Game.Screens.Play completionProgressDelegate?.Cancel(); completionProgressDelegate = Schedule(delegate { - var score = CreateScore(); - - if (DrawableRuleset.ReplayScore == null) - { - scoreManager.Import(score).ContinueWith(_ => Schedule(() => - { - // screen may be in the exiting transition phase. - if (this.IsCurrentScreen()) - this.Push(CreateResults(score)); - })); - } + if (DrawableRuleset.ReplayScore != null) + this.Push(CreateResults(DrawableRuleset.ReplayScore.ScoreInfo)); else - this.Push(CreateResults(score)); + { + var score = new Score + { + ScoreInfo = CreateScore(), + Replay = recordingReplay + }; + + using (var stream = new MemoryStream()) + { + new LegacyScoreEncoder(score, gameplayBeatmap).Encode(stream); + + scoreManager.Import(score.ScoreInfo, new LegacyByteArrayReader(stream.ToArray(), "replay.osr")) + .ContinueWith(imported => Schedule(() => + { + // screen may be in the exiting transition phase. + if (this.IsCurrentScreen()) + this.Push(CreateResults(imported.Result)); + })); + } + } }); } From 96a849f897375c906d3ab8b672e8522aa93d3da4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Mar 2020 14:55:49 +0900 Subject: [PATCH 0573/2376] Add remaining replay recorders --- .../UI/CatchReplayRecorder.cs | 23 +++++++++++++++++++ .../UI/DrawableCatchRuleset.cs | 2 ++ .../UI/DrawableManiaRuleset.cs | 2 ++ .../UI/ManiaReplayRecorder.cs | 23 +++++++++++++++++++ .../UI/DrawableTaikoRuleset.cs | 2 ++ .../UI/TaikoReplayRecorder.cs | 23 +++++++++++++++++++ 6 files changed, 75 insertions(+) create mode 100644 osu.Game.Rulesets.Catch/UI/CatchReplayRecorder.cs create mode 100644 osu.Game.Rulesets.Mania/UI/ManiaReplayRecorder.cs create mode 100644 osu.Game.Rulesets.Taiko/UI/TaikoReplayRecorder.cs diff --git a/osu.Game.Rulesets.Catch/UI/CatchReplayRecorder.cs b/osu.Game.Rulesets.Catch/UI/CatchReplayRecorder.cs new file mode 100644 index 0000000000..8bede32c59 --- /dev/null +++ b/osu.Game.Rulesets.Catch/UI/CatchReplayRecorder.cs @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Game.Replays; +using osu.Game.Rulesets.Catch.Replays; +using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.UI; +using osuTK; + +namespace osu.Game.Rulesets.Catch.UI +{ + public class CatchReplayRecorder : ReplayRecorder + { + public CatchReplayRecorder(Replay target) + : base(target) + { + } + + protected override ReplayFrame HandleFrame(Vector2 position, List actions, ReplayFrame previousFrame) + => new CatchReplayFrame(Time.Current, position.X, actions.Contains(CatchAction.Dash), previousFrame as CatchReplayFrame); + } +} diff --git a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs index fd8a1d175d..594c7a57c7 100644 --- a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs @@ -32,6 +32,8 @@ namespace osu.Game.Rulesets.Catch.UI protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new CatchFramedReplayInputHandler(replay); + protected override ReplayRecorder CreateReplayRecorder(Replay replay) => new CatchReplayRecorder(replay); + protected override Playfield CreatePlayfield() => new CatchPlayfield(Beatmap.BeatmapInfo.BaseDifficulty, CreateDrawableRepresentation); public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new CatchPlayfieldAdjustmentContainer(); diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index 2c497541a8..e5ec054fa7 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -85,5 +85,7 @@ namespace osu.Game.Rulesets.Mania.UI } protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new ManiaFramedReplayInputHandler(replay); + + protected override ReplayRecorder CreateReplayRecorder(Replay replay) => new ManiaReplayRecorder(replay); } } diff --git a/osu.Game.Rulesets.Mania/UI/ManiaReplayRecorder.cs b/osu.Game.Rulesets.Mania/UI/ManiaReplayRecorder.cs new file mode 100644 index 0000000000..57cbc1ff17 --- /dev/null +++ b/osu.Game.Rulesets.Mania/UI/ManiaReplayRecorder.cs @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Game.Replays; +using osu.Game.Rulesets.Mania.Replays; +using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.UI; +using osuTK; + +namespace osu.Game.Rulesets.Mania.UI +{ + public class ManiaReplayRecorder : ReplayRecorder + { + public ManiaReplayRecorder(Replay replay) + : base(replay) + { + } + + protected override ReplayFrame HandleFrame(Vector2 position, List actions, ReplayFrame previousFrame) + => new ManiaReplayFrame(Time.Current, actions.ToArray()); + } +} diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs index 9196bbf13e..e4a4b555a7 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs @@ -65,5 +65,7 @@ namespace osu.Game.Rulesets.Taiko.UI } protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new TaikoFramedReplayInputHandler(replay); + + protected override ReplayRecorder CreateReplayRecorder(Replay replay) => new TaikoReplayRecorder(replay); } } diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoReplayRecorder.cs b/osu.Game.Rulesets.Taiko/UI/TaikoReplayRecorder.cs new file mode 100644 index 0000000000..4330ae6464 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/UI/TaikoReplayRecorder.cs @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Game.Replays; +using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Taiko.Replays; +using osu.Game.Rulesets.UI; +using osuTK; + +namespace osu.Game.Rulesets.Taiko.UI +{ + public class TaikoReplayRecorder : ReplayRecorder + { + public TaikoReplayRecorder(Replay replay) + : base(replay) + { + } + + protected override ReplayFrame HandleFrame(Vector2 position, List actions, ReplayFrame previousFrame) => + new TaikoReplayFrame(Time.Current, actions.ToArray()); + } +} From 388cf5c83a40ed2650cb947cda677c681f51a481 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Mar 2020 15:38:54 +0900 Subject: [PATCH 0574/2376] Fix catch positional data being incorrectly recorded --- osu.Game.Rulesets.Catch/UI/CatchReplayRecorder.cs | 9 ++++++--- osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs | 2 +- osu.Game.Rulesets.Mania/UI/ManiaReplayRecorder.cs | 2 +- osu.Game.Rulesets.Osu/UI/OsuReplayRecorder.cs | 4 ++-- osu.Game.Rulesets.Taiko/UI/TaikoReplayRecorder.cs | 2 +- osu.Game.Tests/Gameplay/TestSceneReplayRecording.cs | 4 ++-- osu.Game/Rulesets/UI/ReplayRecorder.cs | 2 +- 7 files changed, 14 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/CatchReplayRecorder.cs b/osu.Game.Rulesets.Catch/UI/CatchReplayRecorder.cs index 8bede32c59..9a4d1f9585 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchReplayRecorder.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchReplayRecorder.cs @@ -12,12 +12,15 @@ namespace osu.Game.Rulesets.Catch.UI { public class CatchReplayRecorder : ReplayRecorder { - public CatchReplayRecorder(Replay target) + private readonly CatchPlayfield playfield; + + public CatchReplayRecorder(Replay target, CatchPlayfield playfield) : base(target) { + this.playfield = playfield; } - protected override ReplayFrame HandleFrame(Vector2 position, List actions, ReplayFrame previousFrame) - => new CatchReplayFrame(Time.Current, position.X, actions.Contains(CatchAction.Dash), previousFrame as CatchReplayFrame); + protected override ReplayFrame HandleFrame(Vector2 mousePosition, List actions, ReplayFrame previousFrame) + => new CatchReplayFrame(Time.Current, playfield.CatcherArea.MovableCatcher.X, actions.Contains(CatchAction.Dash), previousFrame as CatchReplayFrame); } } diff --git a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs index 594c7a57c7..ebe45aa3ab 100644 --- a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Catch.UI protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new CatchFramedReplayInputHandler(replay); - protected override ReplayRecorder CreateReplayRecorder(Replay replay) => new CatchReplayRecorder(replay); + protected override ReplayRecorder CreateReplayRecorder(Replay replay) => new CatchReplayRecorder(replay, (CatchPlayfield)Playfield); protected override Playfield CreatePlayfield() => new CatchPlayfield(Beatmap.BeatmapInfo.BaseDifficulty, CreateDrawableRepresentation); diff --git a/osu.Game.Rulesets.Mania/UI/ManiaReplayRecorder.cs b/osu.Game.Rulesets.Mania/UI/ManiaReplayRecorder.cs index 57cbc1ff17..18275000a2 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaReplayRecorder.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaReplayRecorder.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Mania.UI { } - protected override ReplayFrame HandleFrame(Vector2 position, List actions, ReplayFrame previousFrame) + protected override ReplayFrame HandleFrame(Vector2 mousePosition, List actions, ReplayFrame previousFrame) => new ManiaReplayFrame(Time.Current, actions.ToArray()); } } diff --git a/osu.Game.Rulesets.Osu/UI/OsuReplayRecorder.cs b/osu.Game.Rulesets.Osu/UI/OsuReplayRecorder.cs index 898212ee6b..b68ea136d5 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuReplayRecorder.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuReplayRecorder.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Osu.UI { } - protected override ReplayFrame HandleFrame(Vector2 position, List actions, ReplayFrame previousFrame) - => new OsuReplayFrame(Time.Current, position, actions.ToArray()); + protected override ReplayFrame HandleFrame(Vector2 mousePosition, List actions, ReplayFrame previousFrame) + => new OsuReplayFrame(Time.Current, mousePosition, actions.ToArray()); } } diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoReplayRecorder.cs b/osu.Game.Rulesets.Taiko/UI/TaikoReplayRecorder.cs index 4330ae6464..1859dabf03 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoReplayRecorder.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoReplayRecorder.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Taiko.UI { } - protected override ReplayFrame HandleFrame(Vector2 position, List actions, ReplayFrame previousFrame) => + protected override ReplayFrame HandleFrame(Vector2 mousePosition, List actions, ReplayFrame previousFrame) => new TaikoReplayFrame(Time.Current, actions.ToArray()); } } diff --git a/osu.Game.Tests/Gameplay/TestSceneReplayRecording.cs b/osu.Game.Tests/Gameplay/TestSceneReplayRecording.cs index fe87ca675b..057d026132 100644 --- a/osu.Game.Tests/Gameplay/TestSceneReplayRecording.cs +++ b/osu.Game.Tests/Gameplay/TestSceneReplayRecording.cs @@ -215,7 +215,7 @@ namespace osu.Game.Tests.Gameplay { } - protected override ReplayFrame HandleFrame(Vector2 position, List actions, ReplayFrame previousFrame) => - new TestReplayFrame(Time.Current, position, actions.ToArray()); + protected override ReplayFrame HandleFrame(Vector2 mousePosition, List actions, ReplayFrame previousFrame) => + new TestReplayFrame(Time.Current, mousePosition, actions.ToArray()); } } diff --git a/osu.Game/Rulesets/UI/ReplayRecorder.cs b/osu.Game/Rulesets/UI/ReplayRecorder.cs index 74e8109d52..c977639584 100644 --- a/osu.Game/Rulesets/UI/ReplayRecorder.cs +++ b/osu.Game/Rulesets/UI/ReplayRecorder.cs @@ -75,7 +75,7 @@ namespace osu.Game.Rulesets.UI target.Frames.Add(frame); } - protected abstract ReplayFrame HandleFrame(Vector2 position, List actions, ReplayFrame previousFrame); + protected abstract ReplayFrame HandleFrame(Vector2 mousePosition, List actions, ReplayFrame previousFrame); } public abstract class ReplayRecorder : Component From 448961b330f3ad087b558ff87cded8ddb7e57540 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Mar 2020 15:39:01 +0900 Subject: [PATCH 0575/2376] Rename incorrect variable --- osu.Game/Rulesets/UI/DrawableRuleset.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index e4e2f5d569..27993ff173 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -264,7 +264,7 @@ namespace osu.Game.Rulesets.UI public override void SetRecordTarget(Replay recordingReplay) { - if (!(KeyBindingInputManager is IHasRecordingHandler recordingInputHandler)) + if (!(KeyBindingInputManager is IHasRecordingHandler recordingInputManager)) throw new InvalidOperationException($"A {nameof(KeyBindingInputManager)} which supports recording is not available"); var recorder = CreateReplayRecorder(recordingReplay); @@ -274,7 +274,7 @@ namespace osu.Game.Rulesets.UI recorder.ScreenSpaceToGamefield = Playfield.ScreenSpaceToGamefield; - recordingInputHandler.Recorder = recorder; + recordingInputManager.Recorder = recorder; } public override void SetReplayScore(Score replayScore) From 02a3c7c025866795894a7eae5757ee41f6034d15 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Mar 2020 15:43:22 +0900 Subject: [PATCH 0576/2376] Fix incorrect ruleset being recorded to file --- osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index 3e3120d99f..0ba595b1c5 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -34,7 +34,7 @@ namespace osu.Game.Scoring.Legacy { using (SerializationWriter sw = new SerializationWriter(stream)) { - sw.Write((byte)score.ScoreInfo.RulesetID); + sw.Write((byte)(score.ScoreInfo.Ruleset.ID ?? 0)); sw.Write(LATEST_VERSION); sw.Write(score.ScoreInfo.Beatmap.MD5Hash); sw.Write(score.ScoreInfo.UserString); From 2feb66d4233b58772b219e9ee29ad96775af1742 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Mar 2020 15:43:34 +0900 Subject: [PATCH 0577/2376] Correctly handle missing positional data --- osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index 0ba595b1c5..515cdc8864 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -95,7 +95,7 @@ namespace osu.Game.Scoring.Legacy foreach (var f in score.Replay.Frames.OfType().Select(f => f.ConvertTo(beatmap))) { - replayData.Append(FormattableString.Invariant($"{f.Time - lastF.Time}|{f.MouseX}|{f.MouseY}|{(int)f.ButtonState},")); + replayData.Append(FormattableString.Invariant($"{f.Time - lastF.Time}|{f.MouseX ?? 0}|{f.MouseY ?? 0}|{(int)f.ButtonState},")); lastF = f; } } From a7bfaad60fdb18115549b2c031c01b0a538f355b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Mar 2020 15:44:39 +0900 Subject: [PATCH 0578/2376] More correctly handle rulesets which don't support replay recording --- osu.Game/Screens/Play/Player.cs | 34 ++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index f0f36db490..7723a84637 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -650,24 +650,28 @@ namespace osu.Game.Screens.Play this.Push(CreateResults(DrawableRuleset.ReplayScore.ScoreInfo)); else { - var score = new Score - { - ScoreInfo = CreateScore(), - Replay = recordingReplay - }; + var score = new Score { ScoreInfo = CreateScore() }; - using (var stream = new MemoryStream()) - { - new LegacyScoreEncoder(score, gameplayBeatmap).Encode(stream); + LegacyByteArrayReader replayReader = null; - scoreManager.Import(score.ScoreInfo, new LegacyByteArrayReader(stream.ToArray(), "replay.osr")) - .ContinueWith(imported => Schedule(() => - { - // screen may be in the exiting transition phase. - if (this.IsCurrentScreen()) - this.Push(CreateResults(imported.Result)); - })); + if (recordingReplay?.Frames.Count > 0) + { + score.Replay = recordingReplay; + + using (var stream = new MemoryStream()) + { + new LegacyScoreEncoder(score, gameplayBeatmap).Encode(stream); + replayReader = new LegacyByteArrayReader(stream.ToArray(), "replay.osr"); + } } + + scoreManager.Import(score.ScoreInfo, replayReader) + .ContinueWith(imported => Schedule(() => + { + // screen may be in the exiting transition phase. + if (this.IsCurrentScreen()) + this.Push(CreateResults(imported.Result)); + })); } }); } From 2735a2250c1236e92cdc9be5a323e570a717d57b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 23 Mar 2020 19:03:42 +0900 Subject: [PATCH 0579/2376] Move replay recorder to final location --- .../Gameplay/TestSceneReplayRecording.cs | 80 +----------------- osu.Game/Rulesets/UI/DrawableRuleset.cs | 2 + osu.Game/Rulesets/UI/ReplayRecorder.cs | 81 +++++++++++++++++++ osu.Game/Rulesets/UI/RulesetInputManager.cs | 16 ++++ 4 files changed, 100 insertions(+), 79 deletions(-) create mode 100644 osu.Game/Rulesets/UI/ReplayRecorder.cs diff --git a/osu.Game.Tests/Gameplay/TestSceneReplayRecording.cs b/osu.Game.Tests/Gameplay/TestSceneReplayRecording.cs index c2b2ebdb98..ab1998a650 100644 --- a/osu.Game.Tests/Gameplay/TestSceneReplayRecording.cs +++ b/osu.Game.Tests/Gameplay/TestSceneReplayRecording.cs @@ -1,13 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; -using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Input.StateChanges; @@ -41,7 +38,7 @@ namespace osu.Game.Tests.Gameplay { new TestRulesetInputManager(new TestSceneModSettings.TestRulesetInfo(), 0, SimultaneousBindingMode.Unique) { - RecordTarget = replay, + Recorder = new TestReplayRecorder(replay), Child = new Container { RelativeSizeAxes = Axes.Both, @@ -171,19 +168,6 @@ namespace osu.Game.Tests.Gameplay public class TestRulesetInputManager : RulesetInputManager { - private ReplayRecorder recorder; - - public Replay RecordTarget - { - set - { - if (recorder != null) - throw new InvalidOperationException("Cannot attach more than one recorder"); - - KeyBindingContainer.Add(recorder = new TestReplayRecorder(value)); - } - } - public TestRulesetInputManager(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) : base(ruleset, variant, unique) { @@ -230,66 +214,4 @@ namespace osu.Game.Tests.Gameplay protected override ReplayFrame HandleFrame(InputState state, List pressedActions, ReplayFrame previousFrame) => new TestReplayFrame(Time.Current, ToLocalSpace(state.Mouse.Position), pressedActions.ToArray()); } - - internal abstract class ReplayRecorder : Component, IKeyBindingHandler - where T : struct - { - private readonly Replay target; - - private readonly List pressedActions = new List(); - - private InputManager inputManager; - - public int RecordFrameRate = 60; - - protected ReplayRecorder(Replay target) - { - this.target = target; - - RelativeSizeAxes = Axes.Both; - - Depth = float.MinValue; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - inputManager = GetContainingInputManager(); - } - - protected override bool OnMouseMove(MouseMoveEvent e) - { - recordFrame(false); - return base.OnMouseMove(e); - } - - public bool OnPressed(T action) - { - pressedActions.Add(action); - recordFrame(true); - return false; - } - - public void OnReleased(T action) - { - pressedActions.Remove(action); - recordFrame(true); - } - - private void recordFrame(bool important) - { - var last = target.Frames.LastOrDefault(); - - if (!important && last != null && Time.Current - last.Time < (1000d / RecordFrameRate)) - return; - - var frame = HandleFrame(inputManager.CurrentState, pressedActions, last); - - if (frame != null) - target.Frames.Add(frame); - } - - protected abstract ReplayFrame HandleFrame(InputState state, List testActions, ReplayFrame previousFrame); - } } diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index d0a2722f58..c8af3be980 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -302,6 +302,8 @@ namespace osu.Game.Rulesets.UI protected virtual ReplayInputHandler CreateReplayInputHandler(Replay replay) => null; + protected virtual ReplayRecorder CreateReplayRecorder(Replay replay) => null; + /// /// Creates a Playfield. /// diff --git a/osu.Game/Rulesets/UI/ReplayRecorder.cs b/osu.Game/Rulesets/UI/ReplayRecorder.cs new file mode 100644 index 0000000000..9e2f898206 --- /dev/null +++ b/osu.Game/Rulesets/UI/ReplayRecorder.cs @@ -0,0 +1,81 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Graphics; +using osu.Framework.Input; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; +using osu.Framework.Input.States; +using osu.Game.Replays; +using osu.Game.Rulesets.Replays; + +namespace osu.Game.Rulesets.UI +{ + public abstract class ReplayRecorder : ReplayRecorder, IKeyBindingHandler + where T : struct + { + private readonly Replay target; + + private readonly List pressedActions = new List(); + + private InputManager inputManager; + + public int RecordFrameRate = 60; + + protected ReplayRecorder(Replay target) + { + this.target = target; + + RelativeSizeAxes = Axes.Both; + + Depth = float.MinValue; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + inputManager = GetContainingInputManager(); + } + + protected override bool OnMouseMove(MouseMoveEvent e) + { + recordFrame(false); + return base.OnMouseMove(e); + } + + public bool OnPressed(T action) + { + pressedActions.Add(action); + recordFrame(true); + return false; + } + + public void OnReleased(T action) + { + pressedActions.Remove(action); + recordFrame(true); + } + + private void recordFrame(bool important) + { + var last = target.Frames.LastOrDefault(); + + if (!important && last != null && Time.Current - last.Time < (1000d / RecordFrameRate)) + return; + + var frame = HandleFrame(inputManager.CurrentState, pressedActions, last); + + if (frame != null) + target.Frames.Add(frame); + } + + protected abstract ReplayFrame HandleFrame(InputState state, List testActions, ReplayFrame previousFrame); + } + + public abstract class ReplayRecorder : Component + { + } +} diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index 7f85c10b56..043e0f56cc 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -26,6 +27,21 @@ namespace osu.Game.Rulesets.UI public abstract class RulesetInputManager : PassThroughInputManager, ICanAttachKeyCounter, IHasReplayHandler where T : struct { + private ReplayRecorder recorder; + + public ReplayRecorder Recorder + { + set + { + if (recorder != null) + throw new InvalidOperationException("Cannot attach more than one recorder"); + + recorder = value; + + KeyBindingContainer.Add(recorder); + } + } + protected override InputState CreateInitialState() { var state = base.CreateInitialState(); From 8484d201d16488e83462bfb5ed722c307d616d0b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Mar 2020 15:54:04 +0900 Subject: [PATCH 0580/2376] Nest and rename test classes --- .../Gameplay/TestSceneReplayRecording.cs | 188 +++++++++--------- 1 file changed, 94 insertions(+), 94 deletions(-) diff --git a/osu.Game.Tests/Gameplay/TestSceneReplayRecording.cs b/osu.Game.Tests/Gameplay/TestSceneReplayRecording.cs index ab1998a650..cd9486a70a 100644 --- a/osu.Game.Tests/Gameplay/TestSceneReplayRecording.cs +++ b/osu.Game.Tests/Gameplay/TestSceneReplayRecording.cs @@ -56,7 +56,7 @@ namespace osu.Game.Tests.Gameplay Anchor = Anchor.Centre, Origin = Anchor.Centre, }, - new TestConsumer() + new TestInputConsumer() } }, } @@ -86,7 +86,7 @@ namespace osu.Game.Tests.Gameplay Anchor = Anchor.Centre, Origin = Anchor.Centre, }, - new TestConsumer() + new TestInputConsumer() } }, } @@ -101,117 +101,117 @@ namespace osu.Game.Tests.Gameplay playbackManager.ReplayInputHandler.SetFrameFromTime(Time.Current - 500); } - } - public class TestFramedReplayInputHandler : FramedReplayInputHandler - { - public TestFramedReplayInputHandler(Replay replay) - : base(replay) + public class TestFramedReplayInputHandler : FramedReplayInputHandler { - } - - public override List GetPendingInputs() - { - return new List + public TestFramedReplayInputHandler(Replay replay) + : base(replay) { - new MousePositionAbsoluteInput - { - Position = GamefieldToScreenSpace(CurrentFrame?.Position ?? Vector2.Zero) - }, - new ReplayState - { - PressedActions = CurrentFrame?.Actions ?? new List() - } - }; - } - } + } - public class TestConsumer : CompositeDrawable, IKeyBindingHandler - { - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Parent.ReceivePositionalInputAt(screenSpacePos); - - private readonly Box box; - - public TestConsumer() - { - Size = new Vector2(30); - - Origin = Anchor.Centre; - - InternalChildren = new Drawable[] + public override List GetPendingInputs() { - box = new Box + return new List { - Colour = Color4.Black, - RelativeSizeAxes = Axes.Both, - }, - }; + new MousePositionAbsoluteInput + { + Position = GamefieldToScreenSpace(CurrentFrame?.Position ?? Vector2.Zero) + }, + new ReplayState + { + PressedActions = CurrentFrame?.Actions ?? new List() + } + }; + } } - protected override bool OnMouseMove(MouseMoveEvent e) + public class TestInputConsumer : CompositeDrawable, IKeyBindingHandler { - Position = e.MousePosition; - return base.OnMouseMove(e); - } + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Parent.ReceivePositionalInputAt(screenSpacePos); - public bool OnPressed(TestAction action) - { - box.Colour = Color4.White; - return true; - } + private readonly Box box; - public void OnReleased(TestAction action) - { - box.Colour = Color4.Black; - } - } - - public class TestRulesetInputManager : RulesetInputManager - { - public TestRulesetInputManager(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) - : base(ruleset, variant, unique) - { - } - - protected override KeyBindingContainer CreateKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) - => new TestKeyBindingContainer(); - - internal class TestKeyBindingContainer : KeyBindingContainer - { - public override IEnumerable DefaultKeyBindings => new[] + public TestInputConsumer() { - new KeyBinding(InputKey.MouseLeft, TestAction.Down), - }; + Size = new Vector2(30); + + Origin = Anchor.Centre; + + InternalChildren = new Drawable[] + { + box = new Box + { + Colour = Color4.Black, + RelativeSizeAxes = Axes.Both, + }, + }; + } + + protected override bool OnMouseMove(MouseMoveEvent e) + { + Position = e.MousePosition; + return base.OnMouseMove(e); + } + + public bool OnPressed(TestAction action) + { + box.Colour = Color4.White; + return true; + } + + public void OnReleased(TestAction action) + { + box.Colour = Color4.Black; + } } - } - public class TestReplayFrame : ReplayFrame - { - public Vector2 Position; - - public List Actions = new List(); - - public TestReplayFrame(double time, Vector2 position, params TestAction[] actions) - : base(time) + public class TestRulesetInputManager : RulesetInputManager { - Position = position; - Actions.AddRange(actions); + public TestRulesetInputManager(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) + : base(ruleset, variant, unique) + { + } + + protected override KeyBindingContainer CreateKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) + => new TestKeyBindingContainer(); + + internal class TestKeyBindingContainer : KeyBindingContainer + { + public override IEnumerable DefaultKeyBindings => new[] + { + new KeyBinding(InputKey.MouseLeft, TestAction.Down), + }; + } } - } - public enum TestAction - { - Down, - } - - internal class TestReplayRecorder : ReplayRecorder - { - public TestReplayRecorder(Replay target) - : base(target) + public class TestReplayFrame : ReplayFrame { + public Vector2 Position; + + public List Actions = new List(); + + public TestReplayFrame(double time, Vector2 position, params TestAction[] actions) + : base(time) + { + Position = position; + Actions.AddRange(actions); + } } - protected override ReplayFrame HandleFrame(InputState state, List pressedActions, ReplayFrame previousFrame) => - new TestReplayFrame(Time.Current, ToLocalSpace(state.Mouse.Position), pressedActions.ToArray()); + public enum TestAction + { + Down, + } + + internal class TestReplayRecorder : ReplayRecorder + { + public TestReplayRecorder(Replay target) + : base(target) + { + } + + protected override ReplayFrame HandleFrame(InputState state, List pressedActions, ReplayFrame previousFrame) => + new TestReplayFrame(Time.Current, ToLocalSpace(state.Mouse.Position), pressedActions.ToArray()); + } } } From 417ff837ac95434809dcf5333ec7246b6dd925fb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Mar 2020 16:22:54 +0900 Subject: [PATCH 0581/2376] Add basic tests --- ...ecording.cs => TestSceneReplayRecorder.cs} | 79 +++++++++++++++++-- 1 file changed, 71 insertions(+), 8 deletions(-) rename osu.Game.Tests/Gameplay/{TestSceneReplayRecording.cs => TestSceneReplayRecorder.cs} (68%) diff --git a/osu.Game.Tests/Gameplay/TestSceneReplayRecording.cs b/osu.Game.Tests/Gameplay/TestSceneReplayRecorder.cs similarity index 68% rename from osu.Game.Tests/Gameplay/TestSceneReplayRecording.cs rename to osu.Game.Tests/Gameplay/TestSceneReplayRecorder.cs index cd9486a70a..e99c399b89 100644 --- a/osu.Game.Tests/Gameplay/TestSceneReplayRecording.cs +++ b/osu.Game.Tests/Gameplay/TestSceneReplayRecorder.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -9,6 +11,8 @@ using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Input.StateChanges; using osu.Framework.Input.States; +using osu.Framework.Testing; +using osu.Framework.Threading; using osu.Game.Graphics.Sprites; using osu.Game.Replays; using osu.Game.Rulesets; @@ -18,16 +22,23 @@ using osu.Game.Tests.Visual; using osu.Game.Tests.Visual.UserInterface; using osuTK; using osuTK.Graphics; +using osuTK.Input; namespace osu.Game.Tests.Gameplay { - public class TestSceneReplayRecording : OsuTestScene + public class TestSceneReplayRecorder : OsuManualInputManagerTestScene { - private readonly TestRulesetInputManager playbackManager; + private TestRulesetInputManager playbackManager; + private TestRulesetInputManager recordingManager; - public TestSceneReplayRecording() + private Replay replay; + + private TestReplayRecorder recorder; + + [SetUp] + public void SetUp() => Schedule(() => { - Replay replay = new Replay(); + replay = new Replay(); Add(new GridContainer { @@ -36,9 +47,9 @@ namespace osu.Game.Tests.Gameplay { new Drawable[] { - new TestRulesetInputManager(new TestSceneModSettings.TestRulesetInfo(), 0, SimultaneousBindingMode.Unique) + recordingManager = new TestRulesetInputManager(new TestSceneModSettings.TestRulesetInfo(), 0, SimultaneousBindingMode.Unique) { - Recorder = new TestReplayRecorder(replay), + Recorder = recorder = new TestReplayRecorder(replay), Child = new Container { RelativeSizeAxes = Axes.Both, @@ -93,13 +104,65 @@ namespace osu.Game.Tests.Gameplay } } }); + }); + + [Test] + public void TestBasic() + { + AddStep("move to center", () => InputManager.MoveMouseTo(recordingManager.ScreenSpaceDrawQuad.Centre)); + AddUntilStep("one frame recorded", () => replay.Frames.Count == 1); + AddAssert("position matches", () => playbackManager.ChildrenOfType().First().Position == recordingManager.ChildrenOfType().First().Position); + } + + [Test] + public void TestHighFrameRate() + { + ScheduledDelegate moveFunction = null; + + AddStep("move to center", () => InputManager.MoveMouseTo(recordingManager.ScreenSpaceDrawQuad.Centre)); + AddStep("much move", () => moveFunction = Scheduler.AddDelayed(() => + InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(-1, 0)), 10, true)); + AddWaitStep("move", 10); + AddStep("stop move", () => moveFunction.Cancel()); + AddAssert("at least 60 frames recorded", () => replay.Frames.Count > 60); + } + + [Test] + public void TestLimitedFrameRate() + { + ScheduledDelegate moveFunction = null; + + AddStep("lower rate", () => recorder.RecordFrameRate = 2); + AddStep("move to center", () => InputManager.MoveMouseTo(recordingManager.ScreenSpaceDrawQuad.Centre)); + AddStep("much move", () => moveFunction = Scheduler.AddDelayed(() => + InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(-1, 0)), 10, true)); + AddWaitStep("move", 10); + AddStep("stop move", () => moveFunction.Cancel()); + AddAssert("less than 10 frames recorded", () => replay.Frames.Count < 10); + } + + [Test] + public void TestLimitedFrameRateWithImportantFrames() + { + ScheduledDelegate moveFunction = null; + + AddStep("lower rate", () => recorder.RecordFrameRate = 2); + AddStep("move to center", () => InputManager.MoveMouseTo(recordingManager.ScreenSpaceDrawQuad.Centre)); + AddStep("much move with press", () => moveFunction = Scheduler.AddDelayed(() => + { + InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(-1, 0)); + InputManager.PressButton(MouseButton.Left); + InputManager.ReleaseButton(MouseButton.Left); + }, 10, true)); + AddWaitStep("move", 10); + AddStep("stop move", () => moveFunction.Cancel()); + AddAssert("at least 60 frames recorded", () => replay.Frames.Count > 60); } protected override void Update() { base.Update(); - - playbackManager.ReplayInputHandler.SetFrameFromTime(Time.Current - 500); + playbackManager?.ReplayInputHandler.SetFrameFromTime(Time.Current - 100); } public class TestFramedReplayInputHandler : FramedReplayInputHandler From e85f45f91125ed1e428772df542294992586601e Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Tue, 24 Mar 2020 22:03:16 +0100 Subject: [PATCH 0582/2376] Move old ScreenTitle to MultiHeaderTitle --- .../Graphics/UserInterface/ScreenTitle.cs | 102 ------------------ .../UserInterface/ScreenTitleTextureIcon.cs | 40 ------- osu.Game/Screens/Multi/Header.cs | 77 ++++++++++++- 3 files changed, 73 insertions(+), 146 deletions(-) delete mode 100644 osu.Game/Graphics/UserInterface/ScreenTitle.cs delete mode 100644 osu.Game/Graphics/UserInterface/ScreenTitleTextureIcon.cs diff --git a/osu.Game/Graphics/UserInterface/ScreenTitle.cs b/osu.Game/Graphics/UserInterface/ScreenTitle.cs deleted file mode 100644 index ecd0508258..0000000000 --- a/osu.Game/Graphics/UserInterface/ScreenTitle.cs +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Game.Graphics.Sprites; -using osuTK; -using osuTK.Graphics; - -namespace osu.Game.Graphics.UserInterface -{ - public abstract class ScreenTitle : CompositeDrawable, IHasAccentColour - { - public const float ICON_WIDTH = ICON_SIZE + spacing; - - public const float ICON_SIZE = 25; - private const float spacing = 6; - private const int text_offset = 2; - - private SpriteIcon iconSprite; - private readonly OsuSpriteText titleText, pageText; - - protected IconUsage Icon - { - set - { - if (iconSprite == null) - throw new InvalidOperationException($"Cannot use {nameof(Icon)} with a custom {nameof(CreateIcon)} function."); - - iconSprite.Icon = value; - } - } - - protected string Title - { - set => titleText.Text = value; - } - - protected string Section - { - set => pageText.Text = value; - } - - public Color4 AccentColour - { - get => pageText.Colour; - set => pageText.Colour = value; - } - - protected virtual Drawable CreateIcon() => iconSprite = new SpriteIcon - { - Size = new Vector2(ICON_SIZE), - }; - - protected ScreenTitle() - { - AutoSizeAxes = Axes.Both; - - InternalChildren = new Drawable[] - { - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Spacing = new Vector2(spacing, 0), - Direction = FillDirection.Horizontal, - Children = new[] - { - CreateIcon().With(t => - { - t.Anchor = Anchor.Centre; - t.Origin = Anchor.Centre; - }), - titleText = new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Font = OsuFont.GetFont(size: 20, weight: FontWeight.Bold), - Margin = new MarginPadding { Bottom = text_offset } - }, - new Circle - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(4), - Colour = Color4.Gray, - }, - pageText = new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Font = OsuFont.GetFont(size: 20), - Margin = new MarginPadding { Bottom = text_offset } - } - } - }, - }; - } - } -} diff --git a/osu.Game/Graphics/UserInterface/ScreenTitleTextureIcon.cs b/osu.Game/Graphics/UserInterface/ScreenTitleTextureIcon.cs deleted file mode 100644 index c2a13970de..0000000000 --- a/osu.Game/Graphics/UserInterface/ScreenTitleTextureIcon.cs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Textures; -using osuTK; - -namespace osu.Game.Graphics.UserInterface -{ - /// - /// A custom icon class for use with based off a texture resource. - /// - public class ScreenTitleTextureIcon : CompositeDrawable - { - private readonly string textureName; - - public ScreenTitleTextureIcon(string textureName) - { - this.textureName = textureName; - } - - [BackgroundDependencyLoader] - private void load(TextureStore textures) - { - Size = new Vector2(ScreenTitle.ICON_SIZE); - - InternalChild = new Sprite - { - RelativeSizeAxes = Axes.Both, - Texture = textures.Get(textureName), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - FillMode = FillMode.Fit - }; - } - } -} diff --git a/osu.Game/Screens/Multi/Header.cs b/osu.Game/Screens/Multi/Header.cs index 0a05472ba3..6f790d703e 100644 --- a/osu.Game/Screens/Multi/Header.cs +++ b/osu.Game/Screens/Multi/Header.cs @@ -6,10 +6,13 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; using osu.Framework.Screens; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.SearchableList; +using osu.Game.Graphics.Sprites; +using osuTK; using osuTK.Graphics; namespace osu.Game.Screens.Multi @@ -43,7 +46,7 @@ namespace osu.Game.Screens.Multi { Anchor = Anchor.CentreLeft, Origin = Anchor.BottomLeft, - X = -ScreenTitle.ICON_WIDTH, + X = -MultiHeaderTitle.ICON_WIDTH, }, breadcrumbs = new HeaderBreadcrumbControl(stack) { @@ -70,18 +73,84 @@ namespace osu.Game.Screens.Multi breadcrumbs.StripColour = colours.Green; } - private class MultiHeaderTitle : ScreenTitle + private class MultiHeaderTitle : CompositeDrawable, IHasAccentColour { + public const float ICON_WIDTH = ICON_SIZE + spacing; + + public const float ICON_SIZE = 25; + private const float spacing = 6; + private const int text_offset = 2; + + private SpriteIcon iconSprite; + private readonly OsuSpriteText titleText, pageText; + public IMultiplayerSubScreen Screen { - set => Section = value.ShortTitle.ToLowerInvariant(); + set => pageText.Text = value.ShortTitle.ToLowerInvariant(); + } + + protected string Title + { + set => titleText.Text = value; + } + + public Color4 AccentColour + { + get => pageText.Colour; + set => pageText.Colour = value; + } + + public MultiHeaderTitle() + : base() + { + AutoSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(spacing, 0), + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + iconSprite = new SpriteIcon + { + Size = new Vector2(ICON_SIZE), + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }, + titleText = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.GetFont(size: 20, weight: FontWeight.Bold), + Margin = new MarginPadding { Bottom = text_offset } + }, + new Circle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(4), + Colour = Color4.Gray, + }, + pageText = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.GetFont(size: 20), + Margin = new MarginPadding { Bottom = text_offset } + } + } + }, + }; } [BackgroundDependencyLoader] private void load(OsuColour colours) { Title = "multi"; - Icon = OsuIcon.Multi; + iconSprite.Icon = OsuIcon.Multi; AccentColour = colours.Yellow; } } From 127c16fccdfc0b836b4221e1692a83e232965257 Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Tue, 24 Mar 2020 22:03:38 +0100 Subject: [PATCH 0583/2376] Implement OverlayTitle component --- osu.Game/Overlays/OverlayTitle.cs | 80 +++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 osu.Game/Overlays/OverlayTitle.cs diff --git a/osu.Game/Overlays/OverlayTitle.cs b/osu.Game/Overlays/OverlayTitle.cs new file mode 100644 index 0000000000..9fafee41b6 --- /dev/null +++ b/osu.Game/Overlays/OverlayTitle.cs @@ -0,0 +1,80 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osuTK; + +namespace osu.Game.Overlays +{ + public abstract class OverlayTitle : CompositeDrawable + { + private readonly OsuSpriteText title; + private readonly Container icon; + + protected string Title + { + set => title.Text = value; + } + + protected string IconTexture + { + set => icon.Child = new OverlayTitleIcon(value); + } + + protected OverlayTitle() + { + AutoSizeAxes = Axes.Both; + + InternalChild = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(10, 0), + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + icon = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Margin = new MarginPadding { Horizontal = 5 }, // compensates for osu-web sprites having around 5px of whitespace on each side + Size = new Vector2(30) + }, + title = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.GetFont(size: 20, weight: FontWeight.Regular), + Margin = new MarginPadding { Vertical = 17.5f } // 15px padding + 2.5px line-height difference compensation + } + } + }; + } + + private class OverlayTitleIcon : Sprite + { + private readonly string textureName; + + public OverlayTitleIcon(string textureName) + { + this.textureName = textureName; + + RelativeSizeAxes = Axes.Both; + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + FillMode = FillMode.Fit; + } + + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + Texture = textures.Get(textureName); + } + } + } +} \ No newline at end of file From a5781d7fc595f6c017da739567233aa1dca63adc Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Tue, 24 Mar 2020 22:08:20 +0100 Subject: [PATCH 0584/2376] Replace ScreenTitle with OverlayTitle and update titles to match new design --- .../UserInterface/TestSceneOverlayHeader.cs | 16 +++++++------- .../BeatmapListing/BeatmapListingHeader.cs | 13 ++++-------- .../Overlays/BeatmapSet/BeatmapSetHeader.cs | 11 ++++------ .../Overlays/Changelog/ChangelogHeader.cs | 19 +++-------------- osu.Game/Overlays/News/NewsHeader.cs | 21 +++---------------- osu.Game/Overlays/OverlayHeader.cs | 8 +++---- osu.Game/Overlays/OverlayTitle.cs | 4 ++-- osu.Game/Overlays/Profile/ProfileHeader.cs | 11 ++++------ .../Rankings/RankingsOverlayHeader.cs | 21 ++++--------------- 9 files changed, 34 insertions(+), 90 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs index 1cd68d1fdd..9dc71c7e74 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs @@ -100,21 +100,21 @@ namespace osu.Game.Tests.Visual.UserInterface private class TestNoBackgroundHeader : OverlayHeader { - protected override ScreenTitle CreateTitle() => new TestTitle(); + protected override OverlayTitle CreateTitle() => new TestTitle(); } private class TestNoControlHeader : OverlayHeader { protected override Drawable CreateBackground() => new OverlayHeaderBackground(@"Headers/changelog"); - protected override ScreenTitle CreateTitle() => new TestTitle(); + protected override OverlayTitle CreateTitle() => new TestTitle(); } private class TestStringTabControlHeader : TabControlOverlayHeader { protected override Drawable CreateBackground() => new OverlayHeaderBackground(@"Headers/news"); - protected override ScreenTitle CreateTitle() => new TestTitle(); + protected override OverlayTitle CreateTitle() => new TestTitle(); protected override Drawable CreateTitleContent() => new OverlayRulesetSelector(); @@ -129,7 +129,7 @@ namespace osu.Game.Tests.Visual.UserInterface { protected override Drawable CreateBackground() => new OverlayHeaderBackground(@"Headers/rankings"); - protected override ScreenTitle CreateTitle() => new TestTitle(); + protected override OverlayTitle CreateTitle() => new TestTitle(); } private enum TestEnum @@ -141,7 +141,7 @@ namespace osu.Game.Tests.Visual.UserInterface private class TestBreadcrumbControlHeader : BreadcrumbControlOverlayHeader { - protected override ScreenTitle CreateTitle() => new TestTitle(); + protected override OverlayTitle CreateTitle() => new TestTitle(); public TestBreadcrumbControlHeader() { @@ -151,15 +151,13 @@ namespace osu.Game.Tests.Visual.UserInterface } } - private class TestTitle : ScreenTitle + private class TestTitle : OverlayTitle { public TestTitle() { Title = "title"; - Section = "section"; + IconTexture = "Icons/changelog"; } - - protected override Drawable CreateIcon() => new ScreenTitleTextureIcon(@"Icons/changelog"); } } } diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs index 5af92914de..1bab200fec 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs @@ -1,24 +1,19 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Graphics; -using osu.Game.Graphics.UserInterface; - namespace osu.Game.Overlays.BeatmapListing { public class BeatmapListingHeader : OverlayHeader { - protected override ScreenTitle CreateTitle() => new BeatmapListingTitle(); + protected override OverlayTitle CreateTitle() => new BeatmapListingTitle(); - private class BeatmapListingTitle : ScreenTitle + private class BeatmapListingTitle : OverlayTitle { public BeatmapListingTitle() { - Title = @"beatmap"; - Section = @"listing"; + Title = "beatmap listing"; + IconTexture = "Icons/changelog"; } - - protected override Drawable CreateIcon() => new ScreenTitleTextureIcon(@"Icons/changelog"); } } } diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeader.cs b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeader.cs index e5e3e276d5..4626589d81 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeader.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeader.cs @@ -3,7 +3,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets; namespace osu.Game.Overlays.BeatmapSet @@ -14,22 +13,20 @@ namespace osu.Game.Overlays.BeatmapSet public BeatmapRulesetSelector RulesetSelector { get; private set; } - protected override ScreenTitle CreateTitle() => new BeatmapHeaderTitle(); + protected override OverlayTitle CreateTitle() => new BeatmapHeaderTitle(); protected override Drawable CreateTitleContent() => RulesetSelector = new BeatmapRulesetSelector { Current = Ruleset }; - private class BeatmapHeaderTitle : ScreenTitle + private class BeatmapHeaderTitle : OverlayTitle { public BeatmapHeaderTitle() { - Title = @"beatmap"; - Section = @"info"; + Title = "beatmap info"; + IconTexture = "Icons/changelog"; } - - protected override Drawable CreateIcon() => new ScreenTitleTextureIcon(@"Icons/changelog"); } } } diff --git a/osu.Game/Overlays/Changelog/ChangelogHeader.cs b/osu.Game/Overlays/Changelog/ChangelogHeader.cs index 532efeb4bd..050bdea03a 100644 --- a/osu.Game/Overlays/Changelog/ChangelogHeader.cs +++ b/osu.Game/Overlays/Changelog/ChangelogHeader.cs @@ -9,7 +9,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics.UserInterface; using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Overlays.Changelog @@ -50,8 +49,6 @@ namespace osu.Game.Overlays.Changelog streamsBackground.Colour = colourProvider.Background5; } - private ChangelogHeaderTitle title; - private void showBuild(ValueChangedEvent e) { if (e.OldValue != null) @@ -63,14 +60,11 @@ namespace osu.Game.Overlays.Changelog Current.Value = e.NewValue.ToString(); updateCurrentStream(); - - title.Version = e.NewValue.UpdateStream.DisplayName; } else { Current.Value = listing_string; Streams.Current.Value = null; - title.Version = null; } } @@ -100,7 +94,7 @@ namespace osu.Game.Overlays.Changelog } }; - protected override ScreenTitle CreateTitle() => title = new ChangelogHeaderTitle(); + protected override OverlayTitle CreateTitle() => new ChangelogHeaderTitle(); public void Populate(List streams) { @@ -116,20 +110,13 @@ namespace osu.Game.Overlays.Changelog Streams.Current.Value = Streams.Items.FirstOrDefault(s => s.Name == Build.Value.UpdateStream.Name); } - private class ChangelogHeaderTitle : ScreenTitle + private class ChangelogHeaderTitle : OverlayTitle { - public string Version - { - set => Section = value ?? listing_string; - } - public ChangelogHeaderTitle() { Title = "changelog"; - Version = null; + IconTexture = "Icons/changelog"; } - - protected override Drawable CreateIcon() => new ScreenTitleTextureIcon(@"Icons/changelog"); } } } diff --git a/osu.Game/Overlays/News/NewsHeader.cs b/osu.Game/Overlays/News/NewsHeader.cs index b55e3ffba0..8214c71b3a 100644 --- a/osu.Game/Overlays/News/NewsHeader.cs +++ b/osu.Game/Overlays/News/NewsHeader.cs @@ -3,7 +3,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Game.Graphics.UserInterface; using System; namespace osu.Game.Overlays.News @@ -12,8 +11,6 @@ namespace osu.Game.Overlays.News { private const string front_page_string = "frontpage"; - private NewsHeaderTitle title; - public readonly Bindable Post = new Bindable(null); public Action ShowFrontPage; @@ -40,36 +37,24 @@ namespace osu.Game.Overlays.News { TabControl.AddItem(e.NewValue); Current.Value = e.NewValue; - - title.IsReadingPost = true; } else { Current.Value = front_page_string; - title.IsReadingPost = false; } } protected override Drawable CreateBackground() => new OverlayHeaderBackground(@"Headers/news"); - protected override ScreenTitle CreateTitle() => title = new NewsHeaderTitle(); + protected override OverlayTitle CreateTitle() => new NewsHeaderTitle(); - private class NewsHeaderTitle : ScreenTitle + private class NewsHeaderTitle : OverlayTitle { - private const string post_string = "post"; - - public bool IsReadingPost - { - set => Section = value ? post_string : front_page_string; - } - public NewsHeaderTitle() { Title = "news"; - IsReadingPost = false; + IconTexture = "Icons/news"; } - - protected override Drawable CreateIcon() => new ScreenTitleTextureIcon(@"Icons/news"); } } } diff --git a/osu.Game/Overlays/OverlayHeader.cs b/osu.Game/Overlays/OverlayHeader.cs index bedf8e5435..f017d66485 100644 --- a/osu.Game/Overlays/OverlayHeader.cs +++ b/osu.Game/Overlays/OverlayHeader.cs @@ -14,7 +14,7 @@ namespace osu.Game.Overlays public abstract class OverlayHeader : Container { private readonly Box titleBackground; - private readonly ScreenTitle title; + private readonly OverlayTitle title; protected readonly FillFlowContainer HeaderInfo; @@ -57,7 +57,6 @@ namespace osu.Game.Overlays Padding = new MarginPadding { Horizontal = UserProfileOverlay.CONTENT_X_MARGIN, - Vertical = 10, }, Children = new[] { @@ -86,7 +85,6 @@ namespace osu.Game.Overlays private void load(OverlayColourProvider colourProvider) { titleBackground.Colour = colourProvider.Dark5; - title.AccentColour = colourProvider.Highlight1; } [NotNull] @@ -96,11 +94,11 @@ namespace osu.Game.Overlays protected virtual Drawable CreateBackground() => Empty(); /// - /// Creates a on the opposite side of the . Used mostly to create . + /// Creates a on the opposite side of the . Used mostly to create . /// [NotNull] protected virtual Drawable CreateTitleContent() => Empty(); - protected abstract ScreenTitle CreateTitle(); + protected abstract OverlayTitle CreateTitle(); } } diff --git a/osu.Game/Overlays/OverlayTitle.cs b/osu.Game/Overlays/OverlayTitle.cs index 9fafee41b6..1c9567428c 100644 --- a/osu.Game/Overlays/OverlayTitle.cs +++ b/osu.Game/Overlays/OverlayTitle.cs @@ -63,7 +63,7 @@ namespace osu.Game.Overlays public OverlayTitleIcon(string textureName) { this.textureName = textureName; - + RelativeSizeAxes = Axes.Both; Anchor = Anchor.Centre; Origin = Anchor.Centre; @@ -77,4 +77,4 @@ namespace osu.Game.Overlays } } } -} \ No newline at end of file +} diff --git a/osu.Game/Overlays/Profile/ProfileHeader.cs b/osu.Game/Overlays/Profile/ProfileHeader.cs index f7c09e33c1..0161d91daa 100644 --- a/osu.Game/Overlays/Profile/ProfileHeader.cs +++ b/osu.Game/Overlays/Profile/ProfileHeader.cs @@ -7,7 +7,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Profile.Header; using osu.Game.Users; @@ -87,19 +86,17 @@ namespace osu.Game.Overlays.Profile } }; - protected override ScreenTitle CreateTitle() => new ProfileHeaderTitle(); + protected override OverlayTitle CreateTitle() => new ProfileHeaderTitle(); private void updateDisplay(User user) => coverContainer.User = user; - private class ProfileHeaderTitle : ScreenTitle + private class ProfileHeaderTitle : OverlayTitle { public ProfileHeaderTitle() { - Title = "player"; - Section = "info"; + Title = "player info"; + IconTexture = "Icons/profile"; } - - protected override Drawable CreateIcon() => new ScreenTitleTextureIcon(@"Icons/profile"); } } } diff --git a/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs b/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs index 99325aa1da..e30c6f07a8 100644 --- a/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs +++ b/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs @@ -3,7 +3,6 @@ using osu.Framework.Graphics; using osu.Framework.Bindables; -using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets; using osu.Game.Users; @@ -18,33 +17,21 @@ namespace osu.Game.Overlays.Rankings private OverlayRulesetSelector rulesetSelector; private CountryFilter countryFilter; - protected override ScreenTitle CreateTitle() => new RankingsTitle - { - Scope = { BindTarget = Current } - }; + protected override OverlayTitle CreateTitle() => new RankingsTitle(); protected override Drawable CreateTitleContent() => rulesetSelector = new OverlayRulesetSelector(); protected override Drawable CreateContent() => countryFilter = new CountryFilter(); - protected override Drawable CreateBackground() => new OverlayHeaderBackground(@"Headers/rankings"); + protected override Drawable CreateBackground() => new OverlayHeaderBackground("Headers/rankings"); - private class RankingsTitle : ScreenTitle + private class RankingsTitle : OverlayTitle { - public readonly Bindable Scope = new Bindable(); - public RankingsTitle() { Title = "ranking"; + IconTexture = "Icons/rankings"; } - - protected override void LoadComplete() - { - base.LoadComplete(); - Scope.BindValueChanged(scope => Section = scope.NewValue.ToString().ToLowerInvariant(), true); - } - - protected override Drawable CreateIcon() => new ScreenTitleTextureIcon(@"Icons/rankings"); } } From 05de65937b950e7e35b3b55dbc6cf58528a54d8b Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Tue, 24 Mar 2020 22:14:15 +0100 Subject: [PATCH 0585/2376] Update ruleset selector design --- osu.Game/Overlays/OverlayRulesetSelector.cs | 2 +- osu.Game/Overlays/OverlayRulesetTabItem.cs | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/OverlayRulesetSelector.cs b/osu.Game/Overlays/OverlayRulesetSelector.cs index b73d38eeb3..8c44157f78 100644 --- a/osu.Game/Overlays/OverlayRulesetSelector.cs +++ b/osu.Game/Overlays/OverlayRulesetSelector.cs @@ -22,7 +22,7 @@ namespace osu.Game.Overlays { AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, - Spacing = new Vector2(25, 0), + Spacing = new Vector2(20, 0), }; } } diff --git a/osu.Game/Overlays/OverlayRulesetTabItem.cs b/osu.Game/Overlays/OverlayRulesetTabItem.cs index 9b4dd5ba1e..9d4afc94d1 100644 --- a/osu.Game/Overlays/OverlayRulesetTabItem.cs +++ b/osu.Game/Overlays/OverlayRulesetTabItem.cs @@ -12,6 +12,7 @@ using osu.Game.Rulesets; using osuTK.Graphics; using osuTK; using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; namespace osu.Game.Overlays { @@ -53,6 +54,8 @@ namespace osu.Game.Overlays Origin = Anchor.Centre, Anchor = Anchor.Centre, Text = value.Name, + Font = OsuFont.GetFont(size: 14), + ShadowColour = Color4.Black.Opacity(0.75f) } }, new HoverClickSounds() From 368bf585217409695bb4b336b97907b23834c560 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 25 Mar 2020 11:08:08 +0900 Subject: [PATCH 0586/2376] Rename and make fields readonly --- .../Formats/LegacyStoryboardDecoderTest.cs | 16 ++++++++-------- .../Storyboards/Drawables/DrawableStoryboard.cs | 2 +- .../Drawables/DrawableStoryboardLayer.cs | 2 +- osu.Game/Storyboards/Storyboard.cs | 4 ++-- osu.Game/Storyboards/StoryboardLayer.cs | 14 +++++++++----- 5 files changed, 21 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs index 76b76aa357..a6945dca90 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs @@ -31,29 +31,29 @@ namespace osu.Game.Tests.Beatmaps.Formats StoryboardLayer background = storyboard.Layers.FirstOrDefault(l => l.Depth == 3); Assert.IsNotNull(background); Assert.AreEqual(16, background.Elements.Count); - Assert.IsTrue(background.EnabledWhenFailing); - Assert.IsTrue(background.EnabledWhenPassing); + Assert.IsTrue(background.VisibleWhenFailing); + Assert.IsTrue(background.VisibleWhenPassing); Assert.AreEqual("Background", background.Name); StoryboardLayer fail = storyboard.Layers.FirstOrDefault(l => l.Depth == 2); Assert.IsNotNull(fail); Assert.AreEqual(0, fail.Elements.Count); - Assert.IsTrue(fail.EnabledWhenFailing); - Assert.IsFalse(fail.EnabledWhenPassing); + Assert.IsTrue(fail.VisibleWhenFailing); + Assert.IsFalse(fail.VisibleWhenPassing); Assert.AreEqual("Fail", fail.Name); StoryboardLayer pass = storyboard.Layers.FirstOrDefault(l => l.Depth == 1); Assert.IsNotNull(pass); Assert.AreEqual(0, pass.Elements.Count); - Assert.IsFalse(pass.EnabledWhenFailing); - Assert.IsTrue(pass.EnabledWhenPassing); + Assert.IsFalse(pass.VisibleWhenFailing); + Assert.IsTrue(pass.VisibleWhenPassing); Assert.AreEqual("Pass", pass.Name); StoryboardLayer foreground = storyboard.Layers.FirstOrDefault(l => l.Depth == 0); Assert.IsNotNull(foreground); Assert.AreEqual(151, foreground.Elements.Count); - Assert.IsTrue(foreground.EnabledWhenFailing); - Assert.IsTrue(foreground.EnabledWhenPassing); + Assert.IsTrue(foreground.VisibleWhenFailing); + Assert.IsTrue(foreground.VisibleWhenPassing); Assert.AreEqual("Foreground", foreground.Name); int spriteCount = background.Elements.Count(x => x.GetType() == typeof(StoryboardSprite)); diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs index 94d7395ecf..bc6e01a729 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs @@ -75,7 +75,7 @@ namespace osu.Game.Storyboards.Drawables private void updateLayerVisibility() { foreach (var layer in Children) - layer.Enabled = passing ? layer.Layer.EnabledWhenPassing : layer.Layer.EnabledWhenFailing; + layer.Enabled = passing ? layer.Layer.VisibleWhenPassing : layer.Layer.VisibleWhenFailing; } } } diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardLayer.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardLayer.cs index 69219fb038..def4eed2ca 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardLayer.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardLayer.cs @@ -21,7 +21,7 @@ namespace osu.Game.Storyboards.Drawables RelativeSizeAxes = Axes.Both; Anchor = Anchor.Centre; Origin = Anchor.Centre; - Enabled = layer.EnabledWhenPassing; + Enabled = layer.VisibleWhenPassing; Masking = layer.Masking; } diff --git a/osu.Game/Storyboards/Storyboard.cs b/osu.Game/Storyboards/Storyboard.cs index 35bfe8c229..2ba8563ba8 100644 --- a/osu.Game/Storyboards/Storyboard.cs +++ b/osu.Game/Storyboards/Storyboard.cs @@ -22,8 +22,8 @@ namespace osu.Game.Storyboards public Storyboard() { layers.Add("Background", new StoryboardLayer("Background", 3)); - layers.Add("Fail", new StoryboardLayer("Fail", 2) { EnabledWhenPassing = false, }); - layers.Add("Pass", new StoryboardLayer("Pass", 1) { EnabledWhenFailing = false, }); + layers.Add("Fail", new StoryboardLayer("Fail", 2) { VisibleWhenPassing = false, }); + layers.Add("Pass", new StoryboardLayer("Pass", 1) { VisibleWhenFailing = false, }); layers.Add("Foreground", new StoryboardLayer("Foreground", 0)); } diff --git a/osu.Game/Storyboards/StoryboardLayer.cs b/osu.Game/Storyboards/StoryboardLayer.cs index e3b74ca609..142bc60deb 100644 --- a/osu.Game/Storyboards/StoryboardLayer.cs +++ b/osu.Game/Storyboards/StoryboardLayer.cs @@ -8,11 +8,15 @@ namespace osu.Game.Storyboards { public class StoryboardLayer { - public string Name; - public int Depth; - public bool Masking; - public bool EnabledWhenPassing = true; - public bool EnabledWhenFailing = true; + public readonly string Name; + + public readonly int Depth; + + public readonly bool Masking; + + public bool VisibleWhenPassing = true; + + public bool VisibleWhenFailing = true; public List Elements = new List(); From f2e0fba1648ee4668b63e3b30d7cb92ad137fe48 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 25 Mar 2020 11:59:59 +0900 Subject: [PATCH 0587/2376] Remove VideoFile from BeatmapMetadata Leaving in database because it's a pain to drop columns. --- osu.Game/Beatmaps/BeatmapMetadata.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapMetadata.cs b/osu.Game/Beatmaps/BeatmapMetadata.cs index 9267527d79..001f319307 100644 --- a/osu.Game/Beatmaps/BeatmapMetadata.cs +++ b/osu.Game/Beatmaps/BeatmapMetadata.cs @@ -52,7 +52,6 @@ namespace osu.Game.Beatmaps public int PreviewTime { get; set; } public string AudioFile { get; set; } public string BackgroundFile { get; set; } - public string VideoFile { get; set; } public override string ToString() => $"{Artist} - {Title} ({Author})"; @@ -82,8 +81,7 @@ namespace osu.Game.Beatmaps && Tags == other.Tags && PreviewTime == other.PreviewTime && AudioFile == other.AudioFile - && BackgroundFile == other.BackgroundFile - && VideoFile == other.VideoFile; + && BackgroundFile == other.BackgroundFile; } } } From b8f20831a18e8cb6bf1dddc7d08ee3b75d325e35 Mon Sep 17 00:00:00 2001 From: voidedWarranties Date: Tue, 24 Mar 2020 20:04:09 -0700 Subject: [PATCH 0588/2376] Video no longer modifies storyboard resolution --- osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs | 4 ++-- osu.Game/Storyboards/Storyboard.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs index ba4eb7209b..269449ef80 100644 --- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs @@ -90,9 +90,9 @@ namespace osu.Game.Beatmaps.Formats case LegacyEventType.Video: { var offset = Parsing.ParseInt(split[1]); - var filename = CleanFilename(split[2]); + var path = CleanFilename(split[2]); - storyboard.GetLayer("Video").Add(new StoryboardVideo(filename, offset)); + storyboard.GetLayer("Video").Add(new StoryboardVideo(path, offset)); break; } diff --git a/osu.Game/Storyboards/Storyboard.cs b/osu.Game/Storyboards/Storyboard.cs index dba8b4e176..a1ddafbacf 100644 --- a/osu.Game/Storyboards/Storyboard.cs +++ b/osu.Game/Storyboards/Storyboard.cs @@ -21,7 +21,7 @@ namespace osu.Game.Storyboards public Storyboard() { - layers.Add("Video", new StoryboardLayer("Video", 4)); + layers.Add("Video", new StoryboardLayer("Video", 4, false)); layers.Add("Background", new StoryboardLayer("Background", 3)); layers.Add("Fail", new StoryboardLayer("Fail", 2) { VisibleWhenPassing = false, }); layers.Add("Pass", new StoryboardLayer("Pass", 1) { VisibleWhenFailing = false, }); @@ -57,7 +57,7 @@ namespace osu.Game.Storyboards public DrawableStoryboard CreateDrawable(WorkingBeatmap working = null) { var drawable = new DrawableStoryboard(this); - drawable.Width = drawable.Height * (BeatmapInfo.WidescreenStoryboard || GetLayer("Video").Elements.Any() ? 16 / 9f : 4 / 3f); + drawable.Width = drawable.Height * (BeatmapInfo.WidescreenStoryboard ? 16 / 9f : 4 / 3f); return drawable; } } From 87db1ba48703d07371f12f822d4d2eb765c1adfb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 25 Mar 2020 14:58:49 +0900 Subject: [PATCH 0589/2376] Remove unused text transform helpers --- osu.Game/Graphics/Sprites/OsuSpriteText.cs | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/osu.Game/Graphics/Sprites/OsuSpriteText.cs b/osu.Game/Graphics/Sprites/OsuSpriteText.cs index cd988c347b..76e46513ba 100644 --- a/osu.Game/Graphics/Sprites/OsuSpriteText.cs +++ b/osu.Game/Graphics/Sprites/OsuSpriteText.cs @@ -1,9 +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.Graphics; using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Transforms; namespace osu.Game.Graphics.Sprites { @@ -15,23 +13,4 @@ namespace osu.Game.Graphics.Sprites Font = OsuFont.Default; } } - - public static class OsuSpriteTextTransformExtensions - { - /// - /// Sets Text to a new value after a duration. - /// - /// A to which further transforms can be added. - public static TransformSequence TransformTextTo(this T spriteText, string newText, double duration = 0, Easing easing = Easing.None) - where T : OsuSpriteText - => spriteText.TransformTo(nameof(OsuSpriteText.Text), newText, duration, easing); - - /// - /// Sets Text to a new value after a duration. - /// - /// A to which further transforms can be added. - public static TransformSequence TransformTextTo(this TransformSequence t, string newText, double duration = 0, Easing easing = Easing.None) - where T : OsuSpriteText - => t.Append(o => o.TransformTextTo(newText, duration, easing)); - } } From 880d138a47276f82323d3187e54375abfc85d252 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 25 Mar 2020 15:12:19 +0900 Subject: [PATCH 0590/2376] Fix intro tests not asserting pass or working at all --- osu.Game.Tests/Visual/Menus/IntroTestScene.cs | 2 ++ osu.Game/Screens/Menu/MainMenu.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 5870ef9813..1ad4d9dca9 100644 --- a/osu.Game.Tests/Visual/Menus/IntroTestScene.cs +++ b/osu.Game.Tests/Visual/Menus/IntroTestScene.cs @@ -64,6 +64,8 @@ namespace osu.Game.Tests.Visual.Menus introStack.Push(CreateScreen()); }); + + AddUntilStep("wait for menu", () => introStack.CurrentScreen is MainMenu); } protected abstract IScreen CreateScreen(); diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 127270f521..dcee5e83b7 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -140,7 +140,7 @@ namespace osu.Game.Screens.Menu preloadSongSelect(); } - [Resolved] + [Resolved(canBeNull: true)] private OsuGame game { get; set; } private void confirmAndExit() @@ -148,7 +148,7 @@ namespace osu.Game.Screens.Menu if (exitConfirmed) return; exitConfirmed = true; - game.PerformFromScreen(menu => menu.Exit()); + game?.PerformFromScreen(menu => menu.Exit()); } private void preloadSongSelect() From b1d4261402088dd44aee59130863d15be2f90add Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 25 Mar 2020 15:45:33 +0900 Subject: [PATCH 0591/2376] Fix track looping state not being reset when entering editor from song select Closes #8432. --- osu.Game/Screens/Select/PlaySongSelect.cs | 2 -- osu.Game/Screens/Select/SongSelect.cs | 3 +++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index 8b0547376d..179aab54a3 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -85,8 +85,6 @@ namespace osu.Game.Screens.Select } } - Beatmap.Value.Track.Looping = false; - SampleConfirm?.Play(); this.Push(player = new PlayerLoader(() => new Player())); diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index b6ec40ab88..895a8ad0c9 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -572,6 +572,9 @@ namespace osu.Game.Screens.Select BeatmapOptions.Hide(); + if (Beatmap.Value.Track != null) + Beatmap.Value.Track.Looping = false; + this.ScaleTo(1.1f, 250, Easing.InSine); this.FadeOut(250); From 8a2aac5f8377a27ceb33e22eeb5fa8dbb4432c9d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 25 Mar 2020 20:21:34 +0900 Subject: [PATCH 0592/2376] Rename conversion methods for clarity --- osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs | 4 ++-- osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs | 4 ++-- osu.Game.Rulesets.Osu/Replays/OsuReplayFrame.cs | 4 ++-- osu.Game.Rulesets.Taiko/Replays/TaikoReplayFrame.cs | 4 ++-- osu.Game/Rulesets/Replays/Types/IConvertibleReplayFrame.cs | 4 ++-- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 2 +- osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs | 2 +- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs b/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs index bc60f16ae8..9dab3ed630 100644 --- a/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs +++ b/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Catch.Replays } } - public void ConvertFrom(LegacyReplayFrame currentFrame, IBeatmap beatmap, ReplayFrame lastFrame = null) + public void FromLegacy(LegacyReplayFrame currentFrame, IBeatmap beatmap, ReplayFrame lastFrame = null) { Position = currentFrame.Position.X / CatchPlayfield.BASE_WIDTH; Dashing = currentFrame.ButtonState == ReplayButtonState.Left1; @@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Catch.Replays } } - public LegacyReplayFrame ConvertTo(IBeatmap beatmap) + public LegacyReplayFrame ToLegacy(IBeatmap beatmap) { ReplayButtonState state = ReplayButtonState.None; diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs b/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs index 4987aa8e4c..b93e372027 100644 --- a/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs +++ b/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Mania.Replays Actions.AddRange(actions); } - public void ConvertFrom(LegacyReplayFrame legacyFrame, IBeatmap beatmap, ReplayFrame lastFrame = null) + public void FromLegacy(LegacyReplayFrame legacyFrame, IBeatmap beatmap, ReplayFrame lastFrame = null) { // We don't need to fully convert, just create the converter var converter = new ManiaBeatmapConverter(beatmap, new ManiaRuleset()); @@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Mania.Replays } } - public LegacyReplayFrame ConvertTo(IBeatmap beatmap) + public LegacyReplayFrame ToLegacy(IBeatmap beatmap) { int keys = 0; diff --git a/osu.Game.Rulesets.Osu/Replays/OsuReplayFrame.cs b/osu.Game.Rulesets.Osu/Replays/OsuReplayFrame.cs index 93cf4db5b1..3db81d70da 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuReplayFrame.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuReplayFrame.cs @@ -26,14 +26,14 @@ namespace osu.Game.Rulesets.Osu.Replays Actions.AddRange(actions); } - public void ConvertFrom(LegacyReplayFrame currentFrame, IBeatmap beatmap, ReplayFrame lastFrame = null) + public void FromLegacy(LegacyReplayFrame currentFrame, IBeatmap beatmap, ReplayFrame lastFrame = null) { Position = currentFrame.Position; if (currentFrame.MouseLeft) Actions.Add(OsuAction.LeftButton); if (currentFrame.MouseRight) Actions.Add(OsuAction.RightButton); } - public LegacyReplayFrame ConvertTo(IBeatmap beatmap) + public LegacyReplayFrame ToLegacy(IBeatmap beatmap) { ReplayButtonState state = ReplayButtonState.None; diff --git a/osu.Game.Rulesets.Taiko/Replays/TaikoReplayFrame.cs b/osu.Game.Rulesets.Taiko/Replays/TaikoReplayFrame.cs index cb4ca35c2b..d2a7329a28 100644 --- a/osu.Game.Rulesets.Taiko/Replays/TaikoReplayFrame.cs +++ b/osu.Game.Rulesets.Taiko/Replays/TaikoReplayFrame.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Taiko.Replays Actions.AddRange(actions); } - public void ConvertFrom(LegacyReplayFrame currentFrame, IBeatmap beatmap, ReplayFrame lastFrame = null) + public void FromLegacy(LegacyReplayFrame currentFrame, IBeatmap beatmap, ReplayFrame lastFrame = null) { if (currentFrame.MouseRight1) Actions.Add(TaikoAction.LeftRim); if (currentFrame.MouseRight2) Actions.Add(TaikoAction.RightRim); @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Taiko.Replays if (currentFrame.MouseLeft2) Actions.Add(TaikoAction.RightCentre); } - public LegacyReplayFrame ConvertTo(IBeatmap beatmap) + public LegacyReplayFrame ToLegacy(IBeatmap beatmap) { ReplayButtonState state = ReplayButtonState.None; diff --git a/osu.Game/Rulesets/Replays/Types/IConvertibleReplayFrame.cs b/osu.Game/Rulesets/Replays/Types/IConvertibleReplayFrame.cs index a240e7aa0e..d9aa615c6e 100644 --- a/osu.Game/Rulesets/Replays/Types/IConvertibleReplayFrame.cs +++ b/osu.Game/Rulesets/Replays/Types/IConvertibleReplayFrame.cs @@ -17,12 +17,12 @@ namespace osu.Game.Rulesets.Replays.Types /// The to extract values from. /// The beatmap. /// The last post-conversion , used to fill in missing delta information. May be null. - void ConvertFrom(LegacyReplayFrame currentFrame, IBeatmap beatmap, ReplayFrame lastFrame = null); + void FromLegacy(LegacyReplayFrame currentFrame, IBeatmap beatmap, ReplayFrame lastFrame = null); /// /// Populates this using values from a . /// /// The beatmap. - LegacyReplayFrame ConvertTo(IBeatmap beatmap); + LegacyReplayFrame ToLegacy(IBeatmap beatmap); } } diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index 495d8c8cc0..58b64e1b8f 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -264,7 +264,7 @@ namespace osu.Game.Scoring.Legacy if (convertible == null) throw new InvalidOperationException($"Legacy replay cannot be converted for the ruleset: {currentRuleset.Description}"); - convertible.ConvertFrom(currentFrame, currentBeatmap, lastFrame); + convertible.FromLegacy(currentFrame, currentBeatmap, lastFrame); var frame = (ReplayFrame)convertible; frame.Time = currentFrame.Time; diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index 515cdc8864..db7e51e833 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -93,7 +93,7 @@ namespace osu.Game.Scoring.Legacy { LegacyReplayFrame lastF = new LegacyReplayFrame(0, 0, 0, ReplayButtonState.None); - foreach (var f in score.Replay.Frames.OfType().Select(f => f.ConvertTo(beatmap))) + foreach (var f in score.Replay.Frames.OfType().Select(f => f.ToLegacy(beatmap))) { replayData.Append(FormattableString.Invariant($"{f.Time - lastF.Time}|{f.MouseX ?? 0}|{f.MouseY ?? 0}|{(int)f.ButtonState},")); lastF = f; From 1e025b7c3187a6dc31d363aeb89ead355f85135d Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 25 Mar 2020 20:58:51 +0300 Subject: [PATCH 0593/2376] Add tests to cover the issue --- .../Visual/Online/TestSceneUserPanel.cs | 32 +++++++++++++++++-- osu.Game/Users/UserPanel.cs | 7 ++-- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs index ccae778745..a38f045e7f 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs @@ -28,7 +28,7 @@ namespace osu.Game.Tests.Visual.Online private readonly Bindable status = new Bindable(); private UserGridPanel peppy; - private UserListPanel evast; + private TestUserListPanel evast; [Resolved] private RulesetStore rulesetStore { get; set; } @@ -38,6 +38,9 @@ namespace osu.Game.Tests.Visual.Online { UserGridPanel flyte; + activity.Value = null; + status.Value = null; + Child = new FillFlowContainer { Anchor = Anchor.Centre, @@ -63,7 +66,7 @@ namespace osu.Game.Tests.Visual.Online IsSupporter = true, SupportLevel = 3, }) { Width = 300 }, - evast = new UserListPanel(new User + evast = new TestUserListPanel(new User { Username = @"Evast", Id = 8195163, @@ -96,7 +99,7 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestUserActivity() { - AddStep("set online status", () => peppy.Status.Value = evast.Status.Value = new UserStatusOnline()); + AddStep("set online status", () => status.Value = new UserStatusOnline()); AddStep("idle", () => activity.Value = null); AddStep("spectating", () => activity.Value = new UserActivity.Spectating()); @@ -109,6 +112,29 @@ namespace osu.Game.Tests.Visual.Online AddStep("modding", () => activity.Value = new UserActivity.Modding()); } + [Test] + public void TestUserActivityChange() + { + AddAssert("visit message is visible", () => evast.LastVisitMessage.IsPresent); + AddStep("set online status", () => status.Value = new UserStatusOnline()); + AddAssert("visit message is not visible", () => !evast.LastVisitMessage.IsPresent); + AddStep("set choosing activity", () => activity.Value = new UserActivity.ChoosingBeatmap()); + AddStep("set offline status", () => status.Value = new UserStatusOffline()); + AddAssert("visit message is visible", () => evast.LastVisitMessage.IsPresent); + AddStep("set online status", () => status.Value = new UserStatusOnline()); + AddAssert("visit message is not visible", () => !evast.LastVisitMessage.IsPresent); + } + private UserActivity soloGameStatusForRuleset(int rulesetId) => new UserActivity.SoloGame(null, rulesetStore.GetRuleset(rulesetId)); + + private class TestUserListPanel : UserListPanel + { + public TestUserListPanel(User user) + : base(user) + { + } + + public new TextFlowContainer LastVisitMessage => base.LastVisitMessage; + } } } diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index d5e6d5f13e..2f3986b4c0 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -36,9 +36,10 @@ namespace osu.Game.Users protected DelayedLoadUnloadWrapper Background { get; private set; } + protected TextFlowContainer LastVisitMessage { get; private set; } + private SpriteIcon statusIcon; private OsuSpriteText statusMessage; - private TextFlowContainer lastVisitMessage; protected UserPanel(User user) { @@ -153,7 +154,7 @@ namespace osu.Game.Users var alignment = rightAlignedChildren ? Anchor.CentreRight : Anchor.CentreLeft; - statusContainer.Add(lastVisitMessage = new TextFlowContainer(t => t.Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold)).With(text => + statusContainer.Add(LastVisitMessage = new TextFlowContainer(t => t.Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold)).With(text => { text.Anchor = alignment; text.Origin = alignment; @@ -193,7 +194,7 @@ namespace osu.Game.Users } // Otherwise use only status - lastVisitMessage.FadeTo(status is UserStatusOffline && User.LastVisit.HasValue ? 1 : 0); + LastVisitMessage.FadeTo(status is UserStatusOffline && User.LastVisit.HasValue ? 1 : 0); statusMessage.Text = status.Message; statusIcon.FadeColour(status.GetAppropriateColour(colours), 500, Easing.OutQuint); From 454e402e882b23f3188543f3d7b1de1ce1bdfb65 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 25 Mar 2020 21:02:45 +0300 Subject: [PATCH 0594/2376] Fix last seen message has been visible when it shouldn't --- osu.Game/Users/UserPanel.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index 2f3986b4c0..6f59f9e443 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -185,6 +185,8 @@ namespace osu.Game.Users { if (status != null) { + LastVisitMessage.FadeTo(status is UserStatusOffline && User.LastVisit.HasValue ? 1 : 0); + // Set status message based on activity (if we have one) and status is not offline if (activity != null && !(status is UserStatusOffline)) { @@ -194,7 +196,6 @@ namespace osu.Game.Users } // Otherwise use only status - LastVisitMessage.FadeTo(status is UserStatusOffline && User.LastVisit.HasValue ? 1 : 0); statusMessage.Text = status.Message; statusIcon.FadeColour(status.GetAppropriateColour(colours), 500, Easing.OutQuint); From 2f5dc93d6119428654b0fa40e4e5e9439a074d64 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Thu, 26 Mar 2020 00:19:54 +0200 Subject: [PATCH 0595/2376] Select recommended difficulty --- osu.Game/Screens/Select/BeatmapCarousel.cs | 10 +++++-- .../Select/Carousel/CarouselBeatmapSet.cs | 28 ++++++++++++++++++- .../Carousel/CarouselGroupEagerSelect.cs | 10 +++++-- 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index fa8974f55a..2c45b3642d 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -23,6 +23,8 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; using osu.Game.Input.Bindings; using osu.Game.Screens.Select.Carousel; +using osu.Game.Online.API; +using osu.Game.Users; namespace osu.Game.Screens.Select { @@ -31,6 +33,8 @@ namespace osu.Game.Screens.Select private const float bleed_top = FilterControl.HEIGHT; private const float bleed_bottom = Footer.HEIGHT; + private readonly Bindable localUser = new Bindable(); + /// /// Triggered when the loaded change and are completely loaded. /// @@ -140,7 +144,7 @@ namespace osu.Game.Screens.Select private BeatmapManager beatmaps { get; set; } [BackgroundDependencyLoader(permitNulls: true)] - private void load(OsuConfigManager config) + private void load(OsuConfigManager config, IAPIProvider api) { config.BindWith(OsuSetting.RandomSelectAlgorithm, RandomAlgorithm); config.BindWith(OsuSetting.SongSelectRightMouseScroll, RightClickScrollingEnabled); @@ -154,6 +158,8 @@ namespace osu.Game.Screens.Select beatmaps.BeatmapRestored += beatmapRestored; loadBeatmapSets(GetLoadableBeatmaps()); + + localUser.BindTo(api.LocalUser); } protected virtual IEnumerable GetLoadableBeatmaps() => beatmaps.GetAllUsableBeatmapSetsEnumerable(); @@ -588,7 +594,7 @@ namespace osu.Game.Screens.Select b.Metadata = beatmapSet.Metadata; } - var set = new CarouselBeatmapSet(beatmapSet); + var set = new CarouselBeatmapSet(beatmapSet, localUser); foreach (var c in set.Beatmaps) { diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs index 8e323c66e2..9f1c39c578 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs @@ -4,19 +4,23 @@ using System; using System.Collections.Generic; using System.Linq; +using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Game.Beatmaps; using osu.Game.Screens.Select.Filter; +using osu.Game.Users; namespace osu.Game.Screens.Select.Carousel { public class CarouselBeatmapSet : CarouselGroupEagerSelect { + private readonly Bindable localUser; + public IEnumerable Beatmaps => InternalChildren.OfType(); public BeatmapSetInfo BeatmapSet; - public CarouselBeatmapSet(BeatmapSetInfo beatmapSet) + public CarouselBeatmapSet(BeatmapSetInfo beatmapSet, Bindable localUser) { BeatmapSet = beatmapSet ?? throw new ArgumentNullException(nameof(beatmapSet)); @@ -24,10 +28,32 @@ namespace osu.Game.Screens.Select.Carousel .Where(b => !b.Hidden) .Select(b => new CarouselBeatmap(b)) .ForEach(AddChild); + + this.localUser = localUser; } protected override DrawableCarouselItem CreateDrawableRepresentation() => new DrawableCarouselBeatmapSet(this); + protected override CarouselItem GetNextToSelect() + { + if (LastSelected == null) + { + decimal? pp = localUser.Value?.Statistics?.PP ?? 60; // TODO: This needs to get ruleset specific statistics + + var recommendedDifficulty = Math.Pow((double)pp, 0.4) * 0.195; + return Children.OfType() + .Where(b => !b.Filtered.Value) + .OrderBy(b => + { + var difference = b.Beatmap.StarDifficulty - recommendedDifficulty; + return difference >= 0 ? difference * 2 : difference * -1; // prefer easier over harder + }) + .FirstOrDefault(); + } + + return base.GetNextToSelect(); + } + public override int CompareTo(FilterCriteria criteria, CarouselItem other) { if (!(other is CarouselBeatmapSet otherSet)) diff --git a/osu.Game/Screens/Select/Carousel/CarouselGroupEagerSelect.cs b/osu.Game/Screens/Select/Carousel/CarouselGroupEagerSelect.cs index 6ce12f7b89..262bea9c71 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselGroupEagerSelect.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselGroupEagerSelect.cs @@ -90,11 +90,15 @@ namespace osu.Game.Screens.Select.Carousel PerformSelection(); } + protected virtual CarouselItem GetNextToSelect() + { + return Children.Skip(lastSelectedIndex).FirstOrDefault(i => !i.Filtered.Value) ?? + Children.Reverse().Skip(InternalChildren.Count - lastSelectedIndex).FirstOrDefault(i => !i.Filtered.Value); + } + protected virtual void PerformSelection() { - CarouselItem nextToSelect = - Children.Skip(lastSelectedIndex).FirstOrDefault(i => !i.Filtered.Value) ?? - Children.Reverse().Skip(InternalChildren.Count - lastSelectedIndex).FirstOrDefault(i => !i.Filtered.Value); + CarouselItem nextToSelect = GetNextToSelect(); if (nextToSelect != null) nextToSelect.State.Value = CarouselItemState.Selected; From e6b2e3b0ed1f4059a9fef74053b7ed5d6ec39d9d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 26 Mar 2020 05:18:12 +0300 Subject: [PATCH 0596/2376] Add osu!catch skin configurations --- .../Skinning/CatchSkinConfiguration.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 osu.Game.Rulesets.Catch/Skinning/CatchSkinConfiguration.cs diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchSkinConfiguration.cs b/osu.Game.Rulesets.Catch/Skinning/CatchSkinConfiguration.cs new file mode 100644 index 0000000000..aea5beaa6b --- /dev/null +++ b/osu.Game.Rulesets.Catch/Skinning/CatchSkinConfiguration.cs @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Rulesets.Catch.Skinning +{ + public enum CatchSkinConfiguration + { + /// + /// The colour to be used for the catcher while on hyper-dashing state. + /// + HyperDash, + + /// + /// The colour to be used for hyper-dash fruits. + /// + HyperDashFruit, + + /// + /// The colour to be used for the "exploding" catcher sprite on beginning of hyper-dashing. + /// + HyperDashAfterImage, + } +} From aa162b1033caff83366debb191f58366561b6555 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 26 Mar 2020 05:30:59 +0300 Subject: [PATCH 0597/2376] Setup hyper-dash colouring test scene --- .../TestSceneHyperDashColouring.cs | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs new file mode 100644 index 0000000000..2041e365ea --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs @@ -0,0 +1,112 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Audio.Sample; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Framework.Testing; +using osu.Game.Audio; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Catch.Objects.Drawables; +using osu.Game.Rulesets.Catch.Skinning; +using osu.Game.Rulesets.Catch.UI; +using osu.Game.Skinning; +using osu.Game.Tests.Visual; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Catch.Tests +{ + public class TestSceneHyperDashColouring : OsuTestScene + { + private Drawable setupSkinHierarchy(Func getChild, bool customHyperDashCatcherColour = false, bool customHyperDashFruitColour = false, bool customHyperDashAfterColour = false) + { + var testSkinProvider = new SkinProvidingContainer(new TestLegacySkin(customHyperDashCatcherColour, customHyperDashFruitColour, customHyperDashAfterColour)); + + var legacySkinTransformer = new SkinProvidingContainer(new CatchLegacySkinTransformer(testSkinProvider)); + + return testSkinProvider + .WithChild(legacySkinTransformer + .WithChild(getChild.Invoke())); + } + + private bool checkFruitHyperDashColour(DrawableFruit fruit, Color4 expectedColour, bool isLegacyFruit) => + isLegacyFruit + ? fruit.ChildrenOfType().First().Drawable.ChildrenOfType().Any(c => c.Colour == expectedColour) + : fruit.ChildrenOfType().First().Drawable.ChildrenOfType().Single(c => c.BorderColour == expectedColour).Any(d => d.Colour == expectedColour); + + private class TestLegacySkin : ISkin + { + public static Color4 CustomHyperDashColour { get; } = Color4.Goldenrod; + public static Color4 CustomHyperDashFruitColour { get; } = Color4.Cyan; + public static Color4 CustomHyperDashAfterColour { get; } = Color4.Lime; + + private readonly bool customHyperDashCatcherColour; + private readonly bool customHyperDashFruitColour; + private readonly bool customHyperDashAfterColour; + + public TestLegacySkin(bool customHyperDashCatcherColour = false, bool customHyperDashFruitColour = false, bool customHyperDashAfterColour = false) + { + this.customHyperDashCatcherColour = customHyperDashCatcherColour; + this.customHyperDashFruitColour = customHyperDashFruitColour; + this.customHyperDashAfterColour = customHyperDashAfterColour; + } + + public Drawable GetDrawableComponent(ISkinComponent component) => null; + + public Texture GetTexture(string componentName) + { + if (componentName == "fruit-pear") + { + // convince CatchLegacySkinTransformer to use the LegacyFruitPiece for pear fruit. + var texture = new Texture(Texture.WhitePixel.TextureGL) + { + Width = 1, + Height = 1, + ScaleAdjust = 1 / 96f + }; + return texture; + } + + return null; + } + + public SampleChannel GetSample(ISampleInfo sampleInfo) => null; + + public IBindable GetConfig(TLookup lookup) + { + switch (lookup) + { + case CatchSkinConfiguration config when config == CatchSkinConfiguration.HyperDash: + if (customHyperDashCatcherColour) + return SkinUtils.As(new Bindable(CustomHyperDashColour)); + + return null; + + case CatchSkinConfiguration config when config == CatchSkinConfiguration.HyperDashFruit: + if (customHyperDashFruitColour) + return SkinUtils.As(new Bindable(CustomHyperDashFruitColour)); + + return null; + + case CatchSkinConfiguration config when config == CatchSkinConfiguration.HyperDashAfterImage: + if (customHyperDashAfterColour) + return SkinUtils.As(new Bindable(CustomHyperDashAfterColour)); + + return null; + } + + return null; + } + } + } +} From 0a368f13d99421d17c34fa48a75db4001115c95a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 26 Mar 2020 05:37:26 +0300 Subject: [PATCH 0598/2376] Add default hyper-dash colour constant on Catcher --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index e361b29a9d..f53e14a8c7 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -21,6 +21,8 @@ namespace osu.Game.Rulesets.Catch.UI { public class Catcher : Container, IKeyBindingHandler { + public static Color4 DefaultHyperDashColour { get; } = Color4.Red; + /// /// Whether we are hyper-dashing or not. /// From 6f2cc5471adabc4392fcf1f63a5de32266016c10 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 26 Mar 2020 05:38:41 +0300 Subject: [PATCH 0599/2376] Add support for custom hyper-dash fruit colouring --- .../Objects/Drawables/FruitPiece.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs index 5797588ded..c8f7c4912e 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs @@ -7,7 +7,10 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Game.Rulesets.Catch.Skinning; +using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Skinning; using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.Objects.Drawables @@ -31,7 +34,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables } [BackgroundDependencyLoader] - private void load(DrawableHitObject drawableObject) + private void load(DrawableHitObject drawableObject, ISkinSource skin) { DrawableCatchHitObject drawableCatchObject = (DrawableCatchHitObject)drawableObject; hitObject = drawableCatchObject.HitObject; @@ -60,6 +63,11 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables }, }); + var hyperDashColour = + skin.GetConfig(CatchSkinConfiguration.HyperDashFruit)?.Value ?? + skin.GetConfig(CatchSkinConfiguration.HyperDash)?.Value ?? + Catcher.DefaultHyperDashColour; + if (hitObject.HyperDash) { AddInternal(new Circle @@ -67,7 +75,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, - BorderColour = Color4.Red, + BorderColour = hyperDashColour, BorderThickness = 12f * RADIUS_ADJUST, Children = new Drawable[] { @@ -77,7 +85,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables Alpha = 0.3f, Blending = BlendingParameters.Additive, RelativeSizeAxes = Axes.Both, - Colour = Color4.Red, + Colour = hyperDashColour, } } }); From d995f3e1cc7eff1604d4fa06ed4f85de7152f020 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 26 Mar 2020 05:39:32 +0300 Subject: [PATCH 0600/2376] Add support for custom hyper-dash legacy fruit colouring --- osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs index 25ee0811d0..99ecf12fd3 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Catch.Objects.Drawables; +using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Skinning; using osuTK; @@ -53,10 +54,15 @@ namespace osu.Game.Rulesets.Catch.Skinning if (drawableCatchObject.HitObject.HyperDash) { + var hyperDashColour = + skin.GetConfig(CatchSkinConfiguration.HyperDashFruit)?.Value ?? + skin.GetConfig(CatchSkinConfiguration.HyperDash)?.Value ?? + Catcher.DefaultHyperDashColour; + var hyperDash = new Sprite { Texture = skin.GetTexture(lookupName), - Colour = Color4.Red, + Colour = hyperDashColour, Anchor = Anchor.Centre, Origin = Anchor.Centre, Blending = BlendingParameters.Additive, From 29274b004cfa1141d3a4c85ec97e8960ccdeca48 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 26 Mar 2020 05:40:38 +0300 Subject: [PATCH 0601/2376] Add hyper-dash fruit colouring test cases --- .../TestSceneHyperDashColouring.cs | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs index 2041e365ea..7fab961aa7 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs @@ -28,6 +28,77 @@ namespace osu.Game.Rulesets.Catch.Tests { public class TestSceneHyperDashColouring : OsuTestScene { + [TestCase(false)] + [TestCase(true)] + public void TestHyperDashFruitColour(bool legacyFruit) + { + DrawableFruit drawableFruit = null; + + AddStep("setup fruit", () => + { + var fruit = new Fruit { IndexInBeatmap = legacyFruit ? 0 : 1, HyperDashTarget = new Banana() }; + fruit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + + Child = setupSkinHierarchy(() => + drawableFruit = new DrawableFruit(fruit) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(4f), + }, false, false); + }); + + AddAssert("fruit colour default-hyperdash", () => checkFruitHyperDashColour(drawableFruit, Catcher.DefaultHyperDashColour, legacyFruit)); + } + + [TestCase(false, true)] + [TestCase(false, false)] + [TestCase(true, true)] + [TestCase(true, false)] + public void TestCustomHyperDashFruitColour(bool legacyFruit, bool customCatcherHyperDashColour) + { + DrawableFruit drawableFruit = null; + + AddStep("setup fruit", () => + { + var fruit = new Fruit { IndexInBeatmap = legacyFruit ? 0 : 1, HyperDashTarget = new Banana() }; + fruit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + + Child = setupSkinHierarchy(() => + drawableFruit = new DrawableFruit(fruit) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(4f), + }, customCatcherHyperDashColour, true); + }); + + AddAssert("fruit colour custom-hyperdash", () => checkFruitHyperDashColour(drawableFruit, TestLegacySkin.CustomHyperDashFruitColour, legacyFruit)); + } + + [TestCase(false)] + [TestCase(true)] + public void TestCustomHyperDashFruitColourFallback(bool legacyFruit) + { + DrawableFruit drawableFruit = null; + + AddStep("setup fruit", () => + { + var fruit = new Fruit { IndexInBeatmap = legacyFruit ? 0 : 1, HyperDashTarget = new Banana() }; + fruit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + + Child = setupSkinHierarchy(() => + drawableFruit = new DrawableFruit(fruit) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(4f), + }, true, false); + }); + + AddAssert("fruit colour catcher-custom-hyperdash", () => checkFruitHyperDashColour(drawableFruit, TestLegacySkin.CustomHyperDashColour, legacyFruit)); + } + private Drawable setupSkinHierarchy(Func getChild, bool customHyperDashCatcherColour = false, bool customHyperDashFruitColour = false, bool customHyperDashAfterColour = false) { var testSkinProvider = new SkinProvidingContainer(new TestLegacySkin(customHyperDashCatcherColour, customHyperDashFruitColour, customHyperDashAfterColour)); From 2b1245f63a279e2eee4521f689ed692ed839d376 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 26 Mar 2020 12:50:00 +0900 Subject: [PATCH 0602/2376] Improve xmldoc in a couple of places --- osu.Game/Rulesets/UI/DrawableRuleset.cs | 2 +- osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 27993ff173..ff6ed5bf17 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -406,7 +406,7 @@ namespace osu.Game.Rulesets.UI public abstract Playfield Playfield { get; } /// - /// Place to put drawables above hit objects but below UI. + /// Content to be placed above hitobjects. Will be affected by frame stability. /// public abstract Container Overlays { get; } diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index e569bb8459..3ba28aad45 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.UI { /// /// A container which consumes a parent gameplay clock and standardises frame counts for children. - /// Will ensure a minimum of 40 frames per clock second is maintained, regardless of any system lag or seeks. + /// Will ensure a minimum of 50 frames per clock second is maintained, regardless of any system lag or seeks. /// public class FrameStabilityContainer : Container, IHasReplayHandler { From d372ddaadd65696972f778ddfd2cb3c3df750ac4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 26 Mar 2020 12:50:18 +0900 Subject: [PATCH 0603/2376] Move break overlay to a location it is not affected by gameplay scale --- osu.Game/Screens/Play/Player.cs | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index dc5bac9fd1..3ff47b868c 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -293,19 +293,26 @@ namespace osu.Game.Screens.Play performImmediateExit(); }, }, - failAnimation = new FailAnimation(DrawableRuleset) { OnComplete = onFailComplete, } + failAnimation = new FailAnimation(DrawableRuleset) { OnComplete = onFailComplete, }, + new Container + { + Name = "Frame-stable elements", + Clock = DrawableRuleset.FrameStableClock, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + ScoreProcessor, + HealthProcessor, + BreakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, DrawableRuleset.GameplayStartTime, ScoreProcessor) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Breaks = working.Beatmap.Breaks + }, + } + }, }); - DrawableRuleset.Overlays.Add(BreakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, DrawableRuleset.GameplayStartTime, ScoreProcessor) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Breaks = working.Beatmap.Breaks - }); - - DrawableRuleset.Overlays.Add(ScoreProcessor); - DrawableRuleset.Overlays.Add(HealthProcessor); - HealthProcessor.IsBreakTime.BindTo(BreakOverlay.IsBreakTime); } From e3a7c8a124b46fc6979c2cbc96565560a91228cb Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 26 Mar 2020 09:11:31 +0300 Subject: [PATCH 0604/2376] Make catcher trails colouring per container --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 73 +++++++++++++++++++++------ 1 file changed, 57 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index f53e14a8c7..68280ab111 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -35,7 +35,30 @@ namespace osu.Game.Rulesets.Catch.UI public Container ExplodingFruitTarget; - public Container AdditiveTarget; + private Container additiveTarget; + private Container dashTrails; + private Container hyperDashTrails; + private Container endGlowSprites; + + public Container AdditiveTarget + { + get => additiveTarget; + set + { + if (additiveTarget == value) + return; + + additiveTarget?.RemoveRange(new[] { dashTrails, hyperDashTrails, endGlowSprites }); + + additiveTarget = value; + additiveTarget?.AddRange(new[] + { + dashTrails ??= new Container { RelativeSizeAxes = Axes.Both, Colour = Color4.White }, + hyperDashTrails ??= new Container { RelativeSizeAxes = Axes.Both, Colour = hyperDashColour }, + endGlowSprites ??= new Container { RelativeSizeAxes = Axes.Both, Colour = hyperDashEndGlowColour }, + }); + } + } public CatcherAnimationState CurrentState { get; private set; } @@ -65,7 +88,7 @@ namespace osu.Game.Rulesets.Catch.UI get => trail; set { - if (value == trail || AdditiveTarget == null) return; + if (value == trail || additiveTarget == null) return; trail = value; @@ -82,6 +105,9 @@ namespace osu.Game.Rulesets.Catch.UI private CatcherSprite currentCatcher; + private Color4 hyperDashColour = DefaultHyperDashColour; + private Color4 hyperDashEndGlowColour = DefaultHyperDashColour; + private int currentDirection; private bool dashing; @@ -213,8 +239,6 @@ namespace osu.Game.Rulesets.Catch.UI /// When this catcher crosses this position, this catcher ends hyper-dashing. public void SetHyperDashState(double modifier = 1, float targetPosition = -1) { - const float hyper_dash_transition_length = 180; - var wasHyperDashing = HyperDashing; if (modifier <= 1 || X == targetPosition) @@ -223,11 +247,7 @@ namespace osu.Game.Rulesets.Catch.UI hyperDashDirection = 0; if (wasHyperDashing) - { - this.FadeColour(Color4.White, hyper_dash_transition_length, Easing.OutQuint); - this.FadeTo(1, hyper_dash_transition_length, Easing.OutQuint); Trail &= Dashing; - } } else { @@ -237,18 +257,37 @@ namespace osu.Game.Rulesets.Catch.UI if (!wasHyperDashing) { - this.FadeColour(Color4.OrangeRed, hyper_dash_transition_length, Easing.OutQuint); - this.FadeTo(0.2f, hyper_dash_transition_length, Easing.OutQuint); Trail = true; - var hyperDashEndGlow = createAdditiveSprite(); - + var hyperDashEndGlow = createAdditiveSprite(endGlowSprites); hyperDashEndGlow.MoveToOffset(new Vector2(0, -10), 1200, Easing.In); hyperDashEndGlow.ScaleTo(hyperDashEndGlow.Scale * 0.95f).ScaleTo(hyperDashEndGlow.Scale * 1.2f, 1200, Easing.In); hyperDashEndGlow.FadeOut(1200); hyperDashEndGlow.Expire(true); } } + + updateCatcherColour(); + } + + private void updateCatcherColour() + { + const float hyper_dash_transition_length = 180; + + if (HyperDashing) + { + this.FadeColour(hyperDashColour == DefaultHyperDashColour ? Color4.OrangeRed : hyperDashColour, hyper_dash_transition_length, Easing.OutQuint); + this.FadeTo(0.2f, hyper_dash_transition_length, Easing.OutQuint); + } + else + { + this.FadeColour(Color4.White, hyper_dash_transition_length, Easing.OutQuint); + this.FadeTo(1f, hyper_dash_transition_length, Easing.OutQuint); + } + + // update hyper-dash colour of the hyper-dashing catcher sprites containers. + hyperDashTrails?.FadeColour(hyperDashColour, hyper_dash_transition_length, Easing.OutQuint); + endGlowSprites?.FadeColour(hyperDashEndGlowColour, hyper_dash_transition_length, Easing.OutQuint); } public bool OnPressed(CatchAction action) @@ -392,7 +431,7 @@ namespace osu.Game.Rulesets.Catch.UI return; } - var additive = createAdditiveSprite(); + var additive = createAdditiveSprite(HyperDashing ? hyperDashTrails : dashTrails); additive.FadeTo(0.4f).FadeOut(800, Easing.OutQuint); additive.Expire(true); @@ -409,21 +448,23 @@ namespace osu.Game.Rulesets.Catch.UI updateCatcher(); } - private CatcherTrailSprite createAdditiveSprite() + private CatcherTrailSprite createAdditiveSprite(Container target) { + if (target == null) + return null; + var tex = (currentCatcher.Drawable as TextureAnimation)?.CurrentFrame ?? ((Sprite)currentCatcher.Drawable).Texture; var sprite = new CatcherTrailSprite(tex) { Anchor = Anchor, Scale = Scale, - Colour = HyperDashing ? Color4.Red : Color4.White, Blending = BlendingParameters.Additive, RelativePositionAxes = RelativePositionAxes, Position = Position }; - AdditiveTarget?.Add(sprite); + target.Add(sprite); return sprite; } From 302fdd834a305697536ac4093b00f88d72751d80 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 26 Mar 2020 09:11:59 +0300 Subject: [PATCH 0605/2376] Add support for custom hyper-dash catcher colouring --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 68280ab111..b3742aa1ad 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -13,13 +13,15 @@ using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawables; +using osu.Game.Rulesets.Catch.Skinning; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Skinning; using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.UI { - public class Catcher : Container, IKeyBindingHandler + public class Catcher : SkinReloadableDrawable, IKeyBindingHandler { public static Color4 DefaultHyperDashColour { get; } = Color4.Red; @@ -133,7 +135,7 @@ namespace osu.Game.Rulesets.Catch.UI [BackgroundDependencyLoader] private void load() { - Children = new Drawable[] + InternalChildren = new Drawable[] { caughtFruit = new Container { @@ -184,7 +186,7 @@ namespace osu.Game.Rulesets.Catch.UI caughtFruit.Add(fruit); - Add(new HitExplosion(fruit) + AddInternal(new HitExplosion(fruit) { X = fruit.X, Scale = new Vector2(fruit.HitObject.Scale) @@ -378,6 +380,15 @@ namespace osu.Game.Rulesets.Catch.UI }); } + protected override void SkinChanged(ISkinSource skin, bool allowFallback) + { + base.SkinChanged(skin, allowFallback); + + hyperDashColour = skin.GetConfig(CatchSkinConfiguration.HyperDash)?.Value ?? DefaultHyperDashColour; + hyperDashEndGlowColour = skin.GetConfig(CatchSkinConfiguration.HyperDashAfterImage)?.Value ?? hyperDashColour; + updateCatcherColour(); + } + protected override void Update() { base.Update(); From fecafc2e48b9e45fa5a70c630832b55e7db6f396 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 26 Mar 2020 09:14:44 +0300 Subject: [PATCH 0606/2376] Fix additive target accidentally clears all of the added containers It sets the AdditiveTarget on the object initializer but then the catcher is set to Child which wipes up all of the existing children (containers added by Catcher through AdditiveTarget setter) --- osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index e0d9ff759d..37501736ff 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -33,10 +33,10 @@ namespace osu.Game.Rulesets.Catch.UI { RelativeSizeAxes = Axes.X; Height = CATCHER_SIZE; - Child = MovableCatcher = new Catcher(difficulty) - { - AdditiveTarget = this, - }; + Child = MovableCatcher = new Catcher(difficulty); + + // this property adds containers to 'this' so it must not be set in the object initializer. + MovableCatcher.AdditiveTarget = this; } public static float GetCatcherSize(BeatmapDifficulty difficulty) From 77b3011394ffdc1afb2517943a025bce713177c2 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 26 Mar 2020 09:19:00 +0300 Subject: [PATCH 0607/2376] Add hyper-dash catcher & trails colouring test cases --- .../TestSceneHyperDashColouring.cs | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs index 7fab961aa7..6bad45f7ba 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs @@ -28,6 +28,140 @@ namespace osu.Game.Rulesets.Catch.Tests { public class TestSceneHyperDashColouring : OsuTestScene { + [Test] + public void TestHyperDashCatcherColour() + { + CatcherArea catcherArea = null; + + AddStep("setup catcher", () => + { + Child = setupSkinHierarchy(() => + catcherArea = new CatcherArea + { + RelativePositionAxes = Axes.None, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(4f), + }, false); + }); + + AddStep("set hyper-dash", () => + { + catcherArea.MovableCatcher.SetHyperDashState(2); + catcherArea.MovableCatcher.FinishTransforms(); + }); + + AddAssert("catcher colour default-hyperdash", () => catcherArea.MovableCatcher.Colour == Color4.OrangeRed); + AddAssert("catcher trails colour default-hyperdash", () => catcherArea.OfType>().Any(c => c.Colour == Catcher.DefaultHyperDashColour)); + + AddStep("clear hyper-dash", () => + { + catcherArea.MovableCatcher.SetHyperDashState(1); + catcherArea.MovableCatcher.FinishTransforms(); + }); + + AddAssert("catcher colour white", () => catcherArea.MovableCatcher.Colour == Color4.White); + } + + [Test] + public void TestCustomHyperDashCatcherColour() + { + CatcherArea catcherArea = null; + + AddStep("setup catcher", () => + { + Child = setupSkinHierarchy(() => + catcherArea = new CatcherArea + { + RelativePositionAxes = Axes.None, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(4f), + }, true); + }); + + AddStep("set hyper-dash", () => + { + catcherArea.MovableCatcher.SetHyperDashState(2); + catcherArea.MovableCatcher.FinishTransforms(); + }); + + AddAssert("catcher colour custom-hyperdash", () => catcherArea.MovableCatcher.Colour == TestLegacySkin.CustomHyperDashColour); + AddAssert("catcher trails colour custom-hyperdash", () => catcherArea.OfType>().Any(c => c.Colour == TestLegacySkin.CustomHyperDashColour)); + + AddStep("clear hyper-dash", () => + { + catcherArea.MovableCatcher.SetHyperDashState(1); + catcherArea.MovableCatcher.FinishTransforms(); + }); + + AddAssert("catcher colour white", () => catcherArea.MovableCatcher.Colour == Color4.White); + } + + [Test] + public void TestHyperDashCatcherEndGlowColour() + { + CatcherArea catcherArea = null; + + AddStep("setup catcher", () => + { + Child = setupSkinHierarchy(() => + catcherArea = new CatcherArea + { + RelativePositionAxes = Axes.None, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(4f), + }, false, false, false); + }); + + AddStep("set hyper-dash", () => catcherArea.MovableCatcher.SetHyperDashState(2)); + AddAssert("end-glow sprite colour default-hyperdash", () => catcherArea.OfType>().Any(c => c.Colour == Catcher.DefaultHyperDashColour)); + } + + [TestCase(true)] + [TestCase(false)] + public void TestCustomHyperDashCatcherEndGlowColour(bool customHyperDashCatcherColour) + { + CatcherArea catcherArea = null; + + AddStep("setup catcher", () => + { + Child = setupSkinHierarchy(() => + catcherArea = new CatcherArea + { + RelativePositionAxes = Axes.None, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(4f), + }, customHyperDashCatcherColour, false, true); + }); + + AddStep("set hyper-dash", () => catcherArea.MovableCatcher.SetHyperDashState(2)); + AddAssert("end-glow sprite colour custom-hyperdash", () => catcherArea.OfType>().Any(c => c.Colour == TestLegacySkin.CustomHyperDashAfterColour)); + } + + [Test] + public void TestCustomHyperDashCatcherEndGlowColourFallback() + { + CatcherArea catcherArea = null; + + AddStep("setup catcher", () => + { + Child = setupSkinHierarchy(() => + catcherArea = new CatcherArea + { + RelativePositionAxes = Axes.None, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(4f), + }, true, false, false); + }); + + AddStep("set hyper-dash", () => catcherArea.MovableCatcher.SetHyperDashState(2)); + AddAssert("end-glow sprite colour catcher-custom-hyperdash", () => catcherArea.OfType>().Any(c => c.Colour == TestLegacySkin.CustomHyperDashColour)); + } + [TestCase(false)] [TestCase(true)] public void TestHyperDashFruitColour(bool legacyFruit) From 07462120e47d46e0a8639f2868538eb822800d86 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 26 Mar 2020 15:28:56 +0900 Subject: [PATCH 0608/2376] Split break tracking into its own component --- .../Visual/Gameplay/TestSceneAutoplay.cs | 3 +- ...eakOverlay.cs => TestSceneBreakTracker.cs} | 53 ++++++++---- .../Graphics/Containers/UserDimContainer.cs | 2 +- osu.Game/Rulesets/UI/DrawableRuleset.cs | 12 ++- osu.Game/Screens/Play/BreakOverlay.cs | 80 ++---------------- osu.Game/Screens/Play/BreakTracker.cs | 82 +++++++++++++++++++ osu.Game/Screens/Play/Player.cs | 43 +++++----- 7 files changed, 160 insertions(+), 115 deletions(-) rename osu.Game.Tests/Visual/Gameplay/{TestSceneBreakOverlay.cs => TestSceneBreakTracker.cs} (80%) create mode 100644 osu.Game/Screens/Play/BreakTracker.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs index afeda5fb7c..8108ce0864 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs @@ -3,6 +3,7 @@ using System.ComponentModel; using System.Linq; +using osu.Framework.Testing; using osu.Game.Rulesets; using osu.Game.Screens.Play; @@ -23,7 +24,7 @@ namespace osu.Game.Tests.Visual.Gameplay { AddUntilStep("score above zero", () => Player.ScoreProcessor.TotalScore.Value > 0); AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 2)); - AddStep("seek to break time", () => Player.GameplayClockContainer.Seek(Player.BreakOverlay.Breaks.First().StartTime)); + AddStep("seek to break time", () => Player.GameplayClockContainer.Seek(Player.ChildrenOfType().First().Breaks.First().StartTime)); AddUntilStep("wait for seek to complete", () => Player.HUDOverlay.Progress.ReferenceClock.CurrentTime >= Player.BreakOverlay.Breaks.First().StartTime); AddAssert("test keys not counting", () => !Player.HUDOverlay.KeyCounter.IsCounting); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBreakOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs similarity index 80% rename from osu.Game.Tests/Visual/Gameplay/TestSceneBreakOverlay.cs rename to osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs index 19dce303ea..d46b4ea289 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBreakOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; +using osu.Framework.Graphics; using osu.Framework.Timing; using osu.Game.Beatmaps.Timing; using osu.Game.Screens.Play; @@ -12,14 +13,16 @@ using osu.Game.Screens.Play; namespace osu.Game.Tests.Visual.Gameplay { [TestFixture] - public class TestSceneBreakOverlay : OsuTestScene + public class TestSceneBreakTracker : OsuTestScene { public override IReadOnlyList RequiredTypes => new[] { typeof(BreakOverlay), }; - private readonly TestBreakOverlay breakOverlay; + private readonly BreakOverlay breakOverlay; + + private readonly TestBreakTracker breakTracker; private readonly IReadOnlyList testBreaks = new List { @@ -35,9 +38,23 @@ namespace osu.Game.Tests.Visual.Gameplay }, }; - public TestSceneBreakOverlay() + public TestSceneBreakTracker() { - Add(breakOverlay = new TestBreakOverlay(true)); + AddRange(new Drawable[] + { + breakTracker = new TestBreakTracker(), + breakOverlay = new BreakOverlay(true) + { + ProcessCustomClock = false, + } + }); + } + + protected override void Update() + { + base.Update(); + + breakOverlay.Clock = breakTracker.Clock; } [Test] @@ -88,7 +105,7 @@ namespace osu.Game.Tests.Visual.Gameplay loadBreaksStep("multiple breaks", testBreaks); seekAndAssertBreak("seek to break start", testBreaks[1].StartTime, true); - AddAssert("is skipped to break #2", () => breakOverlay.CurrentBreakIndex == 1); + AddAssert("is skipped to break #2", () => breakTracker.CurrentBreakIndex == 1); seekAndAssertBreak("seek to break middle", testBreaks[1].StartTime + testBreaks[1].Duration / 2, true); seekAndAssertBreak("seek to break end", testBreaks[1].EndTime, false); @@ -110,7 +127,7 @@ namespace osu.Game.Tests.Visual.Gameplay private void addShowBreakStep(double seconds) { - AddStep($"show '{seconds}s' break", () => breakOverlay.Breaks = new List + AddStep($"show '{seconds}s' break", () => breakOverlay.Breaks = breakTracker.Breaks = new List { new BreakPeriod { @@ -122,12 +139,12 @@ namespace osu.Game.Tests.Visual.Gameplay private void setClock(bool useManual) { - AddStep($"set {(useManual ? "manual" : "realtime")} clock", () => breakOverlay.SwitchClock(useManual)); + AddStep($"set {(useManual ? "manual" : "realtime")} clock", () => breakTracker.SwitchClock(useManual)); } private void loadBreaksStep(string breakDescription, IReadOnlyList breaks) { - AddStep($"load {breakDescription}", () => breakOverlay.Breaks = breaks); + AddStep($"load {breakDescription}", () => breakOverlay.Breaks = breakTracker.Breaks = breaks); seekAndAssertBreak("seek back to 0", 0, false); } @@ -151,17 +168,18 @@ namespace osu.Game.Tests.Visual.Gameplay private void seekAndAssertBreak(string seekStepDescription, double time, bool shouldBeBreak) { - AddStep(seekStepDescription, () => breakOverlay.ManualClockTime = time); + AddStep(seekStepDescription, () => breakTracker.ManualClockTime = time); AddAssert($"is{(!shouldBeBreak ? " not" : string.Empty)} break time", () => { - breakOverlay.ProgressTime(); - return breakOverlay.IsBreakTime.Value == shouldBeBreak; + breakTracker.ProgressTime(); + return breakTracker.IsBreakTime.Value == shouldBeBreak; }); } - private class TestBreakOverlay : BreakOverlay + private class TestBreakTracker : BreakTracker { - private readonly FramedClock framedManualClock; + public readonly FramedClock FramedManualClock; + private readonly ManualClock manualClock; private IFrameBasedClock originalClock; @@ -173,20 +191,19 @@ namespace osu.Game.Tests.Visual.Gameplay set => manualClock.CurrentTime = value; } - public TestBreakOverlay(bool letterboxing) - : base(letterboxing) + public TestBreakTracker() { - framedManualClock = new FramedClock(manualClock = new ManualClock()); + FramedManualClock = new FramedClock(manualClock = new ManualClock()); ProcessCustomClock = false; } public void ProgressTime() { - framedManualClock.ProcessFrame(); + FramedManualClock.ProcessFrame(); Update(); } - public void SwitchClock(bool setManual) => Clock = setManual ? framedManualClock : originalClock; + public void SwitchClock(bool setManual) => Clock = setManual ? FramedManualClock : originalClock; protected override void LoadComplete() { diff --git a/osu.Game/Graphics/Containers/UserDimContainer.cs b/osu.Game/Graphics/Containers/UserDimContainer.cs index 4485ce3447..39c1fdad52 100644 --- a/osu.Game/Graphics/Containers/UserDimContainer.cs +++ b/osu.Game/Graphics/Containers/UserDimContainer.cs @@ -40,7 +40,7 @@ namespace osu.Game.Graphics.Containers /// /// Whether player is in break time. - /// Must be bound to to allow for dim adjustments in gameplay. + /// Must be bound to to allow for dim adjustments in gameplay. /// public readonly IBindable IsBreakTime = new Bindable(); diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index ff6ed5bf17..5062c92afe 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -72,9 +72,9 @@ namespace osu.Game.Rulesets.UI /// public override Playfield Playfield => playfield.Value; - private Container overlays; + public override Container Overlays { get; } = new Container { RelativeSizeAxes = Axes.Both }; - public override Container Overlays => overlays; + public override Container FrameStableComponents { get; } = new Container { RelativeSizeAxes = Axes.Both }; public override GameplayClock FrameStableClock => frameStabilityContainer.GameplayClock; @@ -187,11 +187,12 @@ namespace osu.Game.Rulesets.UI FrameStablePlayback = FrameStablePlayback, Children = new Drawable[] { + FrameStableComponents, KeyBindingInputManager .WithChild(CreatePlayfieldAdjustmentContainer() .WithChild(Playfield) ), - overlays = new Container { RelativeSizeAxes = Axes.Both } + Overlays, } }, }; @@ -410,6 +411,11 @@ namespace osu.Game.Rulesets.UI /// public abstract Container Overlays { get; } + /// + /// Components to be run potentially multiple times in line with frame-stable gameplay. + /// + public abstract Container FrameStableComponents { get; } + /// /// The frame-stable clock which is being used for playfield display. /// diff --git a/osu.Game/Screens/Play/BreakOverlay.cs b/osu.Game/Screens/Play/BreakOverlay.cs index ee8be87352..89f51315f2 100644 --- a/osu.Game/Screens/Play/BreakOverlay.cs +++ b/osu.Game/Screens/Play/BreakOverlay.cs @@ -2,8 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -16,8 +14,6 @@ namespace osu.Game.Screens.Play { public class BreakOverlay : Container { - private readonly ScoreProcessor scoreProcessor; - /// /// The duration of the break overlay fading. /// @@ -37,10 +33,6 @@ namespace osu.Game.Screens.Play { breaks = value; - // reset index in case the new breaks list is smaller than last one - isBreakTime.Value = false; - CurrentBreakIndex = 0; - if (IsLoaded) initializeBreaks(); } @@ -48,27 +40,17 @@ namespace osu.Game.Screens.Play public override bool RemoveCompletedTransforms => false; - /// - /// Whether the gameplay is currently in a break. - /// - public IBindable IsBreakTime => isBreakTime; - - protected int CurrentBreakIndex; - - private readonly BindableBool isBreakTime = new BindableBool(); - private readonly Container remainingTimeAdjustmentBox; private readonly Container remainingTimeBox; private readonly RemainingTimeCounter remainingTimeCounter; - private readonly BreakInfo info; private readonly BreakArrows breakArrows; - private readonly double gameplayStartTime; - public BreakOverlay(bool letterboxing, double gameplayStartTime = 0, ScoreProcessor scoreProcessor = null) + public BreakOverlay(bool letterboxing, ScoreProcessor scoreProcessor = null) { - this.gameplayStartTime = gameplayStartTime; - this.scoreProcessor = scoreProcessor; RelativeSizeAxes = Axes.Both; + + BreakInfo info; + Child = fadeContainer = new Container { Alpha = 0, @@ -119,13 +101,11 @@ namespace osu.Game.Screens.Play } }; - if (scoreProcessor != null) bindProcessor(scoreProcessor); - } - - [BackgroundDependencyLoader(true)] - private void load(GameplayClock clock) - { - if (clock != null) Clock = clock; + if (scoreProcessor != null) + { + info.AccuracyDisplay.Current.BindTo(scoreProcessor.Accuracy); + info.GradeDisplay.Current.BindTo(scoreProcessor.Rank); + } } protected override void LoadComplete() @@ -134,42 +114,6 @@ namespace osu.Game.Screens.Play initializeBreaks(); } - protected override void Update() - { - base.Update(); - updateBreakTimeBindable(); - } - - private void updateBreakTimeBindable() => - isBreakTime.Value = getCurrentBreak()?.HasEffect == true - || Clock.CurrentTime < gameplayStartTime - || scoreProcessor?.HasCompleted == true; - - private BreakPeriod getCurrentBreak() - { - if (breaks?.Count > 0) - { - var time = Clock.CurrentTime; - - if (time > breaks[CurrentBreakIndex].EndTime) - { - while (time > breaks[CurrentBreakIndex].EndTime && CurrentBreakIndex < breaks.Count - 1) - CurrentBreakIndex++; - } - else - { - while (time < breaks[CurrentBreakIndex].StartTime && CurrentBreakIndex > 0) - CurrentBreakIndex--; - } - - var closest = breaks[CurrentBreakIndex]; - - return closest.Contains(time) ? closest : null; - } - - return null; - } - private void initializeBreaks() { FinishTransforms(true); @@ -207,11 +151,5 @@ namespace osu.Game.Screens.Play } } } - - private void bindProcessor(ScoreProcessor processor) - { - info.AccuracyDisplay.Current.BindTo(processor.Accuracy); - info.GradeDisplay.Current.BindTo(processor.Rank); - } } } diff --git a/osu.Game/Screens/Play/BreakTracker.cs b/osu.Game/Screens/Play/BreakTracker.cs new file mode 100644 index 0000000000..64262d52b5 --- /dev/null +++ b/osu.Game/Screens/Play/BreakTracker.cs @@ -0,0 +1,82 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Beatmaps.Timing; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Screens.Play +{ + public class BreakTracker : Component + { + private readonly ScoreProcessor scoreProcessor; + + private readonly double gameplayStartTime; + + /// + /// Whether the gameplay is currently in a break. + /// + public IBindable IsBreakTime => isBreakTime; + + protected int CurrentBreakIndex; + + private readonly BindableBool isBreakTime = new BindableBool(); + + private IReadOnlyList breaks; + + public IReadOnlyList Breaks + { + get => breaks; + set + { + breaks = value; + + // reset index in case the new breaks list is smaller than last one + isBreakTime.Value = false; + CurrentBreakIndex = 0; + } + } + + public BreakTracker(double gameplayStartTime = 0, ScoreProcessor scoreProcessor = null) + { + this.gameplayStartTime = gameplayStartTime; + this.scoreProcessor = scoreProcessor; + } + + protected override void Update() + { + base.Update(); + + isBreakTime.Value = getCurrentBreak()?.HasEffect == true + || Clock.CurrentTime < gameplayStartTime + || scoreProcessor?.HasCompleted == true; + } + + private BreakPeriod getCurrentBreak() + { + if (breaks?.Count > 0) + { + var time = Clock.CurrentTime; + + if (time > breaks[CurrentBreakIndex].EndTime) + { + while (time > breaks[CurrentBreakIndex].EndTime && CurrentBreakIndex < breaks.Count - 1) + CurrentBreakIndex++; + } + else + { + while (time < breaks[CurrentBreakIndex].StartTime && CurrentBreakIndex > 0) + CurrentBreakIndex--; + } + + var closest = breaks[CurrentBreakIndex]; + + return closest.Contains(time) ? closest : null; + } + + return null; + } + } +} diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 3ff47b868c..9ad500039e 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -76,6 +76,8 @@ namespace osu.Game.Screens.Play public BreakOverlay BreakOverlay; + private BreakTracker breakTracker; + protected ScoreProcessor ScoreProcessor { get; private set; } protected HealthProcessor HealthProcessor { get; private set; } @@ -204,7 +206,7 @@ namespace osu.Game.Screens.Play foreach (var mod in Mods.Value.OfType()) mod.ApplyToHealthProcessor(HealthProcessor); - BreakOverlay.IsBreakTime.BindValueChanged(onBreakTimeChanged, true); + breakTracker.IsBreakTime.BindValueChanged(onBreakTimeChanged, true); } private void addUnderlayComponents(Container target) @@ -231,6 +233,18 @@ namespace osu.Game.Screens.Play DrawableRuleset, new ComboEffects(ScoreProcessor) }); + + DrawableRuleset.FrameStableComponents.AddRange(new Drawable[] + { + ScoreProcessor, + HealthProcessor, + breakTracker = new BreakTracker(DrawableRuleset.GameplayStartTime, ScoreProcessor) + { + Breaks = working.Beatmap.Breaks + } + }); + + HealthProcessor.IsBreakTime.BindTo(breakTracker.IsBreakTime); } private void addOverlayComponents(Container target, WorkingBeatmap working) @@ -294,26 +308,13 @@ namespace osu.Game.Screens.Play }, }, failAnimation = new FailAnimation(DrawableRuleset) { OnComplete = onFailComplete, }, - new Container + BreakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks) { - Name = "Frame-stable elements", Clock = DrawableRuleset.FrameStableClock, - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - ScoreProcessor, - HealthProcessor, - BreakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, DrawableRuleset.GameplayStartTime, ScoreProcessor) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Breaks = working.Beatmap.Breaks - }, - } + ProcessCustomClock = false, + Breaks = working.Beatmap.Breaks }, }); - - HealthProcessor.IsBreakTime.BindTo(BreakOverlay.IsBreakTime); } private void onBreakTimeChanged(ValueChangedEvent isBreakTime) @@ -325,7 +326,7 @@ namespace osu.Game.Screens.Play private void updatePauseOnFocusLostState() => HUDOverlay.HoldToQuit.PauseOnFocusLost = PauseOnFocusLost && !DrawableRuleset.HasReplayLoaded.Value - && !BreakOverlay.IsBreakTime.Value; + && !breakTracker.IsBreakTime.Value; private IBeatmap loadPlayableBeatmap() { @@ -547,7 +548,7 @@ namespace osu.Game.Screens.Play PauseOverlay.Hide(); // breaks and time-based conditions may allow instant resume. - if (BreakOverlay.IsBreakTime.Value) + if (breakTracker.IsBreakTime.Value) completeResume(); else DrawableRuleset.RequestResume(completeResume); @@ -581,8 +582,8 @@ namespace osu.Game.Screens.Play Background.BlurAmount.Value = 0; // bind component bindables. - Background.IsBreakTime.BindTo(BreakOverlay.IsBreakTime); - DimmableStoryboard.IsBreakTime.BindTo(BreakOverlay.IsBreakTime); + Background.IsBreakTime.BindTo(breakTracker.IsBreakTime); + DimmableStoryboard.IsBreakTime.BindTo(breakTracker.IsBreakTime); Background.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground); DimmableStoryboard.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground); From 2949e8dc27d66ec99df393b12543fecd8241b471 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 26 Mar 2020 16:58:23 +0900 Subject: [PATCH 0609/2376] Reduce spread of stacked fruit --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index e361b29a9d..8fa9c61b6f 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -141,14 +141,14 @@ namespace osu.Game.Rulesets.Catch.UI var ourRadius = fruit.DisplayRadius; float theirRadius = 0; - const float allowance = 6; + const float allowance = 10; while (caughtFruit.Any(f => f.LifetimeEnd == double.MaxValue && Vector2Extensions.Distance(f.Position, fruit.Position) < (ourRadius + (theirRadius = f.DrawSize.X / 2 * f.Scale.X)) / (allowance / 2))) { var diff = (ourRadius + theirRadius) / allowance; - fruit.X += (RNG.NextSingle() - 0.5f) * 2 * diff; + fruit.X += (RNG.NextSingle() - 0.5f) * diff * 2; fruit.Y -= RNG.NextSingle() * diff; } From 8e4896fbbecce162e327d1c4af60dce652985c6b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 26 Mar 2020 17:13:53 +0900 Subject: [PATCH 0610/2376] Make slider judgements count towards base score / accuracy --- osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs | 2 -- osu.Game.Rulesets.Osu/Objects/SliderTick.cs | 2 -- 2 files changed, 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs index a8fd3764c5..ac6c6905e4 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs @@ -34,8 +34,6 @@ namespace osu.Game.Rulesets.Osu.Objects public class SliderRepeatJudgement : OsuJudgement { - public override bool IsBonus => true; - protected override int NumericResultFor(HitResult result) => result == MaxResult ? 30 : 0; } } diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs index 212a84c04a..22f3f559db 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs @@ -36,8 +36,6 @@ namespace osu.Game.Rulesets.Osu.Objects public class SliderTickJudgement : OsuJudgement { - public override bool IsBonus => true; - protected override int NumericResultFor(HitResult result) => result == MaxResult ? 10 : 0; } } From 6555ab6ede959af102069e1c29ae4d29924d8242 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 26 Mar 2020 17:18:27 +0900 Subject: [PATCH 0611/2376] Only play slider end sounds if tracking --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 2d5b9d874c..35d58b7111 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -11,6 +11,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Skinning; +using osu.Game.Rulesets.Scoring; using osuTK.Graphics; using osu.Game.Skinning; @@ -193,7 +194,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (userTriggered || Time.Current < slider.EndTime) return; - ApplyResult(r => r.Type = r.Judgement.MaxResult); + ApplyResult(r => r.Type = Ball.Tracking ? r.Judgement.MaxResult : HitResult.Miss); } protected override void UpdateStateTransforms(ArmedState state) From f80efd10c22aefe8678374ce3b14075d619462b8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 26 Mar 2020 19:51:02 +0900 Subject: [PATCH 0612/2376] Avoid using a miss judgement --- .../Objects/Drawables/DrawableSlider.cs | 10 +++++++++- .../Rulesets/Objects/Drawables/DrawableHitObject.cs | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 35d58b7111..5c7f4a42b3 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -194,7 +194,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (userTriggered || Time.Current < slider.EndTime) return; - ApplyResult(r => r.Type = Ball.Tracking ? r.Judgement.MaxResult : HitResult.Miss); + ApplyResult(r => r.Type = r.Judgement.MaxResult); + } + + public override void PlaySamples() + { + // rather than doing it this way, we should probably attach the sample to the tail circle. + // this can only be done after we stop using LegacyLastTick. + if (TailCircle.Result.Type != HitResult.Miss) + base.PlaySamples(); } protected override void UpdateStateTransforms(ArmedState state) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index aa29e42fac..5b5802fa9d 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -344,7 +344,7 @@ namespace osu.Game.Rulesets.Objects.Drawables /// Plays all the hit sounds for this . /// This is invoked automatically when this is hit. /// - public void PlaySamples() => Samples?.Play(); + public virtual void PlaySamples() => Samples?.Play(); protected override void Update() { From c1ac57e70fc05e11e6d085f2829eef31d524328e Mon Sep 17 00:00:00 2001 From: Lucas A Date: Thu, 26 Mar 2020 12:14:44 +0100 Subject: [PATCH 0613/2376] Add back visual tests and add easing to alpha fade. --- .../Visual/Gameplay/TestSceneFailingLayer.cs | 31 +++++++++++++++++++ osu.Game/Screens/Play/HUD/FailingLayer.cs | 7 ++++- 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs new file mode 100644 index 0000000000..3016890ade --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.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 NUnit.Framework; +using osu.Game.Screens.Play.HUD; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public class TestSceneFailingLayer : OsuTestScene + { + private readonly FailingLayer layer; + + public TestSceneFailingLayer() + { + Child = layer = new FailingLayer(); + } + + [Test] + public void TestLayerFading() + { + AddSliderStep("current health", 0.0, 1.0, 1.0, val => + { + layer.Current.Value = val; + }); + + AddStep("set health to 0.10", () => layer.Current.Value = 0.10); + AddWaitStep("wait for fade to finish", 5); + AddStep("set health to 1", () => layer.Current.Value = 1f); + } + } +} diff --git a/osu.Game/Screens/Play/HUD/FailingLayer.cs b/osu.Game/Screens/Play/HUD/FailingLayer.cs index 5f4037c14d..97d2458674 100644 --- a/osu.Game/Screens/Play/HUD/FailingLayer.cs +++ b/osu.Game/Screens/Play/HUD/FailingLayer.cs @@ -5,6 +5,7 @@ using System; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; +using osu.Framework.Utils; using osu.Game.Graphics; namespace osu.Game.Screens.Play.HUD @@ -16,6 +17,8 @@ namespace osu.Game.Screens.Play.HUD { private const float max_alpha = 0.4f; + private const int fade_time = 400; + private readonly Box box; /// @@ -41,7 +44,9 @@ namespace osu.Game.Screens.Play.HUD protected override void Update() { - box.Alpha = (float)Math.Clamp(max_alpha * (1 - Current.Value / LowHealthThreshold), 0, max_alpha); + box.Alpha = (float)Interpolation.ValueAt(Math.Clamp(Clock.ElapsedFrameTime, 0, fade_time), box.Alpha, + Math.Clamp(max_alpha * (1 - Current.Value / LowHealthThreshold), 0, max_alpha), 0, fade_time, Easing.Out); + base.Update(); } } From e33055e2c455eae6c030d9740c08560149e9cbd1 Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Thu, 26 Mar 2020 14:19:36 +0100 Subject: [PATCH 0614/2376] Simplify active tab font changes and expose necessary fields in OsuTabItem --- .../Graphics/UserInterface/OsuTabControl.cs | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuTabControl.cs b/osu.Game/Graphics/UserInterface/OsuTabControl.cs index ca9f1330f9..c2feca171b 100644 --- a/osu.Game/Graphics/UserInterface/OsuTabControl.cs +++ b/osu.Game/Graphics/UserInterface/OsuTabControl.cs @@ -113,13 +113,13 @@ namespace osu.Game.Graphics.UserInterface private const float transition_length = 500; - private void fadeActive() + protected void FadeHovered() { Bar.FadeIn(transition_length, Easing.OutQuint); Text.FadeColour(Color4.White, transition_length, Easing.OutQuint); } - private void fadeInactive() + protected void FadeUnhovered() { Bar.FadeOut(transition_length, Easing.OutQuint); Text.FadeColour(AccentColour, transition_length, Easing.OutQuint); @@ -128,14 +128,14 @@ namespace osu.Game.Graphics.UserInterface protected override bool OnHover(HoverEvent e) { if (!Active.Value) - fadeActive(); + FadeHovered(); return true; } protected override void OnHoverLost(HoverLostEvent e) { if (!Active.Value) - fadeInactive(); + FadeUnhovered(); } [BackgroundDependencyLoader] @@ -172,13 +172,19 @@ namespace osu.Game.Graphics.UserInterface }, new HoverClickSounds() }; - - Active.BindValueChanged(active => Text.Font = Text.Font.With(Typeface.Torus, weight: active.NewValue ? FontWeight.Bold : FontWeight.Medium), true); } - protected override void OnActivated() => fadeActive(); + protected override void OnActivated() + { + Text.Font = Text.Font.With(weight: FontWeight.Bold); + FadeHovered(); + } - protected override void OnDeactivated() => fadeInactive(); + protected override void OnDeactivated() + { + Text.Font = Text.Font.With(weight: FontWeight.Medium); + FadeUnhovered(); + } } } } From 816418742ea4641b1a6c18b3ec35a33aae73d6b7 Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Thu, 26 Mar 2020 15:43:48 +0100 Subject: [PATCH 0615/2376] Update header tab control --- osu.Game/Overlays/OverlayTabControl.cs | 21 +++++++++++--------- osu.Game/Overlays/TabControlOverlayHeader.cs | 14 ++++++------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/osu.Game/Overlays/OverlayTabControl.cs b/osu.Game/Overlays/OverlayTabControl.cs index aa96f0e19b..a1cbf2c1e7 100644 --- a/osu.Game/Overlays/OverlayTabControl.cs +++ b/osu.Game/Overlays/OverlayTabControl.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; @@ -35,17 +36,22 @@ namespace osu.Game.Overlays protected OverlayTabControl() { TabContainer.Masking = false; - TabContainer.Spacing = new Vector2(15, 0); + TabContainer.Spacing = new Vector2(20, 0); AddInternal(bar = new Box { RelativeSizeAxes = Axes.X, - Height = 2, Anchor = Anchor.BottomLeft, - Origin = Anchor.CentreLeft + Origin = Anchor.BottomLeft }); } + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + AccentColour = colourProvider.Highlight1; + } + protected override Dropdown CreateDropdown() => null; protected override TabItem CreateTabItem(T value) => new OverlayTabItem(value); @@ -90,7 +96,7 @@ namespace osu.Game.Overlays Bar = new ExpandingBar { Anchor = Anchor.BottomCentre, - ExpandedSize = 7.5f, + ExpandedSize = 5f, CollapsedSize = 0 }, new HoverClickSounds() @@ -119,6 +125,7 @@ namespace osu.Game.Overlays { HoverAction(); Text.Font = Text.Font.With(weight: FontWeight.Bold); + Text.FadeColour(Color4.White, 120, Easing.InQuad); } protected override void OnDeactivated() @@ -135,11 +142,7 @@ namespace osu.Game.Overlays OnDeactivated(); } - protected virtual void HoverAction() - { - Bar.Expand(); - Text.FadeColour(Color4.White, 120, Easing.InQuad); - } + protected virtual void HoverAction() => Bar.Expand(); protected virtual void UnhoverAction() { diff --git a/osu.Game/Overlays/TabControlOverlayHeader.cs b/osu.Game/Overlays/TabControlOverlayHeader.cs index b199a2a0cf..d6d53eec58 100644 --- a/osu.Game/Overlays/TabControlOverlayHeader.cs +++ b/osu.Game/Overlays/TabControlOverlayHeader.cs @@ -22,6 +22,7 @@ namespace osu.Game.Overlays { protected OsuTabControl TabControl; + private readonly Box controlBackground; private readonly BindableWithCurrent current = new BindableWithCurrent(); public Bindable Current @@ -30,8 +31,6 @@ namespace osu.Game.Overlays set => current.Current = value; } - private readonly Box controlBackground; - protected TabControlOverlayHeader() { HeaderInfo.Add(new Container @@ -56,7 +55,6 @@ namespace osu.Game.Overlays [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) { - TabControl.AccentColour = colourProvider.Highlight1; controlBackground.Colour = colourProvider.Dark4; } @@ -65,14 +63,16 @@ namespace osu.Game.Overlays public class OverlayHeaderTabControl : OverlayTabControl { + private const float bar_height = 1; + public OverlayHeaderTabControl() { - BarHeight = 1; RelativeSizeAxes = Axes.None; AutoSizeAxes = Axes.X; Anchor = Anchor.BottomLeft; Origin = Anchor.BottomLeft; - Height = 35; + Height = 47; + BarHeight = bar_height; } protected override TabItem CreateTabItem(T value) => new OverlayHeaderTabItem(value); @@ -82,7 +82,6 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.Y, AutoSizeAxes = Axes.X, Direction = FillDirection.Horizontal, - Spacing = new Vector2(5, 0), }; private class OverlayHeaderTabItem : OverlayTabItem @@ -92,7 +91,8 @@ namespace osu.Game.Overlays { Text.Text = value.ToString().ToLower(); Text.Font = OsuFont.GetFont(size: 14); - Bar.ExpandedSize = 5; + Text.Margin = new MarginPadding { Vertical = 16.5f }; // 15px padding + 1.5px line-height difference compensation + Bar.Margin = new MarginPadding { Bottom = bar_height }; } } } From 46ebf6ef7827f633485ac4c8b5cbee2ed66b191b Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Thu, 26 Mar 2020 15:44:22 +0100 Subject: [PATCH 0616/2376] Update user profile section tabs and rename classes for better readibility --- osu.Game/Overlays/UserProfileOverlay.cs | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs index 045a52a0c7..44f3acb564 100644 --- a/osu.Game/Overlays/UserProfileOverlay.cs +++ b/osu.Game/Overlays/UserProfileOverlay.cs @@ -24,7 +24,7 @@ namespace osu.Game.Overlays private GetUserRequest userReq; protected ProfileHeader Header; private ProfileSectionsContainer sectionsContainer; - private ProfileTabControl tabs; + private ProfileSectionTabControl tabs; public const float CONTENT_X_MARGIN = 70; @@ -62,7 +62,7 @@ namespace osu.Game.Overlays } : Array.Empty(); - tabs = new ProfileTabControl + tabs = new ProfileSectionTabControl { RelativeSizeAxes = Axes.X, Anchor = Anchor.TopCentre, @@ -149,19 +149,23 @@ namespace osu.Game.Overlays } } - private class ProfileTabControl : OverlayTabControl + private class ProfileSectionTabControl : OverlayTabControl { - public ProfileTabControl() + private const float bar_height = 2; + + public ProfileSectionTabControl() { TabContainer.RelativeSizeAxes &= ~Axes.X; TabContainer.AutoSizeAxes |= Axes.X; TabContainer.Anchor |= Anchor.x1; TabContainer.Origin |= Anchor.x1; + + BarHeight = bar_height; } - protected override TabItem CreateTabItem(ProfileSection value) => new ProfileTabItem(value) + protected override TabItem CreateTabItem(ProfileSection value) => new ProfileSectionTabItem(value) { - AccentColour = AccentColour + AccentColour = AccentColour, }; [BackgroundDependencyLoader] @@ -170,12 +174,14 @@ namespace osu.Game.Overlays AccentColour = colourProvider.Highlight1; } - private class ProfileTabItem : OverlayTabItem + private class ProfileSectionTabItem : OverlayTabItem { - public ProfileTabItem(ProfileSection value) + public ProfileSectionTabItem(ProfileSection value) : base(value) { Text.Text = value.Title; + Bar.ExpandedSize = 10; + Bar.Margin = new MarginPadding { Bottom = bar_height }; } } } From da996ffe748d2d30821284f1ccf48ad0fa0d193d Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Thu, 26 Mar 2020 15:44:53 +0100 Subject: [PATCH 0617/2376] Update header breadcrumb tab control --- .../Overlays/BreadcrumbControlOverlayHeader.cs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/BreadcrumbControlOverlayHeader.cs b/osu.Game/Overlays/BreadcrumbControlOverlayHeader.cs index 1d8411dfcc..81315f9638 100644 --- a/osu.Game/Overlays/BreadcrumbControlOverlayHeader.cs +++ b/osu.Game/Overlays/BreadcrumbControlOverlayHeader.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.UserInterface; @@ -16,6 +17,13 @@ namespace osu.Game.Overlays public OverlayHeaderBreadcrumbControl() { RelativeSizeAxes = Axes.X; + Height = 47; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + AccentColour = colourProvider.Light2; } protected override TabItem CreateTabItem(string value) => new ControlTabItem(value); @@ -27,10 +35,18 @@ namespace osu.Game.Overlays public ControlTabItem(string value) : base(value) { + RelativeSizeAxes = Axes.Y; Text.Font = Text.Font.With(size: 14); - Chevron.Y = 3; + Text.Anchor = Anchor.CentreLeft; + Text.Origin = Anchor.CentreLeft; + Chevron.Y = 1; Bar.Height = 0; } + + // base OsuTabItem makes font bold on activation, we don't want that here + protected override void OnActivated() => FadeHovered(); + + protected override void OnDeactivated() => FadeUnhovered(); } } } From 9a30ff5a00c91b3e0b5b606b11efeb0402c58f8a Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Thu, 26 Mar 2020 16:11:58 +0100 Subject: [PATCH 0618/2376] Fix code quality issues --- osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs | 1 - osu.Game/Overlays/OverlayHeader.cs | 4 +--- osu.Game/Overlays/TabControlOverlayHeader.cs | 1 - osu.Game/Screens/Multi/Header.cs | 3 +-- 4 files changed, 2 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs index 9dc71c7e74..c81ec9f663 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs @@ -8,7 +8,6 @@ using System.Collections.Generic; using osu.Framework.Graphics; using osu.Game.Graphics.Sprites; using osu.Framework.Allocation; -using osu.Game.Graphics.UserInterface; using osu.Framework.Graphics.Shapes; using osuTK.Graphics; diff --git a/osu.Game/Overlays/OverlayHeader.cs b/osu.Game/Overlays/OverlayHeader.cs index f017d66485..4ac0f697c3 100644 --- a/osu.Game/Overlays/OverlayHeader.cs +++ b/osu.Game/Overlays/OverlayHeader.cs @@ -6,7 +6,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics.UserInterface; using osuTK.Graphics; namespace osu.Game.Overlays @@ -14,7 +13,6 @@ namespace osu.Game.Overlays public abstract class OverlayHeader : Container { private readonly Box titleBackground; - private readonly OverlayTitle title; protected readonly FillFlowContainer HeaderInfo; @@ -60,7 +58,7 @@ namespace osu.Game.Overlays }, Children = new[] { - title = CreateTitle().With(title => + CreateTitle().With(title => { title.Anchor = Anchor.CentreLeft; title.Origin = Anchor.CentreLeft; diff --git a/osu.Game/Overlays/TabControlOverlayHeader.cs b/osu.Game/Overlays/TabControlOverlayHeader.cs index d6d53eec58..ab1a6aff78 100644 --- a/osu.Game/Overlays/TabControlOverlayHeader.cs +++ b/osu.Game/Overlays/TabControlOverlayHeader.cs @@ -10,7 +10,6 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; -using osuTK; namespace osu.Game.Overlays { diff --git a/osu.Game/Screens/Multi/Header.cs b/osu.Game/Screens/Multi/Header.cs index 6f790d703e..7a2d3a6239 100644 --- a/osu.Game/Screens/Multi/Header.cs +++ b/osu.Game/Screens/Multi/Header.cs @@ -81,7 +81,7 @@ namespace osu.Game.Screens.Multi private const float spacing = 6; private const int text_offset = 2; - private SpriteIcon iconSprite; + private readonly SpriteIcon iconSprite; private readonly OsuSpriteText titleText, pageText; public IMultiplayerSubScreen Screen @@ -101,7 +101,6 @@ namespace osu.Game.Screens.Multi } public MultiHeaderTitle() - : base() { AutoSizeAxes = Axes.Both; From 543f584595be39f4b429a45d5f0131bd452a5d0c Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Thu, 26 Mar 2020 16:44:46 +0100 Subject: [PATCH 0619/2376] Adjust user profile tabs --- osu.Game/Overlays/UserProfileOverlay.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs index 44f3acb564..6ec30f7707 100644 --- a/osu.Game/Overlays/UserProfileOverlay.cs +++ b/osu.Game/Overlays/UserProfileOverlay.cs @@ -67,7 +67,6 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.X, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Height = 34 }; Add(new Box @@ -160,6 +159,7 @@ namespace osu.Game.Overlays TabContainer.Anchor |= Anchor.x1; TabContainer.Origin |= Anchor.x1; + Height = 36 + bar_height; BarHeight = bar_height; } @@ -180,6 +180,8 @@ namespace osu.Game.Overlays : base(value) { Text.Text = value.Title; + Text.Font = Text.Font.With(size: 16); + Text.Margin = new MarginPadding { Bottom = 10 + bar_height }; Bar.ExpandedSize = 10; Bar.Margin = new MarginPadding { Bottom = bar_height }; } From 01c9112f82136510ae96dbd918e698ee9623ae81 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Thu, 26 Mar 2020 17:09:22 +0100 Subject: [PATCH 0620/2376] Add a null check to prevent NRE when playing the "no video" version of a beatmap. --- osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs index 00df388d09..d4dbdf1ea8 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs @@ -55,6 +55,8 @@ namespace osu.Game.Storyboards.Drawables { base.LoadComplete(); + if (videoSprite == null) return; + using (videoSprite.BeginAbsoluteSequence(0)) videoSprite.FadeIn(500); } From 83410315c64a5362325ee9fe02293af2fd5247b8 Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Thu, 26 Mar 2020 17:18:01 +0100 Subject: [PATCH 0621/2376] Make fields private --- osu.Game/Screens/Multi/Header.cs | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/Multi/Header.cs b/osu.Game/Screens/Multi/Header.cs index 7a2d3a6239..5b8e8a7fd9 100644 --- a/osu.Game/Screens/Multi/Header.cs +++ b/osu.Game/Screens/Multi/Header.cs @@ -75,25 +75,20 @@ namespace osu.Game.Screens.Multi private class MultiHeaderTitle : CompositeDrawable, IHasAccentColour { - public const float ICON_WIDTH = ICON_SIZE + spacing; + public const float ICON_WIDTH = icon_size + spacing; - public const float ICON_SIZE = 25; + private const float icon_size = 25; private const float spacing = 6; private const int text_offset = 2; private readonly SpriteIcon iconSprite; - private readonly OsuSpriteText titleText, pageText; + private readonly OsuSpriteText title, pageText; public IMultiplayerSubScreen Screen { set => pageText.Text = value.ShortTitle.ToLowerInvariant(); } - protected string Title - { - set => titleText.Text = value; - } - public Color4 AccentColour { get => pageText.Colour; @@ -115,11 +110,11 @@ namespace osu.Game.Screens.Multi { iconSprite = new SpriteIcon { - Size = new Vector2(ICON_SIZE), + Size = new Vector2(icon_size), Anchor = Anchor.Centre, Origin = Anchor.Centre }, - titleText = new OsuSpriteText + title = new OsuSpriteText { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -148,7 +143,7 @@ namespace osu.Game.Screens.Multi [BackgroundDependencyLoader] private void load(OsuColour colours) { - Title = "multi"; + title.Text = "multi"; iconSprite.Icon = OsuIcon.Multi; AccentColour = colours.Yellow; } From ee112c6f507e295a414721e4049f679583b9ab24 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Thu, 26 Mar 2020 18:42:08 +0200 Subject: [PATCH 0622/2376] Move and change logic --- osu.Game/Screens/Select/BeatmapCarousel.cs | 33 ++++++++++++++++--- .../Select/Carousel/CarouselBeatmapSet.cs | 12 +++---- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 2c45b3642d..65472f8a0e 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -24,7 +24,8 @@ using osu.Game.Graphics.Cursor; using osu.Game.Input.Bindings; using osu.Game.Screens.Select.Carousel; using osu.Game.Online.API; -using osu.Game.Users; +using osu.Game.Rulesets; +using osu.Game.Online.API.Requests; namespace osu.Game.Screens.Select { @@ -33,7 +34,7 @@ namespace osu.Game.Screens.Select private const float bleed_top = FilterControl.HEIGHT; private const float bleed_bottom = Footer.HEIGHT; - private readonly Bindable localUser = new Bindable(); + private readonly Bindable recommendedStarDifficulty = new Bindable(); /// /// Triggered when the loaded change and are completely loaded. @@ -143,8 +144,11 @@ namespace osu.Game.Screens.Select [Resolved] private BeatmapManager beatmaps { get; set; } + [Resolved] + private IAPIProvider api { get; set; } + [BackgroundDependencyLoader(permitNulls: true)] - private void load(OsuConfigManager config, IAPIProvider api) + private void load(OsuConfigManager config, Bindable decoupledRuleset) { config.BindWith(OsuSetting.RandomSelectAlgorithm, RandomAlgorithm); config.BindWith(OsuSetting.SongSelectRightMouseScroll, RightClickScrollingEnabled); @@ -159,7 +163,26 @@ namespace osu.Game.Screens.Select loadBeatmapSets(GetLoadableBeatmaps()); - localUser.BindTo(api.LocalUser); + decoupledRuleset.BindValueChanged(UpdateRecommendedStarDifficulty, true); + } + + protected void UpdateRecommendedStarDifficulty(ValueChangedEvent ruleset) + { + if (api.LocalUser.Value is GuestUser) + { + recommendedStarDifficulty.Value = 0; + return; + } + + var req = new GetUserRequest(api.LocalUser.Value.Id, ruleset.NewValue); + + req.Success += result => + { + // algorithm taken from https://github.com/ppy/osu-web/blob/e6e2825516449e3d0f3f5e1852c6bdd3428c3437/app/Models/User.php#L1505 + recommendedStarDifficulty.Value = Math.Pow((double)(result.Statistics.PP ?? 0), 0.4) * 0.195; + }; + + api.PerformAsync(req); } protected virtual IEnumerable GetLoadableBeatmaps() => beatmaps.GetAllUsableBeatmapSetsEnumerable(); @@ -594,7 +617,7 @@ namespace osu.Game.Screens.Select b.Metadata = beatmapSet.Metadata; } - var set = new CarouselBeatmapSet(beatmapSet, localUser); + var set = new CarouselBeatmapSet(beatmapSet, recommendedStarDifficulty); foreach (var c in set.Beatmaps) { diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs index 9f1c39c578..064840d99a 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs @@ -8,19 +8,18 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Game.Beatmaps; using osu.Game.Screens.Select.Filter; -using osu.Game.Users; namespace osu.Game.Screens.Select.Carousel { public class CarouselBeatmapSet : CarouselGroupEagerSelect { - private readonly Bindable localUser; + private readonly Bindable recommendedStarDifficulty = new Bindable(); public IEnumerable Beatmaps => InternalChildren.OfType(); public BeatmapSetInfo BeatmapSet; - public CarouselBeatmapSet(BeatmapSetInfo beatmapSet, Bindable localUser) + public CarouselBeatmapSet(BeatmapSetInfo beatmapSet, Bindable recommendedStarDifficulty) { BeatmapSet = beatmapSet ?? throw new ArgumentNullException(nameof(beatmapSet)); @@ -29,7 +28,7 @@ namespace osu.Game.Screens.Select.Carousel .Select(b => new CarouselBeatmap(b)) .ForEach(AddChild); - this.localUser = localUser; + this.recommendedStarDifficulty.BindTo(recommendedStarDifficulty); } protected override DrawableCarouselItem CreateDrawableRepresentation() => new DrawableCarouselBeatmapSet(this); @@ -38,14 +37,11 @@ namespace osu.Game.Screens.Select.Carousel { if (LastSelected == null) { - decimal? pp = localUser.Value?.Statistics?.PP ?? 60; // TODO: This needs to get ruleset specific statistics - - var recommendedDifficulty = Math.Pow((double)pp, 0.4) * 0.195; return Children.OfType() .Where(b => !b.Filtered.Value) .OrderBy(b => { - var difference = b.Beatmap.StarDifficulty - recommendedDifficulty; + var difference = b.Beatmap.StarDifficulty - recommendedStarDifficulty.Value; return difference >= 0 ? difference * 2 : difference * -1; // prefer easier over harder }) .FirstOrDefault(); From bbbaaae3ee8bbf6d48498deef378ca1974b2ff17 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Thu, 26 Mar 2020 19:18:16 +0200 Subject: [PATCH 0623/2376] Write tests --- .../SongSelect/TestSceneBeatmapCarousel.cs | 31 +++++++++++++++++++ osu.Game/Screens/Select/BeatmapCarousel.cs | 8 ++--- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 0cc37bbd57..b9b52a28cb 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Text; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -579,6 +580,34 @@ namespace osu.Game.Tests.Visual.SongSelect checkVisibleItemCount(true, 15); } + [Test] + public void TestSelectRecommendedDifficulty() + { + void setRecommendedAndExpect(double recommended, int expectedSet, int expectedDiff) + { + AddStep($"Recommend SR {recommended}", () => carousel.RecommendedStarDifficulty.Value = recommended); + advanceSelection(direction: 1, diff: false); + waitForSelection(expectedSet, expectedDiff); + } + + createCarousel(); + AddStep("Add beatmaps", () => + { + for (int i = 1; i <= 7; i++) + { + var set = createTestBeatmapSet(i); + carousel.UpdateBeatmapSet(set); + } + }); + waitForSelection(1, 1); + setRecommendedAndExpect(1, 2, 1); + setRecommendedAndExpect(3.9, 3, 1); + setRecommendedAndExpect(4.1, 4, 2); + setRecommendedAndExpect(5.6, 5, 2); + setRecommendedAndExpect(5.7, 6, 3); + setRecommendedAndExpect(10, 7, 3); + } + private void loadBeatmaps(List beatmapSets = null, Func initialCriteria = null) { createCarousel(); @@ -781,6 +810,8 @@ namespace osu.Game.Tests.Visual.SongSelect { public new List Items => base.Items; + public new Bindable RecommendedStarDifficulty => base.RecommendedStarDifficulty; + public bool PendingFilterTask => PendingFilter != null; protected override IEnumerable GetLoadableBeatmaps() => Enumerable.Empty(); diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 65472f8a0e..9aa4938886 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -34,7 +34,7 @@ namespace osu.Game.Screens.Select private const float bleed_top = FilterControl.HEIGHT; private const float bleed_bottom = Footer.HEIGHT; - private readonly Bindable recommendedStarDifficulty = new Bindable(); + protected readonly Bindable RecommendedStarDifficulty = new Bindable(); /// /// Triggered when the loaded change and are completely loaded. @@ -170,7 +170,7 @@ namespace osu.Game.Screens.Select { if (api.LocalUser.Value is GuestUser) { - recommendedStarDifficulty.Value = 0; + RecommendedStarDifficulty.Value = 0; return; } @@ -179,7 +179,7 @@ namespace osu.Game.Screens.Select req.Success += result => { // algorithm taken from https://github.com/ppy/osu-web/blob/e6e2825516449e3d0f3f5e1852c6bdd3428c3437/app/Models/User.php#L1505 - recommendedStarDifficulty.Value = Math.Pow((double)(result.Statistics.PP ?? 0), 0.4) * 0.195; + RecommendedStarDifficulty.Value = Math.Pow((double)(result.Statistics.PP ?? 0), 0.4) * 0.195; }; api.PerformAsync(req); @@ -617,7 +617,7 @@ namespace osu.Game.Screens.Select b.Metadata = beatmapSet.Metadata; } - var set = new CarouselBeatmapSet(beatmapSet, recommendedStarDifficulty); + var set = new CarouselBeatmapSet(beatmapSet, RecommendedStarDifficulty); foreach (var c in set.Beatmaps) { From 902734b75e8a0b0ceb65e2c5c46f3f2d7bbd2972 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Thu, 26 Mar 2020 20:32:43 +0200 Subject: [PATCH 0624/2376] Add failing test --- .../SongSelect/TestSceneBeatmapCarousel.cs | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 0cc37bbd57..efe79d88ab 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -83,6 +83,38 @@ namespace osu.Game.Tests.Visual.SongSelect waitForSelection(set_count, 3); } + [Test] + public void TestTraversalHold() + { + var sets = new List(); + + for (int i = 0; i < 20; i++) + { + var set = createTestBeatmapSet(i); + sets.Add(set); + } + + loadBeatmaps(sets); + + void selectNextAndAssert(int amount) + { + setSelected(1, 1); + AddStep($"Next beatmap {amount} times", () => + { + for (int i = 0; i < amount; i++) + { + carousel.SelectNext(); + } + }); + waitForSelection(amount + 1); + } + + for (int i = 1; i < 15; i += i) + { + selectNextAndAssert(i); + } + } + /// /// Test filtering /// From e707adb7738fcf6a72ea4bdb8c4c5a9ecfd361cb Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Thu, 26 Mar 2020 21:16:10 +0200 Subject: [PATCH 0625/2376] Increase amount of test sets --- osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index efe79d88ab..31114dfd25 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -87,8 +87,9 @@ namespace osu.Game.Tests.Visual.SongSelect public void TestTraversalHold() { var sets = new List(); + const int create_this_many_sets = 200; - for (int i = 0; i < 20; i++) + for (int i = 0; i < create_this_many_sets; i++) { var set = createTestBeatmapSet(i); sets.Add(set); @@ -109,7 +110,7 @@ namespace osu.Game.Tests.Visual.SongSelect waitForSelection(amount + 1); } - for (int i = 1; i < 15; i += i) + for (int i = 1; i < create_this_many_sets; i += i) { selectNextAndAssert(i); } From f75c0826018a72977b3edca3ae469e93f6a28dee Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Mar 2020 15:50:11 +0900 Subject: [PATCH 0626/2376] Fix osu!mania replays recording incorrectly when key mod applied --- .../Replays/ManiaReplayFrame.cs | 21 +++++++------------ osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 6 +++--- osu.Game/Screens/Play/Player.cs | 2 +- 3 files changed, 11 insertions(+), 18 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs b/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs index b93e372027..8c73c36e99 100644 --- a/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs +++ b/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.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.Beatmaps; using osu.Game.Replays.Legacy; using osu.Game.Rulesets.Mania.Beatmaps; @@ -26,13 +27,7 @@ namespace osu.Game.Rulesets.Mania.Replays public void FromLegacy(LegacyReplayFrame legacyFrame, IBeatmap beatmap, ReplayFrame lastFrame = null) { - // We don't need to fully convert, just create the converter - var converter = new ManiaBeatmapConverter(beatmap, new ManiaRuleset()); - - // NB: Via co-op mod, osu-stable can have two stages with floor(col/2) and ceil(col/2) columns. This will need special handling - // elsewhere in the game if we do choose to support the old co-op mod anyway. For now, assume that there is only one stage. - - var stage = new StageDefinition { Columns = converter.TargetColumns }; + var maniaBeatmap = (ManiaBeatmap)beatmap; var normalAction = ManiaAction.Key1; var specialAction = ManiaAction.Special1; @@ -42,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Replays while (activeColumns > 0) { - var isSpecial = stage.IsSpecialColumn(counter); + var isSpecial = maniaBeatmap.Stages.First().IsSpecialColumn(counter); if ((activeColumns & 1) > 0) Actions.Add(isSpecial ? specialAction : normalAction); @@ -59,17 +54,15 @@ namespace osu.Game.Rulesets.Mania.Replays public LegacyReplayFrame ToLegacy(IBeatmap beatmap) { + var maniaBeatmap = (ManiaBeatmap)beatmap; + int keys = 0; - var converter = new ManiaBeatmapConverter(beatmap, new ManiaRuleset()); - - var stage = new StageDefinition { Columns = converter.TargetColumns }; - var specialColumns = new List(); - for (int i = 0; i < converter.TargetColumns; i++) + for (int i = 0; i < maniaBeatmap.TotalColumns; i++) { - if (stage.IsSpecialColumn(i)) + if (maniaBeatmap.Stages.First().IsSpecialColumn(i)) specialColumns.Add(i); } diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index 58b64e1b8f..c356dd246d 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -45,9 +45,6 @@ namespace osu.Game.Scoring.Legacy if (workingBeatmap is DummyWorkingBeatmap) throw new BeatmapNotFoundException(); - currentBeatmap = workingBeatmap.Beatmap; - scoreInfo.Beatmap = currentBeatmap.BeatmapInfo; - scoreInfo.User = new User { Username = sr.ReadString() }; // MD5Hash @@ -68,6 +65,9 @@ namespace osu.Game.Scoring.Legacy scoreInfo.Mods = currentRuleset.ConvertFromLegacyMods((LegacyMods)sr.ReadInt32()).ToArray(); + currentBeatmap = workingBeatmap.GetPlayableBeatmap(currentRuleset.RulesetInfo, scoreInfo.Mods); + scoreInfo.Beatmap = currentBeatmap.BeatmapInfo; + /* score.HpGraphString = */ sr.ReadString(); diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index dc5bac9fd1..c570f4bf4f 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -657,7 +657,7 @@ namespace osu.Game.Screens.Play using (var stream = new MemoryStream()) { - new LegacyScoreEncoder(score, gameplayBeatmap).Encode(stream); + new LegacyScoreEncoder(score, gameplayBeatmap.PlayableBeatmap).Encode(stream); replayReader = new LegacyByteArrayReader(stream.ToArray(), "replay.osr"); } } From d36f5fb96f34cf22d84902d425a8d17583472ce1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Mar 2020 18:03:02 +0900 Subject: [PATCH 0627/2376] Fix animated follow points not (re)animating after rewind --- .../Drawables/Connections/FollowPoint.cs | 4 ++- .../Connections/FollowPointConnection.cs | 12 ++++----- osu.Game/Skinning/IAnimationTimeReference.cs | 25 +++++++++++++++++++ osu.Game/Skinning/LegacySkinExtensions.cs | 23 ++++++++++++++++- 4 files changed, 55 insertions(+), 9 deletions(-) create mode 100644 osu.Game/Skinning/IAnimationTimeReference.cs diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs index 7e530ca047..8bb324d02e 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections /// /// A single follow point positioned between two adjacent s. /// - public class FollowPoint : Container + public class FollowPoint : Container, IAnimationTimeReference { private const float width = 8; @@ -45,5 +45,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections } }, confineMode: ConfineMode.NoScaling); } + + public double AnimationStartTime { get; set; } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs index d0935e46f7..6f09bbcd57 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs @@ -116,6 +116,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections int point = 0; + ClearInternal(); + for (int d = (int)(spacing * 1.5); d < distance - spacing; d += spacing) { float fraction = (float)d / distance; @@ -126,13 +128,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections FollowPoint fp; - if (InternalChildren.Count > point) - { - fp = (FollowPoint)InternalChildren[point]; - fp.ClearTransforms(); - } - else - AddInternal(fp = new FollowPoint()); + AddInternal(fp = new FollowPoint()); fp.Position = pointStartPosition; fp.Rotation = rotation; @@ -142,6 +138,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections if (firstTransformStartTime == null) firstTransformStartTime = fadeInTime; + fp.AnimationStartTime = fadeInTime; + using (fp.BeginAbsoluteSequence(fadeInTime)) { fp.FadeIn(osuEnd.TimeFadeIn); diff --git a/osu.Game/Skinning/IAnimationTimeReference.cs b/osu.Game/Skinning/IAnimationTimeReference.cs new file mode 100644 index 0000000000..bcff10a24b --- /dev/null +++ b/osu.Game/Skinning/IAnimationTimeReference.cs @@ -0,0 +1,25 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Timing; + +namespace osu.Game.Skinning +{ + /// + /// Denotes an object which provides a reference time to start animations from. + /// + [Cached] + public interface IAnimationTimeReference + { + /// + /// The reference clock. + /// + IFrameBasedClock Clock { get; } + + /// + /// The time which animations should be started from, relative to . + /// + double AnimationStartTime { get; } + } +} diff --git a/osu.Game/Skinning/LegacySkinExtensions.cs b/osu.Game/Skinning/LegacySkinExtensions.cs index 52328d43b2..8765b161d4 100644 --- a/osu.Game/Skinning/LegacySkinExtensions.cs +++ b/osu.Game/Skinning/LegacySkinExtensions.cs @@ -3,10 +3,12 @@ using System.Collections.Generic; using System.Linq; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; +using osu.Framework.Timing; namespace osu.Game.Skinning { @@ -22,7 +24,7 @@ namespace osu.Game.Skinning if (textures.Length > 0) { - var animation = new TextureAnimation + var animation = new SkinnableTextureAnimation { DefaultFrameLength = getFrameLength(source, applyConfigFrameRate, textures), Repeat = looping, @@ -53,6 +55,25 @@ namespace osu.Game.Skinning } } + public class SkinnableTextureAnimation : TextureAnimation + { + [Resolved(canBeNull: true)] + private IAnimationTimeReference timeReference { get; set; } + + public SkinnableTextureAnimation() + : base(false) + { + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + if (timeReference != null) + Clock = new FramedOffsetClock(timeReference.Clock) { Offset = -timeReference.AnimationStartTime }; + } + } + private const double default_frame_time = 1000 / 60d; private static double getFrameLength(ISkin source, bool applyConfigFrameRate, Texture[] textures) From 6788b7f9cd9222c4dffa9fe46792b4a179e053c4 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Fri, 27 Mar 2020 09:43:51 +0100 Subject: [PATCH 0628/2376] Add test for loading storyboards with missing video file. --- .../Resources/storyboard_no_video.osu | 31 ++++++++++++++++ .../Visual/Gameplay/TestSceneStoryboard.cs | 37 +++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 osu.Game.Tests/Resources/storyboard_no_video.osu diff --git a/osu.Game.Tests/Resources/storyboard_no_video.osu b/osu.Game.Tests/Resources/storyboard_no_video.osu new file mode 100644 index 0000000000..25f1ff6361 --- /dev/null +++ b/osu.Game.Tests/Resources/storyboard_no_video.osu @@ -0,0 +1,31 @@ +osu file format v14 + +[Events] +//Background and Video events +0,0,"BG.jpg",0,0 +Video,0,"video.avi" +//Break Periods +//Storyboard Layer 0 (Background) +//Storyboard Layer 1 (Fail) +//Storyboard Layer 2 (Pass) +//Storyboard Layer 3 (Foreground) +//Storyboard Layer 4 (Overlay) +//Storyboard Sound Samples + +[TimingPoints] +1674,333.333333333333,4,2,1,70,1,0 +1674,-100,4,2,1,70,0,0 +3340,-100,4,2,1,70,0,0 +3507,-100,4,2,1,70,0,0 +3673,-100,4,2,1,70,0,0 + +[Colours] +Combo1 : 240,80,80 +Combo2 : 171,252,203 +Combo3 : 128,128,255 +Combo4 : 249,254,186 + +[HitObjects] +148,303,1674,5,6,3:2:0:0: +378,252,1840,1,0,0:0:0:0: +389,270,2340,5,2,0:1:0:0: diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs index ff8437311e..9f1492a25f 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs @@ -9,8 +9,12 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Timing; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Formats; +using osu.Game.IO; using osu.Game.Overlays; +using osu.Game.Storyboards; using osu.Game.Storyboards.Drawables; +using osu.Game.Tests.Resources; using osuTK.Graphics; namespace osu.Game.Tests.Visual.Gameplay @@ -54,7 +58,11 @@ namespace osu.Game.Tests.Visual.Gameplay State = { Value = Visibility.Visible }, } }); + } + [Test] + public void TestStoryboard() + { AddStep("Restart", restart); AddToggleStep("Passing", passing => { @@ -62,6 +70,12 @@ namespace osu.Game.Tests.Visual.Gameplay }); } + [Test] + public void TestStoryboardMissingVideo() + { + AddStep("Load storyboard with missing video", loadStoryboardNoVideo); + } + [BackgroundDependencyLoader] private void load() { @@ -94,5 +108,28 @@ namespace osu.Game.Tests.Visual.Gameplay storyboardContainer.Add(storyboard); decoupledClock.ChangeSource(working.Track); } + + private void loadStoryboardNoVideo() + { + if (storyboard != null) + storyboardContainer.Remove(storyboard); + + var decoupledClock = new DecoupleableInterpolatingFramedClock { IsCoupled = true }; + storyboardContainer.Clock = decoupledClock; + + Storyboard sb; + + using (var str = TestResources.OpenResource("storyboard_no_video.osu")) + using (var bfr = new LineBufferedReader(str)) + { + var decoder = new LegacyStoryboardDecoder(); + sb = decoder.Decode(bfr); + } + + storyboard = sb.CreateDrawable(Beatmap.Value); + + storyboardContainer.Add(storyboard); + decoupledClock.ChangeSource(Beatmap.Value.Track); + } } } From 4106700771f8581ca07846d291aa343c121f0884 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Mar 2020 20:51:44 +0900 Subject: [PATCH 0629/2376] 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 7e17f9da16..b147fdd05b 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 3894c06994..781c566b5f 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -23,7 +23,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 9cc9792ecf..a2c6106931 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + @@ -79,7 +79,7 @@ - + From 7b24cc325f0ed61490766307ac0a681d5a9bb766 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Fri, 27 Mar 2020 20:57:57 +0300 Subject: [PATCH 0630/2376] Implement OverlayScrollContainer component --- .../TestSceneOverlayScrollContainer.cs | 90 ++++++++++ osu.Game/Overlays/OverlayScrollContainer.cs | 169 ++++++++++++++++++ 2 files changed, 259 insertions(+) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs create mode 100644 osu.Game/Overlays/OverlayScrollContainer.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs new file mode 100644 index 0000000000..1fc85c3c04 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs @@ -0,0 +1,90 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics.Containers; +using osu.Game.Overlays; +using System; +using System.Collections.Generic; +using osu.Framework.Graphics; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Shapes; +using osuTK.Graphics; +using NUnit.Framework; +using osu.Framework.Utils; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public class TestSceneOverlayScrollContainer : OsuManualInputManagerTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(OverlayScrollContainer) + }; + + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); + + private OverlayScrollContainer scroll; + + private int invocationCount; + + [SetUp] + public void SetUp() => Schedule(() => + { + Add(scroll = new OverlayScrollContainer + { + RelativeSizeAxes = Axes.Both, + Child = new Container + { + Height = 3000, + RelativeSizeAxes = Axes.X, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Gray + } + } + }); + + invocationCount = 0; + + scroll.Button.Action += () => invocationCount++; + }); + + [Test] + public void TestButtonVisibility() + { + AddAssert("button is hidden", () => scroll.Button.State.Value == Visibility.Hidden); + + AddStep("scroll to end", () => scroll.ScrollToEnd(false)); + AddAssert("button is visible", () => scroll.Button.State.Value == Visibility.Visible); + + AddStep("scroll to start", () => scroll.ScrollToStart(false)); + AddAssert("button is hidden", () => scroll.Button.State.Value == Visibility.Hidden); + } + + [Test] + public void TestButtonAction() + { + AddStep("scroll to end", () => scroll.ScrollToEnd(false)); + + AddStep("invoke action", () => scroll.Button.Action.Invoke()); + + AddUntilStep("scrolled back to start", () => Precision.AlmostEquals(scroll.Current, 0, 0.1f)); + } + + [Test] + public void TestMultipleClicks() + { + AddStep("scroll to end", () => scroll.ScrollToEnd(false)); + + AddAssert("invocation count is 0", () => invocationCount == 0); + + AddStep("hover button", () => InputManager.MoveMouseTo(scroll.Button)); + AddRepeatStep("click button", () => InputManager.Click(MouseButton.Left), 3); + + AddAssert("invocation count is 1", () => invocationCount == 1); + } + } +} diff --git a/osu.Game/Overlays/OverlayScrollContainer.cs b/osu.Game/Overlays/OverlayScrollContainer.cs new file mode 100644 index 0000000000..1a875ded95 --- /dev/null +++ b/osu.Game/Overlays/OverlayScrollContainer.cs @@ -0,0 +1,169 @@ +// 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.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; +using osu.Game.Graphics.Containers; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Overlays +{ + /// + /// which provides . Mostly used in . + /// + public class OverlayScrollContainer : OsuScrollContainer + { + /// + /// Scroll position at which the will be shown. + /// + private const int button_scroll_position = 200; + + public ScrollToTopButton Button { get; } + + private float currentTarget; + + public OverlayScrollContainer() + { + AddInternal(Button = new ScrollToTopButton + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Margin = new MarginPadding(20), + Action = () => + { + ScrollToStart(); + currentTarget = Target; + Button.State.Value = Visibility.Hidden; + } + }); + } + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + if (ScrollContent.DrawHeight + button_scroll_position < DrawHeight) + { + Button.State.Value = Visibility.Hidden; + return; + } + + if (Target == currentTarget) + return; + + currentTarget = Target; + Button.State.Value = Current > button_scroll_position ? Visibility.Visible : Visibility.Hidden; + } + + public class ScrollToTopButton : VisibilityContainer + { + private const int fade_duration = 500; + + public Action Action + { + get => button.Action; + set => button.Action = value; + } + + public override bool PropagatePositionalInputSubTree => true; + + protected override bool StartHidden => true; + + private readonly Button button; + + public ScrollToTopButton() + { + Size = new Vector2(50); + Child = button = new Button(); + } + + protected override bool OnMouseDown(MouseDownEvent e) => true; + + protected override void PopIn() => button.FadeIn(fade_duration, Easing.OutQuint); + + protected override void PopOut() => button.FadeOut(fade_duration, Easing.OutQuint); + + private class Button : OsuHoverContainer + { + public override bool PropagatePositionalInputSubTree => Alpha == 1; + + protected override IEnumerable EffectTargets => new[] { background }; + + private Color4 flashColour; + + private readonly Container content; + private readonly Box background; + + public Button() + { + RelativeSizeAxes = Axes.Both; + Add(content = new CircularContainer + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Masking = true, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Offset = new Vector2(0f, 1f), + Radius = 3f, + Colour = Color4.Black.Opacity(0.25f), + }, + Children = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both + }, + new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(15), + Icon = FontAwesome.Solid.ChevronUp + } + } + }); + + TooltipText = "Scroll to top"; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + IdleColour = colourProvider.Background6; + HoverColour = colourProvider.Background5; + flashColour = colourProvider.Light1; + } + + protected override bool OnClick(ClickEvent e) + { + background.FlashColour(flashColour, 800, Easing.OutQuint); + return base.OnClick(e); + } + + protected override bool OnMouseDown(MouseDownEvent e) + { + content.ScaleTo(0.75f, 2000, Easing.OutQuint); + return true; + } + + protected override void OnMouseUp(MouseUpEvent e) + { + content.ScaleTo(1, 1000, Easing.OutElastic); + base.OnMouseUp(e); + } + } + } + } +} From 46af4bce32eb176c459514b04aac08a95cf44e35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Mar 2020 19:42:45 +0100 Subject: [PATCH 0631/2376] Cover regression in autoplay test --- osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs index 8108ce0864..5ee17aeea2 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs @@ -6,6 +6,7 @@ using System.Linq; using osu.Framework.Testing; using osu.Game.Rulesets; using osu.Game.Screens.Play; +using osu.Game.Screens.Play.Break; namespace osu.Game.Tests.Visual.Gameplay { @@ -27,7 +28,8 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("seek to break time", () => Player.GameplayClockContainer.Seek(Player.ChildrenOfType().First().Breaks.First().StartTime)); AddUntilStep("wait for seek to complete", () => Player.HUDOverlay.Progress.ReferenceClock.CurrentTime >= Player.BreakOverlay.Breaks.First().StartTime); - AddAssert("test keys not counting", () => !Player.HUDOverlay.KeyCounter.IsCounting); + AddAssert("keys not counting", () => !Player.HUDOverlay.KeyCounter.IsCounting); + AddAssert("overlay displays 100% accuracy", () => Player.BreakOverlay.ChildrenOfType().Single().AccuracyDisplay.Current.Value == 1); AddStep("rewind", () => Player.GameplayClockContainer.Seek(-80000)); AddUntilStep("key counter reset", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0)); } From adc759771ff73e3b3b023b624348cf9655c9f017 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Mar 2020 19:47:42 +0100 Subject: [PATCH 0632/2376] Hook up score processor in player --- 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 118cea324c..8693035103 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -308,7 +308,7 @@ namespace osu.Game.Screens.Play }, }, failAnimation = new FailAnimation(DrawableRuleset) { OnComplete = onFailComplete, }, - BreakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks) + BreakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, ScoreProcessor) { Clock = DrawableRuleset.FrameStableClock, ProcessCustomClock = false, From 3a3bfe9a5ea14477da9fdc67d42b9f6fe16598e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Mar 2020 21:19:49 +0100 Subject: [PATCH 0633/2376] Reorder children to fix pause overlay z-order --- osu.Game/Screens/Play/Player.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 8693035103..63ec3b0d2d 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -251,6 +251,12 @@ namespace osu.Game.Screens.Play { target.AddRange(new[] { + BreakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, ScoreProcessor) + { + Clock = DrawableRuleset.FrameStableClock, + ProcessCustomClock = false, + Breaks = working.Beatmap.Breaks + }, // display the cursor above some HUD elements. DrawableRuleset.Cursor?.CreateProxy() ?? new Container(), DrawableRuleset.ResumeOverlay?.CreateProxy() ?? new Container(), @@ -308,12 +314,6 @@ namespace osu.Game.Screens.Play }, }, failAnimation = new FailAnimation(DrawableRuleset) { OnComplete = onFailComplete, }, - BreakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, ScoreProcessor) - { - Clock = DrawableRuleset.FrameStableClock, - ProcessCustomClock = false, - Breaks = working.Beatmap.Breaks - }, }); } From 15fb1a099e4d96e725a6c46072fdf6b782bfd529 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 28 Mar 2020 00:42:51 +0100 Subject: [PATCH 0634/2376] Modify assert to avoid false failures In headless tests it was possible for TestInstantLoad() to erroneously fail. There were two scenarios in which LoadingSpinner could be null: 1. If the test runner was quick enough, the assert could end up running even before Loader.OnEntering() had even had a chance to, meaning that the spinner was never even actually assigned to or instantiated at that point in time. 2. Even if Loader.OnEntering() had managed to run, there was also a possibility that the spinner itself wasn't loaded at the point of checking the assertion. As the spinner is accessed through ChildrenOfType(), which only checks InternalChildren and ignores all currently-loading drawables, it would therefore return null. As null != 0, both of these cases would actually fail the test (this is best seen running headless, preferably with a [Repeat] attribute attached). To resolve, allow the spinner to be null at the point of asserting and duplicate the assertion step at the end. This weakens the test, as case (1) should probably be waited for and case (2) could be solved with exposition as protected in the base, but when attempting to wait for the loader itself to be loaded there were also cases where the appropriate until step would take so much time that the spinner would actually become visible in line with the delayed display logic, so this is a best-effort attempt to address both points without radical changes. --- osu.Game.Tests/Visual/Menus/TestSceneLoader.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneLoader.cs b/osu.Game.Tests/Visual/Menus/TestSceneLoader.cs index b3064ba9be..c44363d9ea 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneLoader.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneLoader.cs @@ -36,8 +36,6 @@ namespace osu.Game.Tests.Visual.Menus [Test] public void TestInstantLoad() { - // visual only, very impossible to test this using asserts. - AddStep("load immediately", () => { loader = new TestLoader(); @@ -46,12 +44,17 @@ namespace osu.Game.Tests.Visual.Menus LoadScreen(loader); }); - AddAssert("spinner did not display", () => loader.LoadingSpinner?.Alpha == 0); + spinnerNotPresentOrHidden(); AddUntilStep("loaded", () => loader.ScreenLoaded); AddUntilStep("not current", () => !loader.IsCurrentScreen()); + + spinnerNotPresentOrHidden(); } + private void spinnerNotPresentOrHidden() => + AddAssert("spinner did not display", () => loader.LoadingSpinner == null || loader.LoadingSpinner.Alpha == 0); + [Test] public void TestDelayedLoad() { From a317ef65b8b5d301689e526bec8efd6b453743bb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 28 Mar 2020 12:18:28 +0900 Subject: [PATCH 0635/2376] Remove default for argument --- osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs | 2 +- osu.Game/Screens/Play/BreakOverlay.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs index d46b4ea289..ff25e609c1 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs @@ -43,7 +43,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddRange(new Drawable[] { breakTracker = new TestBreakTracker(), - breakOverlay = new BreakOverlay(true) + breakOverlay = new BreakOverlay(true, null) { ProcessCustomClock = false, } diff --git a/osu.Game/Screens/Play/BreakOverlay.cs b/osu.Game/Screens/Play/BreakOverlay.cs index 89f51315f2..c978f4e96d 100644 --- a/osu.Game/Screens/Play/BreakOverlay.cs +++ b/osu.Game/Screens/Play/BreakOverlay.cs @@ -45,7 +45,7 @@ namespace osu.Game.Screens.Play private readonly RemainingTimeCounter remainingTimeCounter; private readonly BreakArrows breakArrows; - public BreakOverlay(bool letterboxing, ScoreProcessor scoreProcessor = null) + public BreakOverlay(bool letterboxing, ScoreProcessor scoreProcessor) { RelativeSizeAxes = Axes.Both; From 45eb03bfe2adc868729915defc057b09e5fb7f90 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 28 Mar 2020 07:43:47 +0300 Subject: [PATCH 0636/2376] Apply review suggestions --- .../TestSceneHyperDashColouring.cs | 32 +++++++------------ 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs index 7fab961aa7..9ab8cf9113 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Catch.Tests }, false, false); }); - AddAssert("fruit colour default-hyperdash", () => checkFruitHyperDashColour(drawableFruit, Catcher.DefaultHyperDashColour, legacyFruit)); + AddAssert("default colour", () => checkFruitHyperDashColour(drawableFruit, Catcher.DefaultHyperDashColour, legacyFruit)); } [TestCase(false, true)] @@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Catch.Tests }, customCatcherHyperDashColour, true); }); - AddAssert("fruit colour custom-hyperdash", () => checkFruitHyperDashColour(drawableFruit, TestLegacySkin.CustomHyperDashFruitColour, legacyFruit)); + AddAssert("custom colour", () => checkFruitHyperDashColour(drawableFruit, TestLegacySkin.CustomHyperDashFruitColour, legacyFruit)); } [TestCase(false)] @@ -96,7 +96,7 @@ namespace osu.Game.Rulesets.Catch.Tests }, true, false); }); - AddAssert("fruit colour catcher-custom-hyperdash", () => checkFruitHyperDashColour(drawableFruit, TestLegacySkin.CustomHyperDashColour, legacyFruit)); + AddAssert("catcher custom colour", () => checkFruitHyperDashColour(drawableFruit, TestLegacySkin.CustomHyperDashColour, legacyFruit)); } private Drawable setupSkinHierarchy(Func getChild, bool customHyperDashCatcherColour = false, bool customHyperDashFruitColour = false, bool customHyperDashAfterColour = false) @@ -139,13 +139,12 @@ namespace osu.Game.Rulesets.Catch.Tests if (componentName == "fruit-pear") { // convince CatchLegacySkinTransformer to use the LegacyFruitPiece for pear fruit. - var texture = new Texture(Texture.WhitePixel.TextureGL) + return new Texture(Texture.WhitePixel.TextureGL) { Width = 1, Height = 1, ScaleAdjust = 1 / 96f }; - return texture; } return null; @@ -155,25 +154,16 @@ namespace osu.Game.Rulesets.Catch.Tests public IBindable GetConfig(TLookup lookup) { - switch (lookup) + if (lookup is CatchSkinConfiguration config) { - case CatchSkinConfiguration config when config == CatchSkinConfiguration.HyperDash: - if (customHyperDashCatcherColour) - return SkinUtils.As(new Bindable(CustomHyperDashColour)); + if (config == CatchSkinConfiguration.HyperDash && customHyperDashCatcherColour) + return SkinUtils.As(new Bindable(CustomHyperDashColour)); - return null; + if (config == CatchSkinConfiguration.HyperDashFruit && customHyperDashFruitColour) + return SkinUtils.As(new Bindable(CustomHyperDashFruitColour)); - case CatchSkinConfiguration config when config == CatchSkinConfiguration.HyperDashFruit: - if (customHyperDashFruitColour) - return SkinUtils.As(new Bindable(CustomHyperDashFruitColour)); - - return null; - - case CatchSkinConfiguration config when config == CatchSkinConfiguration.HyperDashAfterImage: - if (customHyperDashAfterColour) - return SkinUtils.As(new Bindable(CustomHyperDashAfterColour)); - - return null; + if (config == CatchSkinConfiguration.HyperDashAfterImage && customHyperDashAfterColour) + return SkinUtils.As(new Bindable(CustomHyperDashAfterColour)); } return null; From fb4b334ce2f9a5e44bf82cd846a7af98f7487388 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 28 Mar 2020 13:39:08 +0900 Subject: [PATCH 0637/2376] Add support for legacy skin sliderstartcircle / sliderstartcircleoverlay --- .../metrics-skin/sliderstartcircle@2x.png | Bin 0 -> 17245 bytes .../sliderstartcircleoverlay@2x.png | Bin 0 -> 50009 bytes .../Objects/Drawables/DrawableHitCircle.cs | 4 +++- .../Objects/Drawables/DrawableSliderHead.cs | 2 ++ osu.Game.Rulesets.Osu/OsuSkinComponents.cs | 1 + .../Skinning/LegacyMainCirclePiece.cs | 21 +++++++++++++++--- .../Skinning/OsuLegacySkinTransformer.cs | 6 +++++ 7 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/sliderstartcircle@2x.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/sliderstartcircleoverlay@2x.png diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/sliderstartcircle@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/sliderstartcircle@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..4d630443cd7e2222d3a676e8d77219873660ebe0 GIT binary patch literal 17245 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%0A6c)ubJ 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 OsuSkinComponents.HitCircle; + public DrawableHitCircle(HitCircle h) : base(h) { @@ -57,7 +59,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables return true; }, }, - CirclePiece = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.HitCircle), _ => new MainCirclePiece()), + CirclePiece = new SkinnableDrawable(new OsuSkinComponent(CirclePieceComponent), _ => new MainCirclePiece()), ApproachCircle = new ApproachCircle { Alpha = 0, diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs index c5609b01e0..a360071f26 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs @@ -14,6 +14,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private readonly IBindable positionBindable = new Bindable(); private readonly IBindable pathVersion = new Bindable(); + protected override OsuSkinComponents CirclePieceComponent => OsuSkinComponents.SliderHeadHitCircle; + private readonly Slider slider; public DrawableSliderHead(Slider slider, HitCircle h) diff --git a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs index 4ea4220faf..b2cdc8ccbf 100644 --- a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs +++ b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs @@ -13,6 +13,7 @@ namespace osu.Game.Rulesets.Osu ApproachCircle, ReverseArrow, HitCircleText, + SliderHeadHitCircle, SliderFollowCircle, SliderBall, SliderBody, diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs index 93ae0371df..38ba4c5974 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs @@ -6,6 +6,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Objects.Drawables; @@ -18,8 +19,12 @@ namespace osu.Game.Rulesets.Osu.Skinning { public class LegacyMainCirclePiece : CompositeDrawable { - public LegacyMainCirclePiece() + private readonly string priorityLookup; + + public LegacyMainCirclePiece(string priorityLookup = null) { + this.priorityLookup = priorityLookup; + Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); } @@ -39,7 +44,7 @@ namespace osu.Game.Rulesets.Osu.Skinning { hitCircleSprite = new Sprite { - Texture = skin.GetTexture("hitcircle"), + Texture = getTextureWithFallback(string.Empty), Colour = drawableObject.AccentColour.Value, Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -51,7 +56,7 @@ namespace osu.Game.Rulesets.Osu.Skinning }, confineMode: ConfineMode.NoScaling), new Sprite { - Texture = skin.GetTexture("hitcircleoverlay"), + Texture = getTextureWithFallback("overlay"), Anchor = Anchor.Centre, Origin = Anchor.Centre, } @@ -65,6 +70,16 @@ namespace osu.Game.Rulesets.Osu.Skinning indexInCurrentCombo.BindTo(osuObject.IndexInCurrentComboBindable); indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true); + + Texture getTextureWithFallback(string name) + { + Texture tex = null; + + if (!string.IsNullOrEmpty(priorityLookup)) + tex = skin.GetTexture($"{priorityLookup}{name}"); + + return tex ?? skin.GetTexture($"hitcircle{name}"); + } } private void updateState(ValueChangedEvent state) diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs index d6c3f443eb..075c536b4c 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs @@ -82,6 +82,12 @@ namespace osu.Game.Rulesets.Osu.Skinning return null; + case OsuSkinComponents.SliderHeadHitCircle: + if (hasHitCircle.Value) + return new LegacyMainCirclePiece("sliderstartcircle"); + + return null; + case OsuSkinComponents.HitCircle: if (hasHitCircle.Value) return new LegacyMainCirclePiece(); From fc3f9ff6faf06324e32c7964d5af915a0b16ffcb Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sat, 28 Mar 2020 12:54:48 +0200 Subject: [PATCH 0638/2376] Don't use drawables for select next --- osu.Game/Screens/Select/BeatmapCarousel.cs | 57 +++++++++------------- 1 file changed, 23 insertions(+), 34 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index fa8974f55a..df2c1236f4 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -253,46 +253,35 @@ namespace osu.Game.Screens.Select /// Whether to skip individual difficulties and only increment over full groups. public void SelectNext(int direction = 1, bool skipDifficulties = true) { - var visibleItems = Items.Where(s => !s.Item.Filtered.Value).ToList(); - - if (!visibleItems.Any()) + if (!root.Children.Where(s => !s.Filtered.Value).ToList().Any()) return; - DrawableCarouselItem drawable = null; + if (skipDifficulties) + selectNextSet(direction, true); + else + selectNextDifficulty(direction); + } - if (selectedBeatmap != null && (drawable = selectedBeatmap.Drawables.FirstOrDefault()) == null) - // if the selected beatmap isn't present yet, we can't correctly change selection. - // we can fix this by changing this method to not reference drawables / Items in the first place. - return; + private void selectNextSet(int direction, bool skipDifficulties) + { + var visibleSets = root.Children.OfType().Where(s => !s.Filtered.Value).ToList(); - int originalIndex = visibleItems.IndexOf(drawable); - int currentIndex = originalIndex; + var item = visibleSets[(visibleSets.IndexOf(selectedBeatmapSet) + direction + visibleSets.Count) % visibleSets.Count]; - // local function to increment the index in the required direction, wrapping over extremities. - int incrementIndex() => currentIndex = (currentIndex + direction + visibleItems.Count) % visibleItems.Count; + if (skipDifficulties) + select(item); + else + select(direction > 0 ? item.Beatmaps.First(b => !b.Filtered.Value) : item.Beatmaps.Last(b => !b.Filtered.Value)); + } - while (incrementIndex() != originalIndex) - { - var item = visibleItems[currentIndex].Item; - - if (item.Filtered.Value || item.State.Value == CarouselItemState.Selected) continue; - - switch (item) - { - case CarouselBeatmap beatmap: - if (skipDifficulties) continue; - - select(beatmap); - return; - - case CarouselBeatmapSet set: - if (skipDifficulties) - select(set); - else - select(direction > 0 ? set.Beatmaps.First(b => !b.Filtered.Value) : set.Beatmaps.Last(b => !b.Filtered.Value)); - return; - } - } + private void selectNextDifficulty(int direction) + { + var difficulties = selectedBeatmapSet.Children.Where(s => !s.Filtered.Value).ToList(); + int index = difficulties.IndexOf(selectedBeatmap); + if (index + direction < 0 || index + direction >= difficulties.Count) + selectNextSet(direction, false); + else + select(difficulties[index + direction]); } /// From 6a0c5c87aa52c908af5f632b58b7849e45988c6e Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sat, 28 Mar 2020 13:06:03 +0200 Subject: [PATCH 0639/2376] Use already existing variable --- osu.Game/Screens/Select/BeatmapCarousel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index df2c1236f4..a6cbf58023 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -253,7 +253,7 @@ namespace osu.Game.Screens.Select /// Whether to skip individual difficulties and only increment over full groups. public void SelectNext(int direction = 1, bool skipDifficulties = true) { - if (!root.Children.Where(s => !s.Filtered.Value).ToList().Any()) + if (!beatmapSets.Where(s => !s.Filtered.Value).ToList().Any()) return; if (skipDifficulties) @@ -264,7 +264,7 @@ namespace osu.Game.Screens.Select private void selectNextSet(int direction, bool skipDifficulties) { - var visibleSets = root.Children.OfType().Where(s => !s.Filtered.Value).ToList(); + var visibleSets = beatmapSets.Where(s => !s.Filtered.Value).ToList(); var item = visibleSets[(visibleSets.IndexOf(selectedBeatmapSet) + direction + visibleSets.Count) % visibleSets.Count]; From 659865b45762ee153f02888e8bbfdc92513b83b3 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sat, 28 Mar 2020 13:08:06 +0200 Subject: [PATCH 0640/2376] Use understandable set id --- osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 31114dfd25..a811e58694 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -91,7 +91,7 @@ namespace osu.Game.Tests.Visual.SongSelect for (int i = 0; i < create_this_many_sets; i++) { - var set = createTestBeatmapSet(i); + var set = createTestBeatmapSet(i + 1); sets.Add(set); } From 63f6269eb0ae7e88a8b810c6c7ba5690a9cea1dd Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sat, 28 Mar 2020 13:10:20 +0200 Subject: [PATCH 0641/2376] Test both ways --- .../Visual/SongSelect/TestSceneBeatmapCarousel.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index a811e58694..b316fcc60b 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -83,8 +83,9 @@ namespace osu.Game.Tests.Visual.SongSelect waitForSelection(set_count, 3); } - [Test] - public void TestTraversalHold() + [TestCase(true)] + [TestCase(false)] + public void TestTraversalHold(bool forwards) { var sets = new List(); const int create_this_many_sets = 200; @@ -99,15 +100,16 @@ namespace osu.Game.Tests.Visual.SongSelect void selectNextAndAssert(int amount) { - setSelected(1, 1); - AddStep($"Next beatmap {amount} times", () => + setSelected(forwards ? 1 : create_this_many_sets, 1); + string text = forwards ? "Next" : "Previous"; + AddStep($"{text} beatmap {amount} times", () => { for (int i = 0; i < amount; i++) { - carousel.SelectNext(); + carousel.SelectNext(forwards ? 1 : -1); } }); - waitForSelection(amount + 1); + waitForSelection(forwards ? amount + 1 : create_this_many_sets - amount); } for (int i = 1; i < create_this_many_sets; i += i) From 87854fc4fabea688d60922552f487037e4873d44 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sat, 28 Mar 2020 13:23:31 +0200 Subject: [PATCH 0642/2376] Rename variable --- osu.Game/Screens/Select/BeatmapCarousel.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index a6cbf58023..91a9b19115 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -266,12 +266,12 @@ namespace osu.Game.Screens.Select { var visibleSets = beatmapSets.Where(s => !s.Filtered.Value).ToList(); - var item = visibleSets[(visibleSets.IndexOf(selectedBeatmapSet) + direction + visibleSets.Count) % visibleSets.Count]; + var nextSet = visibleSets[(visibleSets.IndexOf(selectedBeatmapSet) + direction + visibleSets.Count) % visibleSets.Count]; if (skipDifficulties) - select(item); + select(nextSet); else - select(direction > 0 ? item.Beatmaps.First(b => !b.Filtered.Value) : item.Beatmaps.Last(b => !b.Filtered.Value)); + select(direction > 0 ? nextSet.Beatmaps.First(b => !b.Filtered.Value) : nextSet.Beatmaps.Last(b => !b.Filtered.Value)); } private void selectNextDifficulty(int direction) From 1c711147f37c289f1519088563fab122c05785cc Mon Sep 17 00:00:00 2001 From: Santeri Nogelainen Date: Sat, 28 Mar 2020 17:22:01 +0200 Subject: [PATCH 0643/2376] Move all carousel rank logic into separate classes (TopLocalRank and CarouselBeatmapRank) --- osu.Game/Online/Leaderboards/TopLocalRank.cs | 78 +++++++++++++++++++ .../Online/Leaderboards/UpdateableRank.cs | 22 ++++-- .../Select/Carousel/CarouselBeatmapRank.cs | 67 ++++++++++++++++ .../Carousel/DrawableCarouselBeatmap.cs | 20 ++++- 4 files changed, 177 insertions(+), 10 deletions(-) create mode 100644 osu.Game/Online/Leaderboards/TopLocalRank.cs create mode 100644 osu.Game/Screens/Select/Carousel/CarouselBeatmapRank.cs diff --git a/osu.Game/Online/Leaderboards/TopLocalRank.cs b/osu.Game/Online/Leaderboards/TopLocalRank.cs new file mode 100644 index 0000000000..40855e6cf8 --- /dev/null +++ b/osu.Game/Online/Leaderboards/TopLocalRank.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.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Online.API; +using osu.Game.Rulesets; +using osu.Game.Scoring; + +namespace osu.Game.Online.Leaderboards +{ + public class TopLocalRank : Container + { + private readonly BeatmapInfo beatmap; + + private ScoreManager scores; + private IBindable ruleset; + private IAPIProvider api; + private UpdateableRank rank; + + /// + /// Raised when the top score is loaded + /// + public Action ScoreLoaded; + + public TopLocalRank(BeatmapInfo beatmap) + { + this.beatmap = beatmap; + + RelativeSizeAxes = Axes.Both; + + InternalChild = rank = new UpdateableRank(null) + { + RelativeSizeAxes = Axes.Both + }; + } + + [BackgroundDependencyLoader] + private void load(ScoreManager scores, IBindable ruleset, IAPIProvider api) + { + this.scores = scores; + this.ruleset = ruleset; + this.api = api; + + FetchAndLoadTopScore(); + } + + public void FetchAndLoadTopScore() + { + var score = fetchTopScore(); + + loadTopScore(score); + } + + private void loadTopScore(ScoreInfo score) + { + Schedule(() => rank.Rank = score?.Rank); + + ScoreLoaded?.Invoke(score); + } + + private ScoreInfo fetchTopScore() + { + if (scores == null || beatmap == null || ruleset?.Value == null || api?.LocalUser.Value == null) + return null; + + return scores.GetAllUsableScores() + .Where(s => s.UserID == api.LocalUser.Value.Id && s.BeatmapInfoID == beatmap.ID && s.RulesetID == ruleset.Value.ID) + .OrderByDescending(s => s.TotalScore) + .FirstOrDefault(); + } + } +} diff --git a/osu.Game/Online/Leaderboards/UpdateableRank.cs b/osu.Game/Online/Leaderboards/UpdateableRank.cs index d9e8957281..8f74fd84fe 100644 --- a/osu.Game/Online/Leaderboards/UpdateableRank.cs +++ b/osu.Game/Online/Leaderboards/UpdateableRank.cs @@ -7,23 +7,31 @@ using osu.Game.Scoring; namespace osu.Game.Online.Leaderboards { - public class UpdateableRank : ModelBackedDrawable + public class UpdateableRank : ModelBackedDrawable { - public ScoreRank Rank + public ScoreRank? Rank { get => Model; set => Model = value; } - public UpdateableRank(ScoreRank rank) + public UpdateableRank(ScoreRank? rank) { Rank = rank; } - protected override Drawable CreateDrawable(ScoreRank rank) => new DrawableRank(rank) + protected override Drawable CreateDrawable(ScoreRank? rank) { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }; + if (rank.HasValue) + { + return new DrawableRank(rank.Value) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + } + + return null; + } } } diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmapRank.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmapRank.cs new file mode 100644 index 0000000000..9ad0dc946e --- /dev/null +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmapRank.cs @@ -0,0 +1,67 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Scoring; + +namespace osu.Game.Online.Leaderboards +{ + public class CarouselBeatmapRank : Container + { + private const int rank_size = 20; + private readonly BeatmapInfo beatmap; + + private TopLocalRank rank; + + public CarouselBeatmapRank(BeatmapInfo beatmap) + { + this.beatmap = beatmap; + + Height = rank_size; + } + + [BackgroundDependencyLoader] + private void load(ScoreManager scores, IBindable ruleset) + { + scores.ItemAdded += scoreChanged; + scores.ItemRemoved += scoreChanged; + ruleset.ValueChanged += _ => rulesetChanged(); + + rank = new TopLocalRank(beatmap) + { + ScoreLoaded = scaleDisplay + }; + + InternalChild = new DelayedLoadWrapper(rank) + { + RelativeSizeAxes = Axes.Both + }; + } + + private void rulesetChanged() + { + rank.FetchAndLoadTopScore(); + } + + private void scoreChanged(ScoreInfo score) + { + if (score.BeatmapInfoID == beatmap.ID) + { + rank.FetchAndLoadTopScore(); + } + } + + private void scaleDisplay(ScoreInfo score) + { + if (score != null) + Width = rank_size * 2; + else + Width = 0; + } + } +} diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index 841bbf415c..a58d706003 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -19,6 +19,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Leaderboards; using osu.Game.Overlays; using osuTK; using osuTK.Graphics; @@ -122,10 +123,23 @@ namespace osu.Game.Screens.Select.Carousel }, } }, - starCounter = new StarCounter + new FillFlowContainer { - Current = (float)beatmap.StarDifficulty, - Scale = new Vector2(0.8f), + Direction = FillDirection.Horizontal, + Spacing = new Vector2(4, 0), + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new CarouselBeatmapRank(beatmap) + { + Scale = new Vector2(0.8f) + }, + starCounter = new StarCounter + { + Current = (float)beatmap.StarDifficulty, + Scale = new Vector2(0.8f), + } + } } } } From faa2b49be41032f14dacc829ce90de8ae38b6783 Mon Sep 17 00:00:00 2001 From: Santeri Nogelainen Date: Sat, 28 Mar 2020 18:13:39 +0200 Subject: [PATCH 0644/2376] Fix namespace for CarouselBeatmapRank, make UpdateableRank in TopLocalRank readonly --- osu.Game/Online/Leaderboards/TopLocalRank.cs | 2 +- osu.Game/Screens/Select/Carousel/CarouselBeatmapRank.cs | 3 ++- osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs | 1 - 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/Leaderboards/TopLocalRank.cs b/osu.Game/Online/Leaderboards/TopLocalRank.cs index 40855e6cf8..83d92f8ffa 100644 --- a/osu.Game/Online/Leaderboards/TopLocalRank.cs +++ b/osu.Game/Online/Leaderboards/TopLocalRank.cs @@ -17,11 +17,11 @@ namespace osu.Game.Online.Leaderboards public class TopLocalRank : Container { private readonly BeatmapInfo beatmap; + private readonly UpdateableRank rank; private ScoreManager scores; private IBindable ruleset; private IAPIProvider api; - private UpdateableRank rank; /// /// Raised when the top score is loaded diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmapRank.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmapRank.cs index 9ad0dc946e..fbd4292138 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmapRank.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmapRank.cs @@ -6,10 +6,11 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; +using osu.Game.Online.Leaderboards; using osu.Game.Rulesets; using osu.Game.Scoring; -namespace osu.Game.Online.Leaderboards +namespace osu.Game.Screens.Select.Carousel { public class CarouselBeatmapRank : Container { diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index a58d706003..4b42d818f5 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -19,7 +19,6 @@ using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; -using osu.Game.Online.Leaderboards; using osu.Game.Overlays; using osuTK; using osuTK.Graphics; From 2c27894527f91317e16e660f61a613c700110282 Mon Sep 17 00:00:00 2001 From: Endrik Date: Sat, 28 Mar 2020 19:58:33 +0200 Subject: [PATCH 0645/2376] Use All instead of ToList Any MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Bartłomiej Dach --- osu.Game/Screens/Select/BeatmapCarousel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 91a9b19115..cd2deb8abe 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -253,7 +253,7 @@ namespace osu.Game.Screens.Select /// Whether to skip individual difficulties and only increment over full groups. public void SelectNext(int direction = 1, bool skipDifficulties = true) { - if (!beatmapSets.Where(s => !s.Filtered.Value).ToList().Any()) + if (beatmapSets.All(s => !s.Filtered.Value)) return; if (skipDifficulties) From b4f05007063dcb46cb6736a817ccb5e18222a511 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sat, 28 Mar 2020 20:21:21 +0200 Subject: [PATCH 0646/2376] Invert logic --- osu.Game/Screens/Select/BeatmapCarousel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index cd2deb8abe..a2c2cde7c7 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -253,7 +253,7 @@ namespace osu.Game.Screens.Select /// Whether to skip individual difficulties and only increment over full groups. public void SelectNext(int direction = 1, bool skipDifficulties = true) { - if (beatmapSets.All(s => !s.Filtered.Value)) + if (beatmapSets.All(s => s.Filtered.Value)) return; if (skipDifficulties) From 8cab303611786eeba91aa2a67bb25633f23e10c6 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sat, 28 Mar 2020 21:02:55 +0200 Subject: [PATCH 0647/2376] Cover skipDifficulties = false in tests --- .../SongSelect/TestSceneBeatmapCarousel.cs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index b316fcc60b..7c3498e034 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -118,6 +118,50 @@ namespace osu.Game.Tests.Visual.SongSelect } } + [Test] + public void TestTraversalHoldDifficulties() + { + var sets = new List(); + + for (int i = 0; i < 20; i++) + { + var set = createTestBeatmapSet(i + 1); + sets.Add(set); + } + + loadBeatmaps(sets); + + void selectNextAndAssert(int amount, bool forwards, int expectedSet, int expectedDiff) + { + // Select very first or very last difficulty + setSelected(forwards ? 1 : 20, forwards ? 1 : 3); + string text = forwards ? "Next" : "Previous"; + AddStep($"{text} difficulty {amount} times", () => + { + for (int i = 0; i < amount; i++) + { + carousel.SelectNext(forwards ? 1 : -1, false); + } + }); + waitForSelection(expectedSet, expectedDiff); + } + + // Selects next set once, difficulty index doesn't change + selectNextAndAssert(3, true, 2, 1); + // Selects next set 16 times (50 // 3 == 16), difficulty index changes twice (50 % 3 == 2) + selectNextAndAssert(50, true, 17, 3); + // Travels around the carousel thrice (200/60 == 3) + // continues to select 20 times (200 % 60 == 20) + // selects next set 6 times (20 // 3 == 6) + // difficulty index changes twice (20 % 3 == 2) + selectNextAndAssert(200, true, 7, 3); + + // All same but in reverse + selectNextAndAssert(3, false, 19, 3); + selectNextAndAssert(50, false, 4, 1); + selectNextAndAssert(200, false, 14, 1); + } + /// /// Test filtering /// From d3114ca858718aa1f69fcd46c1c8faabdc3228f0 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sat, 28 Mar 2020 23:12:13 +0200 Subject: [PATCH 0648/2376] Don't snake when hit --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs index b9cee71ca1..30abce7696 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs @@ -87,6 +87,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public void UpdateSnakingPosition(Vector2 start, Vector2 end) { + if (IsHit) return; + bool isRepeatAtEnd = sliderRepeat.RepeatIndex % 2 == 0; List curve = ((PlaySliderBody)drawableSlider.Body.Drawable).CurrentCurve; From a2b3fe180e096a6f85ab034370821b917ce79345 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 29 Mar 2020 14:30:45 +0900 Subject: [PATCH 0649/2376] Add the ability to disable user input on specific DrawableHitObjects --- .../Rulesets/Objects/Drawables/DrawableHitObject.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 5b5802fa9d..9aad125ed1 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -38,6 +38,19 @@ namespace osu.Game.Rulesets.Objects.Drawables private readonly Lazy> nestedHitObjects = new Lazy>(); public IReadOnlyList NestedHitObjects => nestedHitObjects.IsValueCreated ? nestedHitObjects.Value : (IReadOnlyList)Array.Empty(); + /// + /// Whether this object should handle any user input events. + /// + public bool HandleUserInput { get; set; } = true; + + public override bool HandlePositionalInput => HandleUserInput; + + public override bool HandleNonPositionalInput => HandleUserInput; + + public override bool PropagatePositionalInputSubTree => HandleUserInput; + + public override bool PropagateNonPositionalInputSubTree => HandleUserInput; + /// /// Invoked when a has been applied by this or a nested . /// From d1b01095ee292b02ad1af51ff6a74b49df8c8929 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 29 Mar 2020 14:31:03 +0900 Subject: [PATCH 0650/2376] Rewrite to reduce code changes and complexities in hit object implementation --- osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 17 ++++++-------- .../Objects/Drawables/DrawableSpinner.cs | 9 ++++---- .../Objects/Drawables/Pieces/SpinnerDisc.cs | 23 ++++++++++++++----- 3 files changed, 29 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index 49c4e7fa45..7b54baa99b 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -30,21 +30,18 @@ namespace osu.Game.Rulesets.Osu.Mods { if (hitObject is DrawableSpinner spinner) { - spinner.Disc.Enabled = false; - spinner.OnUpdate += autoSpin; + spinner.HandleUserInput = false; + spinner.OnUpdate += onSpinnerUpdate; } } } - private void autoSpin(Drawable drawable) + private void onSpinnerUpdate(Drawable drawable) { - if (drawable is DrawableSpinner spinner) - { - if (spinner.Disc.Valid) - spinner.Disc.Rotate(MathUtils.RadiansToDegrees((float)spinner.Clock.ElapsedFrameTime * 0.03f)); - if (!spinner.SpmCounter.IsPresent) - spinner.SpmCounter.FadeIn(spinner.HitObject.TimeFadeIn); - } + var spinner = (DrawableSpinner)drawable; + + spinner.Disc.Tracking = true; + spinner.Disc.Rotate(MathUtils.RadiansToDegrees((float)spinner.Clock.ElapsedFrameTime * 0.03f)); } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 0ec7f2ebfe..3c8ab0f5ab 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -176,17 +176,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void Update() { - Disc.Tracking = OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false; - if (!SpmCounter.IsPresent && Disc.Tracking) - SpmCounter.FadeIn(HitObject.TimeFadeIn); - base.Update(); + if (HandleUserInput) + Disc.Tracking = OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false; } protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); + if (!SpmCounter.IsPresent && Disc.Tracking) + SpmCounter.FadeIn(HitObject.TimeFadeIn); + circle.Rotation = Disc.Rotation; Ticks.Rotation = Disc.Rotation; SpmCounter.SetRotation(Disc.RotationAbsolute); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs index 0c089c1fed..d4ef039b79 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs @@ -50,9 +50,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces get => tracking; set { - if ((Enabled && value) == tracking) return; + if (value == tracking) return; - tracking = Enabled && value; + tracking = value; background.FadeTo(tracking ? tracking_alpha : idle_alpha, 100); } @@ -73,9 +73,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces } } - public bool Valid => spinner.StartTime <= Time.Current && spinner.EndTime > Time.Current; - - public bool Enabled { get; set; } = true; + /// + /// Whether currently in the correct time range to allow spinning. + /// + private bool isSpinnableTime => spinner.StartTime <= Time.Current && spinner.EndTime > Time.Current; protected override bool OnMouseMove(MouseMoveEvent e) { @@ -101,7 +102,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces var delta = thisAngle - lastAngle; - if (Valid && tracking) + if (tracking) Rotate(delta); lastAngle = thisAngle; @@ -118,8 +119,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces Rotation = (float)Interpolation.Lerp(Rotation, currentRotation / 2, Math.Clamp(Math.Abs(Time.Elapsed) / 40, 0, 1)); } + /// + /// Rotate the disc by the provided angle (in addition to any existing rotation). + /// + /// + /// Will be a no-op if not a valid time to spin. + /// + /// The delta angle. public void Rotate(float angle) { + if (!isSpinnableTime) + return; + if (!rotationTransferred) { currentRotation = Rotation * 2; From 2ab8267f84090b9fcab62bbd143ec163e1f0c0f3 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sun, 29 Mar 2020 10:50:43 +0300 Subject: [PATCH 0651/2376] Add a comment --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs index 30abce7696..2704680d54 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs @@ -87,6 +87,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public void UpdateSnakingPosition(Vector2 start, Vector2 end) { + // When the repeat is hit, the arrow should fade out on spot, + // it should no longer follow snaking if (IsHit) return; bool isRepeatAtEnd = sliderRepeat.RepeatIndex % 2 == 0; From 4f5557096c238ac602b59d3dab39605b93cb2ca2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 29 Mar 2020 22:51:28 +0900 Subject: [PATCH 0652/2376] Fix auto mod results not displaying correctly --- osu.Game/Screens/Play/Player.cs | 64 ++++++++++++++------------- osu.Game/Screens/Play/ReplayPlayer.cs | 6 +++ 2 files changed, 40 insertions(+), 30 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 63ec3b0d2d..5da53ad2c9 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -637,6 +637,39 @@ namespace osu.Game.Screens.Play return base.OnExiting(next); } + protected virtual void GotoRanking() + { + if (DrawableRuleset.ReplayScore != null) + { + // if a replay is present, we likely don't want to import into the local database. + this.Push(CreateResults(CreateScore())); + return; + } + + LegacyByteArrayReader replayReader = null; + + var score = new Score { ScoreInfo = CreateScore() }; + + if (recordingReplay?.Frames.Count > 0) + { + score.Replay = recordingReplay; + + using (var stream = new MemoryStream()) + { + new LegacyScoreEncoder(score, gameplayBeatmap.PlayableBeatmap).Encode(stream); + replayReader = new LegacyByteArrayReader(stream.ToArray(), "replay.osr"); + } + } + + scoreManager.Import(score.ScoreInfo, replayReader) + .ContinueWith(imported => Schedule(() => + { + // screen may be in the exiting transition phase. + if (this.IsCurrentScreen()) + this.Push(CreateResults(imported.Result)); + })); + } + private void fadeOut(bool instant = false) { float fadeOutDuration = instant ? 0 : 250; @@ -649,36 +682,7 @@ namespace osu.Game.Screens.Play private void scheduleGotoRanking() { completionProgressDelegate?.Cancel(); - completionProgressDelegate = Schedule(delegate - { - if (DrawableRuleset.ReplayScore != null) - this.Push(CreateResults(DrawableRuleset.ReplayScore.ScoreInfo)); - else - { - var score = new Score { ScoreInfo = CreateScore() }; - - LegacyByteArrayReader replayReader = null; - - if (recordingReplay?.Frames.Count > 0) - { - score.Replay = recordingReplay; - - using (var stream = new MemoryStream()) - { - new LegacyScoreEncoder(score, gameplayBeatmap.PlayableBeatmap).Encode(stream); - replayReader = new LegacyByteArrayReader(stream.ToArray(), "replay.osr"); - } - } - - scoreManager.Import(score.ScoreInfo, replayReader) - .ContinueWith(imported => Schedule(() => - { - // screen may be in the exiting transition phase. - if (this.IsCurrentScreen()) - this.Push(CreateResults(imported.Result)); - })); - } - }); + completionProgressDelegate = Schedule(GotoRanking); } #endregion diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index 8708b5f634..74c853340d 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.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.Screens; using osu.Game.Scoring; namespace osu.Game.Screens.Play @@ -23,6 +24,11 @@ namespace osu.Game.Screens.Play DrawableRuleset?.SetReplayScore(score); } + protected override void GotoRanking() + { + this.Push(CreateResults(DrawableRuleset.ReplayScore.ScoreInfo)); + } + protected override ScoreInfo CreateScore() => score.ScoreInfo; } } From 11826800fb834ccd9b04b98db23c713fdfc6b4e9 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sun, 29 Mar 2020 17:00:26 +0300 Subject: [PATCH 0653/2376] Test slider snaking --- .../TestSceneSliderSnaking.cs | 294 ++++++++++++++++++ 1 file changed, 294 insertions(+) create mode 100644 osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs new file mode 100644 index 0000000000..3e40713f52 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs @@ -0,0 +1,294 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Bindables; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Framework.Timing; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Configuration; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; +using osu.Game.Storyboards; +using osuTK; +using static osu.Game.Tests.Visual.OsuTestScene.ClockBackedTestWorkingBeatmap; + +namespace osu.Game.Rulesets.Osu.Tests +{ + [TestFixture] + public class TestSceneSliderSnaking : TestSceneOsuPlayer + { + [Resolved] + private AudioManager audioManager { get; set; } + + private TrackVirtualManual track; + + protected override bool Autoplay => true; + + private readonly Bindable snakingIn = new Bindable(); + private readonly Bindable snakingOut = new Bindable(); + + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) + { + var working = new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager); + track = (TrackVirtualManual)working.Track; + return working; + } + + [BackgroundDependencyLoader] + private void load(RulesetConfigCache configCache) + { + var config = (OsuRulesetConfigManager)configCache.GetConfigFor(Ruleset.Value.CreateInstance()); + config.BindWith(OsuRulesetSetting.SnakingInSliders, snakingIn); + config.BindWith(OsuRulesetSetting.SnakingOutSliders, snakingOut); + } + + private DrawableSlider slider; + private DrawableSliderRepeat repeat; + private Vector2 vector; + + [SetUpSteps] + public override void SetUpSteps() + { + base.SetUpSteps(); + + AddUntilStep("wait for track to start running", () => track.IsRunning); + } + + [Test] + public void TestSnaking() + { + AddStep("retrieve 1st slider", () => slider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.First()); + testLinear(true); + testLinear(false); + AddStep("retrieve 2nd slider", () => slider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.Skip(1).First()); + testRepeating(true); + testRepeating(false); + AddStep("retrieve 3rd slider", () => slider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.Skip(2).First()); + testDoubleRepeating(true); + testDoubleRepeating(false); + + // Test arrow stays in place + setSnaking(true); + addSeekStep(13500); + AddStep("retrieve 2nd slider repeat", () => + { + var drawable = Player.DrawableRuleset.Playfield.AllHitObjects.Skip(1).First(); + repeat = drawable.ChildrenOfType>().First().Children.First(); + }); + AddStep("Save repeat vector", () => vector = repeat.Position); + addSeekStep(13700); + AddAssert("Repeat vector is same", () => Precision.AlmostEquals(vector.X, repeat.Position.X, 1) && Precision.AlmostEquals(vector.Y, repeat.Position.Y, 1)); + } + + private void testLinear(bool snaking) + { + var increased = snaking ? "increased" : "is same"; + + setSnaking(snaking); + addSeekStep(1800); + AddStep("Save end vector", () => + { + var body = (PlaySliderBody)slider.Body.Drawable; + vector = body.CurrentCurve.Last(); + }); + addSeekStep(1900); + AddAssert($"End vector {increased}", () => + { + var body = (PlaySliderBody)slider.Body.Drawable; + var last = body.CurrentCurve.Last(); + return snaking ? last.X > vector.X && last.Y > vector.Y : last == vector; + }); + addSeekStep(3100); + AddStep("Save start vector", () => + { + var body = (PlaySliderBody)slider.Body.Drawable; + vector = body.CurrentCurve.First(); + }); + addSeekStep(3200); + AddAssert($"Start vector {increased}", () => + { + var body = (PlaySliderBody)slider.Body.Drawable; + var first = body.CurrentCurve.First(); + return snaking ? first.X > vector.X && first.Y > vector.Y : first == vector; + }); + } + + private void testRepeating(bool snaking) + { + var increased = snaking ? "increased" : "is same"; + var decreased = snaking ? "decreased" : "is same"; + + setSnaking(snaking); + addSeekStep(8800); + AddStep("Save end vector", () => + { + var body = (PlaySliderBody)slider.Body.Drawable; + vector = body.CurrentCurve.Last(); + }); + addSeekStep(8900); + AddAssert($"End vector {increased}", () => + { + var body = (PlaySliderBody)slider.Body.Drawable; + var last = body.CurrentCurve.Last(); + return snaking ? last.X > vector.X && last.Y > vector.Y : last == vector; + }); + addSeekStep(10100); + AddStep("Save start vector", () => + { + var body = (PlaySliderBody)slider.Body.Drawable; + vector = body.CurrentCurve.First(); + }); + addSeekStep(10200); + AddAssert("Start vector is same", () => + { + var body = (PlaySliderBody)slider.Body.Drawable; + var first = body.CurrentCurve.First(); + return first == vector; + }); + addSeekStep(13700); + AddStep("Save end vector", () => + { + var body = (PlaySliderBody)slider.Body.Drawable; + vector = body.CurrentCurve.Last(); + }); + addSeekStep(13800); + AddAssert($"End vector {decreased}", () => + { + var body = (PlaySliderBody)slider.Body.Drawable; + var last = body.CurrentCurve.Last(); + return snaking ? last.X < vector.X && last.Y < vector.Y : last == vector; + }); + } + + private void testDoubleRepeating(bool snaking) + { + var increased = snaking ? "increased" : "is same"; + + setSnaking(snaking); + addSeekStep(18800); + AddStep("Save end vector", () => + { + var body = (PlaySliderBody)slider.Body.Drawable; + vector = body.CurrentCurve.Last(); + }); + addSeekStep(18900); + AddAssert($"End vector {increased}", () => + { + var body = (PlaySliderBody)slider.Body.Drawable; + var last = body.CurrentCurve.Last(); + return snaking ? last.X > vector.X && last.Y > vector.Y : last == vector; + }); + addSeekStep(20100); + AddStep("Save start vector", () => + { + var body = (PlaySliderBody)slider.Body.Drawable; + vector = body.CurrentCurve.First(); + }); + addSeekStep(20200); + AddAssert("Start vector is same", () => + { + var body = (PlaySliderBody)slider.Body.Drawable; + var first = body.CurrentCurve.First(); + return first == vector; + }); + addSeekStep(23700); + AddStep("Save end vector", () => + { + var body = (PlaySliderBody)slider.Body.Drawable; + vector = body.CurrentCurve.Last(); + }); + addSeekStep(23800); + AddAssert("End vector is same", () => + { + var body = (PlaySliderBody)slider.Body.Drawable; + var last = body.CurrentCurve.Last(); + return last == vector; + }); + addSeekStep(27300); + AddStep("Save start vector", () => + { + var body = (PlaySliderBody)slider.Body.Drawable; + vector = body.CurrentCurve.First(); + }); + addSeekStep(27400); + AddAssert($"Start vector {increased}", () => + { + var body = (PlaySliderBody)slider.Body.Drawable; + var first = body.CurrentCurve.First(); + return snaking ? first.X > vector.X && first.Y > vector.Y : first == vector; + }); + } + + private void setSnaking(bool value) + { + var text = value ? "Enable" : "Disable"; + AddStep($"{text} snaking", () => + { + snakingIn.Value = value; + snakingOut.Value = value; + }); + } + + private void addSeekStep(double time) + { + AddStep($"seek to {time}", () => track.Seek(time)); + + AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100)); + } + + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap + { + HitObjects = new List + { + new Slider + { + StartTime = 3000, + Position = new Vector2(100, 100), + Path = new SliderPath(PathType.PerfectCurve, new[] + { + Vector2.Zero, + new Vector2(300, 200) + }), + }, + new Slider + { + StartTime = 10000, + Position = new Vector2(100, 100), + Path = new SliderPath(PathType.PerfectCurve, new[] + { + Vector2.Zero, + new Vector2(300, 200) + }), + RepeatCount = 1, + }, + + new Slider + { + StartTime = 20000, + Position = new Vector2(100, 100), + Path = new SliderPath(PathType.PerfectCurve, new[] + { + Vector2.Zero, + new Vector2(300, 200) + }), + RepeatCount = 2, + }, + + new HitCircle + { + StartTime = 99999, + } + } + }; + } +} From 653480b2f855d103405f7e4bdd6c041fd5967eed Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 29 Mar 2020 23:29:46 +0900 Subject: [PATCH 0654/2376] Add regression test --- .../Visual/Gameplay/TestSceneAllRulesetPlayers.cs | 4 ---- .../Visual/Gameplay/TestSceneAutoplay.cs | 15 +++++++++++++-- osu.Game/Screens/Ranking/ResultsScreen.cs | 8 ++++---- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAllRulesetPlayers.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAllRulesetPlayers.cs index 83a7b896d2..b7dcad3825 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneAllRulesetPlayers.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAllRulesetPlayers.cs @@ -4,7 +4,6 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Screens; using osu.Game.Configuration; using osu.Game.Rulesets; using osu.Game.Rulesets.Catch; @@ -74,9 +73,6 @@ namespace osu.Game.Tests.Visual.Gameplay Beatmap.Value = working; SelectedMods.Value = new[] { ruleset.GetAllMods().First(m => m is ModNoFail) }; - Player?.Exit(); - Player = null; - Player = CreatePlayer(ruleset); LoadScreen(Player); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs index 5ee17aeea2..43fb848ab8 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs @@ -5,8 +5,11 @@ using System.ComponentModel; using System.Linq; using osu.Framework.Testing; using osu.Game.Rulesets; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play; using osu.Game.Screens.Play.Break; +using osu.Game.Screens.Ranking; namespace osu.Game.Tests.Visual.Gameplay { @@ -17,8 +20,8 @@ namespace osu.Game.Tests.Visual.Gameplay protected override Player CreatePlayer(Ruleset ruleset) { - SelectedMods.Value = SelectedMods.Value.Concat(new[] { ruleset.GetAutoplayMod() }).ToArray(); - return new TestPlayer(false, false); + SelectedMods.Value = new[] { ruleset.GetAutoplayMod() }; + return new TestPlayer(false); } protected override void AddCheckSteps() @@ -32,6 +35,14 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("overlay displays 100% accuracy", () => Player.BreakOverlay.ChildrenOfType().Single().AccuracyDisplay.Current.Value == 1); AddStep("rewind", () => Player.GameplayClockContainer.Seek(-80000)); AddUntilStep("key counter reset", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0)); + + AddStep("complete", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime())); + AddUntilStep("results displayed", () => getResultsScreen() != null); + + AddAssert("score has combo", () => getResultsScreen().Score.Combo > 100); + AddAssert("score has no misses", () => getResultsScreen().Score.Statistics[HitResult.Miss] == 0); + + ResultsScreen getResultsScreen() => Stack.CurrentScreen as ResultsScreen; } } } diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 803b33a998..5e0c30c4c0 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -31,13 +31,13 @@ namespace osu.Game.Screens.Ranking [Resolved(CanBeNull = true)] private Player player { get; set; } - private readonly ScoreInfo score; + public readonly ScoreInfo Score; private Drawable bottomPanel; public ResultsScreen(ScoreInfo score) { - this.score = score; + this.Score = score; } [BackgroundDependencyLoader] @@ -47,7 +47,7 @@ namespace osu.Game.Screens.Ranking { new ResultsScrollContainer { - Child = new ScorePanel(score) + Child = new ScorePanel(Score) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -77,7 +77,7 @@ namespace osu.Game.Screens.Ranking Direction = FillDirection.Horizontal, Children = new Drawable[] { - new ReplayDownloadButton(score) { Width = 300 }, + new ReplayDownloadButton(Score) { Width = 300 }, new RetryButton { Width = 300 }, } } From ce2fa23baf1c55e83ba051033975a606d1b100ba Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sun, 29 Mar 2020 17:43:18 +0300 Subject: [PATCH 0655/2376] Include a test for miss --- .../TestSceneSliderSnaking.cs | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs index 3e40713f52..a53e06dc0f 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs @@ -32,7 +32,8 @@ namespace osu.Game.Rulesets.Osu.Tests private TrackVirtualManual track; - protected override bool Autoplay => true; + protected override bool Autoplay => autoplay; + private bool autoplay; private readonly Bindable snakingIn = new Bindable(); private readonly Bindable snakingOut = new Bindable(); @@ -57,16 +58,15 @@ namespace osu.Game.Rulesets.Osu.Tests private Vector2 vector; [SetUpSteps] - public override void SetUpSteps() - { - base.SetUpSteps(); - - AddUntilStep("wait for track to start running", () => track.IsRunning); - } + public override void SetUpSteps() { } [Test] public void TestSnaking() { + AddStep("have autoplay", () => autoplay = true); + base.SetUpSteps(); + AddUntilStep("wait for track to start running", () => track.IsRunning); + AddStep("retrieve 1st slider", () => slider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.First()); testLinear(true); testLinear(false); @@ -76,9 +76,19 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("retrieve 3rd slider", () => slider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.Skip(2).First()); testDoubleRepeating(true); testDoubleRepeating(false); + } - // Test arrow stays in place + [TestCase(true)] + [TestCase(false)] + public void TestArrowStays(bool isHit) + { + var isSame = isHit ? "is same" : "decreased"; + var enable = isHit ? "enable" : "disable"; + + AddStep($"{enable} autoplay", () => autoplay = isHit); setSnaking(true); + base.SetUpSteps(); + addSeekStep(13500); AddStep("retrieve 2nd slider repeat", () => { @@ -87,7 +97,7 @@ namespace osu.Game.Rulesets.Osu.Tests }); AddStep("Save repeat vector", () => vector = repeat.Position); addSeekStep(13700); - AddAssert("Repeat vector is same", () => Precision.AlmostEquals(vector.X, repeat.Position.X, 1) && Precision.AlmostEquals(vector.Y, repeat.Position.Y, 1)); + AddAssert($"Repeat vector {isSame}", () => isHit ? Precision.AlmostEquals(vector.X, repeat.X, 1) && Precision.AlmostEquals(vector.Y, repeat.Y, 1) : repeat.X < vector.X && repeat.Y < vector.Y); } private void testLinear(bool snaking) From 07c7233b3d4f68acc11090ab78801bb6d6dbd97b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 29 Mar 2020 23:46:28 +0900 Subject: [PATCH 0656/2376] Change int div comments --- .../Visual/SongSelect/TestSceneBeatmapCarousel.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 7c3498e034..c76ce628ba 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -148,11 +148,13 @@ namespace osu.Game.Tests.Visual.SongSelect // Selects next set once, difficulty index doesn't change selectNextAndAssert(3, true, 2, 1); - // Selects next set 16 times (50 // 3 == 16), difficulty index changes twice (50 % 3 == 2) + + // Selects next set 16 times (50 \ 3 == 16), difficulty index changes twice (50 % 3 == 2) selectNextAndAssert(50, true, 17, 3); - // Travels around the carousel thrice (200/60 == 3) - // continues to select 20 times (200 % 60 == 20) - // selects next set 6 times (20 // 3 == 6) + + // Travels around the carousel thrice (200 \ 60 == 3) + // continues to select 20 times (200 \ 60 == 20) + // selects next set 6 times (20 \ 3 == 6) // difficulty index changes twice (20 % 3 == 2) selectNextAndAssert(200, true, 7, 3); From 66a990cd5e20ebf0022626900bb95ba972e466ba Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 29 Mar 2020 23:50:16 +0900 Subject: [PATCH 0657/2376] Remove redundant this --- osu.Game/Screens/Ranking/ResultsScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 5e0c30c4c0..d063d8749f 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -37,7 +37,7 @@ namespace osu.Game.Screens.Ranking public ResultsScreen(ScoreInfo score) { - this.Score = score; + Score = score; } [BackgroundDependencyLoader] From 6e68b968f8afba0ebd5824fda4a94b9495fbb055 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 29 Mar 2020 23:52:50 +0900 Subject: [PATCH 0658/2376] Hide "retry" button on results screen after watching a replay --- osu.Game/Screens/Play/ReplayPlayer.cs | 3 +++ osu.Game/Screens/Ranking/ResultsScreen.cs | 29 +++++++++++++++-------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index 74c853340d..0d2ddb7b01 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -3,6 +3,7 @@ using osu.Framework.Screens; using osu.Game.Scoring; +using osu.Game.Screens.Ranking; namespace osu.Game.Screens.Play { @@ -29,6 +30,8 @@ namespace osu.Game.Screens.Play this.Push(CreateResults(DrawableRuleset.ReplayScore.ScoreInfo)); } + protected override ResultsScreen CreateResults(ScoreInfo score) => new ResultsScreen(score, false); + protected override ScoreInfo CreateScore() => score.ScoreInfo; } } diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index d063d8749f..1c08b763fe 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -33,16 +33,21 @@ namespace osu.Game.Screens.Ranking public readonly ScoreInfo Score; + private readonly bool allowRetry; + private Drawable bottomPanel; - public ResultsScreen(ScoreInfo score) + public ResultsScreen(ScoreInfo score, bool allowRetry = true) { Score = score; + this.allowRetry = allowRetry; } [BackgroundDependencyLoader] private void load() { + FillFlowContainer buttons; + InternalChildren = new[] { new ResultsScrollContainer @@ -68,7 +73,7 @@ namespace osu.Game.Screens.Ranking RelativeSizeAxes = Axes.Both, Colour = Color4Extensions.FromHex("#333") }, - new FillFlowContainer + buttons = new FillFlowContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -78,7 +83,6 @@ namespace osu.Game.Screens.Ranking Children = new Drawable[] { new ReplayDownloadButton(Score) { Width = 300 }, - new RetryButton { Width = 300 }, } } } @@ -87,15 +91,20 @@ namespace osu.Game.Screens.Ranking if (player != null) { - AddInternal(new HotkeyRetryOverlay + if (allowRetry) { - Action = () => - { - if (!this.IsCurrentScreen()) return; + buttons.Add(new RetryButton { Width = 300 }); - player?.Restart(); - }, - }); + AddInternal(new HotkeyRetryOverlay + { + Action = () => + { + if (!this.IsCurrentScreen()) return; + + player?.Restart(); + }, + }); + } } } From a72f0f57f6662bec55cdce569e6d071328f7fdcc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Mar 2020 00:05:07 +0900 Subject: [PATCH 0659/2376] Refactor tests for readability --- .../SongSelect/TestSceneBeatmapCarousel.cs | 67 +++++++++---------- 1 file changed, 31 insertions(+), 36 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index c76ce628ba..f29d532857 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -85,67 +85,48 @@ namespace osu.Game.Tests.Visual.SongSelect [TestCase(true)] [TestCase(false)] - public void TestTraversalHold(bool forwards) + public void TestTraversalBeyondVisible(bool forwards) { var sets = new List(); - const int create_this_many_sets = 200; - for (int i = 0; i < create_this_many_sets; i++) - { - var set = createTestBeatmapSet(i + 1); - sets.Add(set); - } + const int total_set_count = 200; + + for (int i = 0; i < total_set_count; i++) + sets.Add(createTestBeatmapSet(i + 1)); loadBeatmaps(sets); + for (int i = 1; i < total_set_count; i += i) + selectNextAndAssert(i); + void selectNextAndAssert(int amount) { - setSelected(forwards ? 1 : create_this_many_sets, 1); - string text = forwards ? "Next" : "Previous"; - AddStep($"{text} beatmap {amount} times", () => + setSelected(forwards ? 1 : total_set_count, 1); + + AddStep($"{(forwards ? "Next" : "Previous")} beatmap {amount} times", () => { for (int i = 0; i < amount; i++) { carousel.SelectNext(forwards ? 1 : -1); } }); - waitForSelection(forwards ? amount + 1 : create_this_many_sets - amount); - } - for (int i = 1; i < create_this_many_sets; i += i) - { - selectNextAndAssert(i); + waitForSelection(forwards ? amount + 1 : total_set_count - amount); } } [Test] - public void TestTraversalHoldDifficulties() + public void TestTraversalBeyondVisibleDifficulties() { var sets = new List(); - for (int i = 0; i < 20; i++) - { - var set = createTestBeatmapSet(i + 1); - sets.Add(set); - } + const int total_set_count = 200; + + for (int i = 0; i < total_set_count; i++) + sets.Add(createTestBeatmapSet(i + 1)); loadBeatmaps(sets); - void selectNextAndAssert(int amount, bool forwards, int expectedSet, int expectedDiff) - { - // Select very first or very last difficulty - setSelected(forwards ? 1 : 20, forwards ? 1 : 3); - string text = forwards ? "Next" : "Previous"; - AddStep($"{text} difficulty {amount} times", () => - { - for (int i = 0; i < amount; i++) - { - carousel.SelectNext(forwards ? 1 : -1, false); - } - }); - waitForSelection(expectedSet, expectedDiff); - } - // Selects next set once, difficulty index doesn't change selectNextAndAssert(3, true, 2, 1); @@ -162,6 +143,20 @@ namespace osu.Game.Tests.Visual.SongSelect selectNextAndAssert(3, false, 19, 3); selectNextAndAssert(50, false, 4, 1); selectNextAndAssert(200, false, 14, 1); + + void selectNextAndAssert(int amount, bool forwards, int expectedSet, int expectedDiff) + { + // Select very first or very last difficulty + setSelected(forwards ? 1 : 20, forwards ? 1 : 3); + + AddStep($"{(forwards ? "Next" : "Previous")} difficulty {amount} times", () => + { + for (int i = 0; i < amount; i++) + carousel.SelectNext(forwards ? 1 : -1, false); + }); + + waitForSelection(expectedSet, expectedDiff); + } } /// From b47a532df353f45ec95ef51e9e6d0f383f832502 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Mar 2020 00:07:48 +0900 Subject: [PATCH 0660/2376] Adjust code formatting slightly --- osu.Game/Screens/Select/BeatmapCarousel.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index a2c2cde7c7..59dddc2baa 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -264,9 +264,9 @@ namespace osu.Game.Screens.Select private void selectNextSet(int direction, bool skipDifficulties) { - var visibleSets = beatmapSets.Where(s => !s.Filtered.Value).ToList(); + var unfilteredSets = beatmapSets.Where(s => !s.Filtered.Value).ToList(); - var nextSet = visibleSets[(visibleSets.IndexOf(selectedBeatmapSet) + direction + visibleSets.Count) % visibleSets.Count]; + var nextSet = unfilteredSets[(unfilteredSets.IndexOf(selectedBeatmapSet) + direction + unfilteredSets.Count) % unfilteredSets.Count]; if (skipDifficulties) select(nextSet); @@ -276,12 +276,14 @@ namespace osu.Game.Screens.Select private void selectNextDifficulty(int direction) { - var difficulties = selectedBeatmapSet.Children.Where(s => !s.Filtered.Value).ToList(); - int index = difficulties.IndexOf(selectedBeatmap); - if (index + direction < 0 || index + direction >= difficulties.Count) + var unfilteredDifficulties = selectedBeatmapSet.Children.Where(s => !s.Filtered.Value).ToList(); + + int index = unfilteredDifficulties.IndexOf(selectedBeatmap); + + if (index + direction < 0 || index + direction >= unfilteredDifficulties.Count) selectNextSet(direction, false); else - select(difficulties[index + direction]); + select(unfilteredDifficulties[index + direction]); } /// From f4c8b6d219001b82ee8494ed00ee1a14d27f4a3a Mon Sep 17 00:00:00 2001 From: Endrik Date: Sun, 29 Mar 2020 18:55:47 +0300 Subject: [PATCH 0661/2376] Fix copy paste oversight --- osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index f29d532857..76a8ee9914 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -120,7 +120,7 @@ namespace osu.Game.Tests.Visual.SongSelect { var sets = new List(); - const int total_set_count = 200; + const int total_set_count = 20; for (int i = 0; i < total_set_count; i++) sets.Add(createTestBeatmapSet(i + 1)); From 98a700ef3a70244f4c8bd9e44631593debf1e269 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Mar 2020 00:58:06 +0900 Subject: [PATCH 0662/2376] Attempt to fix tests by skipping one break at a time --- .../Visual/Gameplay/TestSceneAutoplay.cs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs index 43fb848ab8..4b1c2ec256 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs @@ -4,6 +4,7 @@ using System.ComponentModel; using System.Linq; using osu.Framework.Testing; +using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; @@ -28,15 +29,16 @@ namespace osu.Game.Tests.Visual.Gameplay { AddUntilStep("score above zero", () => Player.ScoreProcessor.TotalScore.Value > 0); AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 2)); - AddStep("seek to break time", () => Player.GameplayClockContainer.Seek(Player.ChildrenOfType().First().Breaks.First().StartTime)); - AddUntilStep("wait for seek to complete", () => - Player.HUDOverlay.Progress.ReferenceClock.CurrentTime >= Player.BreakOverlay.Breaks.First().StartTime); + seekToBreak(0); AddAssert("keys not counting", () => !Player.HUDOverlay.KeyCounter.IsCounting); AddAssert("overlay displays 100% accuracy", () => Player.BreakOverlay.ChildrenOfType().Single().AccuracyDisplay.Current.Value == 1); AddStep("rewind", () => Player.GameplayClockContainer.Seek(-80000)); AddUntilStep("key counter reset", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0)); - AddStep("complete", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime())); + seekToBreak(0); + seekToBreak(1); + + AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime())); AddUntilStep("results displayed", () => getResultsScreen() != null); AddAssert("score has combo", () => getResultsScreen().Score.Combo > 100); @@ -44,5 +46,13 @@ namespace osu.Game.Tests.Visual.Gameplay ResultsScreen getResultsScreen() => Stack.CurrentScreen as ResultsScreen; } + + private void seekToBreak(int breakIndex) + { + AddStep($"seek to break {breakIndex}", () => Player.GameplayClockContainer.Seek(destBreak().StartTime)); + AddUntilStep("wait for seek to complete", () => Player.HUDOverlay.Progress.ReferenceClock.CurrentTime >= destBreak().StartTime); + + BreakPeriod destBreak() => Player.ChildrenOfType().First().Breaks.ElementAt(breakIndex); + } } } From d99b445720b0b6a6d994ee148192d2903626a116 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Mar 2020 09:59:52 +0900 Subject: [PATCH 0663/2376] Move non-headless tests to correct namespace --- .../{ => Visual}/Gameplay/TestSceneReplayRecorder.cs | 3 +-- .../{ => Visual}/Gameplay/TestSceneReplayRecording.cs | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) rename osu.Game.Tests/{ => Visual}/Gameplay/TestSceneReplayRecorder.cs (99%) rename osu.Game.Tests/{ => Visual}/Gameplay/TestSceneReplayRecording.cs (99%) diff --git a/osu.Game.Tests/Gameplay/TestSceneReplayRecorder.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs similarity index 99% rename from osu.Game.Tests/Gameplay/TestSceneReplayRecorder.cs rename to osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs index 734991b868..c7455583e4 100644 --- a/osu.Game.Tests/Gameplay/TestSceneReplayRecorder.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs @@ -17,13 +17,12 @@ using osu.Game.Replays; using osu.Game.Rulesets; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.UI; -using osu.Game.Tests.Visual; using osu.Game.Tests.Visual.UserInterface; using osuTK; using osuTK.Graphics; using osuTK.Input; -namespace osu.Game.Tests.Gameplay +namespace osu.Game.Tests.Visual.Gameplay { public class TestSceneReplayRecorder : OsuManualInputManagerTestScene { diff --git a/osu.Game.Tests/Gameplay/TestSceneReplayRecording.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs similarity index 99% rename from osu.Game.Tests/Gameplay/TestSceneReplayRecording.cs rename to osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs index 057d026132..7822f07957 100644 --- a/osu.Game.Tests/Gameplay/TestSceneReplayRecording.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs @@ -13,12 +13,11 @@ using osu.Game.Replays; using osu.Game.Rulesets; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.UI; -using osu.Game.Tests.Visual; using osu.Game.Tests.Visual.UserInterface; using osuTK; using osuTK.Graphics; -namespace osu.Game.Tests.Gameplay +namespace osu.Game.Tests.Visual.Gameplay { public class TestSceneReplayRecording : OsuTestScene { From 09d860d5f5701edaaa292f0797774d6758123952 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Mar 2020 11:52:11 +0900 Subject: [PATCH 0664/2376] Fix imports with no matching beatmap IDs still retaining a potentially invalid set ID --- osu.Game/Beatmaps/BeatmapManager.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index abb3f8ac42..40ffb40f52 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -87,7 +87,7 @@ namespace osu.Game.Beatmaps protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path)?.ToLowerInvariant() == ".osz"; - protected override Task Populate(BeatmapSetInfo beatmapSet, ArchiveReader archive, CancellationToken cancellationToken = default) + protected override async Task Populate(BeatmapSetInfo beatmapSet, ArchiveReader archive, CancellationToken cancellationToken = default) { if (archive != null) beatmapSet.Beatmaps = createBeatmapDifficulties(beatmapSet.Files); @@ -103,7 +103,11 @@ namespace osu.Game.Beatmaps validateOnlineIds(beatmapSet); - return updateQueue.UpdateAsync(beatmapSet, cancellationToken); + await updateQueue.UpdateAsync(beatmapSet, cancellationToken); + + // ensure at least one beatmap was able to retrieve an online ID, else drop the set ID. + if (!beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0)) + beatmapSet.OnlineBeatmapSetID = null; } protected override void PreImport(BeatmapSetInfo beatmapSet) From 7db9bd798c4b0d9dedc9097c416d61ce62197fa2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Mar 2020 11:59:51 +0900 Subject: [PATCH 0665/2376] Remove handle overrides --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 9aad125ed1..0011faefbb 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -43,10 +43,6 @@ namespace osu.Game.Rulesets.Objects.Drawables /// public bool HandleUserInput { get; set; } = true; - public override bool HandlePositionalInput => HandleUserInput; - - public override bool HandleNonPositionalInput => HandleUserInput; - public override bool PropagatePositionalInputSubTree => HandleUserInput; public override bool PropagateNonPositionalInputSubTree => HandleUserInput; From 7ecce713bb736069be7f1037df45770482d6f349 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Mar 2020 15:05:40 +0900 Subject: [PATCH 0666/2376] Keep provided IDs where possible if not online --- osu.Game/Beatmaps/BeatmapManager.cs | 13 ++++++++----- osu.Game/Online/API/APIRequest.cs | 4 ++-- osu.Game/Online/API/Requests/GetRankingsRequest.cs | 3 ++- osu.Game/Online/API/Requests/PaginatedAPIRequest.cs | 2 +- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 40ffb40f52..797a5160c9 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -451,12 +451,15 @@ namespace osu.Game.Beatmaps var res = req.Result; - beatmap.Status = res.Status; - beatmap.BeatmapSet.Status = res.BeatmapSet.Status; - beatmap.BeatmapSet.OnlineBeatmapSetID = res.OnlineBeatmapSetID; - beatmap.OnlineBeatmapID = res.OnlineBeatmapID; + if (res != null) + { + beatmap.Status = res.Status; + beatmap.BeatmapSet.Status = res.BeatmapSet.Status; + beatmap.BeatmapSet.OnlineBeatmapSetID = res.OnlineBeatmapSetID; + beatmap.OnlineBeatmapID = res.OnlineBeatmapID; - LogForModel(set, $"Online retrieval mapped {beatmap} to {res.OnlineBeatmapSetID} / {res.OnlineBeatmapID}."); + LogForModel(set, $"Online retrieval mapped {beatmap} to {res.OnlineBeatmapSetID} / {res.OnlineBeatmapID}."); + } } catch (Exception e) { diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs index 30c1018c1e..6a6c7b72a8 100644 --- a/osu.Game/Online/API/APIRequest.cs +++ b/osu.Game/Online/API/APIRequest.cs @@ -12,11 +12,11 @@ namespace osu.Game.Online.API /// An API request with a well-defined response type. /// /// Type of the response (used for deserialisation). - public abstract class APIRequest : APIRequest + public abstract class APIRequest : APIRequest where T : class { protected override WebRequest CreateWebRequest() => new OsuJsonWebRequest(Uri); - public T Result => ((OsuJsonWebRequest)WebRequest).ResponseObject; + public T Result => ((OsuJsonWebRequest)WebRequest)?.ResponseObject; protected APIRequest() { diff --git a/osu.Game/Online/API/Requests/GetRankingsRequest.cs b/osu.Game/Online/API/Requests/GetRankingsRequest.cs index 941691c4c1..1bbaa73bbb 100644 --- a/osu.Game/Online/API/Requests/GetRankingsRequest.cs +++ b/osu.Game/Online/API/Requests/GetRankingsRequest.cs @@ -6,7 +6,8 @@ using osu.Game.Rulesets; namespace osu.Game.Online.API.Requests { - public abstract class GetRankingsRequest : APIRequest + public abstract class GetRankingsRequest : APIRequest where TModel : class + { private readonly RulesetInfo ruleset; private readonly int page; diff --git a/osu.Game/Online/API/Requests/PaginatedAPIRequest.cs b/osu.Game/Online/API/Requests/PaginatedAPIRequest.cs index 52e12f04ee..bddc34a0dc 100644 --- a/osu.Game/Online/API/Requests/PaginatedAPIRequest.cs +++ b/osu.Game/Online/API/Requests/PaginatedAPIRequest.cs @@ -6,7 +6,7 @@ using osu.Framework.IO.Network; namespace osu.Game.Online.API.Requests { - public abstract class PaginatedAPIRequest : APIRequest + public abstract class PaginatedAPIRequest : APIRequest where T : class { private readonly int page; private readonly int itemsPerPage; From f71c8cb30ff8c11084cc48bbe40fbfe437dbd089 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Mar 2020 15:07:56 +0900 Subject: [PATCH 0667/2376] Only drop online set ID if beatmap IDs were stripped in online retrieval --- osu.Game/Beatmaps/BeatmapManager.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 797a5160c9..6542866936 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -103,11 +103,19 @@ namespace osu.Game.Beatmaps validateOnlineIds(beatmapSet); + bool hadOnlineBeatmapIDs = beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0); + await updateQueue.UpdateAsync(beatmapSet, cancellationToken); - // ensure at least one beatmap was able to retrieve an online ID, else drop the set ID. - if (!beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0)) - beatmapSet.OnlineBeatmapSetID = null; + // 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) From b9277165f788361acb43b3c57da49ce605d44155 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 30 Mar 2020 15:11:15 +0900 Subject: [PATCH 0668/2376] Refactor test to support custom hitobjects --- .../TestSceneNoteLock.cs | 126 +++++++++++++----- 1 file changed, 94 insertions(+), 32 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs index e2b8364f3e..af82a05c4f 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; +using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Screens; using osu.Framework.Utils; using osu.Game.Beatmaps; @@ -23,8 +24,6 @@ namespace osu.Game.Rulesets.Osu.Tests { public class TestSceneNoteLock : RateAdjustedBeatmapTestScene { - private const double time_first_circle = 1500; - private const double time_second_circle = 1600; private const double early_miss_window = 1000; // time after -1000 to -500 is considered a miss private const double late_miss_window = 500; // time after +500 is considered a miss @@ -37,13 +36,31 @@ namespace osu.Game.Rulesets.Osu.Tests [Test] public void TestClickSecondCircleBeforeFirstCircleTime() { - performTest(new List + const double time_first_circle = 1500; + const double time_second_circle = 1600; + + var hitObjects = new List + { + new TestHitCircle + { + StartTime = time_first_circle, + Position = position_first_circle + }, + new TestHitCircle + { + StartTime = time_second_circle, + Position = position_second_circle + } + }; + + performTest(hitObjects, new List { new OsuReplayFrame { Time = time_first_circle - 100, Position = position_second_circle, Actions = { OsuAction.LeftButton } } }); - addJudgementAssert(HitResult.Miss, HitResult.Miss); - addJudgementOffsetAssert(late_miss_window); + addJudgementAssert(hitObjects[0], HitResult.Miss); + addJudgementAssert(hitObjects[1], HitResult.Miss); + addJudgementOffsetAssert(hitObjects[0], late_miss_window); } /// @@ -52,13 +69,31 @@ namespace osu.Game.Rulesets.Osu.Tests [Test] public void TestClickSecondCircleAtFirstCircleTime() { - performTest(new List + const double time_first_circle = 1500; + const double time_second_circle = 1600; + + var hitObjects = new List + { + new TestHitCircle + { + StartTime = time_first_circle, + Position = position_first_circle + }, + new TestHitCircle + { + StartTime = time_second_circle, + Position = position_second_circle + } + }; + + performTest(hitObjects, new List { new OsuReplayFrame { Time = time_first_circle, Position = position_second_circle, Actions = { OsuAction.LeftButton } } }); - addJudgementAssert(HitResult.Miss, HitResult.Great); - addJudgementOffsetAssert(0); + addJudgementAssert(hitObjects[0], HitResult.Miss); + addJudgementAssert(hitObjects[1], HitResult.Great); + addJudgementOffsetAssert(hitObjects[0], 0); } /// @@ -67,13 +102,31 @@ namespace osu.Game.Rulesets.Osu.Tests [Test] public void TestClickSecondCircleAfterFirstCircleTime() { - performTest(new List + const double time_first_circle = 1500; + const double time_second_circle = 1600; + + var hitObjects = new List + { + new TestHitCircle + { + StartTime = time_first_circle, + Position = position_first_circle + }, + new TestHitCircle + { + StartTime = time_second_circle, + Position = position_second_circle + } + }; + + performTest(hitObjects, new List { new OsuReplayFrame { Time = time_first_circle + 100, Position = position_second_circle, Actions = { OsuAction.LeftButton } } }); - addJudgementAssert(HitResult.Miss, HitResult.Great); - addJudgementOffsetAssert(100); + addJudgementAssert(hitObjects[0], HitResult.Miss); + addJudgementAssert(hitObjects[1], HitResult.Great); + addJudgementOffsetAssert(hitObjects[0], 100); } /// @@ -82,49 +135,58 @@ namespace osu.Game.Rulesets.Osu.Tests [Test] public void TestClickSecondCircleBeforeFirstCircleTimeWithFirstCircleJudged() { - performTest(new List + const double time_first_circle = 1500; + const double time_second_circle = 1600; + + var hitObjects = new List + { + new TestHitCircle + { + StartTime = time_first_circle, + Position = position_first_circle + }, + new TestHitCircle + { + StartTime = time_second_circle, + Position = position_second_circle + } + }; + + performTest(hitObjects, new List { new OsuReplayFrame { Time = time_first_circle - 200, Position = position_first_circle, Actions = { OsuAction.LeftButton } }, new OsuReplayFrame { Time = time_first_circle - 100, Position = position_second_circle, Actions = { OsuAction.RightButton } } }); - addJudgementAssert(HitResult.Great, HitResult.Great); + addJudgementAssert(hitObjects[0], HitResult.Great); + addJudgementAssert(hitObjects[1], HitResult.Great); + addJudgementOffsetAssert(hitObjects[0], -200); // time_first_circle - 200 + addJudgementOffsetAssert(hitObjects[0], -200); // time_second_circle - first_circle_time - 100 } - private void addJudgementAssert(HitResult firstCircle, HitResult secondCircle) + private void addJudgementAssert(OsuHitObject hitObject, HitResult result) { - AddAssert($"first circle judgement is {firstCircle}", () => judgementResults.Single(r => r.HitObject.StartTime == time_first_circle).Type == firstCircle); - AddAssert($"second circle judgement is {secondCircle}", () => judgementResults.Single(r => r.HitObject.StartTime == time_second_circle).Type == secondCircle); + AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judgement is {result}", + () => judgementResults.Single(r => r.HitObject == hitObject).Type == result); } - private void addJudgementOffsetAssert(double offset) + private void addJudgementOffsetAssert(OsuHitObject hitObject, double offset) { - AddAssert($"first circle judged at {offset}", () => Precision.AlmostEquals(judgementResults.Single(r => r.HitObject.StartTime == time_first_circle).TimeOffset, offset, 100)); + AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judged at {offset}", + () => Precision.AlmostEquals(judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, offset, 100)); } private ScoreAccessibleReplayPlayer currentPlayer; private List judgementResults; private bool allJudgedFired; - private void performTest(List frames) + private void performTest(List hitObjects, List frames) { AddStep("load player", () => { Beatmap.Value = CreateWorkingBeatmap(new Beatmap { - HitObjects = - { - new TestHitCircle - { - StartTime = time_first_circle, - Position = position_first_circle - }, - new TestHitCircle - { - StartTime = time_second_circle, - Position = position_second_circle - } - }, + HitObjects = hitObjects, BeatmapInfo = { BaseDifficulty = new BeatmapDifficulty { SliderTickRate = 3 }, From e51097da9e7ee6f6128fd5e9b19e23117a85904b Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 30 Mar 2020 09:29:00 +0300 Subject: [PATCH 0669/2376] Add a legacy skin provider above the test skin --- .../TestSceneHyperDashColouring.cs | 122 +++++++++--------- 1 file changed, 63 insertions(+), 59 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs index 9ab8cf9113..fea2939eae 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs @@ -1,9 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Linq; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -28,6 +28,9 @@ namespace osu.Game.Rulesets.Catch.Tests { public class TestSceneHyperDashColouring : OsuTestScene { + [Resolved] + private SkinManager skins { get; set; } + [TestCase(false)] [TestCase(true)] public void TestHyperDashFruitColour(bool legacyFruit) @@ -36,19 +39,21 @@ namespace osu.Game.Rulesets.Catch.Tests AddStep("setup fruit", () => { - var fruit = new Fruit { IndexInBeatmap = legacyFruit ? 0 : 1, HyperDashTarget = new Banana() }; + var fruit = new Fruit { HyperDashTarget = new Banana() }; fruit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - Child = setupSkinHierarchy(() => - drawableFruit = new DrawableFruit(fruit) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(4f), - }, false, false); + Child = setupSkinHierarchy(drawableFruit = new DrawableFruit(fruit) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(4f), + }, false, false, false, legacyFruit); }); - AddAssert("default colour", () => checkFruitHyperDashColour(drawableFruit, Catcher.DefaultHyperDashColour, legacyFruit)); + AddAssert("default colour", () => + legacyFruit + ? checkLegacyFruitHyperDashColour(drawableFruit, Catcher.DefaultHyperDashColour) + : checkFruitHyperDashColour(drawableFruit, Catcher.DefaultHyperDashColour)); } [TestCase(false, true)] @@ -61,19 +66,21 @@ namespace osu.Game.Rulesets.Catch.Tests AddStep("setup fruit", () => { - var fruit = new Fruit { IndexInBeatmap = legacyFruit ? 0 : 1, HyperDashTarget = new Banana() }; + var fruit = new Fruit { HyperDashTarget = new Banana() }; fruit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - Child = setupSkinHierarchy(() => - drawableFruit = new DrawableFruit(fruit) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(4f), - }, customCatcherHyperDashColour, true); + Child = setupSkinHierarchy(drawableFruit = new DrawableFruit(fruit) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(4f), + }, customCatcherHyperDashColour, false, true, legacyFruit); }); - AddAssert("custom colour", () => checkFruitHyperDashColour(drawableFruit, TestLegacySkin.CustomHyperDashFruitColour, legacyFruit)); + AddAssert("custom colour", () => + legacyFruit + ? checkLegacyFruitHyperDashColour(drawableFruit, TestSkin.CustomHyperDashFruitColour) + : checkFruitHyperDashColour(drawableFruit, TestSkin.CustomHyperDashFruitColour)); } [TestCase(false)] @@ -84,71 +91,68 @@ namespace osu.Game.Rulesets.Catch.Tests AddStep("setup fruit", () => { - var fruit = new Fruit { IndexInBeatmap = legacyFruit ? 0 : 1, HyperDashTarget = new Banana() }; + var fruit = new Fruit { HyperDashTarget = new Banana() }; fruit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - Child = setupSkinHierarchy(() => + Child = setupSkinHierarchy( drawableFruit = new DrawableFruit(fruit) { Anchor = Anchor.Centre, Origin = Anchor.Centre, Scale = new Vector2(4f), - }, true, false); + }, true, false, false, legacyFruit); }); - AddAssert("catcher custom colour", () => checkFruitHyperDashColour(drawableFruit, TestLegacySkin.CustomHyperDashColour, legacyFruit)); + AddAssert("catcher custom colour", () => + legacyFruit + ? checkLegacyFruitHyperDashColour(drawableFruit, TestSkin.CustomHyperDashColour) + : checkFruitHyperDashColour(drawableFruit, TestSkin.CustomHyperDashColour)); } - private Drawable setupSkinHierarchy(Func getChild, bool customHyperDashCatcherColour = false, bool customHyperDashFruitColour = false, bool customHyperDashAfterColour = false) + private Drawable setupSkinHierarchy(Drawable child, bool customCatcherColour = false, bool customAfterColour = false, bool customFruitColour = false, bool legacySkin = true) { - var testSkinProvider = new SkinProvidingContainer(new TestLegacySkin(customHyperDashCatcherColour, customHyperDashFruitColour, customHyperDashAfterColour)); + var testSkinProvider = new SkinProvidingContainer(new TestSkin(customCatcherColour, customAfterColour, customFruitColour)); - var legacySkinTransformer = new SkinProvidingContainer(new CatchLegacySkinTransformer(testSkinProvider)); + if (legacySkin) + { + var legacySkinProvider = new SkinProvidingContainer(skins.GetSkin(DefaultLegacySkin.Info)); + var legacySkinTransformer = new SkinProvidingContainer(new CatchLegacySkinTransformer(testSkinProvider)); - return testSkinProvider - .WithChild(legacySkinTransformer - .WithChild(getChild.Invoke())); + return legacySkinProvider + .WithChild(testSkinProvider + .WithChild(legacySkinTransformer + .WithChild(child))); + } + + return testSkinProvider.WithChild(child); } - private bool checkFruitHyperDashColour(DrawableFruit fruit, Color4 expectedColour, bool isLegacyFruit) => - isLegacyFruit - ? fruit.ChildrenOfType().First().Drawable.ChildrenOfType().Any(c => c.Colour == expectedColour) - : fruit.ChildrenOfType().First().Drawable.ChildrenOfType().Single(c => c.BorderColour == expectedColour).Any(d => d.Colour == expectedColour); + private bool checkFruitHyperDashColour(DrawableFruit fruit, Color4 expectedColour) => + fruit.ChildrenOfType().First().Drawable.ChildrenOfType().Single(c => c.BorderColour == expectedColour).Any(d => d.Colour == expectedColour); - private class TestLegacySkin : ISkin + private bool checkLegacyFruitHyperDashColour(DrawableFruit fruit, Color4 expectedColour) => + fruit.ChildrenOfType().First().Drawable.ChildrenOfType().Any(c => c.Colour == expectedColour); + + private class TestSkin : ISkin { public static Color4 CustomHyperDashColour { get; } = Color4.Goldenrod; public static Color4 CustomHyperDashFruitColour { get; } = Color4.Cyan; public static Color4 CustomHyperDashAfterColour { get; } = Color4.Lime; - private readonly bool customHyperDashCatcherColour; - private readonly bool customHyperDashFruitColour; - private readonly bool customHyperDashAfterColour; + private readonly bool customCatcherColour; + private readonly bool customAfterColour; + private readonly bool customFruitColour; - public TestLegacySkin(bool customHyperDashCatcherColour = false, bool customHyperDashFruitColour = false, bool customHyperDashAfterColour = false) + public TestSkin(bool customCatcherColour = false, bool customAfterColour = false, bool customFruitColour = false) { - this.customHyperDashCatcherColour = customHyperDashCatcherColour; - this.customHyperDashFruitColour = customHyperDashFruitColour; - this.customHyperDashAfterColour = customHyperDashAfterColour; + this.customCatcherColour = customCatcherColour; + this.customAfterColour = customAfterColour; + this.customFruitColour = customFruitColour; } public Drawable GetDrawableComponent(ISkinComponent component) => null; - public Texture GetTexture(string componentName) - { - if (componentName == "fruit-pear") - { - // convince CatchLegacySkinTransformer to use the LegacyFruitPiece for pear fruit. - return new Texture(Texture.WhitePixel.TextureGL) - { - Width = 1, - Height = 1, - ScaleAdjust = 1 / 96f - }; - } - - return null; - } + public Texture GetTexture(string componentName) => null; public SampleChannel GetSample(ISampleInfo sampleInfo) => null; @@ -156,13 +160,13 @@ namespace osu.Game.Rulesets.Catch.Tests { if (lookup is CatchSkinConfiguration config) { - if (config == CatchSkinConfiguration.HyperDash && customHyperDashCatcherColour) + if (config == CatchSkinConfiguration.HyperDash && customCatcherColour) return SkinUtils.As(new Bindable(CustomHyperDashColour)); - if (config == CatchSkinConfiguration.HyperDashFruit && customHyperDashFruitColour) + if (config == CatchSkinConfiguration.HyperDashFruit && customFruitColour) return SkinUtils.As(new Bindable(CustomHyperDashFruitColour)); - if (config == CatchSkinConfiguration.HyperDashAfterImage && customHyperDashAfterColour) + if (config == CatchSkinConfiguration.HyperDashAfterImage && customAfterColour) return SkinUtils.As(new Bindable(CustomHyperDashAfterColour)); } From 16a4525a9cdbe35aecc28579937a0606e919f5de Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 30 Mar 2020 09:33:47 +0300 Subject: [PATCH 0670/2376] CatchSkinConfiguration -> CatchSkinColour --- .../TestSceneHyperDashColouring.cs | 8 ++++---- osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs | 4 ++-- .../{CatchSkinConfiguration.cs => CatchSkinColour.cs} | 2 +- osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) rename osu.Game.Rulesets.Catch/Skinning/{CatchSkinConfiguration.cs => CatchSkinColour.cs} (94%) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs index fea2939eae..ebc3d3bff1 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs @@ -158,15 +158,15 @@ namespace osu.Game.Rulesets.Catch.Tests public IBindable GetConfig(TLookup lookup) { - if (lookup is CatchSkinConfiguration config) + if (lookup is CatchSkinColour config) { - if (config == CatchSkinConfiguration.HyperDash && customCatcherColour) + if (config == CatchSkinColour.HyperDash && customCatcherColour) return SkinUtils.As(new Bindable(CustomHyperDashColour)); - if (config == CatchSkinConfiguration.HyperDashFruit && customFruitColour) + if (config == CatchSkinColour.HyperDashFruit && customFruitColour) return SkinUtils.As(new Bindable(CustomHyperDashFruitColour)); - if (config == CatchSkinConfiguration.HyperDashAfterImage && customAfterColour) + if (config == CatchSkinColour.HyperDashAfterImage && customAfterColour) return SkinUtils.As(new Bindable(CustomHyperDashAfterColour)); } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs index c8f7c4912e..16818746b5 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs @@ -64,8 +64,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables }); var hyperDashColour = - skin.GetConfig(CatchSkinConfiguration.HyperDashFruit)?.Value ?? - skin.GetConfig(CatchSkinConfiguration.HyperDash)?.Value ?? + skin.GetConfig(CatchSkinColour.HyperDashFruit)?.Value ?? + skin.GetConfig(CatchSkinColour.HyperDash)?.Value ?? Catcher.DefaultHyperDashColour; if (hitObject.HyperDash) diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchSkinConfiguration.cs b/osu.Game.Rulesets.Catch/Skinning/CatchSkinColour.cs similarity index 94% rename from osu.Game.Rulesets.Catch/Skinning/CatchSkinConfiguration.cs rename to osu.Game.Rulesets.Catch/Skinning/CatchSkinColour.cs index aea5beaa6b..2ad8f89739 100644 --- a/osu.Game.Rulesets.Catch/Skinning/CatchSkinConfiguration.cs +++ b/osu.Game.Rulesets.Catch/Skinning/CatchSkinColour.cs @@ -3,7 +3,7 @@ namespace osu.Game.Rulesets.Catch.Skinning { - public enum CatchSkinConfiguration + public enum CatchSkinColour { /// /// The colour to be used for the catcher while on hyper-dashing state. diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs index 99ecf12fd3..5235058c52 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs @@ -55,8 +55,8 @@ namespace osu.Game.Rulesets.Catch.Skinning if (drawableCatchObject.HitObject.HyperDash) { var hyperDashColour = - skin.GetConfig(CatchSkinConfiguration.HyperDashFruit)?.Value ?? - skin.GetConfig(CatchSkinConfiguration.HyperDash)?.Value ?? + skin.GetConfig(CatchSkinColour.HyperDashFruit)?.Value ?? + skin.GetConfig(CatchSkinColour.HyperDash)?.Value ?? Catcher.DefaultHyperDashColour; var hyperDash = new Sprite From 0d202929921e26afb043ea637bb7c9a722b0f7d6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 30 Mar 2020 16:14:56 +0900 Subject: [PATCH 0671/2376] Fix ticks/spinners contributing to notelock --- .../Objects/Drawables/DrawableSlider.cs | 2 +- .../Objects/Drawables/DrawableSliderHead.cs | 2 +- osu.Game.Rulesets.Osu/Objects/Slider.cs | 2 +- .../Objects/SliderHeadCircle.cs | 9 ++++ osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 53 +++++++++++-------- 5 files changed, 42 insertions(+), 26 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 5c7f4a42b3..b017eacf70 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -124,7 +124,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables case SliderTailCircle tail: return new DrawableSliderTail(slider, tail); - case HitCircle head: + case SliderHeadCircle head: return new DrawableSliderHead(slider, head) { OnShake = Shake }; case SliderTick tick: diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs index c5609b01e0..563282e18f 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private readonly Slider slider; - public DrawableSliderHead(Slider slider, HitCircle h) + public DrawableSliderHead(Slider slider, SliderHeadCircle h) : base(h) { this.slider = slider; diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index db1f46d8e2..e5d6c20738 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -155,7 +155,7 @@ namespace osu.Game.Rulesets.Osu.Objects break; case SliderEventType.Head: - AddNested(HeadCircle = new SliderCircle + AddNested(HeadCircle = new SliderHeadCircle { StartTime = e.Time, Position = Position, diff --git a/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs new file mode 100644 index 0000000000..f6d46aeef5 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs @@ -0,0 +1,9 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Rulesets.Osu.Objects +{ + public class SliderHeadCircle : HitCircle + { + } +} diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index e36d32d01a..97e002edd0 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -12,6 +12,7 @@ using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables.Connections; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Skinning; @@ -126,10 +127,7 @@ namespace osu.Game.Rulesets.Osu.UI /// The of the judged . private void missAllEarlier(JudgementResult result) { - // Hitobjects that count as bonus should not cause other hitobjects to get missed. - // E.g. For the sequence slider-head -> circle -> slider-tick, hitting the tick before the circle should not cause the circle to be missed. - // E.g. For the sequence spinner -> circle -> spinner-bonus, hitting the bonus before the circle should not cause the circle to be missed. - if (result.Judgement.IsBonus) + if (!contributesToNoteLock(result.HitObject)) return; // The minimum start time required for hitobjects so that they aren't missed. @@ -140,35 +138,44 @@ namespace osu.Game.Rulesets.Osu.UI if (obj.HitObject.StartTime >= minimumTime) break; - attemptMiss(obj); + performMiss(obj); foreach (var n in obj.NestedHitObjects) { if (n.HitObject.StartTime >= minimumTime) break; - attemptMiss(n); + performMiss(n); } } - - static void attemptMiss(DrawableHitObject obj) - { - if (!(obj is DrawableOsuHitObject osuObject)) - throw new InvalidOperationException($"{obj.GetType()} is not a {nameof(DrawableOsuHitObject)}."); - - // Hitobjects that have already been judged cannot be missed. - if (osuObject.Judged) - return; - - // Hitobjects that count as bonus should not be missed. - // For the sequence slider-head -> slider-tick -> circle, hitting the circle before the tick should not cause the tick to be missed. - if (osuObject.Result.Judgement.IsBonus) - return; - - osuObject.MissForcefully(); - } } + private void performMiss(DrawableHitObject obj) + { + if (!(obj is DrawableOsuHitObject osuObject)) + throw new InvalidOperationException($"{obj.GetType()} is not a {nameof(DrawableOsuHitObject)}."); + + // Hitobjects that have already been judged cannot be missed. + if (osuObject.Judged) + return; + + // Hitobjects that count as bonus should not be missed. + // For the sequence slider-head -> slider-tick -> circle, hitting the circle before the tick should not cause the tick to be missed. + if (!contributesToNoteLock(obj.HitObject)) + return; + + osuObject.MissForcefully(); + } + + /// + /// Whether a hitobject contributes to notelock. + /// Only hit circles and slider start circles contribute to notelock. + /// + /// The hitobject to test. + /// Whether contributes to notelock. + private bool contributesToNoteLock(HitObject hitObject) + => hitObject is HitCircle && !(hitObject is SliderTailCircle); + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => HitObjectContainer.ReceivePositionalInputAt(screenSpacePos); private class ApproachCircleProxyContainer : LifetimeManagementContainer From e074c3e5e99f69349471cf21e8a72323f390417b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 30 Mar 2020 16:15:07 +0900 Subject: [PATCH 0672/2376] Add additional tests --- .../TestSceneNoteLock.cs | 182 ++++++++++++++++-- 1 file changed, 166 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs index af82a05c4f..a33fb54ff6 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.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; @@ -11,6 +12,8 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Replays; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Replays; @@ -27,9 +30,6 @@ namespace osu.Game.Rulesets.Osu.Tests private const double early_miss_window = 1000; // time after -1000 to -500 is considered a miss private const double late_miss_window = 500; // time after +500 is considered a miss - private static readonly Vector2 position_first_circle = Vector2.Zero; - private static readonly Vector2 position_second_circle = new Vector2(80); - /// /// Tests clicking the second circle before the first hitobject's start time, while the first hitobject HAS NOT been judged. /// @@ -38,24 +38,26 @@ namespace osu.Game.Rulesets.Osu.Tests { const double time_first_circle = 1500; const double time_second_circle = 1600; + Vector2 positionFirstCircle = Vector2.Zero; + Vector2 positionSecondCircle = new Vector2(80); var hitObjects = new List { new TestHitCircle { StartTime = time_first_circle, - Position = position_first_circle + Position = positionFirstCircle }, new TestHitCircle { StartTime = time_second_circle, - Position = position_second_circle + Position = positionSecondCircle } }; performTest(hitObjects, new List { - new OsuReplayFrame { Time = time_first_circle - 100, Position = position_second_circle, Actions = { OsuAction.LeftButton } } + new OsuReplayFrame { Time = time_first_circle - 100, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } } }); addJudgementAssert(hitObjects[0], HitResult.Miss); @@ -71,24 +73,26 @@ namespace osu.Game.Rulesets.Osu.Tests { const double time_first_circle = 1500; const double time_second_circle = 1600; + Vector2 positionFirstCircle = Vector2.Zero; + Vector2 positionSecondCircle = new Vector2(80); var hitObjects = new List { new TestHitCircle { StartTime = time_first_circle, - Position = position_first_circle + Position = positionFirstCircle }, new TestHitCircle { StartTime = time_second_circle, - Position = position_second_circle + Position = positionSecondCircle } }; performTest(hitObjects, new List { - new OsuReplayFrame { Time = time_first_circle, Position = position_second_circle, Actions = { OsuAction.LeftButton } } + new OsuReplayFrame { Time = time_first_circle, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } } }); addJudgementAssert(hitObjects[0], HitResult.Miss); @@ -104,24 +108,26 @@ namespace osu.Game.Rulesets.Osu.Tests { const double time_first_circle = 1500; const double time_second_circle = 1600; + Vector2 positionFirstCircle = Vector2.Zero; + Vector2 positionSecondCircle = new Vector2(80); var hitObjects = new List { new TestHitCircle { StartTime = time_first_circle, - Position = position_first_circle + Position = positionFirstCircle }, new TestHitCircle { StartTime = time_second_circle, - Position = position_second_circle + Position = positionSecondCircle } }; performTest(hitObjects, new List { - new OsuReplayFrame { Time = time_first_circle + 100, Position = position_second_circle, Actions = { OsuAction.LeftButton } } + new OsuReplayFrame { Time = time_first_circle + 100, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } } }); addJudgementAssert(hitObjects[0], HitResult.Miss); @@ -137,25 +143,27 @@ namespace osu.Game.Rulesets.Osu.Tests { const double time_first_circle = 1500; const double time_second_circle = 1600; + Vector2 positionFirstCircle = Vector2.Zero; + Vector2 positionSecondCircle = new Vector2(80); var hitObjects = new List { new TestHitCircle { StartTime = time_first_circle, - Position = position_first_circle + Position = positionFirstCircle }, new TestHitCircle { StartTime = time_second_circle, - Position = position_second_circle + Position = positionSecondCircle } }; performTest(hitObjects, new List { - new OsuReplayFrame { Time = time_first_circle - 200, Position = position_first_circle, Actions = { OsuAction.LeftButton } }, - new OsuReplayFrame { Time = time_first_circle - 100, Position = position_second_circle, Actions = { OsuAction.RightButton } } + new OsuReplayFrame { Time = time_first_circle - 200, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } }, + new OsuReplayFrame { Time = time_first_circle - 100, Position = positionSecondCircle, Actions = { OsuAction.RightButton } } }); addJudgementAssert(hitObjects[0], HitResult.Great); @@ -164,12 +172,133 @@ namespace osu.Game.Rulesets.Osu.Tests addJudgementOffsetAssert(hitObjects[0], -200); // time_second_circle - first_circle_time - 100 } + [Test] + public void TestMissSliderHeadAndHitAllSliderTicks() + { + const double time_slider = 1500; + const double time_circle = 1510; + Vector2 positionCircle = Vector2.Zero; + Vector2 positionSlider = new Vector2(80); + + var hitObjects = new List + { + new TestHitCircle + { + StartTime = time_circle, + Position = positionCircle + }, + new TestSlider + { + StartTime = time_slider, + Position = positionSlider, + Path = new SliderPath(PathType.Linear, new[] + { + Vector2.Zero, + new Vector2(25, 0), + }) + } + }; + + performTest(hitObjects, new List + { + new OsuReplayFrame { Time = time_slider, Position = positionCircle, Actions = { OsuAction.LeftButton } }, + new OsuReplayFrame { Time = time_slider + 10, Position = positionSlider, Actions = { OsuAction.RightButton } } + }); + + addJudgementAssert(hitObjects[0], HitResult.Great); + addJudgementAssert(hitObjects[1], HitResult.Great); + addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.Miss); + addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.Great); + } + + [Test] + public void TestHitSliderTicksBeforeCircle() + { + const double time_slider = 1500; + const double time_circle = 1510; + Vector2 positionCircle = Vector2.Zero; + Vector2 positionSlider = new Vector2(80); + + var hitObjects = new List + { + new TestHitCircle + { + StartTime = time_circle, + Position = positionCircle + }, + new TestSlider + { + StartTime = time_slider, + Position = positionSlider, + Path = new SliderPath(PathType.Linear, new[] + { + Vector2.Zero, + new Vector2(25, 0), + }) + } + }; + + performTest(hitObjects, new List + { + new OsuReplayFrame { Time = time_slider, Position = positionSlider, Actions = { OsuAction.LeftButton } }, + new OsuReplayFrame { Time = time_circle + late_miss_window - 100, Position = positionCircle, Actions = { OsuAction.RightButton } }, + new OsuReplayFrame { Time = time_circle + late_miss_window - 90, Position = positionSlider, Actions = { OsuAction.LeftButton } }, + }); + + addJudgementAssert(hitObjects[0], HitResult.Great); + addJudgementAssert(hitObjects[1], HitResult.Great); + addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.Great); + addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.Great); + } + + [Test] + public void TestHitCircleBeforeSpinner() + { + const double time_spinner = 1500; + const double time_circle = 1510; + Vector2 positionCircle = Vector2.Zero; + + var hitObjects = new List + { + new TestSpinner + { + StartTime = time_spinner, + Position = new Vector2(256, 192), + EndTime = time_spinner + 1000, + }, + new TestHitCircle + { + StartTime = time_circle, + Position = positionCircle + }, + }; + + performTest(hitObjects, new List + { + new OsuReplayFrame { Time = time_spinner, Position = positionCircle, Actions = { OsuAction.LeftButton } }, + new OsuReplayFrame { Time = time_spinner + 10, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } }, + new OsuReplayFrame { Time = time_spinner + 20, Position = new Vector2(256, 172), Actions = { OsuAction.RightButton } }, + new OsuReplayFrame { Time = time_spinner + 30, Position = new Vector2(276, 192), Actions = { OsuAction.RightButton } }, + new OsuReplayFrame { Time = time_spinner + 40, Position = new Vector2(256, 212), Actions = { OsuAction.RightButton } }, + new OsuReplayFrame { Time = time_spinner + 50, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } }, + }); + + addJudgementAssert(hitObjects[0], HitResult.Great); + addJudgementAssert(hitObjects[1], HitResult.Great); + } + private void addJudgementAssert(OsuHitObject hitObject, HitResult result) { AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judgement is {result}", () => judgementResults.Single(r => r.HitObject == hitObject).Type == result); } + private void addJudgementAssert(string name, Func hitObject, HitResult result) + { + AddAssert($"{name} judgement is {result}", + () => judgementResults.Single(r => r.HitObject == hitObject()).Type == result); + } + private void addJudgementOffsetAssert(OsuHitObject hitObject, double offset) { AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judged at {offset}", @@ -225,6 +354,27 @@ namespace osu.Game.Rulesets.Osu.Tests protected override HitWindows CreateHitWindows() => new TestHitWindows(); } + private class TestSlider : Slider + { + public TestSlider() + { + DefaultsApplied += () => + { + HeadCircle.HitWindows = new TestHitWindows(); + TailCircle.HitWindows = new TestHitWindows(); + }; + } + } + + private class TestSpinner : Spinner + { + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + { + base.ApplyDefaultsToSelf(controlPointInfo, difficulty); + SpinsRequired = 1; + } + } + private class TestHitWindows : HitWindows { private static readonly DifficultyRange[] ranges = From 812583a4cd6e714626f0132a0351b62c1eea99db Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Mar 2020 16:17:42 +0900 Subject: [PATCH 0673/2376] Remove stray newline --- osu.Game/Online/API/Requests/GetRankingsRequest.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Online/API/Requests/GetRankingsRequest.cs b/osu.Game/Online/API/Requests/GetRankingsRequest.cs index 1bbaa73bbb..ddc3298ca7 100644 --- a/osu.Game/Online/API/Requests/GetRankingsRequest.cs +++ b/osu.Game/Online/API/Requests/GetRankingsRequest.cs @@ -7,7 +7,6 @@ using osu.Game.Rulesets; namespace osu.Game.Online.API.Requests { public abstract class GetRankingsRequest : APIRequest where TModel : class - { private readonly RulesetInfo ruleset; private readonly int page; From 744f6c3ca7be99511c5732c0fa4c8688a3acbd5e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 30 Mar 2020 16:33:46 +0900 Subject: [PATCH 0674/2376] Rename method + adjust comments --- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 97e002edd0..994b3d9718 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -127,7 +127,7 @@ namespace osu.Game.Rulesets.Osu.UI /// The of the judged . private void missAllEarlier(JudgementResult result) { - if (!contributesToNoteLock(result.HitObject)) + if (!causesNoteLockMisses(result.HitObject)) return; // The minimum start time required for hitobjects so that they aren't missed. @@ -159,21 +159,18 @@ namespace osu.Game.Rulesets.Osu.UI if (osuObject.Judged) return; - // Hitobjects that count as bonus should not be missed. - // For the sequence slider-head -> slider-tick -> circle, hitting the circle before the tick should not cause the tick to be missed. - if (!contributesToNoteLock(obj.HitObject)) + if (!causesNoteLockMisses(obj.HitObject)) return; osuObject.MissForcefully(); } /// - /// Whether a hitobject contributes to notelock. - /// Only hit circles and slider start circles contribute to notelock. + /// Whether a can be missed and causes other hitobjects to be missed during notelock. /// - /// The hitobject to test. - /// Whether contributes to notelock. - private bool contributesToNoteLock(HitObject hitObject) + /// The to test. + /// Whether contributes to notelock misses. + private bool causesNoteLockMisses(HitObject hitObject) => hitObject is HitCircle && !(hitObject is SliderTailCircle); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => HitObjectContainer.ReceivePositionalInputAt(screenSpacePos); From 0044d00d07111399bbd12f0f4350bcb13a288f9b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Mar 2020 16:53:23 +0900 Subject: [PATCH 0675/2376] 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 7e17f9da16..fd2532257b 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 3894c06994..fdf9703d79 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -23,7 +23,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 9cc9792ecf..a286d1d460 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + @@ -79,7 +79,7 @@ - + From 796976db3c046f967e0403c9d15e4d305cbe3435 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 30 Mar 2020 17:00:53 +0900 Subject: [PATCH 0676/2376] Completely ignore spinners from note lock --- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 994b3d9718..db8a47e4a2 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -90,7 +90,14 @@ namespace osu.Game.Rulesets.Osu.UI private bool checkHittable(DrawableOsuHitObject osuHitObject) { - var lastObject = HitObjectContainer.AliveObjects.GetPrevious(osuHitObject); + DrawableHitObject lastObject = osuHitObject; + + // Get the last hitobject that contributes to note lock + while ((lastObject = HitObjectContainer.AliveObjects.GetPrevious(lastObject)) != null) + { + if (contributesToNoteLock(lastObject.HitObject)) + break; + } // If there is no previous object alive, allow the hit. if (lastObject == null) @@ -166,10 +173,19 @@ namespace osu.Game.Rulesets.Osu.UI } /// - /// Whether a can be missed and causes other hitobjects to be missed during notelock. + /// Whether a is contributes to note lock. + /// Future contributing s will not be hittable until the start time of the last contributing is reached. /// /// The to test. - /// Whether contributes to notelock misses. + /// Whether causes note lock. + private bool contributesToNoteLock(HitObject hitObject) + => hitObject is HitCircle || hitObject is Slider; + + /// + /// Whether a can be missed and causes other s to be missed when hit out-of-order during note lock. + /// + /// The to test. + /// Whether contributes to note lock misses. private bool causesNoteLockMisses(HitObject hitObject) => hitObject is HitCircle && !(hitObject is SliderTailCircle); From 1ff60b73d70deb25d9133f42b17b813711759bbc Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 30 Mar 2020 17:01:29 +0900 Subject: [PATCH 0677/2376] Refactor tests a bit --- .../TestSceneNoteLock.cs | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs index a33fb54ff6..2c69540951 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Tests private const double late_miss_window = 500; // time after +500 is considered a miss /// - /// Tests clicking the second circle before the first hitobject's start time, while the first hitobject HAS NOT been judged. + /// Tests clicking a future circle before the first circle's start time, while the first circle HAS NOT been judged. /// [Test] public void TestClickSecondCircleBeforeFirstCircleTime() @@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Osu.Tests } /// - /// Tests clicking the second circle at the first hitobject's start time, while the first hitobject HAS NOT been judged. + /// Tests clicking a future circle at the first circle's start time, while the first circle HAS NOT been judged. /// [Test] public void TestClickSecondCircleAtFirstCircleTime() @@ -101,7 +101,7 @@ namespace osu.Game.Rulesets.Osu.Tests } /// - /// Tests clicking the second circle after the first hitobject's start time, while the first hitobject HAS NOT been judged. + /// Tests clicking a future circle after the first circle's start time, while the first circle HAS NOT been judged. /// [Test] public void TestClickSecondCircleAfterFirstCircleTime() @@ -136,7 +136,7 @@ namespace osu.Game.Rulesets.Osu.Tests } /// - /// Tests clicking the second circle before the first hitobject's start time, while the first hitobject HAS been judged. + /// Tests clicking a future circle before the first circle's start time, while the first circle HAS been judged. /// [Test] public void TestClickSecondCircleBeforeFirstCircleTimeWithFirstCircleJudged() @@ -172,6 +172,9 @@ namespace osu.Game.Rulesets.Osu.Tests addJudgementOffsetAssert(hitObjects[0], -200); // time_second_circle - first_circle_time - 100 } + /// + /// Tests clicking a future circle after a slider's start time, but hitting all slider ticks. + /// [Test] public void TestMissSliderHeadAndHitAllSliderTicks() { @@ -211,6 +214,9 @@ namespace osu.Game.Rulesets.Osu.Tests addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.Great); } + /// + /// Tests clicking hitting future slider ticks before a circle. + /// [Test] public void TestHitSliderTicksBeforeCircle() { @@ -251,11 +257,14 @@ namespace osu.Game.Rulesets.Osu.Tests addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.Great); } + /// + /// Tests clicking a future circle before a spinner. + /// [Test] public void TestHitCircleBeforeSpinner() { const double time_spinner = 1500; - const double time_circle = 1510; + const double time_circle = 1800; Vector2 positionCircle = Vector2.Zero; var hitObjects = new List @@ -275,7 +284,7 @@ namespace osu.Game.Rulesets.Osu.Tests performTest(hitObjects, new List { - new OsuReplayFrame { Time = time_spinner, Position = positionCircle, Actions = { OsuAction.LeftButton } }, + new OsuReplayFrame { Time = time_spinner - 100, Position = positionCircle, Actions = { OsuAction.LeftButton } }, new OsuReplayFrame { Time = time_spinner + 10, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } }, new OsuReplayFrame { Time = time_spinner + 20, Position = new Vector2(256, 172), Actions = { OsuAction.RightButton } }, new OsuReplayFrame { Time = time_spinner + 30, Position = new Vector2(276, 192), Actions = { OsuAction.RightButton } }, From 4719aac235727c684a2f3d20e80eceff02c4f801 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 30 Mar 2020 17:18:09 +0900 Subject: [PATCH 0678/2376] Add basic mania skin parsing --- osu.Game/Beatmaps/Formats/LegacyDecoder.cs | 12 +- .../Skinning/LegacyManiaSkinConfiguration.cs | 30 +++++ osu.Game/Skinning/LegacyManiaSkinDecoder.cs | 106 ++++++++++++++++++ 3 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Skinning/LegacyManiaSkinConfiguration.cs create mode 100644 osu.Game/Skinning/LegacyManiaSkinDecoder.cs diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index e28e235788..bbc0aad467 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -41,6 +41,7 @@ namespace osu.Game.Beatmaps.Formats section = Section.None; } + OnBeginNewSection(section); continue; } @@ -57,6 +58,14 @@ namespace osu.Game.Beatmaps.Formats protected virtual bool ShouldSkipLine(string line) => string.IsNullOrWhiteSpace(line) || line.AsSpan().TrimStart().StartsWith("//".AsSpan(), StringComparison.Ordinal); + /// + /// Invoked when a new has been entered. + /// + /// The entered . + protected virtual void OnBeginNewSection(Section section) + { + } + protected virtual void ParseLine(T output, Section section, string line) { line = StripComments(line); @@ -139,7 +148,8 @@ namespace osu.Game.Beatmaps.Formats Colours, HitObjects, Variables, - Fonts + Fonts, + Mania } internal class LegacyDifficultyControlPoint : DifficultyControlPoint diff --git a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs new file mode 100644 index 0000000000..5dd185879b --- /dev/null +++ b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs @@ -0,0 +1,30 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; + +namespace osu.Game.Skinning +{ + public class LegacyManiaSkinConfiguration + { + public readonly int Keys; + + public readonly float[] ColumnLineWidth; + public readonly float[] ColumnSpacing; + public readonly float[] ColumnWidth; + + public float HitPosition = 124.8f; // (480 - 402) * 1.6f + + public LegacyManiaSkinConfiguration(int keys) + { + Keys = keys; + + ColumnLineWidth = new float[keys + 1]; + ColumnSpacing = new float[keys - 1]; + ColumnWidth = new float[keys]; + + ColumnLineWidth.AsSpan().Fill(2); + ColumnWidth.AsSpan().Fill(48); + } + } +} diff --git a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs new file mode 100644 index 0000000000..153a2c9626 --- /dev/null +++ b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs @@ -0,0 +1,106 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using osu.Game.Beatmaps.Formats; + +namespace osu.Game.Skinning +{ + public class LegacyManiaSkinDecoder : LegacyDecoder> + { + private const float size_scale_factor = 1.6f; + + public LegacyManiaSkinDecoder() + : base(1) + { + } + + private readonly List pendingLines = new List(); + private LegacyManiaSkinConfiguration currentConfig; + + protected override void OnBeginNewSection(Section section) + { + base.OnBeginNewSection(section); + + // If a new section is reached with pending lines remaining, they can all be discarded as there isn't a valid configuration to parse them into. + pendingLines.Clear(); + currentConfig = null; + } + + protected override void ParseLine(List output, Section section, string line) + { + line = StripComments(line); + + switch (section) + { + case Section.Mania: + var pair = SplitKeyVal(line); + + switch (pair.Key) + { + case "Keys": + currentConfig = new LegacyManiaSkinConfiguration(int.Parse(pair.Value, CultureInfo.InvariantCulture)); + output.Add(currentConfig); + + // All existing lines can be flushed now that we have a valid configuration. + flushPendingLines(); + break; + + default: + pendingLines.Add(line); + + // Hold all lines until a "Keys" item is found. + if (currentConfig != null) + flushPendingLines(); + break; + } + + break; + } + } + + private void flushPendingLines() + { + Debug.Assert(currentConfig != null); + + foreach (var line in pendingLines) + { + var pair = SplitKeyVal(line); + + switch (pair.Key) + { + case "ColumnLineWidth": + parseArrayValue(pair.Value, currentConfig.ColumnLineWidth); + break; + + case "ColumnSpacing": + parseArrayValue(pair.Value, currentConfig.ColumnSpacing); + break; + + case "ColumnWidth": + parseArrayValue(pair.Value, currentConfig.ColumnWidth); + break; + + case "HitPosition": + currentConfig.HitPosition = (480 - float.Parse(pair.Value, CultureInfo.InvariantCulture)) * size_scale_factor; + break; + } + } + } + + private void parseArrayValue(string value, float[] output) + { + string[] values = value.Split(','); + + for (int i = 0; i < values.Length; i++) + { + if (i >= output.Length) + break; + + output[i] = float.Parse(values[i], CultureInfo.InvariantCulture) * size_scale_factor; + } + } + } +} From 4406f441654726dd21c810349b0eeb6935ba7d65 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Mar 2020 17:26:35 +0900 Subject: [PATCH 0679/2376] Remove osu!catch GotoFrame usage --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 5 ++++- osu.Game/Skinning/LegacySkinExtensions.cs | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index e361b29a9d..bc0311bd2d 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; +using osu.Framework.Timing; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Objects; @@ -379,7 +380,9 @@ namespace osu.Game.Rulesets.Catch.UI } currentCatcher.Show(); - (currentCatcher.Drawable as IAnimation)?.GotoFrame(0); + + if (currentCatcher.Drawable.Clock is FramedOffsetClock offsetClock) + offsetClock.Offset = -Time.Current; } private void beginTrail() diff --git a/osu.Game/Skinning/LegacySkinExtensions.cs b/osu.Game/Skinning/LegacySkinExtensions.cs index 8765b161d4..de0add6ba3 100644 --- a/osu.Game/Skinning/LegacySkinExtensions.cs +++ b/osu.Game/Skinning/LegacySkinExtensions.cs @@ -71,6 +71,8 @@ namespace osu.Game.Skinning if (timeReference != null) Clock = new FramedOffsetClock(timeReference.Clock) { Offset = -timeReference.AnimationStartTime }; + else + Clock = new FramedOffsetClock(Clock); } } From 881ec146afca5c8560c811ea3e1370b227aa6a3a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 30 Mar 2020 17:36:57 +0900 Subject: [PATCH 0680/2376] Ignore duplicate configs --- osu.Game/Skinning/LegacyManiaSkinDecoder.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs index 153a2c9626..ae6c8eeb15 100644 --- a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs +++ b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Globalization; +using System.Linq; using osu.Game.Beatmaps.Formats; namespace osu.Game.Skinning @@ -42,7 +43,10 @@ namespace osu.Game.Skinning { case "Keys": currentConfig = new LegacyManiaSkinConfiguration(int.Parse(pair.Value, CultureInfo.InvariantCulture)); - output.Add(currentConfig); + + // Silently ignore duplicate configurations. + if (output.All(c => c.Keys != currentConfig.Keys)) + output.Add(currentConfig); // All existing lines can be flushed now that we have a valid configuration. flushPendingLines(); From 1ce4f7c8545893786590da52a52184da8008af1b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 30 Mar 2020 17:37:08 +0900 Subject: [PATCH 0681/2376] Add tests --- .../Resources/mania-skin-duplicate.ini | 9 ++ .../Resources/mania-skin-extra-data.ini | 4 + .../Resources/mania-skin-multiple.ini | 9 ++ .../Resources/mania-skin-single.ini | 4 + .../Skins/LegacyManiaSkinDecoderTest.cs | 87 +++++++++++++++++++ 5 files changed, 113 insertions(+) create mode 100644 osu.Game.Tests/Resources/mania-skin-duplicate.ini create mode 100644 osu.Game.Tests/Resources/mania-skin-extra-data.ini create mode 100644 osu.Game.Tests/Resources/mania-skin-multiple.ini create mode 100644 osu.Game.Tests/Resources/mania-skin-single.ini create mode 100644 osu.Game.Tests/Skins/LegacyManiaSkinDecoderTest.cs diff --git a/osu.Game.Tests/Resources/mania-skin-duplicate.ini b/osu.Game.Tests/Resources/mania-skin-duplicate.ini new file mode 100644 index 0000000000..2f4fa92c52 --- /dev/null +++ b/osu.Game.Tests/Resources/mania-skin-duplicate.ini @@ -0,0 +1,9 @@ +[Mania] +Keys: 4 +ColumnWidth: 10,10,10,10 +HitPosition: 470 + +[Mania] +Keys: 4 +ColumnWidth: 20,20,20,20 +HitPosition: 460 \ No newline at end of file diff --git a/osu.Game.Tests/Resources/mania-skin-extra-data.ini b/osu.Game.Tests/Resources/mania-skin-extra-data.ini new file mode 100644 index 0000000000..e538b5335a --- /dev/null +++ b/osu.Game.Tests/Resources/mania-skin-extra-data.ini @@ -0,0 +1,4 @@ +[Mania] +Keys: 4 +ColumnWidth: 10,10,10,10,10,10,10 +HitPosition: 470 \ No newline at end of file diff --git a/osu.Game.Tests/Resources/mania-skin-multiple.ini b/osu.Game.Tests/Resources/mania-skin-multiple.ini new file mode 100644 index 0000000000..247c7738a0 --- /dev/null +++ b/osu.Game.Tests/Resources/mania-skin-multiple.ini @@ -0,0 +1,9 @@ +[Mania] +Keys: 4 +ColumnWidth: 10,10,10,10 +HitPosition: 470 + +[Mania] +Keys: 2 +ColumnWidth: 20,20 +HitPosition: 460 \ No newline at end of file diff --git a/osu.Game.Tests/Resources/mania-skin-single.ini b/osu.Game.Tests/Resources/mania-skin-single.ini new file mode 100644 index 0000000000..3ae38fd75e --- /dev/null +++ b/osu.Game.Tests/Resources/mania-skin-single.ini @@ -0,0 +1,4 @@ +[Mania] +Keys: 4 +ColumnWidth: 10,10,10,10 +HitPosition: 470 \ No newline at end of file diff --git a/osu.Game.Tests/Skins/LegacyManiaSkinDecoderTest.cs b/osu.Game.Tests/Skins/LegacyManiaSkinDecoderTest.cs new file mode 100644 index 0000000000..736f97f39f --- /dev/null +++ b/osu.Game.Tests/Skins/LegacyManiaSkinDecoderTest.cs @@ -0,0 +1,87 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.IO; +using osu.Game.Skinning; +using osu.Game.Tests.Resources; + +namespace osu.Game.Tests.Skins +{ + [TestFixture] + public class LegacyManiaSkinDecoderTest + { + [Test] + public void TestParseSingleConfig() + { + var decoder = new LegacyManiaSkinDecoder(); + + using (var resStream = TestResources.OpenResource("mania-skin-single.ini")) + using (var stream = new LineBufferedReader(resStream)) + { + var configs = decoder.Decode(stream); + + Assert.That(configs.Count, Is.EqualTo(1)); + Assert.That(configs[0].Keys, Is.EqualTo(4)); + Assert.That(configs[0].ColumnWidth, Is.EquivalentTo(new float[] { 16, 16, 16, 16 })); + Assert.That(configs[0].HitPosition, Is.EqualTo(16)); + } + } + + [Test] + public void TestParseMultipleConfig() + { + var decoder = new LegacyManiaSkinDecoder(); + + using (var resStream = TestResources.OpenResource("mania-skin-multiple.ini")) + using (var stream = new LineBufferedReader(resStream)) + { + var configs = decoder.Decode(stream); + + Assert.That(configs.Count, Is.EqualTo(2)); + + Assert.That(configs[0].Keys, Is.EqualTo(4)); + Assert.That(configs[0].ColumnWidth, Is.EquivalentTo(new float[] { 16, 16, 16, 16 })); + Assert.That(configs[0].HitPosition, Is.EqualTo(16)); + + Assert.That(configs[1].Keys, Is.EqualTo(2)); + Assert.That(configs[1].ColumnWidth, Is.EquivalentTo(new float[] { 32, 32 })); + Assert.That(configs[1].HitPosition, Is.EqualTo(32)); + } + } + + [Test] + public void TestParseDuplicateConfig() + { + var decoder = new LegacyManiaSkinDecoder(); + + using (var resStream = TestResources.OpenResource("mania-skin-single.ini")) + using (var stream = new LineBufferedReader(resStream)) + { + var configs = decoder.Decode(stream); + + Assert.That(configs.Count, Is.EqualTo(1)); + Assert.That(configs[0].Keys, Is.EqualTo(4)); + Assert.That(configs[0].ColumnWidth, Is.EquivalentTo(new float[] { 16, 16, 16, 16 })); + Assert.That(configs[0].HitPosition, Is.EqualTo(16)); + } + } + + [Test] + public void TestParseWithUnnecessaryExtraData() + { + var decoder = new LegacyManiaSkinDecoder(); + + using (var resStream = TestResources.OpenResource("mania-skin-extra-data.ini")) + using (var stream = new LineBufferedReader(resStream)) + { + var configs = decoder.Decode(stream); + + Assert.That(configs.Count, Is.EqualTo(1)); + Assert.That(configs[0].Keys, Is.EqualTo(4)); + Assert.That(configs[0].ColumnWidth, Is.EquivalentTo(new float[] { 16, 16, 16, 16 })); + Assert.That(configs[0].HitPosition, Is.EqualTo(16)); + } + } + } +} From 43367dbe35c313f70156d5ad31d9fe508c69ef01 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2020 08:49:50 +0000 Subject: [PATCH 0682/2376] Bump Microsoft.Build.Traversal from 2.0.24 to 2.0.32 Bumps [Microsoft.Build.Traversal](https://github.com/Microsoft/MSBuildSdks) from 2.0.24 to 2.0.32. - [Release notes](https://github.com/Microsoft/MSBuildSdks/releases) - [Changelog](https://github.com/microsoft/MSBuildSdks/blob/master/RELEASE.md) - [Commits](https://github.com/Microsoft/MSBuildSdks/compare/Microsoft.Build.Traversal.2.0.24...Microsoft.Build.Traversal.2.0.32) Signed-off-by: dependabot-preview[bot] --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index 6858d4044d..0223dc7330 100644 --- a/global.json +++ b/global.json @@ -5,6 +5,6 @@ "version": "3.1.100" }, "msbuild-sdks": { - "Microsoft.Build.Traversal": "2.0.24" + "Microsoft.Build.Traversal": "2.0.32" } } \ No newline at end of file From c4df49954f39d7c5d07352987c281ed1b5296e3b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Mar 2020 18:35:01 +0900 Subject: [PATCH 0683/2376] Reword comment --- .../Objects/Drawables/DrawableSliderRepeat.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs index 2704680d54..b04d484195 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs @@ -87,8 +87,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public void UpdateSnakingPosition(Vector2 start, Vector2 end) { - // When the repeat is hit, the arrow should fade out on spot, - // it should no longer follow snaking + // When the repeat is hit, the arrow should fade out on spot rather than following the slider if (IsHit) return; bool isRepeatAtEnd = sliderRepeat.RepeatIndex % 2 == 0; From 113660b6219b7729f28d35d0136a9d0634a59f61 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Mar 2020 18:56:35 +0900 Subject: [PATCH 0684/2376] Merge if statements --- osu.Game/Screens/Ranking/ResultsScreen.cs | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 1c08b763fe..cfba1e6e3e 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -89,22 +89,19 @@ namespace osu.Game.Screens.Ranking } }; - if (player != null) + if (player != null && allowRetry) { - if (allowRetry) + buttons.Add(new RetryButton { Width = 300 }); + + AddInternal(new HotkeyRetryOverlay { - buttons.Add(new RetryButton { Width = 300 }); - - AddInternal(new HotkeyRetryOverlay + Action = () => { - Action = () => - { - if (!this.IsCurrentScreen()) return; + if (!this.IsCurrentScreen()) return; - player?.Restart(); - }, - }); - } + player?.Restart(); + }, + }); } } From 35647d59a6f46e9f9284995edcd43af377954158 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Mar 2020 19:09:05 +0900 Subject: [PATCH 0685/2376] Add failing test --- .../TestSceneOverlayScrollContainer.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs index 1fc85c3c04..684436459f 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs @@ -74,6 +74,20 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("scrolled back to start", () => Precision.AlmostEquals(scroll.Current, 0, 0.1f)); } + [Test] + public void TestClick() + { + AddStep("scroll to end", () => scroll.ScrollToEnd(false)); + + AddStep("click button", () => + { + InputManager.MoveMouseTo(scroll.Button); + InputManager.Click(MouseButton.Left); + }); + + AddUntilStep("scrolled back to start", () => Precision.AlmostEquals(scroll.Current, 0, 0.1f)); + } + [Test] public void TestMultipleClicks() { From 0d4830550e4f3cbf6c51599c6b90b6f36816b7ef Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Mar 2020 19:15:44 +0900 Subject: [PATCH 0686/2376] Fix tooltips not showing inside ManualInputManagerTestScenes --- osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs b/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs index 0da3ae7f87..64f4d7b95b 100644 --- a/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs +++ b/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs @@ -26,16 +26,16 @@ namespace osu.Game.Tests.Visual protected OsuManualInputManagerTestScene() { + MenuCursorContainer cursorContainer; + base.Content.AddRange(new Drawable[] { InputManager = new ManualInputManager { UseParentInput = true, Child = new GlobalActionContainer(null) - { - RelativeSizeAxes = Axes.Both, - Child = content = new MenuCursorContainer { RelativeSizeAxes = Axes.Both } - }, + .WithChild((cursorContainer = new MenuCursorContainer { RelativeSizeAxes = Axes.Both }) + .WithChild(content = new OsuTooltipContainer(cursorContainer.Cursor) { RelativeSizeAxes = Axes.Both })) }, new Container { From f96229c572e812cf2f7fc99b9dee05f62faf9a36 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 30 Mar 2020 13:21:22 +0300 Subject: [PATCH 0687/2376] Add support for HitCircleOverlayAboveNumber legacy skin property --- osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs | 5 +++++ osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs index 38ba4c5974..0480449d05 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs @@ -80,6 +80,11 @@ namespace osu.Game.Rulesets.Osu.Skinning return tex ?? skin.GetTexture($"hitcircle{name}"); } + + bool overlayAboveNumber = skin.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumber)?.Value ?? true; + + if (!overlayAboveNumber) + ChangeInternalChildDepth(hitCircleText, -float.MaxValue); } private void updateState(ValueChangedEvent state) diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs index 5d99960f10..c6920bd03e 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs @@ -11,6 +11,7 @@ namespace osu.Game.Rulesets.Osu.Skinning SliderPathRadius, AllowSliderBallTint, CursorExpand, - CursorRotate + CursorRotate, + HitCircleOverlayAboveNumber } } From 179bd1ce7ebe2384fc819bdac20865f5176fd7d7 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 30 Mar 2020 13:38:04 +0300 Subject: [PATCH 0688/2376] Fix failing test --- osu.Game/Overlays/OverlayScrollContainer.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/OverlayScrollContainer.cs b/osu.Game/Overlays/OverlayScrollContainer.cs index 1a875ded95..a9524b9d32 100644 --- a/osu.Game/Overlays/OverlayScrollContainer.cs +++ b/osu.Game/Overlays/OverlayScrollContainer.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -83,7 +84,10 @@ namespace osu.Game.Overlays public ScrollToTopButton() { Size = new Vector2(50); - Child = button = new Button(); + Child = button = new Button + { + AreaState = { BindTarget = State } + }; } protected override bool OnMouseDown(MouseDownEvent e) => true; @@ -94,7 +98,9 @@ namespace osu.Game.Overlays private class Button : OsuHoverContainer { - public override bool PropagatePositionalInputSubTree => Alpha == 1; + public readonly Bindable AreaState = new Bindable(); + + public override bool HandlePositionalInput => AreaState.Value == Visibility.Visible; protected override IEnumerable EffectTargets => new[] { background }; From 9890544b3692df822dcc97b25fdfad7cd800b0e5 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 30 Mar 2020 13:42:18 +0300 Subject: [PATCH 0689/2376] Move implementation to better place --- .../Skinning/LegacyMainCirclePiece.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs index 0480449d05..e7486ef9b0 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs @@ -62,6 +62,11 @@ namespace osu.Game.Rulesets.Osu.Skinning } }; + bool overlayAboveNumber = skin.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumber)?.Value ?? true; + + if (!overlayAboveNumber) + ChangeInternalChildDepth(hitCircleText, -float.MaxValue); + state.BindTo(drawableObject.State); state.BindValueChanged(updateState, true); @@ -80,11 +85,6 @@ namespace osu.Game.Rulesets.Osu.Skinning return tex ?? skin.GetTexture($"hitcircle{name}"); } - - bool overlayAboveNumber = skin.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumber)?.Value ?? true; - - if (!overlayAboveNumber) - ChangeInternalChildDepth(hitCircleText, -float.MaxValue); } private void updateState(ValueChangedEvent state) From 3cae0cedeea80e1dc6206f26e5526a5b7e22662a Mon Sep 17 00:00:00 2001 From: Lucas A Date: Mon, 30 Mar 2020 12:59:39 +0200 Subject: [PATCH 0690/2376] Add a game setting to disable the layer --- osu.Game/Configuration/OsuConfigManager.cs | 2 ++ .../Settings/Sections/Gameplay/GeneralSettings.cs | 6 ++++++ osu.Game/Screens/Play/HUD/FailingLayer.cs | 8 +++++++- 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 41f6747b74..6fed5ea5a2 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -87,6 +87,7 @@ namespace osu.Game.Configuration Set(OsuSetting.ShowInterface, true); Set(OsuSetting.ShowProgressGraph, true); Set(OsuSetting.ShowHealthDisplayWhenCantFail, true); + Set(OsuSetting.FadePlayfieldWhenHealthLow, true); Set(OsuSetting.KeyOverlay, false); Set(OsuSetting.ScoreMeter, ScoreMeterType.HitErrorBoth); @@ -181,6 +182,7 @@ namespace osu.Game.Configuration ShowInterface, ShowProgressGraph, ShowHealthDisplayWhenCantFail, + FadePlayfieldWhenHealthLow, MouseDisableButtons, MouseDisableWheel, AudioOffset, diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs index 2d2cd42213..4b75910454 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs @@ -53,6 +53,12 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay Keywords = new[] { "hp", "bar" } }, new SettingsCheckbox + { + LabelText = "Fade playfield to red when health is low", + Bindable = config.GetBindable(OsuSetting.FadePlayfieldWhenHealthLow), + Keywords = new[] { "hp", "low", "playfield", "red" } + }, + new SettingsCheckbox { LabelText = "Always show key overlay", Bindable = config.GetBindable(OsuSetting.KeyOverlay) diff --git a/osu.Game/Screens/Play/HUD/FailingLayer.cs b/osu.Game/Screens/Play/HUD/FailingLayer.cs index 97d2458674..761178b93d 100644 --- a/osu.Game/Screens/Play/HUD/FailingLayer.cs +++ b/osu.Game/Screens/Play/HUD/FailingLayer.cs @@ -3,9 +3,11 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Utils; +using osu.Game.Configuration; using osu.Game.Graphics; namespace osu.Game.Screens.Play.HUD @@ -21,6 +23,8 @@ namespace osu.Game.Screens.Play.HUD private readonly Box box; + private Bindable enabled; + /// /// The threshold under which the current player life should be considered low and the layer should start fading in. /// @@ -37,9 +41,11 @@ namespace osu.Game.Screens.Play.HUD } [BackgroundDependencyLoader] - private void load(OsuColour color) + private void load(OsuColour color, OsuConfigManager config) { box.Colour = color.Red; + enabled = config.GetBindable(OsuSetting.FadePlayfieldWhenHealthLow); + enabled.BindValueChanged(e => this.FadeTo(e.NewValue ? 1 : 0, fade_time, Easing.OutQuint), true); } protected override void Update() From 655fab6a976007486b2de2d037a55f2fcb7c1d06 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 30 Mar 2020 23:02:07 +0900 Subject: [PATCH 0691/2376] Add mania skinnable test helpers --- .../Skinning/ColumnTestContainer.cs | 38 +++++++++++ .../Skinning/ManiaHitObjectTestScene.cs | 67 +++++++++++++++++++ .../Skinning/ManiaSkinnableTestScene.cs | 58 ++++++++++++++++ 3 files changed, 163 insertions(+) create mode 100644 osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs create mode 100644 osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs create mode 100644 osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs new file mode 100644 index 0000000000..c807e98871 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs @@ -0,0 +1,38 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Mania.UI; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Mania.Tests.Skinning +{ + /// + /// A container to be used in a to provide a resolvable dependency. + /// + public class ColumnTestContainer : Container + { + protected override Container Content => content; + + private readonly Container content; + + [Cached] + private readonly Column column; + + public ColumnTestContainer(int column, ManiaAction action) + { + this.column = new Column(column) + { + Action = { Value = action }, + AccentColour = Color4.Orange + }; + + InternalChild = content = new ManiaInputManager(new ManiaRuleset().RulesetInfo, 4) + { + RelativeSizeAxes = Axes.Both + }; + } + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs new file mode 100644 index 0000000000..e65982b240 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs @@ -0,0 +1,67 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Timing; +using osu.Game.Rulesets.Mania.Objects.Drawables; +using osu.Game.Rulesets.UI.Scrolling; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Mania.Tests.Skinning +{ + /// + /// A test scene for a mania hitobject. + /// + public abstract class ManiaHitObjectTestScene : ManiaSkinnableTestScene + { + [BackgroundDependencyLoader] + private void load() + { + SetContents(() => new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Height = 0.7f, + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + new ColumnTestContainer(0, ManiaAction.Key1) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Width = 80, + Child = new ScrollingHitObjectContainer + { + RelativeSizeAxes = Axes.Both, + Clock = new FramedClock(new StopwatchClock()), + }.With(c => + { + c.Add(CreateHitObject().With(h => h.AccentColour.Value = Color4.Orange)); + }) + }, + new ColumnTestContainer(1, ManiaAction.Key2) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Width = 80, + Child = new ScrollingHitObjectContainer + { + RelativeSizeAxes = Axes.Both, + Clock = new FramedClock(new StopwatchClock()), + }.With(c => + { + c.Add(CreateHitObject().With(h => h.AccentColour.Value = Color4.Orange)); + }) + }, + } + }); + } + + protected abstract DrawableManiaHitObject CreateHitObject(); + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs new file mode 100644 index 0000000000..41fb7c727e --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs @@ -0,0 +1,58 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Rulesets.UI.Scrolling.Algorithms; +using osu.Game.Tests.Visual; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Mania.Tests.Skinning +{ + /// + /// A test scene for skinnable mania components. + /// + public abstract class ManiaSkinnableTestScene : SkinnableTestScene + { + [Cached(Type = typeof(IScrollingInfo))] + private readonly TestScrollingInfo scrollingInfo = new TestScrollingInfo(); + + protected ManiaSkinnableTestScene() + { + scrollingInfo.Direction.Value = ScrollingDirection.Down; + + Add(new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.SlateGray.Opacity(0.2f), + Depth = 1 + }); + } + + [Test] + public void TestScrollingDown() + { + AddStep("change direction to down", () => scrollingInfo.Direction.Value = ScrollingDirection.Down); + } + + [Test] + public void TestScrollingUp() + { + AddStep("change direction to up", () => scrollingInfo.Direction.Value = ScrollingDirection.Up); + } + + private class TestScrollingInfo : IScrollingInfo + { + public readonly Bindable Direction = new Bindable(); + + IBindable IScrollingInfo.Direction => Direction; + IBindable IScrollingInfo.TimeRange { get; } = new Bindable(1000); + IScrollAlgorithm IScrollingInfo.Algorithm { get; } = new ConstantScrollAlgorithm(); + } + } +} From bd87a4cde8212d97173be42b0bd0caa697f7a84d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 30 Mar 2020 23:03:36 +0900 Subject: [PATCH 0692/2376] Re-namespace testscene --- .../{ => Skinning}/TestSceneDrawableJudgement.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename osu.Game.Rulesets.Mania.Tests/{ => Skinning}/TestSceneDrawableJudgement.cs (96%) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs similarity index 96% rename from osu.Game.Rulesets.Mania.Tests/TestSceneDrawableJudgement.cs rename to osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs index 692d079c16..a6bc64550f 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableJudgement.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs @@ -6,13 +6,13 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Extensions; using osu.Framework.Graphics; -using osu.Game.Tests.Visual; -using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; +using osu.Game.Tests.Visual; -namespace osu.Game.Rulesets.Mania.Tests +namespace osu.Game.Rulesets.Mania.Tests.Skinning { public class TestSceneDrawableJudgement : SkinnableTestScene { From 6ff2273b64bcb9600a71888673e78332050aa292 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 30 Mar 2020 23:07:32 +0900 Subject: [PATCH 0693/2376] Make column + stage cached --- osu.Game.Rulesets.Mania/UI/Column.cs | 1 + osu.Game.Rulesets.Mania/UI/ManiaStage.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 63c573d344..0eccd27944 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -19,6 +19,7 @@ using osuTK; namespace osu.Game.Rulesets.Mania.UI { + [Cached] public class Column : ScrollingPlayfield, IKeyBindingHandler, IHasAccentColour { public const float COLUMN_WIDTH = 80; diff --git a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs b/osu.Game.Rulesets.Mania/UI/ManiaStage.cs index bfe9f1085b..bd21663c4e 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaStage.cs @@ -23,6 +23,7 @@ namespace osu.Game.Rulesets.Mania.UI /// /// A collection of s. /// + [Cached] public class ManiaStage : ScrollingPlayfield { public const float COLUMN_SPACING = 1; From c1789140d5aa964c5ee9525aef2f76ae8816ae9d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 30 Mar 2020 23:17:32 +0900 Subject: [PATCH 0694/2376] Prepare skin transformer for mania components --- .../Skinning/ManiaLegacySkinTransformer.cs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs index f3739ce7c2..444f153c66 100644 --- a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.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.Graphics; using osu.Framework.Graphics.Textures; using osu.Framework.Audio.Sample; @@ -15,9 +16,19 @@ namespace osu.Game.Rulesets.Mania.Skinning { private readonly ISkin source; - public ManiaLegacySkinTransformer(ISkin source) + private Lazy isLegacySkin; + + public ManiaLegacySkinTransformer(ISkinSource source) { this.source = source; + + source.SourceChanged += sourceChanged; + sourceChanged(); + } + + private void sourceChanged() + { + isLegacySkin = new Lazy(() => source.GetConfig(LegacySkinConfiguration.LegacySetting.Version) != null); } public Drawable GetDrawableComponent(ISkinComponent component) @@ -26,6 +37,12 @@ namespace osu.Game.Rulesets.Mania.Skinning { case GameplaySkinComponent resultComponent: return getResult(resultComponent); + + case ManiaSkinComponent maniaComponent: + if (!isLegacySkin.Value) + return null; + + break; } return null; From c3cde7a16383909f3e1e42abc61fcc935568e1ce Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 30 Mar 2020 23:18:38 +0900 Subject: [PATCH 0695/2376] Combine files --- osu.Game.Rulesets.Mania/ManiaSkinComponent.cs | 4 ++++ osu.Game.Rulesets.Mania/ManiaSkinComponents.cs | 9 --------- 2 files changed, 4 insertions(+), 9 deletions(-) delete mode 100644 osu.Game.Rulesets.Mania/ManiaSkinComponents.cs diff --git a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs index 69bd4b0ecf..5340ebc01f 100644 --- a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs +++ b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs @@ -16,4 +16,8 @@ namespace osu.Game.Rulesets.Mania protected override string ComponentName => Component.ToString().ToLower(); } + + public enum ManiaSkinComponents + { + } } diff --git a/osu.Game.Rulesets.Mania/ManiaSkinComponents.cs b/osu.Game.Rulesets.Mania/ManiaSkinComponents.cs deleted file mode 100644 index 6d85816e5a..0000000000 --- a/osu.Game.Rulesets.Mania/ManiaSkinComponents.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -namespace osu.Game.Rulesets.Mania -{ - public enum ManiaSkinComponents - { - } -} From a8f7d7ea422ba684ed3d88e53c0907f9c188c69c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 30 Mar 2020 23:21:10 +0900 Subject: [PATCH 0696/2376] Add structure for mania configuration lookups --- .../Skinning/ManiaLegacySkinTransformer.cs | 2 +- .../LegacyManiaSkinConfigurationLookup.cs | 23 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs index 444f153c66..ffc69fae49 100644 --- a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Mania.Skinning case GameplaySkinComponent resultComponent: return getResult(resultComponent); - case ManiaSkinComponent maniaComponent: + case ManiaSkinComponent _: if (!isLegacySkin.Value) return null; diff --git a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs new file mode 100644 index 0000000000..bbdd445f66 --- /dev/null +++ b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Skinning +{ + public class LegacyManiaSkinConfigurationLookup + { + public readonly int Keys; + public readonly LegacyManiaSkinConfigurationLookups Lookup; + public readonly int? TargetColumn; + + public LegacyManiaSkinConfigurationLookup(int keys, LegacyManiaSkinConfigurationLookups lookup, int? targetColumn = null) + { + Keys = keys; + Lookup = lookup; + TargetColumn = targetColumn; + } + } + + public enum LegacyManiaSkinConfigurationLookups + { + } +} From 522bbc1e9c4a84209531fb5538b5d9a2ad799445 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Mar 2020 21:37:20 +0200 Subject: [PATCH 0697/2376] Support widescreen per-layer storyboard masking --- .../Drawables/DrawableStoryboard.cs | 2 +- .../Drawables/DrawableStoryboardLayer.cs | 33 ++++++++++++++----- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs index bc6e01a729..c4d796e30b 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs @@ -50,7 +50,7 @@ namespace osu.Game.Storyboards.Drawables AddInternal(Content = new Container { - Size = new Vector2(640, 480), + RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, }); diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardLayer.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardLayer.cs index def4eed2ca..2ada83c3b4 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardLayer.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardLayer.cs @@ -8,7 +8,7 @@ using osu.Framework.Graphics.Containers; namespace osu.Game.Storyboards.Drawables { - public class DrawableStoryboardLayer : LifetimeManagementContainer + public class DrawableStoryboardLayer : CompositeDrawable { public StoryboardLayer Layer { get; } public bool Enabled; @@ -23,17 +23,34 @@ namespace osu.Game.Storyboards.Drawables Origin = Anchor.Centre; Enabled = layer.VisibleWhenPassing; Masking = layer.Masking; + + InternalChild = new LayerElementContainer(layer); } - [BackgroundDependencyLoader] - private void load(CancellationToken? cancellationToken) + private class LayerElementContainer : LifetimeManagementContainer { - foreach (var element in Layer.Elements) - { - cancellationToken?.ThrowIfCancellationRequested(); + private readonly StoryboardLayer storyboardLayer; - if (element.IsDrawable) - AddInternal(element.CreateDrawable()); + public LayerElementContainer(StoryboardLayer layer) + { + storyboardLayer = layer; + + Width = 640; + Height = 480; + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + } + + [BackgroundDependencyLoader] + private void load(CancellationToken? cancellationToken) + { + foreach (var element in storyboardLayer.Elements) + { + cancellationToken?.ThrowIfCancellationRequested(); + + if (element.IsDrawable) + AddInternal(element.CreateDrawable()); + } } } } From f6f5de7ad16fd416bdbb7ad11fa205be085f66c6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 31 Mar 2020 10:13:50 +0900 Subject: [PATCH 0698/2376] Allow LineBufferedReader to keep underlying stream open --- osu.Game/IO/LineBufferedReader.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/IO/LineBufferedReader.cs b/osu.Game/IO/LineBufferedReader.cs index aab761afd8..018321dc9a 100644 --- a/osu.Game/IO/LineBufferedReader.cs +++ b/osu.Game/IO/LineBufferedReader.cs @@ -17,9 +17,9 @@ namespace osu.Game.IO private readonly StreamReader streamReader; private readonly Queue lineBuffer; - public LineBufferedReader(Stream stream) + public LineBufferedReader(Stream stream, bool leaveOpen = false) { - streamReader = new StreamReader(stream); + streamReader = new StreamReader(stream, Encoding.UTF8, true, 1024, leaveOpen); lineBuffer = new Queue(); } From 2b5e9885f6df85c45c0062fe24956af5e92b85c4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 31 Mar 2020 10:14:36 +0900 Subject: [PATCH 0699/2376] Implement mania skin reading functionality --- osu.Game/Skinning/LegacyBeatmapSkin.cs | 2 ++ osu.Game/Skinning/LegacySkin.cs | 38 +++++++++++++++++++++----- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/osu.Game/Skinning/LegacyBeatmapSkin.cs b/osu.Game/Skinning/LegacyBeatmapSkin.cs index fa7e895a28..1c39fc41bb 100644 --- a/osu.Game/Skinning/LegacyBeatmapSkin.cs +++ b/osu.Game/Skinning/LegacyBeatmapSkin.cs @@ -9,6 +9,8 @@ namespace osu.Game.Skinning { public class LegacyBeatmapSkin : LegacySkin { + protected override bool AllowManiaSkin => false; + public LegacyBeatmapSkin(BeatmapInfo beatmap, IResourceStore storage, AudioManager audioManager) : base(createSkinInfo(beatmap), new LegacySkinResourceStore(beatmap.BeatmapSet, storage), audioManager, beatmap.Path) { diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index c71a321e74..fe190740b3 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -26,12 +26,16 @@ namespace osu.Game.Skinning [CanBeNull] protected IResourceStore Samples; + protected virtual bool AllowManiaSkin => true; + public new LegacySkinConfiguration Configuration { get => base.Configuration as LegacySkinConfiguration; set => base.Configuration = value; } + private readonly Dictionary maniaConfigurations = new Dictionary(); + public LegacySkin(SkinInfo skin, IResourceStore storage, AudioManager audioManager) : this(skin, new LegacySkinResourceStore(skin, storage), audioManager, "skin.ini") { @@ -40,15 +44,26 @@ namespace osu.Game.Skinning protected LegacySkin(SkinInfo skin, IResourceStore storage, AudioManager audioManager, string filename) : base(skin) { - Stream stream = storage?.GetStream(filename); - - if (stream != null) + using (var stream = storage?.GetStream(filename)) { - using (LineBufferedReader reader = new LineBufferedReader(stream)) - Configuration = new LegacySkinDecoder().Decode(reader); + if (stream != null) + { + using (LineBufferedReader reader = new LineBufferedReader(stream, true)) + Configuration = new LegacySkinDecoder().Decode(reader); + + stream.Seek(0, SeekOrigin.Begin); + + using (LineBufferedReader reader = new LineBufferedReader(stream)) + { + var maniaList = new LegacyManiaSkinDecoder().Decode(reader); + + foreach (var config in maniaList) + maniaConfigurations[config.Keys] = config; + } + } + else + Configuration = new LegacySkinConfiguration { LegacyVersion = LegacySkinConfiguration.LATEST_VERSION }; } - else - Configuration = new LegacySkinConfiguration { LegacyVersion = LegacySkinConfiguration.LATEST_VERSION }; if (storage != null) { @@ -105,6 +120,15 @@ namespace osu.Game.Skinning case SkinCustomColourLookup customColour: return SkinUtils.As(getCustomColour(customColour.Lookup.ToString())); + case LegacyManiaSkinConfigurationLookup legacy: + if (!AllowManiaSkin) + return null; + + if (!maniaConfigurations.TryGetValue(legacy.Keys, out _)) + maniaConfigurations[legacy.Keys] = new LegacyManiaSkinConfiguration(legacy.Keys); + + break; + default: // handles lookups like GlobalSkinConfiguration From 44727eb2b831c51cb339ed1ca1c2dfd96387fbb4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 30 Mar 2020 23:14:30 +0900 Subject: [PATCH 0700/2376] Implement column background skinning --- .../Skinning/TestSceneColumnBackground.cs | 49 +++++++ osu.Game.Rulesets.Mania/ManiaSkinComponent.cs | 1 + .../Skinning/LegacyColumnBackground.cs | 133 ++++++++++++++++++ .../Skinning/ManiaLegacySkinTransformer.cs | 8 +- osu.Game.Rulesets.Mania/UI/Column.cs | 8 +- .../UI/Components/DefaultColumnBackground.cs | 90 ++++++++++++ .../LegacyManiaSkinConfigurationLookup.cs | 3 + 7 files changed, 288 insertions(+), 4 deletions(-) create mode 100644 osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnBackground.cs create mode 100644 osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs create mode 100644 osu.Game.Rulesets.Mania/UI/Components/DefaultColumnBackground.cs diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnBackground.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnBackground.cs new file mode 100644 index 0000000000..ca323b5911 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnBackground.cs @@ -0,0 +1,49 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Mania.UI.Components; +using osu.Game.Skinning; +using osuTK; + +namespace osu.Game.Rulesets.Mania.Tests.Skinning +{ + public class TestSceneColumnBackground : ManiaSkinnableTestScene + { + [BackgroundDependencyLoader] + private void load() + { + SetContents(() => new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.8f), + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + new ColumnTestContainer(0, ManiaAction.Key1) + { + RelativeSizeAxes = Axes.Both, + Width = 0.5f, + Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground), _ => new DefaultColumnBackground()) + { + RelativeSizeAxes = Axes.Both + } + }, + new ColumnTestContainer(1, ManiaAction.Key2) + { + RelativeSizeAxes = Axes.Both, + Width = 0.5f, + Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground), _ => new DefaultColumnBackground()) + { + RelativeSizeAxes = Axes.Both + } + } + } + }); + } + } +} diff --git a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs index 5340ebc01f..ca932c5319 100644 --- a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs +++ b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs @@ -19,5 +19,6 @@ namespace osu.Game.Rulesets.Mania public enum ManiaSkinComponents { + ColumnBackground } } diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs new file mode 100644 index 0000000000..96b28964d3 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs @@ -0,0 +1,133 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Bindings; +using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Skinning; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Mania.Skinning +{ + public class LegacyColumnBackground : CompositeDrawable, IKeyBindingHandler + { + private readonly IBindable direction = new Bindable(); + + private Container lightContainer; + private Sprite light; + + [Resolved] + private Column column { get; set; } + + [Resolved(CanBeNull = true)] + private ManiaStage stage { get; set; } + + public LegacyColumnBackground() + { + RelativeSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load(ISkinSource skin, IScrollingInfo scrollingInfo) + { + string lightImage = skin.GetConfig( + new LegacyManiaSkinConfigurationLookup(stage?.Columns.Count ?? 4, LegacyManiaSkinConfigurationLookups.LightImage, 0))?.Value + ?? "mania-stage-light"; + + float leftLineWidth = skin.GetConfig( + new LegacyManiaSkinConfigurationLookup(stage?.Columns.Count ?? 4, LegacyManiaSkinConfigurationLookups.LeftLineWidth, column.Index)) + ?.Value ?? 1; + float rightLineWidth = skin.GetConfig( + new LegacyManiaSkinConfigurationLookup(stage?.Columns.Count ?? 4, LegacyManiaSkinConfigurationLookups.RightLineWidth, column.Index)) + ?.Value ?? 1; + + bool hasLeftLine = leftLineWidth > 0; + bool hasRightLine = rightLineWidth > 0 && skin.GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value >= 2.4m + || stage == null || column.Index == stage.Columns.Count - 1; + + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black + }, + new Box + { + RelativeSizeAxes = Axes.Y, + Width = leftLineWidth, + Alpha = hasLeftLine ? 1 : 0 + }, + new Box + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + RelativeSizeAxes = Axes.Y, + Width = rightLineWidth, + Alpha = hasRightLine ? 1 : 0 + }, + lightContainer = new Container + { + Origin = Anchor.BottomCentre, + RelativeSizeAxes = Axes.Both, + Child = light = new Sprite + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Texture = skin.GetTexture(lightImage), + RelativeSizeAxes = Axes.X, + Width = 1, + Alpha = 0 + } + } + }; + + direction.BindTo(scrollingInfo.Direction); + direction.BindValueChanged(onDirectionChanged, true); + } + + private void onDirectionChanged(ValueChangedEvent direction) + { + if (direction.NewValue == ScrollingDirection.Up) + { + lightContainer.Anchor = Anchor.TopCentre; + lightContainer.Scale = new Vector2(1, -1); + } + else + { + lightContainer.Anchor = Anchor.BottomCentre; + lightContainer.Scale = Vector2.One; + } + } + + public bool OnPressed(ManiaAction action) + { + if (action == column.Action.Value) + { + light.FadeIn(); + light.ScaleTo(Vector2.One); + } + + return false; + } + + public void OnReleased(ManiaAction action) + { + // Todo: Should be 400 * 100 / CurrentBPM + const double animation_length = 250; + + if (action == column.Action.Value) + { + light.FadeTo(0, animation_length); + light.ScaleTo(new Vector2(1, 0), animation_length); + } + } + } +} diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs index ffc69fae49..12145975f1 100644 --- a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs @@ -38,10 +38,16 @@ namespace osu.Game.Rulesets.Mania.Skinning case GameplaySkinComponent resultComponent: return getResult(resultComponent); - case ManiaSkinComponent _: + case ManiaSkinComponent maniaComponent: if (!isLegacySkin.Value) return null; + switch (maniaComponent.Component) + { + case ManiaSkinComponents.ColumnBackground: + return new LegacyColumnBackground(); + } + break; } diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 0eccd27944..70e2782a7b 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -15,6 +15,7 @@ using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; using osu.Game.Rulesets.Mania.UI.Components; using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Skinning; using osuTK; namespace osu.Game.Rulesets.Mania.UI @@ -32,7 +33,6 @@ namespace osu.Game.Rulesets.Mania.UI public readonly Bindable Action = new Bindable(); - private readonly ColumnBackground background; private readonly ColumnKeyArea keyArea; private readonly ColumnHitObjectArea hitObjectArea; @@ -46,7 +46,10 @@ namespace osu.Game.Rulesets.Mania.UI RelativeSizeAxes = Axes.Y; Width = COLUMN_WIDTH; - background = new ColumnBackground { RelativeSizeAxes = Axes.Both }; + Drawable background = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground), _ => new DefaultColumnBackground()) + { + RelativeSizeAxes = Axes.Both + }; Container hitTargetContainer; @@ -130,7 +133,6 @@ namespace osu.Game.Rulesets.Mania.UI accentColour = value; - background.AccentColour = value; keyArea.AccentColour = value; hitObjectArea.AccentColour = value; } diff --git a/osu.Game.Rulesets.Mania/UI/Components/DefaultColumnBackground.cs b/osu.Game.Rulesets.Mania/UI/Components/DefaultColumnBackground.cs new file mode 100644 index 0000000000..4b4bc157d5 --- /dev/null +++ b/osu.Game.Rulesets.Mania/UI/Components/DefaultColumnBackground.cs @@ -0,0 +1,90 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Bindings; +using osu.Game.Rulesets.UI.Scrolling; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Mania.UI.Components +{ + public class DefaultColumnBackground : CompositeDrawable, IKeyBindingHandler + { + private readonly IBindable direction = new Bindable(); + + private Color4 brightColour; + private Color4 dimColour; + + private Box background; + private Box backgroundOverlay; + + [Resolved] + private Column column { get; set; } + + public DefaultColumnBackground() + { + RelativeSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load(IScrollingInfo scrollingInfo) + { + InternalChildren = new[] + { + background = new Box + { + Name = "Background", + RelativeSizeAxes = Axes.Both, + }, + backgroundOverlay = new Box + { + Name = "Background Gradient Overlay", + RelativeSizeAxes = Axes.Both, + Height = 0.5f, + Blending = BlendingParameters.Additive, + Alpha = 0 + } + }; + + background.Colour = column.AccentColour.Darken(5); + brightColour = column.AccentColour.Opacity(0.6f); + dimColour = column.AccentColour.Opacity(0); + + direction.BindTo(scrollingInfo.Direction); + direction.BindValueChanged(onDirectionChanged, true); + } + + private void onDirectionChanged(ValueChangedEvent direction) + { + if (direction.NewValue == ScrollingDirection.Up) + { + backgroundOverlay.Anchor = backgroundOverlay.Origin = Anchor.TopLeft; + backgroundOverlay.Colour = ColourInfo.GradientVertical(brightColour, dimColour); + } + else + { + backgroundOverlay.Anchor = backgroundOverlay.Origin = Anchor.BottomLeft; + backgroundOverlay.Colour = ColourInfo.GradientVertical(dimColour, brightColour); + } + } + + public bool OnPressed(ManiaAction action) + { + if (action == column.Action.Value) + backgroundOverlay.FadeTo(1, 50, Easing.OutQuint).Then().FadeTo(0.5f, 250, Easing.OutQuint); + return false; + } + + public void OnReleased(ManiaAction action) + { + if (action == column.Action.Value) + backgroundOverlay.FadeTo(0, 250, Easing.OutQuint); + } + } +} diff --git a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs index bbdd445f66..9e83217afc 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs @@ -19,5 +19,8 @@ namespace osu.Game.Skinning public enum LegacyManiaSkinConfigurationLookups { + LightImage, + LeftLineWidth, + RightLineWidth } } From cb1513b37466189fad1044a429a80516200a12a1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 31 Mar 2020 11:23:33 +0900 Subject: [PATCH 0701/2376] Add mania key area skinning --- .../Skinning/TestSceneKeyArea.cs | 58 ++++++++ .../TestSceneColumn.cs | 1 - osu.Game.Rulesets.Mania/ManiaSkinComponent.cs | 1 + .../Skinning/LegacyKeyArea.cs | 113 ++++++++++++++++ .../Skinning/ManiaLegacySkinTransformer.cs | 8 +- osu.Game.Rulesets.Mania/UI/Column.cs | 10 +- .../UI/Components/ColumnKeyArea.cs | 124 ------------------ .../UI/Components/DefaultKeyArea.cs | 114 ++++++++++++++++ .../LegacyManiaSkinConfigurationLookup.cs | 2 + 9 files changed, 298 insertions(+), 133 deletions(-) create mode 100644 osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneKeyArea.cs create mode 100644 osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs delete mode 100644 osu.Game.Rulesets.Mania/UI/Components/ColumnKeyArea.cs create mode 100644 osu.Game.Rulesets.Mania/UI/Components/DefaultKeyArea.cs diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneKeyArea.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneKeyArea.cs new file mode 100644 index 0000000000..1e6f00205a --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneKeyArea.cs @@ -0,0 +1,58 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Mania.Skinning; +using osu.Game.Rulesets.Mania.UI.Components; +using osu.Game.Skinning; +using osuTK; + +namespace osu.Game.Rulesets.Mania.Tests.Skinning +{ + public class TestSceneKeyArea : ManiaSkinnableTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(DefaultKeyArea), + typeof(LegacyKeyArea) + }; + + [BackgroundDependencyLoader] + private void load() + { + SetContents(() => new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.8f), + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + new ColumnTestContainer(0, ManiaAction.Key1) + { + RelativeSizeAxes = Axes.Both, + Width = 0.5f, + Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea), _ => new DefaultKeyArea()) + { + RelativeSizeAxes = Axes.Both + }, + }, + new ColumnTestContainer(1, ManiaAction.Key2) + { + RelativeSizeAxes = Axes.Both, + Width = 0.5f, + Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea), _ => new DefaultKeyArea()) + { + RelativeSizeAxes = Axes.Both + }, + }, + } + }); + } + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneColumn.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneColumn.cs index d94a986dae..9aad08c433 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneColumn.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneColumn.cs @@ -28,7 +28,6 @@ namespace osu.Game.Rulesets.Mania.Tests { typeof(Column), typeof(ColumnBackground), - typeof(ColumnKeyArea), typeof(ColumnHitObjectArea) }; diff --git a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs index 5340ebc01f..da5993ef26 100644 --- a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs +++ b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs @@ -19,5 +19,6 @@ namespace osu.Game.Rulesets.Mania public enum ManiaSkinComponents { + KeyArea } } diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs new file mode 100644 index 0000000000..8a57953d60 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.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 osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Bindings; +using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Skinning; +using osuTK; + +namespace osu.Game.Rulesets.Mania.Skinning +{ + public class LegacyKeyArea : CompositeDrawable, IKeyBindingHandler + { + private readonly IBindable direction = new Bindable(); + + private Container directionContainer; + private Sprite upSprite; + private Sprite downSprite; + + [Resolved(CanBeNull = true)] + private ManiaStage stage { get; set; } + + [Resolved] + private Column column { get; set; } + + public LegacyKeyArea() + { + RelativeSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load(ISkinSource skin, IScrollingInfo scrollingInfo) + { + int fallbackColumn = column.Index % 2 + 1; + + string upImage = skin.GetConfig( + new LegacyManiaSkinConfigurationLookup(stage?.Columns.Count ?? 4, LegacyManiaSkinConfigurationLookups.KeyImage, column.Index))?.Value + ?? $"mania-key{fallbackColumn}"; + + string downImage = skin.GetConfig( + new LegacyManiaSkinConfigurationLookup(stage?.Columns.Count ?? 4, LegacyManiaSkinConfigurationLookups.KeyImageDown, column.Index))?.Value + ?? $"mania-key{fallbackColumn}D"; + + InternalChild = directionContainer = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + upSprite = new Sprite + { + Origin = Anchor.BottomCentre, + Texture = skin.GetTexture(upImage), + RelativeSizeAxes = Axes.X, + Width = 1 + }, + downSprite = new Sprite + { + Origin = Anchor.BottomCentre, + Texture = skin.GetTexture(downImage), + RelativeSizeAxes = Axes.X, + Width = 1, + Alpha = 0 + } + } + }; + + direction.BindTo(scrollingInfo.Direction); + direction.BindValueChanged(onDirectionChanged, true); + } + + private void onDirectionChanged(ValueChangedEvent direction) + { + if (direction.NewValue == ScrollingDirection.Up) + { + directionContainer.Anchor = directionContainer.Origin = Anchor.TopCentre; + upSprite.Anchor = downSprite.Anchor = Anchor.TopCentre; + upSprite.Scale = downSprite.Scale = new Vector2(1, -1); + } + else + { + directionContainer.Anchor = directionContainer.Origin = Anchor.BottomCentre; + upSprite.Anchor = downSprite.Anchor = Anchor.BottomCentre; + upSprite.Scale = downSprite.Scale = Vector2.One; + } + } + + public bool OnPressed(ManiaAction action) + { + if (action == column.Action.Value) + { + upSprite.FadeTo(0); + downSprite.FadeTo(1); + } + + return false; + } + + public void OnReleased(ManiaAction action) + { + if (action == column.Action.Value) + { + upSprite.FadeTo(1); + downSprite.FadeTo(0); + } + } + } +} diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs index ffc69fae49..b71e7b9f14 100644 --- a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs @@ -38,10 +38,16 @@ namespace osu.Game.Rulesets.Mania.Skinning case GameplaySkinComponent resultComponent: return getResult(resultComponent); - case ManiaSkinComponent _: + case ManiaSkinComponent maniaComponent: if (!isLegacySkin.Value) return null; + switch (maniaComponent.Component) + { + case ManiaSkinComponents.KeyArea: + return new LegacyKeyArea(); + } + break; } diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 0eccd27944..62c1afde7d 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -15,6 +15,7 @@ using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; using osu.Game.Rulesets.Mania.UI.Components; using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Skinning; using osuTK; namespace osu.Game.Rulesets.Mania.UI @@ -33,7 +34,6 @@ namespace osu.Game.Rulesets.Mania.UI public readonly Bindable Action = new Bindable(); private readonly ColumnBackground background; - private readonly ColumnKeyArea keyArea; private readonly ColumnHitObjectArea hitObjectArea; internal readonly Container TopLevelContainer; @@ -71,10 +71,9 @@ namespace osu.Game.Rulesets.Mania.UI } } }, - keyArea = new ColumnKeyArea + new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea), _ => new DefaultKeyArea()) { - RelativeSizeAxes = Axes.X, - Height = ManiaStage.HIT_TARGET_POSITION, + RelativeSizeAxes = Axes.Both }, background, TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both } @@ -95,8 +94,6 @@ namespace osu.Game.Rulesets.Mania.UI Top = dir.NewValue == ScrollingDirection.Up ? NotePiece.NOTE_HEIGHT / 2 : 0, Bottom = dir.NewValue == ScrollingDirection.Down ? NotePiece.NOTE_HEIGHT / 2 : 0 }; - - keyArea.Anchor = keyArea.Origin = dir.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft; }, true); } @@ -131,7 +128,6 @@ namespace osu.Game.Rulesets.Mania.UI accentColour = value; background.AccentColour = value; - keyArea.AccentColour = value; hitObjectArea.AccentColour = value; } } diff --git a/osu.Game.Rulesets.Mania/UI/Components/ColumnKeyArea.cs b/osu.Game.Rulesets.Mania/UI/Components/ColumnKeyArea.cs deleted file mode 100644 index 60fc2713b3..0000000000 --- a/osu.Game.Rulesets.Mania/UI/Components/ColumnKeyArea.cs +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Colour; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Effects; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Input.Bindings; -using osu.Game.Graphics; -using osu.Game.Rulesets.UI.Scrolling; -using osuTK; -using osuTK.Graphics; - -namespace osu.Game.Rulesets.Mania.UI.Components -{ - public class ColumnKeyArea : CompositeDrawable, IKeyBindingHandler, IHasAccentColour - { - private const float key_icon_size = 10; - private const float key_icon_corner_radius = 3; - - private readonly IBindable action = new Bindable(); - private readonly IBindable direction = new Bindable(); - - private Container keyIcon; - - [BackgroundDependencyLoader] - private void load(IBindable action, IScrollingInfo scrollingInfo) - { - this.action.BindTo(action); - - Drawable gradient; - - InternalChildren = new[] - { - gradient = new Box - { - Name = "Key gradient", - RelativeSizeAxes = Axes.Both, - Alpha = 0.5f - }, - keyIcon = new Container - { - Name = "Key icon", - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(key_icon_size), - Masking = true, - CornerRadius = key_icon_corner_radius, - BorderThickness = 2, - BorderColour = Color4.White, // Not true - Children = new[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - AlwaysPresent = true - } - } - } - }; - - direction.BindTo(scrollingInfo.Direction); - direction.BindValueChanged(dir => - { - gradient.Colour = ColourInfo.GradientVertical( - dir.NewValue == ScrollingDirection.Up ? Color4.Black : Color4.Black.Opacity(0), - dir.NewValue == ScrollingDirection.Up ? Color4.Black.Opacity(0) : Color4.Black); - }, true); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - updateColours(); - } - - private Color4 accentColour; - - public Color4 AccentColour - { - get => accentColour; - set - { - if (accentColour == value) - return; - - accentColour = value; - - updateColours(); - } - } - - private void updateColours() - { - if (!IsLoaded) - return; - - keyIcon.EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Glow, - Radius = 5, - Colour = accentColour.Opacity(0.5f), - }; - } - - public bool OnPressed(ManiaAction action) - { - if (action == this.action.Value) - keyIcon.ScaleTo(1.4f, 50, Easing.OutQuint).Then().ScaleTo(1.3f, 250, Easing.OutQuint); - return false; - } - - public void OnReleased(ManiaAction action) - { - if (action == this.action.Value) - keyIcon.ScaleTo(1f, 125, Easing.OutQuint); - } - } -} diff --git a/osu.Game.Rulesets.Mania/UI/Components/DefaultKeyArea.cs b/osu.Game.Rulesets.Mania/UI/Components/DefaultKeyArea.cs new file mode 100644 index 0000000000..982a18cb60 --- /dev/null +++ b/osu.Game.Rulesets.Mania/UI/Components/DefaultKeyArea.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 osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Bindings; +using osu.Game.Rulesets.UI.Scrolling; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Mania.UI.Components +{ + public class DefaultKeyArea : CompositeDrawable, IKeyBindingHandler + { + private const float key_icon_size = 10; + private const float key_icon_corner_radius = 3; + + private readonly IBindable direction = new Bindable(); + + private Container directionContainer; + private Container keyIcon; + private Drawable gradient; + + [Resolved] + private Column column { get; set; } + + public DefaultKeyArea() + { + RelativeSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load(IScrollingInfo scrollingInfo) + { + InternalChild = directionContainer = new Container + { + RelativeSizeAxes = Axes.X, + Height = ManiaStage.HIT_TARGET_POSITION, + Children = new[] + { + gradient = new Box + { + Name = "Key gradient", + RelativeSizeAxes = Axes.Both, + Alpha = 0.5f + }, + keyIcon = new Container + { + Name = "Key icon", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(key_icon_size), + Masking = true, + CornerRadius = key_icon_corner_radius, + BorderThickness = 2, + BorderColour = Color4.White, // Not true + Children = new[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true + } + } + } + } + }; + + keyIcon.EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Radius = 5, + Colour = column.AccentColour.Opacity(0.5f), + }; + + direction.BindTo(scrollingInfo.Direction); + direction.BindValueChanged(onDirectionChanged, true); + } + + private void onDirectionChanged(ValueChangedEvent direction) + { + if (direction.NewValue == ScrollingDirection.Up) + { + directionContainer.Anchor = directionContainer.Origin = Anchor.TopLeft; + gradient.Colour = ColourInfo.GradientVertical(Color4.Black, Color4.Black.Opacity(0)); + } + else + { + directionContainer.Anchor = directionContainer.Origin = Anchor.BottomLeft; + gradient.Colour = ColourInfo.GradientVertical(Color4.Black.Opacity(0), Color4.Black); + } + } + + public bool OnPressed(ManiaAction action) + { + if (action == column.Action.Value) + keyIcon.ScaleTo(1.4f, 50, Easing.OutQuint).Then().ScaleTo(1.3f, 250, Easing.OutQuint); + return false; + } + + public void OnReleased(ManiaAction action) + { + if (action == column.Action.Value) + keyIcon.ScaleTo(1f, 125, Easing.OutQuint); + } + } +} diff --git a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs index bbdd445f66..bdb016d3b1 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs @@ -19,5 +19,7 @@ namespace osu.Game.Skinning public enum LegacyManiaSkinConfigurationLookups { + KeyImage, + KeyImageDown } } From 6d4f9247ea5e36d163c05b5fecc4b84f6a0447fc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 31 Mar 2020 11:49:18 +0900 Subject: [PATCH 0702/2376] Revert "Remove osu!catch GotoFrame usage" This reverts commit 4406f441654726dd21c810349b0eeb6935ba7d65. --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 5 +---- osu.Game/Skinning/LegacySkinExtensions.cs | 2 -- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index bc0311bd2d..e361b29a9d 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; -using osu.Framework.Timing; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Objects; @@ -380,9 +379,7 @@ namespace osu.Game.Rulesets.Catch.UI } currentCatcher.Show(); - - if (currentCatcher.Drawable.Clock is FramedOffsetClock offsetClock) - offsetClock.Offset = -Time.Current; + (currentCatcher.Drawable as IAnimation)?.GotoFrame(0); } private void beginTrail() diff --git a/osu.Game/Skinning/LegacySkinExtensions.cs b/osu.Game/Skinning/LegacySkinExtensions.cs index de0add6ba3..8765b161d4 100644 --- a/osu.Game/Skinning/LegacySkinExtensions.cs +++ b/osu.Game/Skinning/LegacySkinExtensions.cs @@ -71,8 +71,6 @@ namespace osu.Game.Skinning if (timeReference != null) Clock = new FramedOffsetClock(timeReference.Clock) { Offset = -timeReference.AnimationStartTime }; - else - Clock = new FramedOffsetClock(Clock); } } From 02237133cb2bc7421363c4f839bebfc784fbd54c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 31 Mar 2020 12:17:44 +0900 Subject: [PATCH 0703/2376] Implement mania hit target skinning --- .../Skinning/TestSceneColumnHitObjectArea.cs | 49 +++++++ osu.Game.Rulesets.Mania/ManiaSkinComponent.cs | 1 + .../Skinning/LegacyHitTarget.cs | 69 ++++++++++ .../Skinning/ManiaLegacySkinTransformer.cs | 8 +- osu.Game.Rulesets.Mania/UI/Column.cs | 41 +----- .../UI/Components/ColumnHitObjectArea.cs | 129 +++++------------- .../UI/Components/DefaultHitTarget.cs | 80 +++++++++++ osu.Game.Rulesets.Mania/UI/HitExplosion.cs | 2 +- .../LegacyManiaSkinConfigurationLookup.cs | 2 + osu.Game/Skinning/LegacySkin.cs | 10 +- 10 files changed, 252 insertions(+), 139 deletions(-) create mode 100644 osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnHitObjectArea.cs create mode 100644 osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs create mode 100644 osu.Game.Rulesets.Mania/UI/Components/DefaultHitTarget.cs diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnHitObjectArea.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnHitObjectArea.cs new file mode 100644 index 0000000000..5d05bca03e --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnHitObjectArea.cs @@ -0,0 +1,49 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Mania.UI.Components; +using osu.Game.Rulesets.UI; +using osuTK; + +namespace osu.Game.Rulesets.Mania.Tests.Skinning +{ + public class TestSceneColumnHitObjectArea : ManiaSkinnableTestScene + { + [BackgroundDependencyLoader] + private void load() + { + SetContents(() => new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.8f), + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + new ColumnTestContainer(0, ManiaAction.Key1) + { + RelativeSizeAxes = Axes.Both, + Width = 0.5f, + Child = new ColumnHitObjectArea(new HitObjectContainer()) + { + RelativeSizeAxes = Axes.Both + } + }, + new ColumnTestContainer(1, ManiaAction.Key2) + { + RelativeSizeAxes = Axes.Both, + Width = 0.5f, + Child = new ColumnHitObjectArea(new HitObjectContainer()) + { + RelativeSizeAxes = Axes.Both + } + } + } + }); + } + } +} diff --git a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs index 5340ebc01f..efea386801 100644 --- a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs +++ b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs @@ -19,5 +19,6 @@ namespace osu.Game.Rulesets.Mania public enum ManiaSkinComponents { + HitTarget } } diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs new file mode 100644 index 0000000000..667245ce2e --- /dev/null +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs @@ -0,0 +1,69 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Skinning; +using osuTK; + +namespace osu.Game.Rulesets.Mania.Skinning +{ + public class LegacyHitTarget : CompositeDrawable + { + private readonly IBindable direction = new Bindable(); + + [Resolved(CanBeNull = true)] + private ManiaStage stage { get; set; } + + private Container directionContainer; + + public LegacyHitTarget() + { + RelativeSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load(ISkinSource skin, IScrollingInfo scrollingInfo) + { + string targetImage = skin.GetConfig( + new LegacyManiaSkinConfigurationLookup(stage?.Columns.Count ?? 4, LegacyManiaSkinConfigurationLookups.HitTargetImage))?.Value + ?? "mania-stage-hint"; + + InternalChild = directionContainer = new Container + { + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Child = new Sprite + { + Texture = skin.GetTexture(targetImage), + Scale = new Vector2(1, 0.9f * 1.6025f), + RelativeSizeAxes = Axes.X, + Width = 1 + } + }; + + direction.BindTo(scrollingInfo.Direction); + direction.BindValueChanged(onDirectionChanged, true); + } + + private void onDirectionChanged(ValueChangedEvent direction) + { + if (direction.NewValue == ScrollingDirection.Up) + { + directionContainer.Anchor = Anchor.TopLeft; + directionContainer.Scale = new Vector2(1, -1); + } + else + { + directionContainer.Anchor = Anchor.BottomLeft; + directionContainer.Scale = Vector2.One; + } + } + } +} diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs index ffc69fae49..b7b515241e 100644 --- a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs @@ -38,10 +38,16 @@ namespace osu.Game.Rulesets.Mania.Skinning case GameplaySkinComponent resultComponent: return getResult(resultComponent); - case ManiaSkinComponent _: + case ManiaSkinComponent maniaComponent: if (!isLegacySkin.Value) return null; + switch (maniaComponent.Component) + { + case ManiaSkinComponents.HitTarget: + return new LegacyHitTarget(); + } + break; } diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 0eccd27944..7d064657f4 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -12,7 +12,6 @@ using osu.Framework.Bindables; using osu.Framework.Input.Bindings; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mania.Objects.Drawables; -using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; using osu.Game.Rulesets.Mania.UI.Components; using osu.Game.Rulesets.UI.Scrolling; using osuTK; @@ -32,12 +31,11 @@ namespace osu.Game.Rulesets.Mania.UI public readonly Bindable Action = new Bindable(); + private readonly ColumnHitObjectArea hitObjectArea; private readonly ColumnBackground background; private readonly ColumnKeyArea keyArea; - private readonly ColumnHitObjectArea hitObjectArea; internal readonly Container TopLevelContainer; - private readonly Container explosionContainer; public Column(int index) { @@ -48,29 +46,11 @@ namespace osu.Game.Rulesets.Mania.UI background = new ColumnBackground { RelativeSizeAxes = Axes.Both }; - Container hitTargetContainer; - InternalChildren = new[] { // For input purposes, the background is added at the highest depth, but is then proxied back below all other elements background.CreateProxy(), - hitTargetContainer = new Container - { - Name = "Hit target + hit objects", - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - hitObjectArea = new ColumnHitObjectArea(HitObjectContainer) - { - RelativeSizeAxes = Axes.Both, - }, - explosionContainer = new Container - { - Name = "Hit explosions", - RelativeSizeAxes = Axes.Both, - } - } - }, + hitObjectArea = new ColumnHitObjectArea(HitObjectContainer) { RelativeSizeAxes = Axes.Both }, keyArea = new ColumnKeyArea { RelativeSizeAxes = Axes.X, @@ -80,22 +60,10 @@ namespace osu.Game.Rulesets.Mania.UI TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both } }; - TopLevelContainer.Add(explosionContainer.CreateProxy()); + TopLevelContainer.Add(hitObjectArea.Explosions.CreateProxy()); Direction.BindValueChanged(dir => { - hitTargetContainer.Padding = new MarginPadding - { - Top = dir.NewValue == ScrollingDirection.Up ? ManiaStage.HIT_TARGET_POSITION : 0, - Bottom = dir.NewValue == ScrollingDirection.Down ? ManiaStage.HIT_TARGET_POSITION : 0, - }; - - explosionContainer.Padding = new MarginPadding - { - Top = dir.NewValue == ScrollingDirection.Up ? NotePiece.NOTE_HEIGHT / 2 : 0, - Bottom = dir.NewValue == ScrollingDirection.Down ? NotePiece.NOTE_HEIGHT / 2 : 0 - }; - keyArea.Anchor = keyArea.Origin = dir.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft; }, true); } @@ -132,7 +100,6 @@ namespace osu.Game.Rulesets.Mania.UI background.AccentColour = value; keyArea.AccentColour = value; - hitObjectArea.AccentColour = value; } } @@ -169,7 +136,7 @@ namespace osu.Game.Rulesets.Mania.UI if (!result.IsHit || !judgedObject.DisplayResult || !DisplayJudgements.Value) return; - explosionContainer.Add(new HitExplosion(judgedObject.AccentColour.Value, judgedObject is DrawableHoldNoteTick) + hitObjectArea.Explosions.Add(new HitExplosion(judgedObject.AccentColour.Value, judgedObject is DrawableHoldNoteTick) { Anchor = Direction.Value == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre, Origin = Anchor.Centre diff --git a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs index 90e78c3899..51928f8b66 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs @@ -3,34 +3,35 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Effects; -using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics; using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; -using osuTK.Graphics; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Mania.UI.Components { - public class ColumnHitObjectArea : CompositeDrawable, IHasAccentColour + public class ColumnHitObjectArea : SkinReloadableDrawable { - private readonly IBindable direction = new Bindable(); + public readonly Container Explosions; + [Resolved(CanBeNull = true)] + private ManiaStage stage { get; set; } + + private readonly IBindable direction = new Bindable(); private readonly Drawable hitTarget; public ColumnHitObjectArea(HitObjectContainer hitObjectContainer) { InternalChildren = new[] { - hitTarget = new DefaultHitTarget + hitTarget = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitTarget), _ => new DefaultHitTarget()) { RelativeSizeAxes = Axes.X, }, - hitObjectContainer + hitObjectContainer, + Explosions = new Container { RelativeSizeAxes = Axes.Both } }; } @@ -38,107 +39,39 @@ namespace osu.Game.Rulesets.Mania.UI.Components private void load(IScrollingInfo scrollingInfo) { direction.BindTo(scrollingInfo.Direction); - direction.BindValueChanged(dir => - { - Anchor anchor = dir.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft; - - hitTarget.Anchor = hitTarget.Origin = anchor; - }, true); + direction.BindValueChanged(onDirectionChanged, true); } - private Color4 accentColour; - - public Color4 AccentColour + protected override void SkinChanged(ISkinSource skin, bool allowFallback) { - get => accentColour; - set - { - if (accentColour == value) - return; - - accentColour = value; - - if (hitTarget is IHasAccentColour colouredHitTarget) - colouredHitTarget.AccentColour = accentColour; - } + base.SkinChanged(skin, allowFallback); + updateHitPosition(); } - private class DefaultHitTarget : CompositeDrawable, IHasAccentColour + private void onDirectionChanged(ValueChangedEvent direction) { - private const float hit_target_bar_height = 2; + updateHitPosition(); + } - private readonly IBindable direction = new Bindable(); + private void updateHitPosition() + { + float hitPosition = CurrentSkin.GetConfig( + new LegacyManiaSkinConfigurationLookup(stage?.Columns.Count ?? 4, LegacyManiaSkinConfigurationLookups.HitPosition))?.Value + ?? ManiaStage.HIT_TARGET_POSITION; - private readonly Container hitTargetLine; - private readonly Drawable hitTargetBar; - - public DefaultHitTarget() + if (direction.Value == ScrollingDirection.Up) { - InternalChildren = new[] - { - hitTargetBar = new Box - { - RelativeSizeAxes = Axes.X, - Height = NotePiece.NOTE_HEIGHT, - Alpha = 0.6f, - Colour = Color4.Black - }, - hitTargetLine = new Container - { - RelativeSizeAxes = Axes.X, - Height = hit_target_bar_height, - Masking = true, - Child = new Box { RelativeSizeAxes = Axes.Both } - }, - }; + hitTarget.Anchor = hitTarget.Origin = Anchor.TopLeft; + + Padding = new MarginPadding { Top = hitPosition }; + Explosions.Padding = new MarginPadding { Top = NotePiece.NOTE_HEIGHT }; } - - [BackgroundDependencyLoader] - private void load(IScrollingInfo scrollingInfo) + else { - direction.BindTo(scrollingInfo.Direction); - direction.BindValueChanged(dir => - { - Anchor anchor = dir.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft; + hitTarget.Anchor = hitTarget.Origin = Anchor.BottomLeft; - hitTargetBar.Anchor = hitTargetBar.Origin = anchor; - hitTargetLine.Anchor = hitTargetLine.Origin = anchor; - }, true); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - updateColours(); - } - - private Color4 accentColour; - - public Color4 AccentColour - { - get => accentColour; - set - { - if (accentColour == value) - return; - - accentColour = value; - - updateColours(); - } - } - - private void updateColours() - { - if (!IsLoaded) - return; - - hitTargetLine.EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Glow, - Radius = 5, - Colour = accentColour.Opacity(0.5f), - }; + Padding = new MarginPadding { Bottom = hitPosition }; + Explosions.Padding = new MarginPadding { Bottom = NotePiece.NOTE_HEIGHT }; } } } diff --git a/osu.Game.Rulesets.Mania/UI/Components/DefaultHitTarget.cs b/osu.Game.Rulesets.Mania/UI/Components/DefaultHitTarget.cs new file mode 100644 index 0000000000..d96b4d864b --- /dev/null +++ b/osu.Game.Rulesets.Mania/UI/Components/DefaultHitTarget.cs @@ -0,0 +1,80 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; +using osu.Game.Rulesets.UI.Scrolling; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Mania.UI.Components +{ + public class DefaultHitTarget : CompositeDrawable + { + private const float hit_target_bar_height = 2; + + private readonly IBindable direction = new Bindable(); + + private Container hitTargetLine; + private Drawable hitTargetBar; + + [Resolved] + private Column column { get; set; } + + public DefaultHitTarget() + { + RelativeSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load(IScrollingInfo scrollingInfo) + { + InternalChildren = new[] + { + hitTargetBar = new Box + { + RelativeSizeAxes = Axes.X, + Height = NotePiece.NOTE_HEIGHT, + Alpha = 0.6f, + Colour = Color4.Black + }, + hitTargetLine = new Container + { + RelativeSizeAxes = Axes.X, + Height = hit_target_bar_height, + Masking = true, + Child = new Box { RelativeSizeAxes = Axes.Both } + }, + }; + + hitTargetLine.EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Radius = 5, + Colour = column.AccentColour.Opacity(0.5f), + }; + + direction.BindTo(scrollingInfo.Direction); + direction.BindValueChanged(onDirectionChanged, true); + } + + private void onDirectionChanged(ValueChangedEvent direction) + { + if (direction.NewValue == ScrollingDirection.Up) + { + hitTargetBar.Anchor = hitTargetBar.Origin = Anchor.TopLeft; + hitTargetLine.Anchor = hitTargetLine.Origin = Anchor.TopLeft; + } + else + { + hitTargetBar.Anchor = hitTargetBar.Origin = Anchor.BottomLeft; + hitTargetLine.Anchor = hitTargetLine.Origin = Anchor.BottomLeft; + } + } + } +} diff --git a/osu.Game.Rulesets.Mania/UI/HitExplosion.cs b/osu.Game.Rulesets.Mania/UI/HitExplosion.cs index 35de47e208..c26697fa79 100644 --- a/osu.Game.Rulesets.Mania/UI/HitExplosion.cs +++ b/osu.Game.Rulesets.Mania/UI/HitExplosion.cs @@ -12,7 +12,7 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Mania.UI { - internal class HitExplosion : CompositeDrawable + public class HitExplosion : CompositeDrawable { public override bool RemoveWhenNotAlive => true; diff --git a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs index bbdd445f66..72cbdb7a18 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs @@ -19,5 +19,7 @@ namespace osu.Game.Skinning public enum LegacyManiaSkinConfigurationLookups { + HitPosition, + HitTargetImage } } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index fe190740b3..75ce983b65 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -124,8 +124,14 @@ namespace osu.Game.Skinning if (!AllowManiaSkin) return null; - if (!maniaConfigurations.TryGetValue(legacy.Keys, out _)) - maniaConfigurations[legacy.Keys] = new LegacyManiaSkinConfiguration(legacy.Keys); + if (!maniaConfigurations.TryGetValue(legacy.Keys, out var existing)) + maniaConfigurations[legacy.Keys] = existing = new LegacyManiaSkinConfiguration(legacy.Keys); + + switch (legacy.Lookup) + { + case LegacyManiaSkinConfigurationLookups.HitPosition: + return SkinUtils.As(new Bindable(existing.HitPosition)); + } break; From 71387016b2f35b878a3812d8afcc7c60db815dd1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 31 Mar 2020 12:26:31 +0900 Subject: [PATCH 0704/2376] Add missing judgement line --- .../Skinning/LegacyHitTarget.cs | 25 +++++++++++++++---- .../Skinning/LegacyManiaSkinConfiguration.cs | 1 + .../LegacyManiaSkinConfigurationLookup.cs | 3 ++- osu.Game/Skinning/LegacyManiaSkinDecoder.cs | 4 +++ osu.Game/Skinning/LegacySkin.cs | 3 +++ 5 files changed, 30 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs index 667245ce2e..3e550808f3 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs @@ -5,6 +5,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.UI.Scrolling; @@ -34,17 +35,31 @@ namespace osu.Game.Rulesets.Mania.Skinning new LegacyManiaSkinConfigurationLookup(stage?.Columns.Count ?? 4, LegacyManiaSkinConfigurationLookups.HitTargetImage))?.Value ?? "mania-stage-hint"; + bool showJudgementLine = skin.GetConfig( + new LegacyManiaSkinConfigurationLookup(stage?.Columns.Count ?? 4, LegacyManiaSkinConfigurationLookups.ShowJudgementLine))?.Value + ?? true; + InternalChild = directionContainer = new Container { Origin = Anchor.CentreLeft, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Child = new Sprite + Children = new Drawable[] { - Texture = skin.GetTexture(targetImage), - Scale = new Vector2(1, 0.9f * 1.6025f), - RelativeSizeAxes = Axes.X, - Width = 1 + new Sprite + { + Texture = skin.GetTexture(targetImage), + Scale = new Vector2(1, 0.9f * 1.6025f), + RelativeSizeAxes = Axes.X, + Width = 1 + }, + new Box + { + Anchor = Anchor.CentreLeft, + RelativeSizeAxes = Axes.X, + Height = 1, + Alpha = showJudgementLine ? 0.9f : 0 + } } }; diff --git a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs index 5dd185879b..56d2652e76 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs @@ -14,6 +14,7 @@ namespace osu.Game.Skinning public readonly float[] ColumnWidth; public float HitPosition = 124.8f; // (480 - 402) * 1.6f + public bool ShowJudgementLine = true; public LegacyManiaSkinConfiguration(int keys) { diff --git a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs index 72cbdb7a18..33c88f3920 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs @@ -20,6 +20,7 @@ namespace osu.Game.Skinning public enum LegacyManiaSkinConfigurationLookups { HitPosition, - HitTargetImage + HitTargetImage, + ShowJudgementLine } } diff --git a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs index ae6c8eeb15..2c6b76847d 100644 --- a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs +++ b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs @@ -90,6 +90,10 @@ namespace osu.Game.Skinning case "HitPosition": currentConfig.HitPosition = (480 - float.Parse(pair.Value, CultureInfo.InvariantCulture)) * size_scale_factor; break; + + case "JudgementLine": + currentConfig.ShowJudgementLine = pair.Value == "1"; + break; } } } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 75ce983b65..94caa78e6d 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -131,6 +131,9 @@ namespace osu.Game.Skinning { case LegacyManiaSkinConfigurationLookups.HitPosition: return SkinUtils.As(new Bindable(existing.HitPosition)); + + case LegacyManiaSkinConfigurationLookups.ShowJudgementLine: + return SkinUtils.As(new Bindable(existing.ShowJudgementLine)); } break; From 323146e4a69489524a2e6f08da79b5607e377a30 Mon Sep 17 00:00:00 2001 From: mcendu Date: Tue, 31 Mar 2020 11:53:17 +0800 Subject: [PATCH 0705/2376] simplify column type check logic --- .../Beatmaps/ColumnType.cs | 12 +++++ .../Beatmaps/StageDefinition.cs | 15 ++++++ osu.Game.Rulesets.Mania/UI/Column.cs | 15 +++--- osu.Game.Rulesets.Mania/UI/ManiaStage.cs | 47 ++++--------------- 4 files changed, 46 insertions(+), 43 deletions(-) create mode 100644 osu.Game.Rulesets.Mania/Beatmaps/ColumnType.cs diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ColumnType.cs b/osu.Game.Rulesets.Mania/Beatmaps/ColumnType.cs new file mode 100644 index 0000000000..8f904530bc --- /dev/null +++ b/osu.Game.Rulesets.Mania/Beatmaps/ColumnType.cs @@ -0,0 +1,12 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Rulesets.Mania.Beatmaps +{ + public enum ColumnType + { + Even, + Odd, + Special + } +} diff --git a/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs b/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs index dff7cb72ce..fae422e6ea 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.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.Rulesets.Mania.UI; namespace osu.Game.Rulesets.Mania.Beatmaps @@ -21,5 +22,19 @@ namespace osu.Game.Rulesets.Mania.Beatmaps /// The 0-based column index. /// Whether the column is a special column. public bool IsSpecialColumn(int column) => Columns % 2 == 1 && column == Columns / 2; + + /// + /// Get the type of column given a column index. + /// + /// The 0-based column index. + /// The type of the column. + public ColumnType GetTypeOfColumn(int column) + { + if (IsSpecialColumn(column)) + return ColumnType.Special; + + int distanceToEdge = Math.Min(column, (Columns - 1) - column); + return distanceToEdge % 2 == 1 ? ColumnType.Odd : ColumnType.Even; + } } } diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 63c573d344..f9d3ddf9ee 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -16,6 +16,7 @@ using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; using osu.Game.Rulesets.Mania.UI.Components; using osu.Game.Rulesets.UI.Scrolling; using osuTK; +using osu.Game.Rulesets.Mania.Beatmaps; namespace osu.Game.Rulesets.Mania.UI { @@ -101,22 +102,24 @@ namespace osu.Game.Rulesets.Mania.UI public override Axes RelativeSizeAxes => Axes.Y; - private bool isSpecial; + private ColumnType columnType; - public bool IsSpecial + public ColumnType ColumnType { - get => isSpecial; + get => columnType; set { - if (isSpecial == value) + if (columnType == value) return; - isSpecial = value; + columnType = value; - Width = isSpecial ? special_column_width : COLUMN_WIDTH; + Width = IsSpecial ? special_column_width : COLUMN_WIDTH; } } + public bool IsSpecial => columnType == ColumnType.Special; + private Color4 accentColour; public Color4 AccentColour diff --git a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs b/osu.Game.Rulesets.Mania/UI/ManiaStage.cs index bfe9f1085b..1a94462e2a 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaStage.cs @@ -39,8 +39,12 @@ namespace osu.Game.Rulesets.Mania.UI private readonly Container topLevelContainer; - private List normalColumnColours = new List(); - private Color4 specialColumnColour; + private readonly Dictionary columnColours = new Dictionary + { + { ColumnType.Even, new Color4(94, 0, 57, 255) }, + { ColumnType.Odd, new Color4(6, 84, 0, 255) }, + { ColumnType.Special, new Color4(0, 48, 63, 255) } + }; public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Columns.Any(c => c.ReceivePositionalInputAt(screenSpacePos)); @@ -125,11 +129,12 @@ namespace osu.Game.Rulesets.Mania.UI for (int i = 0; i < definition.Columns; i++) { - var isSpecial = definition.IsSpecialColumn(i); + var columnType = definition.GetTypeOfColumn(i); var column = new Column(firstColumnIndex + i) { - IsSpecial = isSpecial, - Action = { Value = isSpecial ? specialColumnStartAction++ : normalColumnStartAction++ } + ColumnType = columnType, + AccentColour = columnColours[columnType], + Action = { Value = columnType == ColumnType.Special ? specialColumnStartAction++ : normalColumnStartAction++ } }; AddColumn(column); @@ -195,38 +200,6 @@ namespace osu.Game.Rulesets.Mania.UI }); } - [BackgroundDependencyLoader] - private void load() - { - normalColumnColours = new List - { - new Color4(94, 0, 57, 255), - new Color4(6, 84, 0, 255) - }; - - specialColumnColour = new Color4(0, 48, 63, 255); - - // Set the special column + colour + key - foreach (var column in Columns) - { - if (!column.IsSpecial) - continue; - - column.AccentColour = specialColumnColour; - } - - var nonSpecialColumns = Columns.Where(c => !c.IsSpecial).ToList(); - - // We'll set the colours of the non-special columns in a separate loop, because the non-special - // column colours are mirrored across their centre and special styles mess with this - for (int i = 0; i < Math.Ceiling(nonSpecialColumns.Count / 2f); i++) - { - Color4 colour = normalColumnColours[i % normalColumnColours.Count]; - nonSpecialColumns[i].AccentColour = colour; - nonSpecialColumns[nonSpecialColumns.Count - 1 - i].AccentColour = colour; - } - } - protected override void Update() { // Due to masking differences, it is not possible to get the width of the columns container automatically From 3fb044c3b659d4040454c9a345b18f1a9c0d9ee9 Mon Sep 17 00:00:00 2001 From: mcendu Date: Tue, 31 Mar 2020 12:09:04 +0800 Subject: [PATCH 0706/2376] rm unnecessary usings --- osu.Game.Rulesets.Mania/UI/ManiaStage.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs b/osu.Game.Rulesets.Mania/UI/ManiaStage.cs index 1a94462e2a..63fc80cdc8 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaStage.cs @@ -1,10 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using System.Linq; -using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; From 89d8bf9780cc18aa039e7ec21e54b4d650b36601 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 31 Mar 2020 13:46:20 +0900 Subject: [PATCH 0707/2376] Fix catcher test resources being at wrong dpi definition --- ...t-catcher-fail.png => fruit-catcher-fail@2x.png} | Bin ...t-catcher-kiai.png => fruit-catcher-kiai@2x.png} | Bin 2 files changed, 0 insertions(+), 0 deletions(-) rename osu.Game.Rulesets.Catch.Tests/Resources/special-skin/{fruit-catcher-fail.png => fruit-catcher-fail@2x.png} (100%) rename osu.Game.Rulesets.Catch.Tests/Resources/special-skin/{fruit-catcher-kiai.png => fruit-catcher-kiai@2x.png} (100%) diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-fail.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-fail@2x.png similarity index 100% rename from osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-fail.png rename to osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-fail@2x.png diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-kiai.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-kiai@2x.png similarity index 100% rename from osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-kiai.png rename to osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-kiai@2x.png From 1fce7cce01639860fc028394c62b42a9ec508934 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 31 Mar 2020 13:45:55 +0900 Subject: [PATCH 0708/2376] Remove ScaleDownToFit as it was not implemented without enough safety --- osu.Game.Rulesets.Catch/UI/CatcherSprite.cs | 2 +- .../Gameplay/TestSceneSkinnableDrawable.cs | 2 +- osu.Game/Skinning/SkinnableDrawable.cs | 18 +++++------------- 3 files changed, 7 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs b/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs index 52eb8d597e..ef69e3d2d1 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Catch.UI public CatcherSprite(CatcherAnimationState state) : base(new CatchSkinComponent(componentFromState(state)), _ => - new DefaultCatcherSprite(state), confineMode: ConfineMode.ScaleDownToFit) + new DefaultCatcherSprite(state), confineMode: ConfineMode.ScaleToFit) { RelativeSizeAxes = Axes.None; Size = new Vector2(CatcherArea.CATCHER_SIZE); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs index ec94053679..d8222f2ad1 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs @@ -182,7 +182,7 @@ namespace osu.Game.Tests.Visual.Gameplay public new Drawable Drawable => base.Drawable; public ExposedSkinnableDrawable(string name, Func defaultImplementation, Func allowFallback = null, - ConfineMode confineMode = ConfineMode.ScaleDownToFit) + ConfineMode confineMode = ConfineMode.ScaleToFit) : base(new TestSkinComponent(name), defaultImplementation, allowFallback, confineMode) { } diff --git a/osu.Game/Skinning/SkinnableDrawable.cs b/osu.Game/Skinning/SkinnableDrawable.cs index fda031e6cb..68a7a8c159 100644 --- a/osu.Game/Skinning/SkinnableDrawable.cs +++ b/osu.Game/Skinning/SkinnableDrawable.cs @@ -92,20 +92,13 @@ namespace osu.Game.Skinning switch (confineMode) { - case ConfineMode.NoScaling: - return; - - case ConfineMode.ScaleDownToFit: - if (Drawable.DrawSize.X <= DrawSize.X && Drawable.DrawSize.Y <= DrawSize.Y) - return; - + case ConfineMode.ScaleToFit: + Drawable.RelativeSizeAxes = Axes.Both; + Drawable.Size = Vector2.One; + Drawable.Scale = Vector2.One; + Drawable.FillMode = FillMode.Fit; break; } - - Drawable.RelativeSizeAxes = Axes.Both; - Drawable.Size = Vector2.One; - Drawable.Scale = Vector2.One; - Drawable.FillMode = FillMode.Fit; } finally { @@ -121,7 +114,6 @@ namespace osu.Game.Skinning /// Don't apply any scaling. This allows the user element to be of any size, exceeding specified bounds. /// NoScaling, - ScaleDownToFit, ScaleToFit, } } From db59d0530ee3f4114f7e24561aa7164cdb88f767 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 31 Mar 2020 14:15:25 +0900 Subject: [PATCH 0709/2376] Remove test coverage of scale down --- .../Visual/Gameplay/TestSceneSkinnableDrawable.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs index d8222f2ad1..3b91243fee 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs @@ -43,16 +43,15 @@ namespace osu.Game.Tests.Visual.Gameplay { new ExposedSkinnableDrawable("default", _ => new DefaultBox(), _ => true), new ExposedSkinnableDrawable("available", _ => new DefaultBox(), _ => true), - new ExposedSkinnableDrawable("available", _ => new DefaultBox(), _ => true, ConfineMode.ScaleToFit), new ExposedSkinnableDrawable("available", _ => new DefaultBox(), _ => true, ConfineMode.NoScaling) } }, }; }); - AddAssert("check sizes", () => fill.Children.Select(c => c.Drawable.DrawWidth).SequenceEqual(new float[] { 30, 30, 30, 50 })); + AddAssert("check sizes", () => fill.Children.Select(c => c.Drawable.DrawWidth).SequenceEqual(new float[] { 30, 30, 50 })); AddStep("adjust scale", () => fill.Scale = new Vector2(2)); - AddAssert("check sizes unchanged by scale", () => fill.Children.Select(c => c.Drawable.DrawWidth).SequenceEqual(new float[] { 30, 30, 30, 50 })); + AddAssert("check sizes unchanged by scale", () => fill.Children.Select(c => c.Drawable.DrawWidth).SequenceEqual(new float[] { 30, 30, 50 })); } [Test] @@ -74,7 +73,6 @@ namespace osu.Game.Tests.Visual.Gameplay Children = new[] { new ExposedSkinnableDrawable("default", _ => new DefaultBox(), _ => true), - new ExposedSkinnableDrawable("available", _ => new DefaultBox(), _ => true), new ExposedSkinnableDrawable("available", _ => new DefaultBox(), _ => true, ConfineMode.ScaleToFit), new ExposedSkinnableDrawable("available", _ => new DefaultBox(), _ => true, ConfineMode.NoScaling) } @@ -82,9 +80,9 @@ namespace osu.Game.Tests.Visual.Gameplay }; }); - AddAssert("check sizes", () => fill.Children.Select(c => c.Drawable.DrawWidth).SequenceEqual(new float[] { 50, 30, 50, 30 })); + AddAssert("check sizes", () => fill.Children.Select(c => c.Drawable.DrawWidth).SequenceEqual(new float[] { 50, 50, 30 })); AddStep("adjust scale", () => fill.Scale = new Vector2(2)); - AddAssert("check sizes unchanged by scale", () => fill.Children.Select(c => c.Drawable.DrawWidth).SequenceEqual(new float[] { 50, 30, 50, 30 })); + AddAssert("check sizes unchanged by scale", () => fill.Children.Select(c => c.Drawable.DrawWidth).SequenceEqual(new float[] { 50, 50, 30 })); } [Test] From 275f96791dd48611599df45864993d2afa749b5c Mon Sep 17 00:00:00 2001 From: mcendu Date: Tue, 31 Mar 2020 13:57:37 +0800 Subject: [PATCH 0710/2376] add regression tests --- .../ManiaColumnTypeTest.cs | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 osu.Game.Rulesets.Mania.Tests/ManiaColumnTypeTest.cs diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaColumnTypeTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaColumnTypeTest.cs new file mode 100644 index 0000000000..40a6e1fdae --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/ManiaColumnTypeTest.cs @@ -0,0 +1,50 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Game.Rulesets.Mania.Beatmaps; +using NUnit.Framework; + +namespace osu.Game.Rulesets.Mania.Tests +{ + [TestFixture] + public class ManiaColumnTypeTest + { + [TestCase(new[] + { + ColumnType.Special + }, 1)] + [TestCase(new[] + { + ColumnType.Odd, + ColumnType.Even, + ColumnType.Even, + ColumnType.Odd + }, 4)] + [TestCase(new[] + { + ColumnType.Odd, + ColumnType.Even, + ColumnType.Odd, + ColumnType.Special, + ColumnType.Odd, + ColumnType.Even, + ColumnType.Odd + }, 7)] + public void Test(IEnumerable expected, int columns) + { + var definition = new StageDefinition + { + Columns = columns + }; + var results = getResults(definition); + Assert.AreEqual(expected, results); + } + + private IEnumerable getResults(StageDefinition definition) + { + for (var i = 0; i < definition.Columns; i++) + yield return definition.GetTypeOfColumn(i); + } + } +} From 2008a7bbecac158efc7c521b44d14ace6d80e28d Mon Sep 17 00:00:00 2001 From: mcendu Date: Tue, 31 Mar 2020 14:03:11 +0800 Subject: [PATCH 0711/2376] fix naming --- osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs | 2 +- osu.Game.Rulesets.Mania/UI/ManiaStage.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs b/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs index fae422e6ea..2557f2acdf 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps return ColumnType.Special; int distanceToEdge = Math.Min(column, (Columns - 1) - column); - return distanceToEdge % 2 == 1 ? ColumnType.Odd : ColumnType.Even; + return distanceToEdge % 2 == 0 ? ColumnType.Odd : ColumnType.Even; } } } diff --git a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs b/osu.Game.Rulesets.Mania/UI/ManiaStage.cs index 63fc80cdc8..b27b23359e 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaStage.cs @@ -39,8 +39,8 @@ namespace osu.Game.Rulesets.Mania.UI private readonly Dictionary columnColours = new Dictionary { - { ColumnType.Even, new Color4(94, 0, 57, 255) }, - { ColumnType.Odd, new Color4(6, 84, 0, 255) }, + { ColumnType.Even, new Color4(6, 84, 0, 255) }, + { ColumnType.Odd, new Color4(94, 0, 57, 255) }, { ColumnType.Special, new Color4(0, 48, 63, 255) } }; From 75e43acb1abfc893394c929b708cb0fbbab1add9 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 31 Mar 2020 15:10:15 +0900 Subject: [PATCH 0712/2376] Add a legacy element to help with texture fallbacks --- .../Skinning/LegacyManiaColumnElement.cs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs new file mode 100644 index 0000000000..231a55a7e2 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs @@ -0,0 +1,41 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Mania.UI; + +namespace osu.Game.Rulesets.Mania.Skinning +{ + /// + /// A which is placed somewhere within a . + /// + public class LegacyManiaColumnElement : CompositeDrawable + { + [Resolved(CanBeNull = true)] + [CanBeNull] + protected ManiaStage Stage { get; private set; } + + [Resolved] + protected Column Column { get; private set; } + + /// + /// The column index to use for texture lookups, in the case of no user-provided configuration. + /// + protected int FallbackColumnIndex { get; private set; } + + [BackgroundDependencyLoader] + private void load() + { + if (Stage == null) + FallbackColumnIndex = Column.Index % 2 + 1; + else + { + int dist = Math.Min(Column.Index, Stage.Columns.Count - Column.Index - 1); + FallbackColumnIndex = dist % 2 + 1; + } + } + } +} From 16439f7d8eff72f8352805680735d8cc8409a90e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 31 Mar 2020 15:15:49 +0900 Subject: [PATCH 0713/2376] Fix incorrect fallback index being used --- osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs index 8a57953d60..6afc86c4fa 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs @@ -14,7 +14,7 @@ using osuTK; namespace osu.Game.Rulesets.Mania.Skinning { - public class LegacyKeyArea : CompositeDrawable, IKeyBindingHandler + public class LegacyKeyArea : LegacyManiaColumnElement, IKeyBindingHandler { private readonly IBindable direction = new Bindable(); @@ -36,15 +36,13 @@ namespace osu.Game.Rulesets.Mania.Skinning [BackgroundDependencyLoader] private void load(ISkinSource skin, IScrollingInfo scrollingInfo) { - int fallbackColumn = column.Index % 2 + 1; - string upImage = skin.GetConfig( new LegacyManiaSkinConfigurationLookup(stage?.Columns.Count ?? 4, LegacyManiaSkinConfigurationLookups.KeyImage, column.Index))?.Value - ?? $"mania-key{fallbackColumn}"; + ?? $"mania-key{FallbackColumnIndex}"; string downImage = skin.GetConfig( new LegacyManiaSkinConfigurationLookup(stage?.Columns.Count ?? 4, LegacyManiaSkinConfigurationLookups.KeyImageDown, column.Index))?.Value - ?? $"mania-key{fallbackColumn}D"; + ?? $"mania-key{FallbackColumnIndex}D"; InternalChild = directionContainer = new Container { From 8a998d600d13ae64d1e838deae92b7f432d30e56 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 31 Mar 2020 15:17:27 +0900 Subject: [PATCH 0714/2376] Fix relax mod pressing too many keys --- osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs index 6286c80d7c..9b0759d9d2 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs @@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Osu.Mods void handleHitCircle(DrawableHitCircle circle) { - if (!circle.IsHovered) + if (!circle.HitArea.IsHovered) return; Debug.Assert(circle.HitObject.HitWindows != null); From bf1fc9f7a035acf9003acb3ac6b2e36a10873002 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 31 Mar 2020 15:18:50 +0900 Subject: [PATCH 0715/2376] 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 fd2532257b..6db4220fad 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index fdf9703d79..4163044273 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -23,7 +23,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index a286d1d460..17430e4b25 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + @@ -79,7 +79,7 @@ - + From ae668e3e87ad3d9d84185d74d0318b68381dcad9 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 31 Mar 2020 15:24:13 +0900 Subject: [PATCH 0716/2376] Fix post-merge errors --- osu.Game.Rulesets.Mania/ManiaSkinComponent.cs | 4 ++-- .../Skinning/ManiaLegacySkinTransformer.cs | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs index 55009d0f5c..72aa0dbd4c 100644 --- a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs +++ b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs @@ -19,8 +19,8 @@ namespace osu.Game.Rulesets.Mania public enum ManiaSkinComponents { - ColumnBackground - HitTarget + ColumnBackground, + HitTarget, KeyArea } } diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs index a929f51966..efc95f3c24 100644 --- a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs @@ -46,8 +46,10 @@ namespace osu.Game.Rulesets.Mania.Skinning { case ManiaSkinComponents.ColumnBackground: return new LegacyColumnBackground(); + case ManiaSkinComponents.HitTarget: return new LegacyHitTarget(); + case ManiaSkinComponents.KeyArea: return new LegacyKeyArea(); } From b926d570ee1bdacb1e7a39e758da09b11a5b4fef Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 31 Mar 2020 15:28:50 +0900 Subject: [PATCH 0717/2376] Allow skinnabledrawable to be auto-sized --- osu.Game/Skinning/SkinnableDrawable.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Skinning/SkinnableDrawable.cs b/osu.Game/Skinning/SkinnableDrawable.cs index fda031e6cb..f6ac6494b4 100644 --- a/osu.Game/Skinning/SkinnableDrawable.cs +++ b/osu.Game/Skinning/SkinnableDrawable.cs @@ -18,6 +18,12 @@ namespace osu.Game.Skinning /// public Drawable Drawable { get; private set; } + public new Axes AutoSizeAxes + { + get => base.AutoSizeAxes; + set => base.AutoSizeAxes = value; + } + private readonly ISkinComponent component; private readonly ConfineMode confineMode; From c4f76ffdaf12dac914c5b94dd3547d37129b326b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 31 Mar 2020 15:29:25 +0900 Subject: [PATCH 0718/2376] Implement mania note skinning --- .../Skinning/TestSceneHoldNote.cs | 24 +++++ .../Skinning/TestSceneNote.cs | 21 +++++ .../TestSceneHitExplosion.cs | 2 +- .../Blueprints/Components/EditNotePiece.cs | 4 +- .../Blueprints/ManiaPlacementBlueprint.cs | 8 +- osu.Game.Rulesets.Mania/ManiaSkinComponent.cs | 5 +- .../Objects/Drawables/DrawableNote.cs | 26 ++---- .../{NotePiece.cs => DefaultNotePiece.cs} | 50 +++++----- .../Skinning/LegacyHoldNoteHeadPiece.cs | 17 ++++ .../Skinning/LegacyHoldNoteTailPiece.cs | 27 ++++++ .../Skinning/LegacyNotePiece.cs | 93 +++++++++++++++++++ .../Skinning/ManiaLegacySkinTransformer.cs | 9 ++ .../UI/Components/ColumnHitObjectArea.cs | 4 +- .../UI/Components/DefaultHitTarget.cs | 2 +- osu.Game.Rulesets.Mania/UI/HitExplosion.cs | 2 +- .../LegacyManiaSkinConfigurationLookup.cs | 5 +- 16 files changed, 247 insertions(+), 52 deletions(-) create mode 100644 osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs create mode 100644 osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneNote.cs rename osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/{NotePiece.cs => DefaultNotePiece.cs} (52%) create mode 100644 osu.Game.Rulesets.Mania/Skinning/LegacyHoldNoteHeadPiece.cs create mode 100644 osu.Game.Rulesets.Mania/Skinning/LegacyHoldNoteTailPiece.cs create mode 100644 osu.Game.Rulesets.Mania/Skinning/LegacyNotePiece.cs diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs new file mode 100644 index 0000000000..19623a5705 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.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.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Objects.Drawables; + +namespace osu.Game.Rulesets.Mania.Tests.Skinning +{ + public class TestSceneHoldNote : ManiaHitObjectTestScene + { + protected override DrawableManiaHitObject CreateHitObject() + { + var note = new HoldNote { Duration = 1000 }; + note.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + + return new DrawableHoldNote(note) + { + Height = 200, + }; + } + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneNote.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneNote.cs new file mode 100644 index 0000000000..bc3bdf0bcb --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneNote.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.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Objects.Drawables; + +namespace osu.Game.Rulesets.Mania.Tests.Skinning +{ + public class TestSceneNote : ManiaHitObjectTestScene + { + protected override DrawableManiaHitObject CreateHitObject() + { + var note = new Note(); + note.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + + return new DrawableNote(note); + } + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHitExplosion.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHitExplosion.cs index 26a1b1b1ec..9a50bc3926 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneHitExplosion.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHitExplosion.cs @@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Mania.Tests Origin = Anchor.Centre, RelativePositionAxes = Axes.Y, Y = -0.25f, - Size = new Vector2(Column.COLUMN_WIDTH, NotePiece.NOTE_HEIGHT), + Size = new Vector2(Column.COLUMN_WIDTH, DefaultNotePiece.NOTE_HEIGHT), }; int runcount = 0; diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditNotePiece.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditNotePiece.cs index 6f85fd9167..8773a39939 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditNotePiece.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditNotePiece.cs @@ -12,12 +12,12 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints.Components { public EditNotePiece() { - Height = NotePiece.NOTE_HEIGHT; + Height = DefaultNotePiece.NOTE_HEIGHT; CornerRadius = 5; Masking = true; - InternalChild = new NotePiece(); + InternalChild = new DefaultNotePiece(); } [BackgroundDependencyLoader] diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs index a3657d3bb9..6ddf212266 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs @@ -122,11 +122,11 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints switch (scrollingInfo.Direction.Value) { case ScrollingDirection.Up: - mousePosition.Y -= NotePiece.NOTE_HEIGHT / 2; + mousePosition.Y -= DefaultNotePiece.NOTE_HEIGHT / 2; break; case ScrollingDirection.Down: - mousePosition.Y += NotePiece.NOTE_HEIGHT / 2; + mousePosition.Y += DefaultNotePiece.NOTE_HEIGHT / 2; break; } @@ -143,11 +143,11 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints switch (scrollingInfo.Direction.Value) { case ScrollingDirection.Up: - hitObjectPosition.Y += NotePiece.NOTE_HEIGHT / 2; + hitObjectPosition.Y += DefaultNotePiece.NOTE_HEIGHT / 2; break; case ScrollingDirection.Down: - hitObjectPosition.Y -= NotePiece.NOTE_HEIGHT / 2; + hitObjectPosition.Y -= DefaultNotePiece.NOTE_HEIGHT / 2; break; } diff --git a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs index 72aa0dbd4c..9df15f424d 100644 --- a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs +++ b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs @@ -21,6 +21,9 @@ namespace osu.Game.Rulesets.Mania { ColumnBackground, HitTarget, - KeyArea + KeyArea, + Note, + HoldNoteHead, + HoldNoteTail, } } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs index 85613d3afb..fdc50048fe 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs @@ -3,13 +3,12 @@ using System.Diagnostics; using osu.Framework.Bindables; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Effects; using osu.Framework.Input.Bindings; using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Mania.Objects.Drawables { @@ -18,7 +17,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables /// public class DrawableNote : DrawableManiaHitObject, IKeyBindingHandler { - private readonly NotePiece headPiece; + protected virtual ManiaSkinComponents Component => ManiaSkinComponents.Note; + + private readonly Drawable headPiece; public DrawableNote(Note hitObject) : base(hitObject) @@ -26,22 +27,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - CornerRadius = 5; - Masking = true; - - AddInternal(headPiece = new NotePiece()); - - AccentColour.BindValueChanged(colour => + AddInternal(headPiece = new SkinnableDrawable(new ManiaSkinComponent(Component), _ => new DefaultNotePiece()) { - headPiece.AccentColour = colour.NewValue; - - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Glow, - Colour = colour.NewValue.Lighten(1f).Opacity(0.2f), - Radius = 10, - }; - }, true); + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + }); } protected override void OnDirectionChanged(ValueChangedEvent e) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/NotePiece.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/DefaultNotePiece.cs similarity index 52% rename from osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/NotePiece.cs rename to osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/DefaultNotePiece.cs index 4521af7dfb..3888612e45 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/NotePiece.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/DefaultNotePiece.cs @@ -7,8 +7,9 @@ using osuTK.Graphics; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics; +using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI.Scrolling; namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces @@ -16,20 +17,24 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces /// /// Represents the static hit markers of notes. /// - internal class NotePiece : Container, IHasAccentColour + internal class DefaultNotePiece : CompositeDrawable { public const float NOTE_HEIGHT = 12; private readonly IBindable direction = new Bindable(); + private readonly IBindable accentColour = new Bindable(); private readonly Box colouredBox; - public NotePiece() + public DefaultNotePiece() { RelativeSizeAxes = Axes.X; Height = NOTE_HEIGHT; - Children = new[] + CornerRadius = 5; + Masking = true; + + InternalChildren = new Drawable[] { new Box { @@ -45,29 +50,32 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces } [BackgroundDependencyLoader] - private void load(IScrollingInfo scrollingInfo) + private void load(IScrollingInfo scrollingInfo, DrawableHitObject drawableObject) { direction.BindTo(scrollingInfo.Direction); - direction.BindValueChanged(dir => - { - colouredBox.Anchor = colouredBox.Origin = dir.NewValue == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre; - }, true); + direction.BindValueChanged(onDirectionChanged, true); + + accentColour.BindTo(drawableObject.AccentColour); + accentColour.BindValueChanged(onAccentChanged, true); } - private Color4 accentColour; - - public Color4 AccentColour + private void onDirectionChanged(ValueChangedEvent direction) { - get => accentColour; - set + colouredBox.Anchor = colouredBox.Origin = direction.NewValue == ScrollingDirection.Up + ? Anchor.TopCentre + : Anchor.BottomCentre; + } + + private void onAccentChanged(ValueChangedEvent accent) + { + colouredBox.Colour = accent.NewValue.Lighten(0.9f); + + EdgeEffect = new EdgeEffectParameters { - if (accentColour == value) - return; - - accentColour = value; - - colouredBox.Colour = AccentColour.Lighten(0.9f); - } + Type = EdgeEffectType.Glow, + Colour = accent.NewValue.Lighten(1f).Opacity(0.2f), + Radius = 10, + }; } } } diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyHoldNoteHeadPiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyHoldNoteHeadPiece.cs new file mode 100644 index 0000000000..ebe7ff09b2 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyHoldNoteHeadPiece.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.Framework.Graphics.Textures; +using osu.Game.Skinning; + +namespace osu.Game.Rulesets.Mania.Skinning +{ + public class LegacyHoldNoteHeadPiece : LegacyNotePiece + { + protected override Texture GetTexture(ISkinSource skin) + { + return GetTextureFromLookup(skin, LegacyManiaSkinConfigurationLookups.HoldNoteHeadImage) + ?? GetTextureFromLookup(skin, LegacyManiaSkinConfigurationLookups.NoteImage); + } + } +} diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyHoldNoteTailPiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyHoldNoteTailPiece.cs new file mode 100644 index 0000000000..085d2bf004 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyHoldNoteTailPiece.cs @@ -0,0 +1,27 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osu.Framework.Graphics.Textures; +using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Skinning; + +namespace osu.Game.Rulesets.Mania.Skinning +{ + public class LegacyHoldNoteTailPiece : LegacyNotePiece + { + protected override void OnDirectionChanged(ValueChangedEvent direction) + { + // Invert the direction + base.OnDirectionChanged(direction.NewValue == ScrollingDirection.Up + ? new ValueChangedEvent(ScrollingDirection.Down, ScrollingDirection.Down) + : new ValueChangedEvent(ScrollingDirection.Up, ScrollingDirection.Up)); + } + + protected override Texture GetTexture(ISkinSource skin) + { + return GetTextureFromLookup(skin, LegacyManiaSkinConfigurationLookups.HoldNoteTailImage) + ?? GetTextureFromLookup(skin, LegacyManiaSkinConfigurationLookups.HoldNoteHeadImage); + } + } +} diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyNotePiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyNotePiece.cs new file mode 100644 index 0000000000..7936965ff8 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyNotePiece.cs @@ -0,0 +1,93 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Skinning; +using osuTK; + +namespace osu.Game.Rulesets.Mania.Skinning +{ + public class LegacyNotePiece : LegacyManiaColumnElement + { + private readonly IBindable direction = new Bindable(); + + private Container directionContainer; + private Sprite noteSprite; + + public LegacyNotePiece() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + } + + [BackgroundDependencyLoader] + private void load(ISkinSource skin, IScrollingInfo scrollingInfo) + { + InternalChild = directionContainer = new Container + { + Anchor = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Child = noteSprite = new Sprite { Texture = GetTexture(skin) } + }; + + direction.BindTo(scrollingInfo.Direction); + direction.BindValueChanged(OnDirectionChanged, true); + } + + protected override void Update() + { + base.Update(); + + if (noteSprite.Texture != null) + { + var scale = DrawWidth / noteSprite.Texture.DisplayWidth; + noteSprite.Scale = new Vector2(scale); + } + } + + protected virtual void OnDirectionChanged(ValueChangedEvent direction) + { + if (direction.NewValue == ScrollingDirection.Up) + { + directionContainer.Origin = Anchor.BottomCentre; + directionContainer.Scale = new Vector2(1, -1); + } + else + { + directionContainer.Origin = Anchor.TopCentre; + directionContainer.Scale = Vector2.One; + } + } + + protected virtual Texture GetTexture(ISkinSource skin) => GetTextureFromLookup(skin, LegacyManiaSkinConfigurationLookups.NoteImage); + + protected Texture GetTextureFromLookup(ISkin skin, LegacyManiaSkinConfigurationLookups lookup) + { + string suffix = string.Empty; + + switch (lookup) + { + case LegacyManiaSkinConfigurationLookups.HoldNoteHeadImage: + suffix = "H"; + break; + + case LegacyManiaSkinConfigurationLookups.HoldNoteTailImage: + suffix = "T"; + break; + } + + string noteImage = skin.GetConfig( + new LegacyManiaSkinConfigurationLookup(Stage?.Columns.Count ?? 4, lookup, Column.Index))?.Value + ?? $"mania-note{FallbackColumnIndex}{suffix}"; + + return skin.GetTexture(noteImage); + } + } +} diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs index efc95f3c24..b8caeaca30 100644 --- a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs @@ -52,6 +52,15 @@ namespace osu.Game.Rulesets.Mania.Skinning case ManiaSkinComponents.KeyArea: return new LegacyKeyArea(); + + case ManiaSkinComponents.Note: + return new LegacyNotePiece(); + + case ManiaSkinComponents.HoldNoteHead: + return new LegacyHoldNoteHeadPiece(); + + case ManiaSkinComponents.HoldNoteTail: + return new LegacyHoldNoteTailPiece(); } break; diff --git a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs index 51928f8b66..fb6e8a87e5 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs @@ -64,14 +64,14 @@ namespace osu.Game.Rulesets.Mania.UI.Components hitTarget.Anchor = hitTarget.Origin = Anchor.TopLeft; Padding = new MarginPadding { Top = hitPosition }; - Explosions.Padding = new MarginPadding { Top = NotePiece.NOTE_HEIGHT }; + Explosions.Padding = new MarginPadding { Top = DefaultNotePiece.NOTE_HEIGHT }; } else { hitTarget.Anchor = hitTarget.Origin = Anchor.BottomLeft; Padding = new MarginPadding { Bottom = hitPosition }; - Explosions.Padding = new MarginPadding { Bottom = NotePiece.NOTE_HEIGHT }; + Explosions.Padding = new MarginPadding { Bottom = DefaultNotePiece.NOTE_HEIGHT }; } } } diff --git a/osu.Game.Rulesets.Mania/UI/Components/DefaultHitTarget.cs b/osu.Game.Rulesets.Mania/UI/Components/DefaultHitTarget.cs index d96b4d864b..e0b099ab9b 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/DefaultHitTarget.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/DefaultHitTarget.cs @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Mania.UI.Components hitTargetBar = new Box { RelativeSizeAxes = Axes.X, - Height = NotePiece.NOTE_HEIGHT, + Height = DefaultNotePiece.NOTE_HEIGHT, Alpha = 0.6f, Colour = Color4.Black }, diff --git a/osu.Game.Rulesets.Mania/UI/HitExplosion.cs b/osu.Game.Rulesets.Mania/UI/HitExplosion.cs index c26697fa79..824b087cb9 100644 --- a/osu.Game.Rulesets.Mania/UI/HitExplosion.cs +++ b/osu.Game.Rulesets.Mania/UI/HitExplosion.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Mania.UI public HitExplosion(Color4 objectColour, bool isSmall = false) { RelativeSizeAxes = Axes.X; - Height = NotePiece.NOTE_HEIGHT; + Height = DefaultNotePiece.NOTE_HEIGHT; // scale roughly in-line with visual appearance of notes Scale = new Vector2(1f, 0.6f); diff --git a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs index 1eae6b41b3..ca4811b3d5 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs @@ -26,6 +26,9 @@ namespace osu.Game.Skinning HitTargetImage, ShowJudgementLine, KeyImage, - KeyImageDown + KeyImageDown, + NoteImage, + HoldNoteHeadImage, + HoldNoteTailImage } } From 9a37a328b619c282b26f423c92deed683a140e0c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 31 Mar 2020 15:39:00 +0900 Subject: [PATCH 0719/2376] Add component overrides for hold note head/tail --- .../Objects/Drawables/DrawableHoldNoteHead.cs | 2 ++ .../Objects/Drawables/DrawableHoldNoteTail.cs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs index 390c64c5e2..a73fe259e4 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs @@ -8,6 +8,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables /// public class DrawableHoldNoteHead : DrawableNote { + protected override ManiaSkinComponents Component => ManiaSkinComponents.HoldNoteHead; + public DrawableHoldNoteHead(DrawableHoldNote holdNote) : base(holdNote.HitObject.Head) { diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs index 568b07c958..31e43d3ee2 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs @@ -18,6 +18,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables /// private const double release_window_lenience = 1.5; + protected override ManiaSkinComponents Component => ManiaSkinComponents.HoldNoteTail; + private readonly DrawableHoldNote holdNote; public DrawableHoldNoteTail(DrawableHoldNote holdNote) From b805ed6bf1faf63c7534e45129bd3808635f6a8e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 31 Mar 2020 15:59:52 +0900 Subject: [PATCH 0720/2376] Flip anchors and origins --- osu.Game.Rulesets.Mania/Skinning/LegacyNotePiece.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyNotePiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyNotePiece.cs index 7936965ff8..e74509febd 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyNotePiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyNotePiece.cs @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Mania.Skinning { InternalChild = directionContainer = new Container { - Anchor = Anchor.TopCentre, + Origin = Anchor.BottomCentre, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Child = noteSprite = new Sprite { Texture = GetTexture(skin) } @@ -56,12 +56,12 @@ namespace osu.Game.Rulesets.Mania.Skinning { if (direction.NewValue == ScrollingDirection.Up) { - directionContainer.Origin = Anchor.BottomCentre; + directionContainer.Anchor = Anchor.TopCentre; directionContainer.Scale = new Vector2(1, -1); } else { - directionContainer.Origin = Anchor.TopCentre; + directionContainer.Anchor = Anchor.BottomCentre; directionContainer.Scale = Vector2.One; } } From 11430d616eb7b47754b84bdb9b6d81334069884a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 31 Mar 2020 16:00:08 +0900 Subject: [PATCH 0721/2376] Allow null hitobject --- .../Objects/Drawables/Pieces/DefaultNotePiece.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/DefaultNotePiece.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/DefaultNotePiece.cs index 3888612e45..29f5217fd8 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/DefaultNotePiece.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/DefaultNotePiece.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 JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osuTK.Graphics; @@ -49,14 +50,17 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces }; } - [BackgroundDependencyLoader] - private void load(IScrollingInfo scrollingInfo, DrawableHitObject drawableObject) + [BackgroundDependencyLoader(true)] + private void load([NotNull] IScrollingInfo scrollingInfo, [CanBeNull] DrawableHitObject drawableObject) { direction.BindTo(scrollingInfo.Direction); direction.BindValueChanged(onDirectionChanged, true); - accentColour.BindTo(drawableObject.AccentColour); - accentColour.BindValueChanged(onAccentChanged, true); + if (drawableObject != null) + { + accentColour.BindTo(drawableObject.AccentColour); + accentColour.BindValueChanged(onAccentChanged, true); + } } private void onDirectionChanged(ValueChangedEvent direction) From 1952fcc0ce0b424607ad8846c4f727548b160ef0 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 31 Mar 2020 16:42:35 +0900 Subject: [PATCH 0722/2376] Implement mania hold note skinning --- .../Blueprints/Components/EditBodyPiece.cs | 4 +- .../Blueprints/HoldNoteSelectionBlueprint.cs | 19 ++- osu.Game.Rulesets.Mania/ManiaSkinComponent.cs | 1 + .../Objects/Drawables/DrawableHoldNote.cs | 23 ++-- .../{BodyPiece.cs => DefaultBodyPiece.cs} | 110 ++++++++---------- .../Skinning/LegacyBodyPiece.cs | 93 +++++++++++++++ .../Skinning/ManiaLegacySkinTransformer.cs | 3 + .../LegacyManiaSkinConfigurationLookup.cs | 3 +- 8 files changed, 174 insertions(+), 82 deletions(-) rename osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/{BodyPiece.cs => DefaultBodyPiece.cs} (70%) create mode 100644 osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditBodyPiece.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditBodyPiece.cs index b99a1157f3..efcfe11dad 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditBodyPiece.cs @@ -7,12 +7,12 @@ using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; namespace osu.Game.Rulesets.Mania.Edit.Blueprints.Components { - public class EditBodyPiece : BodyPiece + public class EditBodyPiece : DefaultBodyPiece { [BackgroundDependencyLoader] private void load(OsuColour colours) { - AccentColour = colours.Yellow; + AccentColour.Value = colours.Yellow; Background.Alpha = 0.5f; Foreground.Alpha = 0; diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs index 56c0b671a0..f1750f4a01 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs @@ -4,13 +4,13 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Rulesets.Mania.Objects.Drawables; -using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; using osu.Game.Rulesets.UI.Scrolling; using osuTK; -using osuTK.Graphics; namespace osu.Game.Rulesets.Mania.Edit.Blueprints { @@ -42,11 +42,18 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints { new HoldNoteNoteSelectionBlueprint(DrawableObject, HoldNotePosition.Start), new HoldNoteNoteSelectionBlueprint(DrawableObject, HoldNotePosition.End), - new BodyPiece + new Container { - AccentColour = Color4.Transparent, - BorderColour = colours.Yellow - }, + RelativeSizeAxes = Axes.Both, + BorderThickness = 1, + BorderColour = colours.Yellow, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true, + } + } }; } diff --git a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs index 9df15f424d..dd1052ad0e 100644 --- a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs +++ b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs @@ -25,5 +25,6 @@ namespace osu.Game.Rulesets.Mania Note, HoldNoteHead, HoldNoteTail, + HoldNoteBody, } } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 14a7c5fda3..7cacaf35a6 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -10,6 +10,7 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Mania.Objects.Drawables { @@ -20,6 +21,10 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { public override bool DisplayResult => false; + public IBindable IsHitting => isHitting; + + private readonly Bindable isHitting = new Bindable(); + public DrawableHoldNoteHead Head => headContainer.Child; public DrawableHoldNoteTail Tail => tailContainer.Child; @@ -27,7 +32,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables private readonly Container tailContainer; private readonly Container tickContainer; - private readonly BodyPiece bodyPiece; + private readonly Drawable bodyPiece; /// /// Time at which the user started holding this hold note. Null if the user is not holding this hold note. @@ -44,18 +49,16 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { RelativeSizeAxes = Axes.X; - AddRangeInternal(new Drawable[] + AddRangeInternal(new[] { - bodyPiece = new BodyPiece { RelativeSizeAxes = Axes.X }, + bodyPiece = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HoldNoteBody), _ => new DefaultBodyPiece()) + { + RelativeSizeAxes = Axes.X + }, tickContainer = new Container { RelativeSizeAxes = Axes.Both }, headContainer = new Container { RelativeSizeAxes = Axes.Both }, tailContainer = new Container { RelativeSizeAxes = Axes.Both }, }); - - AccentColour.BindValueChanged(colour => - { - bodyPiece.AccentColour = colour.NewValue; - }, true); } protected override void AddNestedHitObject(DrawableHitObject hitObject) @@ -168,7 +171,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables return; HoldStartTime = Time.Current; - bodyPiece.Hitting = true; + isHitting.Value = true; } public void OnReleased(ManiaAction action) @@ -194,7 +197,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables private void endHold() { HoldStartTime = null; - bodyPiece.Hitting = false; + isHitting.Value = false; } } } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/BodyPiece.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/DefaultBodyPiece.cs similarity index 70% rename from osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/BodyPiece.cs rename to osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/DefaultBodyPiece.cs index 43f9ae2783..d1e6264c61 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/BodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/DefaultBodyPiece.cs @@ -2,6 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System; +using JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Framework.Bindables; using osuTK.Graphics; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -9,26 +12,38 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Layout; -using osu.Game.Graphics; +using osu.Game.Rulesets.Objects.Drawables; namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces { /// /// Represents length-wise portion of a hold note. /// - public class BodyPiece : Container, IHasAccentColour + public class DefaultBodyPiece : CompositeDrawable { - private readonly Container subtractionLayer; + protected readonly Bindable AccentColour = new Bindable(); - protected readonly Drawable Background; - protected readonly BufferedContainer Foreground; - private readonly BufferedContainer subtractionContainer; + private readonly LayoutValue subtractionCache = new LayoutValue(Invalidation.DrawSize); + private readonly IBindable isHitting = new Bindable(); - public BodyPiece() + protected Drawable Background { get; private set; } + protected BufferedContainer Foreground { get; private set; } + + private BufferedContainer subtractionContainer; + private Container subtractionLayer; + + public DefaultBodyPiece() { + RelativeSizeAxes = Axes.Both; Blending = BlendingParameters.Additive; - Children = new[] + AddLayout(subtractionCache); + } + + [BackgroundDependencyLoader(true)] + private void load([CanBeNull] DrawableHitObject drawableObject) + { + InternalChildren = new[] { Background = new Box { RelativeSizeAxes = Axes.Both }, Foreground = new BufferedContainer @@ -66,43 +81,37 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces } }; - AddLayout(subtractionCache); - } + var holdNote = (DrawableHoldNote)drawableObject; - protected override void LoadComplete() - { - base.LoadComplete(); - - updateAccentColour(); - } - - private Color4 accentColour; - - public Color4 AccentColour - { - get => accentColour; - set + if (drawableObject != null) { - if (accentColour == value) - return; - - accentColour = value; - - updateAccentColour(); + AccentColour.BindTo(drawableObject.AccentColour); + AccentColour.BindValueChanged(onAccentChanged, true); } + + isHitting.BindTo(holdNote.IsHitting); + isHitting.BindValueChanged(_ => onAccentChanged(new ValueChangedEvent(AccentColour.Value, AccentColour.Value)), true); } - public bool Hitting + private void onAccentChanged(ValueChangedEvent accent) { - get => hitting; - set - { - hitting = value; - updateAccentColour(); - } - } + Foreground.Colour = accent.NewValue.Opacity(0.5f); + Background.Colour = accent.NewValue.Opacity(0.7f); - private readonly LayoutValue subtractionCache = new LayoutValue(Invalidation.DrawSize); + const float animation_length = 50; + + Foreground.ClearTransforms(false, nameof(Foreground.Colour)); + + if (isHitting.Value) + { + // wait for the next sync point + double synchronisedOffset = animation_length * 2 - Time.Current % (animation_length * 2); + using (Foreground.BeginDelayedSequence(synchronisedOffset)) + Foreground.FadeColour(accent.NewValue.Lighten(0.2f), animation_length).Then().FadeColour(Foreground.Colour, animation_length).Loop(); + } + + subtractionCache.Invalidate(); + } protected override void Update() { @@ -125,30 +134,5 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces subtractionCache.Validate(); } } - - private bool hitting; - - private void updateAccentColour() - { - if (!IsLoaded) - return; - - Foreground.Colour = AccentColour.Opacity(0.5f); - Background.Colour = AccentColour.Opacity(0.7f); - - const float animation_length = 50; - - Foreground.ClearTransforms(false, nameof(Foreground.Colour)); - - if (hitting) - { - // wait for the next sync point - double synchronisedOffset = animation_length * 2 - Time.Current % (animation_length * 2); - using (Foreground.BeginDelayedSequence(synchronisedOffset)) - Foreground.FadeColour(AccentColour.Lighten(0.2f), animation_length).Then().FadeColour(Foreground.Colour, animation_length).Loop(); - } - - subtractionCache.Invalidate(); - } } } diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs new file mode 100644 index 0000000000..e7fb331079 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs @@ -0,0 +1,93 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Animations; +using osu.Game.Rulesets.Mania.Objects.Drawables; +using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Skinning; +using osuTK; + +namespace osu.Game.Rulesets.Mania.Skinning +{ + public class LegacyBodyPiece : LegacyManiaColumnElement + { + private readonly IBindable direction = new Bindable(); + private readonly IBindable isHitting = new Bindable(); + + private Drawable sprite; + + [Resolved(CanBeNull = true)] + private ManiaStage stage { get; set; } + + [Resolved] + private Column column { get; set; } + + public LegacyBodyPiece() + { + RelativeSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load(ISkinSource skin, IScrollingInfo scrollingInfo, DrawableHitObject drawableObject) + { + string imageName = skin.GetConfig( + new LegacyManiaSkinConfigurationLookup(stage?.Columns.Count ?? 4, LegacyManiaSkinConfigurationLookups.HoldNoteBodyImage, column.Index))?.Value + ?? $"mania-note{FallbackColumnIndex}L"; + + sprite = skin.GetAnimation(imageName, true, true).With(d => + { + if (d == null) + return; + + if (d is TextureAnimation animation) + animation.IsPlaying = false; + + d.Anchor = Anchor.TopCentre; + d.RelativeSizeAxes = Axes.Both; + d.Size = Vector2.One; + d.FillMode = FillMode.Stretch; + // Todo: Wrap + }); + + if (sprite != null) + InternalChild = sprite; + + direction.BindTo(scrollingInfo.Direction); + direction.BindValueChanged(onDirectionChanged, true); + + var holdNote = (DrawableHoldNote)drawableObject; + isHitting.BindTo(holdNote.IsHitting); + isHitting.BindValueChanged(onIsHittingChanged, true); + } + + private void onIsHittingChanged(ValueChangedEvent isHitting) + { + if (!(sprite is TextureAnimation animation)) + return; + + animation.IsPlaying = isHitting.NewValue; + } + + private void onDirectionChanged(ValueChangedEvent direction) + { + if (sprite == null) + return; + + if (direction.NewValue == ScrollingDirection.Up) + { + sprite.Origin = Anchor.BottomCentre; + sprite.Scale = new Vector2(1, -1); + } + else + { + sprite.Origin = Anchor.TopCentre; + sprite.Scale = Vector2.One; + } + } + } +} diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs index b8caeaca30..69e6a0d238 100644 --- a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs @@ -61,6 +61,9 @@ namespace osu.Game.Rulesets.Mania.Skinning case ManiaSkinComponents.HoldNoteTail: return new LegacyHoldNoteTailPiece(); + + case ManiaSkinComponents.HoldNoteBody: + return new LegacyBodyPiece(); } break; diff --git a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs index ca4811b3d5..72556a79b4 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs @@ -29,6 +29,7 @@ namespace osu.Game.Skinning KeyImageDown, NoteImage, HoldNoteHeadImage, - HoldNoteTailImage + HoldNoteTailImage, + HoldNoteBodyImage, } } From 3cd353d3872ef728ccba8ec45a4d525c23edb380 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 31 Mar 2020 16:57:58 +0900 Subject: [PATCH 0723/2376] Fix possible nullrefs --- .../Objects/Drawables/Pieces/DefaultBodyPiece.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/DefaultBodyPiece.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/DefaultBodyPiece.cs index d1e6264c61..0ee0a14df3 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/DefaultBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/DefaultBodyPiece.cs @@ -81,15 +81,15 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces } }; - var holdNote = (DrawableHoldNote)drawableObject; - if (drawableObject != null) { + var holdNote = (DrawableHoldNote)drawableObject; + AccentColour.BindTo(drawableObject.AccentColour); - AccentColour.BindValueChanged(onAccentChanged, true); + isHitting.BindTo(holdNote.IsHitting); } - isHitting.BindTo(holdNote.IsHitting); + AccentColour.BindValueChanged(onAccentChanged, true); isHitting.BindValueChanged(_ => onAccentChanged(new ValueChangedEvent(AccentColour.Value, AccentColour.Value)), true); } From 9602ab17b0c32fd6f4c30ba0e72f373f8c88bb92 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 31 Mar 2020 17:13:42 +0900 Subject: [PATCH 0724/2376] Fix replay imports failing for certain mod combinations --- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index c356dd246d..a4a560c8e4 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -28,10 +28,11 @@ namespace osu.Game.Scoring.Legacy { var score = new Score { - ScoreInfo = new ScoreInfo(), Replay = new Replay() }; + WorkingBeatmap workingBeatmap; + using (SerializationReader sr = new SerializationReader(stream)) { currentRuleset = GetRuleset(sr.ReadByte()); @@ -41,7 +42,7 @@ namespace osu.Game.Scoring.Legacy var version = sr.ReadInt32(); - var workingBeatmap = GetBeatmap(sr.ReadString()); + workingBeatmap = GetBeatmap(sr.ReadString()); if (workingBeatmap is DummyWorkingBeatmap) throw new BeatmapNotFoundException(); @@ -113,6 +114,10 @@ namespace osu.Game.Scoring.Legacy CalculateAccuracy(score.ScoreInfo); + // 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; + return score; } From 5179635b2dc855a1873e94da8512476c3bebf255 Mon Sep 17 00:00:00 2001 From: mcendu Date: Tue, 31 Mar 2020 17:08:05 +0800 Subject: [PATCH 0725/2376] add shorthand method for config retrieval --- .../Skinning/LegacyManiaColumnElement.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs index 231a55a7e2..694c167f7f 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs @@ -4,8 +4,10 @@ using System; using JetBrains.Annotations; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Mania.UI; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Mania.Skinning { @@ -37,5 +39,9 @@ namespace osu.Game.Rulesets.Mania.Skinning FallbackColumnIndex = dist % 2 + 1; } } + + protected IBindable GetManiaSkinConfig(ISkinSource skin, LegacyManiaSkinConfigurationLookups lookup) + => skin.GetConfig( + new LegacyManiaSkinConfigurationLookup(Stage?.Columns.Count ?? 4, lookup, Column.Index)); } } From ec3d21e2b7e30aafcabc7bcde879f5ab34c0af57 Mon Sep 17 00:00:00 2001 From: mcendu Date: Tue, 31 Mar 2020 17:18:53 +0800 Subject: [PATCH 0726/2376] convert older elements to LegacyManiaColumnElement Also added xmldoc for new shorthand method. --- .../Skinning/LegacyColumnBackground.cs | 20 +++++++------------ .../Skinning/LegacyHitTarget.cs | 9 +++------ .../Skinning/LegacyManiaColumnElement.cs | 7 ++++++- 3 files changed, 16 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs index 96b28964d3..44354ed057 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs @@ -16,19 +16,13 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Mania.Skinning { - public class LegacyColumnBackground : CompositeDrawable, IKeyBindingHandler + public class LegacyColumnBackground : LegacyManiaColumnElement, IKeyBindingHandler { private readonly IBindable direction = new Bindable(); private Container lightContainer; private Sprite light; - [Resolved] - private Column column { get; set; } - - [Resolved(CanBeNull = true)] - private ManiaStage stage { get; set; } - public LegacyColumnBackground() { RelativeSizeAxes = Axes.Both; @@ -38,19 +32,19 @@ namespace osu.Game.Rulesets.Mania.Skinning private void load(ISkinSource skin, IScrollingInfo scrollingInfo) { string lightImage = skin.GetConfig( - new LegacyManiaSkinConfigurationLookup(stage?.Columns.Count ?? 4, LegacyManiaSkinConfigurationLookups.LightImage, 0))?.Value + new LegacyManiaSkinConfigurationLookup(Stage?.Columns.Count ?? 4, LegacyManiaSkinConfigurationLookups.LightImage, 0))?.Value ?? "mania-stage-light"; float leftLineWidth = skin.GetConfig( - new LegacyManiaSkinConfigurationLookup(stage?.Columns.Count ?? 4, LegacyManiaSkinConfigurationLookups.LeftLineWidth, column.Index)) + new LegacyManiaSkinConfigurationLookup(Stage?.Columns.Count ?? 4, LegacyManiaSkinConfigurationLookups.LeftLineWidth, Column.Index)) ?.Value ?? 1; float rightLineWidth = skin.GetConfig( - new LegacyManiaSkinConfigurationLookup(stage?.Columns.Count ?? 4, LegacyManiaSkinConfigurationLookups.RightLineWidth, column.Index)) + new LegacyManiaSkinConfigurationLookup(Stage?.Columns.Count ?? 4, LegacyManiaSkinConfigurationLookups.RightLineWidth, Column.Index)) ?.Value ?? 1; bool hasLeftLine = leftLineWidth > 0; bool hasRightLine = rightLineWidth > 0 && skin.GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value >= 2.4m - || stage == null || column.Index == stage.Columns.Count - 1; + || Stage == null || Column.Index == Stage.Columns.Count - 1; InternalChildren = new Drawable[] { @@ -109,7 +103,7 @@ namespace osu.Game.Rulesets.Mania.Skinning public bool OnPressed(ManiaAction action) { - if (action == column.Action.Value) + if (action == Column.Action.Value) { light.FadeIn(); light.ScaleTo(Vector2.One); @@ -123,7 +117,7 @@ namespace osu.Game.Rulesets.Mania.Skinning // Todo: Should be 400 * 100 / CurrentBPM const double animation_length = 250; - if (action == column.Action.Value) + if (action == Column.Action.Value) { light.FadeTo(0, animation_length); light.ScaleTo(new Vector2(1, 0), animation_length); diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs index 3e550808f3..dd909a39ca 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs @@ -14,13 +14,10 @@ using osuTK; namespace osu.Game.Rulesets.Mania.Skinning { - public class LegacyHitTarget : CompositeDrawable + public class LegacyHitTarget : LegacyManiaColumnElement { private readonly IBindable direction = new Bindable(); - [Resolved(CanBeNull = true)] - private ManiaStage stage { get; set; } - private Container directionContainer; public LegacyHitTarget() @@ -32,11 +29,11 @@ namespace osu.Game.Rulesets.Mania.Skinning private void load(ISkinSource skin, IScrollingInfo scrollingInfo) { string targetImage = skin.GetConfig( - new LegacyManiaSkinConfigurationLookup(stage?.Columns.Count ?? 4, LegacyManiaSkinConfigurationLookups.HitTargetImage))?.Value + new LegacyManiaSkinConfigurationLookup(Stage?.Columns.Count ?? 4, LegacyManiaSkinConfigurationLookups.HitTargetImage))?.Value ?? "mania-stage-hint"; bool showJudgementLine = skin.GetConfig( - new LegacyManiaSkinConfigurationLookup(stage?.Columns.Count ?? 4, LegacyManiaSkinConfigurationLookups.ShowJudgementLine))?.Value + new LegacyManiaSkinConfigurationLookup(Stage?.Columns.Count ?? 4, LegacyManiaSkinConfigurationLookups.ShowJudgementLine))?.Value ?? true; InternalChild = directionContainer = new Container diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs index 694c167f7f..4a51080594 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs @@ -40,7 +40,12 @@ namespace osu.Game.Rulesets.Mania.Skinning } } - protected IBindable GetManiaSkinConfig(ISkinSource skin, LegacyManiaSkinConfigurationLookups lookup) + /// + /// Retrieve a per-column skin configuration. + /// + /// The skin from which configuration is retrieved. + /// The value to retrieve. + protected IBindable GetManiaSkinConfig(ISkin skin, LegacyManiaSkinConfigurationLookups lookup) => skin.GetConfig( new LegacyManiaSkinConfigurationLookup(Stage?.Columns.Count ?? 4, lookup, Column.Index)); } From c0f8c1dc2836f811786fe11d6051c2646ccb04d3 Mon Sep 17 00:00:00 2001 From: mcendu Date: Tue, 31 Mar 2020 17:22:46 +0800 Subject: [PATCH 0727/2376] rename variable used for mania lookup key storage --- osu.Game/Skinning/LegacySkin.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 94caa78e6d..bcab84ddd9 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -120,14 +120,14 @@ namespace osu.Game.Skinning case SkinCustomColourLookup customColour: return SkinUtils.As(getCustomColour(customColour.Lookup.ToString())); - case LegacyManiaSkinConfigurationLookup legacy: + case LegacyManiaSkinConfigurationLookup maniaLookup: if (!AllowManiaSkin) return null; - if (!maniaConfigurations.TryGetValue(legacy.Keys, out var existing)) - maniaConfigurations[legacy.Keys] = existing = new LegacyManiaSkinConfiguration(legacy.Keys); + if (!maniaConfigurations.TryGetValue(maniaLookup.Keys, out var existing)) + maniaConfigurations[maniaLookup.Keys] = existing = new LegacyManiaSkinConfiguration(maniaLookup.Keys); - switch (legacy.Lookup) + switch (maniaLookup.Lookup) { case LegacyManiaSkinConfigurationLookups.HitPosition: return SkinUtils.As(new Bindable(existing.HitPosition)); From 71fc240aeea75d0544a8da624c36d0b9c78ee56f Mon Sep 17 00:00:00 2001 From: mcendu Date: Tue, 31 Mar 2020 17:32:05 +0800 Subject: [PATCH 0728/2376] make mania skin elements use new method --- osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs | 3 +-- .../Skinning/LegacyColumnBackground.cs | 9 +++------ osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs | 6 ++---- osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs | 6 ++---- .../Skinning/LegacyManiaColumnElement.cs | 5 +++-- osu.Game.Rulesets.Mania/Skinning/LegacyNotePiece.cs | 3 +-- 6 files changed, 12 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs index e7fb331079..643d92ff41 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs @@ -35,8 +35,7 @@ namespace osu.Game.Rulesets.Mania.Skinning [BackgroundDependencyLoader] private void load(ISkinSource skin, IScrollingInfo scrollingInfo, DrawableHitObject drawableObject) { - string imageName = skin.GetConfig( - new LegacyManiaSkinConfigurationLookup(stage?.Columns.Count ?? 4, LegacyManiaSkinConfigurationLookups.HoldNoteBodyImage, column.Index))?.Value + string imageName = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.HoldNoteBodyImage)?.Value ?? $"mania-note{FallbackColumnIndex}L"; sprite = skin.GetAnimation(imageName, true, true).With(d => diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs index 44354ed057..b94996c81d 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs @@ -31,15 +31,12 @@ namespace osu.Game.Rulesets.Mania.Skinning [BackgroundDependencyLoader] private void load(ISkinSource skin, IScrollingInfo scrollingInfo) { - string lightImage = skin.GetConfig( - new LegacyManiaSkinConfigurationLookup(Stage?.Columns.Count ?? 4, LegacyManiaSkinConfigurationLookups.LightImage, 0))?.Value + string lightImage = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.LightImage, 0)?.Value ?? "mania-stage-light"; - float leftLineWidth = skin.GetConfig( - new LegacyManiaSkinConfigurationLookup(Stage?.Columns.Count ?? 4, LegacyManiaSkinConfigurationLookups.LeftLineWidth, Column.Index)) + float leftLineWidth = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.LeftLineWidth) ?.Value ?? 1; - float rightLineWidth = skin.GetConfig( - new LegacyManiaSkinConfigurationLookup(Stage?.Columns.Count ?? 4, LegacyManiaSkinConfigurationLookups.RightLineWidth, Column.Index)) + float rightLineWidth = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.RightLineWidth) ?.Value ?? 1; bool hasLeftLine = leftLineWidth > 0; diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs index dd909a39ca..c0093f5ca1 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs @@ -28,12 +28,10 @@ namespace osu.Game.Rulesets.Mania.Skinning [BackgroundDependencyLoader] private void load(ISkinSource skin, IScrollingInfo scrollingInfo) { - string targetImage = skin.GetConfig( - new LegacyManiaSkinConfigurationLookup(Stage?.Columns.Count ?? 4, LegacyManiaSkinConfigurationLookups.HitTargetImage))?.Value + string targetImage = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.HitTargetImage)?.Value ?? "mania-stage-hint"; - bool showJudgementLine = skin.GetConfig( - new LegacyManiaSkinConfigurationLookup(Stage?.Columns.Count ?? 4, LegacyManiaSkinConfigurationLookups.ShowJudgementLine))?.Value + bool showJudgementLine = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ShowJudgementLine)?.Value ?? true; InternalChild = directionContainer = new Container diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs index 6afc86c4fa..d2541772cc 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs @@ -36,12 +36,10 @@ namespace osu.Game.Rulesets.Mania.Skinning [BackgroundDependencyLoader] private void load(ISkinSource skin, IScrollingInfo scrollingInfo) { - string upImage = skin.GetConfig( - new LegacyManiaSkinConfigurationLookup(stage?.Columns.Count ?? 4, LegacyManiaSkinConfigurationLookups.KeyImage, column.Index))?.Value + string upImage = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.KeyImage)?.Value ?? $"mania-key{FallbackColumnIndex}"; - string downImage = skin.GetConfig( - new LegacyManiaSkinConfigurationLookup(stage?.Columns.Count ?? 4, LegacyManiaSkinConfigurationLookups.KeyImageDown, column.Index))?.Value + string downImage = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.KeyImageDown)?.Value ?? $"mania-key{FallbackColumnIndex}D"; InternalChild = directionContainer = new Container diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs index 4a51080594..bf7405bb44 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs @@ -45,8 +45,9 @@ namespace osu.Game.Rulesets.Mania.Skinning /// /// The skin from which configuration is retrieved. /// The value to retrieve. - protected IBindable GetManiaSkinConfig(ISkin skin, LegacyManiaSkinConfigurationLookups lookup) + /// The index of the column to which the entry applies. + protected IBindable GetManiaSkinConfig(ISkin skin, LegacyManiaSkinConfigurationLookups lookup, int? index = null) => skin.GetConfig( - new LegacyManiaSkinConfigurationLookup(Stage?.Columns.Count ?? 4, lookup, Column.Index)); + new LegacyManiaSkinConfigurationLookup(Stage?.Columns.Count ?? 4, lookup, index ?? Column.Index)); } } diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyNotePiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyNotePiece.cs index e74509febd..d2ceb06d0b 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyNotePiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyNotePiece.cs @@ -83,8 +83,7 @@ namespace osu.Game.Rulesets.Mania.Skinning break; } - string noteImage = skin.GetConfig( - new LegacyManiaSkinConfigurationLookup(Stage?.Columns.Count ?? 4, lookup, Column.Index))?.Value + string noteImage = GetManiaSkinConfig(skin, lookup)?.Value ?? $"mania-note{FallbackColumnIndex}{suffix}"; return skin.GetTexture(noteImage); From b7d73f96eaf24ab49f4d67f0903fd768f1dcf577 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 31 Mar 2020 18:33:00 +0900 Subject: [PATCH 0729/2376] Fix osu!catch catcher hit area being too large --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 8fa9c61b6f..13935e036b 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -37,10 +37,15 @@ namespace osu.Game.Rulesets.Catch.UI public CatcherAnimationState CurrentState { get; private set; } + /// + /// The width of the catcher which can receive fruit. Equivalent to "catchMargin" in osu-stable. + /// + private const float allowed_catch_range = 0.8f; + /// /// Width of the area that can be used to attempt catches during gameplay. /// - internal float CatchWidth => CatcherArea.CATCHER_SIZE * Math.Abs(Scale.X); + internal float CatchWidth => CatcherArea.CATCHER_SIZE * Math.Abs(Scale.X) * allowed_catch_range; protected bool Dashing { From 977e1a3bfec706663347ecb167219971ee738e9f Mon Sep 17 00:00:00 2001 From: mcendu Date: Tue, 31 Mar 2020 17:48:37 +0800 Subject: [PATCH 0730/2376] split shortcut into two methods --- .../Skinning/LegacyManiaColumnElement.cs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs index bf7405bb44..5386d05504 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs @@ -40,14 +40,23 @@ namespace osu.Game.Rulesets.Mania.Skinning } } + /// + /// Retrieve a per-column-count skin configuration. + /// + /// The skin from which configuration is retrieved. + /// The value to retrieve. + /// If not null, denotes the index of the column to which the entry applies. + protected IBindable GetManiaSkinConfig(ISkin skin, LegacyManiaSkinConfigurationLookups lookup, int? index = null) + => skin.GetConfig( + new LegacyManiaSkinConfigurationLookup(Stage?.Columns.Count ?? 4, lookup, index)); + /// /// Retrieve a per-column skin configuration. /// /// The skin from which configuration is retrieved. /// The value to retrieve. - /// The index of the column to which the entry applies. - protected IBindable GetManiaSkinConfig(ISkin skin, LegacyManiaSkinConfigurationLookups lookup, int? index = null) - => skin.GetConfig( - new LegacyManiaSkinConfigurationLookup(Stage?.Columns.Count ?? 4, lookup, index ?? Column.Index)); + /// The index of the column to which the entry applies. Defaults to the column index. + protected IBindable GetPerColumnSkinConfig(ISkin skin, LegacyManiaSkinConfigurationLookups lookup, int? index = null) + => GetManiaSkinConfig(skin, lookup, index ?? Column.Index); } } From ecc305bb6384ce948391c7f27a15c56e92bc650f Mon Sep 17 00:00:00 2001 From: mcendu Date: Tue, 31 Mar 2020 17:54:51 +0800 Subject: [PATCH 0731/2376] extract superclass for all mania skinning elements --- .../Skinning/LegacyManiaColumnElement.cs | 16 +--------- .../Skinning/LegacyManiaElement.cs | 32 +++++++++++++++++++ 2 files changed, 33 insertions(+), 15 deletions(-) create mode 100644 osu.Game.Rulesets.Mania/Skinning/LegacyManiaElement.cs diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs index 5386d05504..7eaf3b5b5e 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs @@ -14,12 +14,8 @@ namespace osu.Game.Rulesets.Mania.Skinning /// /// A which is placed somewhere within a . /// - public class LegacyManiaColumnElement : CompositeDrawable + public class LegacyManiaColumnElement : LegacyManiaElement { - [Resolved(CanBeNull = true)] - [CanBeNull] - protected ManiaStage Stage { get; private set; } - [Resolved] protected Column Column { get; private set; } @@ -40,16 +36,6 @@ namespace osu.Game.Rulesets.Mania.Skinning } } - /// - /// Retrieve a per-column-count skin configuration. - /// - /// The skin from which configuration is retrieved. - /// The value to retrieve. - /// If not null, denotes the index of the column to which the entry applies. - protected IBindable GetManiaSkinConfig(ISkin skin, LegacyManiaSkinConfigurationLookups lookup, int? index = null) - => skin.GetConfig( - new LegacyManiaSkinConfigurationLookup(Stage?.Columns.Count ?? 4, lookup, index)); - /// /// Retrieve a per-column skin configuration. /// diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyManiaElement.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyManiaElement.cs new file mode 100644 index 0000000000..2fb229862f --- /dev/null +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyManiaElement.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.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Mania.UI; +using osu.Game.Skinning; + +namespace osu.Game.Rulesets.Mania.Skinning +{ + /// + /// A mania legacy skin element. + /// + public class LegacyManiaElement : CompositeDrawable + { + [Resolved(CanBeNull = true)] + [CanBeNull] + protected ManiaStage Stage { get; private set; } + + /// + /// Retrieve a per-column-count skin configuration. + /// + /// The skin from which configuration is retrieved. + /// The value to retrieve. + /// If not null, denotes the index of the column to which the entry applies. + protected virtual IBindable GetManiaSkinConfig(ISkin skin, LegacyManiaSkinConfigurationLookups lookup, int? index = null) + => skin.GetConfig( + new LegacyManiaSkinConfigurationLookup(Stage?.Columns.Count ?? 4, lookup, index)); + } +} From d41ff8c4b45edbb1fe075d048fd4c34c0f8dc3fd Mon Sep 17 00:00:00 2001 From: mcendu Date: Tue, 31 Mar 2020 17:58:29 +0800 Subject: [PATCH 0732/2376] remove Column field from LegacyHitTarget --- osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs | 3 +-- .../Skinning/LegacyManiaColumnElement.cs | 11 ++--------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs index c0093f5ca1..53e4f3cd14 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs @@ -7,14 +7,13 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; -using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; using osuTK; namespace osu.Game.Rulesets.Mania.Skinning { - public class LegacyHitTarget : LegacyManiaColumnElement + public class LegacyHitTarget : LegacyManiaElement { private readonly IBindable direction = new Bindable(); diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs index 7eaf3b5b5e..79e5673ff2 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; @@ -36,13 +35,7 @@ namespace osu.Game.Rulesets.Mania.Skinning } } - /// - /// Retrieve a per-column skin configuration. - /// - /// The skin from which configuration is retrieved. - /// The value to retrieve. - /// The index of the column to which the entry applies. Defaults to the column index. - protected IBindable GetPerColumnSkinConfig(ISkin skin, LegacyManiaSkinConfigurationLookups lookup, int? index = null) - => GetManiaSkinConfig(skin, lookup, index ?? Column.Index); + protected override IBindable GetManiaSkinConfig(ISkin skin, LegacyManiaSkinConfigurationLookups lookup, int? index = null) + => base.GetManiaSkinConfig(skin, lookup, index ?? Column.Index); } } From 3e0991d350667ab96aefa315b701895f283aecc5 Mon Sep 17 00:00:00 2001 From: mcendu Date: Tue, 31 Mar 2020 18:00:56 +0800 Subject: [PATCH 0733/2376] fix indent --- osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs index b94996c81d..22478670dc 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs @@ -35,9 +35,9 @@ namespace osu.Game.Rulesets.Mania.Skinning ?? "mania-stage-light"; float leftLineWidth = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.LeftLineWidth) - ?.Value ?? 1; + ?.Value ?? 1; float rightLineWidth = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.RightLineWidth) - ?.Value ?? 1; + ?.Value ?? 1; bool hasLeftLine = leftLineWidth > 0; bool hasRightLine = rightLineWidth > 0 && skin.GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value >= 2.4m From 03b90fe2dbed5a7851e4a689e42edcc86968970c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 31 Mar 2020 19:01:49 +0900 Subject: [PATCH 0734/2376] Remove local application of same margin in CatchDifficultyCalculator --- .../Difficulty/CatchDifficultyCalculator.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 5880a227c2..4d9dbbbc5f 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -72,10 +72,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty protected override Skill[] CreateSkills(IBeatmap beatmap) { using (var catcher = new Catcher(beatmap.BeatmapInfo.BaseDifficulty)) - { halfCatcherWidth = catcher.CatchWidth * 0.5f; - halfCatcherWidth *= 0.8f; // We're only using 80% of the catcher's width to simulate imperfect gameplay. - } return new Skill[] { From df2379fb0e85415407c84ae993a054305d556c90 Mon Sep 17 00:00:00 2001 From: mcendu Date: Tue, 31 Mar 2020 18:10:43 +0800 Subject: [PATCH 0735/2376] remove unnecessary using --- osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs index 22478670dc..b03b2fce45 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; -using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; using osuTK; From e26fbd5ed87681d8577fc5d9dced39cfd199b9cf Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 31 Mar 2020 13:45:59 +0300 Subject: [PATCH 0736/2376] Remove overcomplicated stuff --- .../TestSceneOverlayScrollContainer.cs | 6 +- osu.Game/Overlays/OverlayScrollContainer.cs | 159 ++++++++---------- 2 files changed, 75 insertions(+), 90 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs index 684436459f..0eccc907a1 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs @@ -55,13 +55,13 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void TestButtonVisibility() { - AddAssert("button is hidden", () => scroll.Button.State.Value == Visibility.Hidden); + AddAssert("button is hidden", () => scroll.Button.Current.Value == Visibility.Hidden); AddStep("scroll to end", () => scroll.ScrollToEnd(false)); - AddAssert("button is visible", () => scroll.Button.State.Value == Visibility.Visible); + AddAssert("button is visible", () => scroll.Button.Current.Value == Visibility.Visible); AddStep("scroll to start", () => scroll.ScrollToStart(false)); - AddAssert("button is hidden", () => scroll.Button.State.Value == Visibility.Hidden); + AddAssert("button is hidden", () => scroll.Button.Current.Value == Visibility.Hidden); } [Test] diff --git a/osu.Game/Overlays/OverlayScrollContainer.cs b/osu.Game/Overlays/OverlayScrollContainer.cs index a9524b9d32..f96d9e3a31 100644 --- a/osu.Game/Overlays/OverlayScrollContainer.cs +++ b/osu.Game/Overlays/OverlayScrollContainer.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -11,6 +10,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Game.Graphics.Containers; using osuTK; @@ -43,7 +43,7 @@ namespace osu.Game.Overlays { ScrollToStart(); currentTarget = Target; - Button.State.Value = Visibility.Hidden; + Button.Current.Value = Visibility.Hidden; } }); } @@ -54,7 +54,7 @@ namespace osu.Game.Overlays if (ScrollContent.DrawHeight + button_scroll_position < DrawHeight) { - Button.State.Value = Visibility.Hidden; + Button.Current.Value = Visibility.Hidden; return; } @@ -62,113 +62,98 @@ namespace osu.Game.Overlays return; currentTarget = Target; - Button.State.Value = Current > button_scroll_position ? Visibility.Visible : Visibility.Hidden; + Button.Current.Value = Current > button_scroll_position ? Visibility.Visible : Visibility.Hidden; } - public class ScrollToTopButton : VisibilityContainer + public class ScrollToTopButton : OsuHoverContainer, IHasCurrentValue { private const int fade_duration = 500; - public Action Action + private readonly BindableWithCurrent current = new BindableWithCurrent(); + + public Bindable Current { - get => button.Action; - set => button.Action = value; + get => current.Current; + set => current.Current = value; } - public override bool PropagatePositionalInputSubTree => true; + protected override IEnumerable EffectTargets => new[] { background }; - protected override bool StartHidden => true; + private Color4 flashColour; - private readonly Button button; + private readonly Container content; + private readonly Box background; public ScrollToTopButton() { Size = new Vector2(50); - Child = button = new Button + Alpha = 0; + Add(content = new CircularContainer { - AreaState = { BindTarget = State } - }; + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Masking = true, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Offset = new Vector2(0f, 1f), + Radius = 3f, + Colour = Color4.Black.Opacity(0.25f), + }, + Children = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both + }, + new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(15), + Icon = FontAwesome.Solid.ChevronUp + } + } + }); + + TooltipText = "Scroll to top"; } - protected override bool OnMouseDown(MouseDownEvent e) => true; - - protected override void PopIn() => button.FadeIn(fade_duration, Easing.OutQuint); - - protected override void PopOut() => button.FadeOut(fade_duration, Easing.OutQuint); - - private class Button : OsuHoverContainer + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) { - public readonly Bindable AreaState = new Bindable(); + IdleColour = colourProvider.Background6; + HoverColour = colourProvider.Background5; + flashColour = colourProvider.Light1; + } - public override bool HandlePositionalInput => AreaState.Value == Visibility.Visible; - - protected override IEnumerable EffectTargets => new[] { background }; - - private Color4 flashColour; - - private readonly Container content; - private readonly Box background; - - public Button() + protected override void LoadComplete() + { + base.LoadComplete(); + Current.BindValueChanged(visibility => { - RelativeSizeAxes = Axes.Both; - Add(content = new CircularContainer - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Masking = true, - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Shadow, - Offset = new Vector2(0f, 1f), - Radius = 3f, - Colour = Color4.Black.Opacity(0.25f), - }, - Children = new Drawable[] - { - background = new Box - { - RelativeSizeAxes = Axes.Both - }, - new SpriteIcon - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(15), - Icon = FontAwesome.Solid.ChevronUp - } - } - }); + Enabled.Value = visibility.NewValue == Visibility.Visible; + this.FadeTo(visibility.NewValue == Visibility.Visible ? 1 : 0, fade_duration, Easing.OutQuint); + }, true); + } - TooltipText = "Scroll to top"; - } + protected override bool OnClick(ClickEvent e) + { + background.FlashColour(flashColour, 800, Easing.OutQuint); + return base.OnClick(e); + } - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) - { - IdleColour = colourProvider.Background6; - HoverColour = colourProvider.Background5; - flashColour = colourProvider.Light1; - } + protected override bool OnMouseDown(MouseDownEvent e) + { + content.ScaleTo(0.75f, 2000, Easing.OutQuint); + return true; + } - protected override bool OnClick(ClickEvent e) - { - background.FlashColour(flashColour, 800, Easing.OutQuint); - return base.OnClick(e); - } - - protected override bool OnMouseDown(MouseDownEvent e) - { - content.ScaleTo(0.75f, 2000, Easing.OutQuint); - return true; - } - - protected override void OnMouseUp(MouseUpEvent e) - { - content.ScaleTo(1, 1000, Easing.OutElastic); - base.OnMouseUp(e); - } + protected override void OnMouseUp(MouseUpEvent e) + { + content.ScaleTo(1, 1000, Easing.OutElastic); + base.OnMouseUp(e); } } } From ff499b7d6b205f234b3f05416ac62ef9729aec45 Mon Sep 17 00:00:00 2001 From: mcendu Date: Tue, 31 Mar 2020 19:12:02 +0800 Subject: [PATCH 0737/2376] fix indent --- osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs index b03b2fce45..b4bf6b1652 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs @@ -34,9 +34,9 @@ namespace osu.Game.Rulesets.Mania.Skinning ?? "mania-stage-light"; float leftLineWidth = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.LeftLineWidth) - ?.Value ?? 1; + ?.Value ?? 1; float rightLineWidth = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.RightLineWidth) - ?.Value ?? 1; + ?.Value ?? 1; bool hasLeftLine = leftLineWidth > 0; bool hasRightLine = rightLineWidth > 0 && skin.GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value >= 2.4m From 03689adda8abadaddac9a823b9990fa2719d3000 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 31 Mar 2020 21:33:59 +0900 Subject: [PATCH 0738/2376] 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 6db4220fad..9e729d8705 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 4163044273..30c11a1cdb 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -22,7 +22,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 17430e4b25..d035f5c4d8 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + From a7eda32a6eea5822049b13373ef14fba213c9bf0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 31 Mar 2020 22:34:41 +0900 Subject: [PATCH 0739/2376] Fix missing comma --- osu.Game.Rulesets.Mania/ManiaSkinComponent.cs | 2 +- osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs index abb919a8af..5969a90e2c 100644 --- a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs +++ b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Mania public enum ManiaSkinComponents { - KeyArea + KeyArea, ColumnBackground } } diff --git a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs index a134f5b135..79c7922ba9 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs @@ -20,7 +20,7 @@ namespace osu.Game.Skinning public enum LegacyManiaSkinConfigurationLookups { KeyImage, - KeyImageDown + KeyImageDown, LightImage, LeftLineWidth, RightLineWidth From a894b42a32e6eeffc22cc2298f2f78067e5faa5b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 31 Mar 2020 22:41:16 +0900 Subject: [PATCH 0740/2376] Fix merge conflict mess --- osu.Game.Rulesets.Mania/UI/Column.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 70a18764f8..0ace5160fa 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -33,13 +33,10 @@ namespace osu.Game.Rulesets.Mania.UI public readonly Bindable Action = new Bindable(); - private readonly ColumnBackground background; - - private readonly ColumnKeyArea keyArea; - private readonly ColumnHitObjectArea hitObjectArea; internal readonly Container TopLevelContainer; + private readonly Container explosionContainer; public Column(int index) @@ -133,8 +130,6 @@ namespace osu.Game.Rulesets.Mania.UI accentColour = value; - background.AccentColour = value; - keyArea.AccentColour = value; hitObjectArea.AccentColour = value; } } From 1e88d3c17a557640fa81675e97f6f389436ef34d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 31 Mar 2020 23:35:23 +0900 Subject: [PATCH 0741/2376] Merge conflict "resolution" --- osu.Android.props | 4 +-- ...her-fail.png => fruit-catcher-fail@2x.png} | Bin ...her-kiai.png => fruit-catcher-kiai@2x.png} | Bin .../Difficulty/CatchDifficultyCalculator.cs | 3 -- osu.Game.Rulesets.Catch/UI/Catcher.cs | 7 +++- osu.Game.Rulesets.Catch/UI/CatcherSprite.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs | 2 +- .../Drawables/Connections/FollowPoint.cs | 4 ++- .../Connections/FollowPointConnection.cs | 12 +++---- .../Skinning/LegacyMainCirclePiece.cs | 5 +++ .../Skinning/OsuSkinConfiguration.cs | 3 +- .../Gameplay/TestSceneSkinnableDrawable.cs | 12 +++---- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 9 +++-- osu.Game/Skinning/IAnimationTimeReference.cs | 25 +++++++++++++ osu.Game/Skinning/LegacySkinExtensions.cs | 23 +++++++++++- osu.Game/Skinning/SkinnableDrawable.cs | 18 +++------- .../Drawables/DrawableStoryboard.cs | 2 +- .../Drawables/DrawableStoryboardLayer.cs | 33 +++++++++++++----- osu.Game/osu.Game.csproj | 4 +-- osu.iOS.props | 6 ++-- 20 files changed, 120 insertions(+), 54 deletions(-) rename osu.Game.Rulesets.Catch.Tests/Resources/special-skin/{fruit-catcher-fail.png => fruit-catcher-fail@2x.png} (100%) rename osu.Game.Rulesets.Catch.Tests/Resources/special-skin/{fruit-catcher-kiai.png => fruit-catcher-kiai@2x.png} (100%) create mode 100644 osu.Game/Skinning/IAnimationTimeReference.cs diff --git a/osu.Android.props b/osu.Android.props index b147fdd05b..9e729d8705 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,7 +51,7 @@ - - + + diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-fail.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-fail@2x.png similarity index 100% rename from osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-fail.png rename to osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-fail@2x.png diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-kiai.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-kiai@2x.png similarity index 100% rename from osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-kiai.png rename to osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-kiai@2x.png diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 5880a227c2..4d9dbbbc5f 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -72,10 +72,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty protected override Skill[] CreateSkills(IBeatmap beatmap) { using (var catcher = new Catcher(beatmap.BeatmapInfo.BaseDifficulty)) - { halfCatcherWidth = catcher.CatchWidth * 0.5f; - halfCatcherWidth *= 0.8f; // We're only using 80% of the catcher's width to simulate imperfect gameplay. - } return new Skill[] { diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 8fa9c61b6f..13935e036b 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -37,10 +37,15 @@ namespace osu.Game.Rulesets.Catch.UI public CatcherAnimationState CurrentState { get; private set; } + /// + /// The width of the catcher which can receive fruit. Equivalent to "catchMargin" in osu-stable. + /// + private const float allowed_catch_range = 0.8f; + /// /// Width of the area that can be used to attempt catches during gameplay. /// - internal float CatchWidth => CatcherArea.CATCHER_SIZE * Math.Abs(Scale.X); + internal float CatchWidth => CatcherArea.CATCHER_SIZE * Math.Abs(Scale.X) * allowed_catch_range; protected bool Dashing { diff --git a/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs b/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs index 52eb8d597e..ef69e3d2d1 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Catch.UI public CatcherSprite(CatcherAnimationState state) : base(new CatchSkinComponent(componentFromState(state)), _ => - new DefaultCatcherSprite(state), confineMode: ConfineMode.ScaleDownToFit) + new DefaultCatcherSprite(state), confineMode: ConfineMode.ScaleToFit) { RelativeSizeAxes = Axes.None; Size = new Vector2(CatcherArea.CATCHER_SIZE); diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs index 6286c80d7c..9b0759d9d2 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs @@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Osu.Mods void handleHitCircle(DrawableHitCircle circle) { - if (!circle.IsHovered) + if (!circle.HitArea.IsHovered) return; Debug.Assert(circle.HitObject.HitWindows != null); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs index 7e530ca047..8bb324d02e 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections /// /// A single follow point positioned between two adjacent s. /// - public class FollowPoint : Container + public class FollowPoint : Container, IAnimationTimeReference { private const float width = 8; @@ -45,5 +45,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections } }, confineMode: ConfineMode.NoScaling); } + + public double AnimationStartTime { get; set; } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs index d0935e46f7..6f09bbcd57 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs @@ -116,6 +116,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections int point = 0; + ClearInternal(); + for (int d = (int)(spacing * 1.5); d < distance - spacing; d += spacing) { float fraction = (float)d / distance; @@ -126,13 +128,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections FollowPoint fp; - if (InternalChildren.Count > point) - { - fp = (FollowPoint)InternalChildren[point]; - fp.ClearTransforms(); - } - else - AddInternal(fp = new FollowPoint()); + AddInternal(fp = new FollowPoint()); fp.Position = pointStartPosition; fp.Rotation = rotation; @@ -142,6 +138,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections if (firstTransformStartTime == null) firstTransformStartTime = fadeInTime; + fp.AnimationStartTime = fadeInTime; + using (fp.BeginAbsoluteSequence(fadeInTime)) { fp.FadeIn(osuEnd.TimeFadeIn); diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs index 38ba4c5974..e7486ef9b0 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs @@ -62,6 +62,11 @@ namespace osu.Game.Rulesets.Osu.Skinning } }; + bool overlayAboveNumber = skin.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumber)?.Value ?? true; + + if (!overlayAboveNumber) + ChangeInternalChildDepth(hitCircleText, -float.MaxValue); + state.BindTo(drawableObject.State); state.BindValueChanged(updateState, true); diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs index 5d99960f10..c6920bd03e 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs @@ -11,6 +11,7 @@ namespace osu.Game.Rulesets.Osu.Skinning SliderPathRadius, AllowSliderBallTint, CursorExpand, - CursorRotate + CursorRotate, + HitCircleOverlayAboveNumber } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs index ec94053679..3b91243fee 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs @@ -43,16 +43,15 @@ namespace osu.Game.Tests.Visual.Gameplay { new ExposedSkinnableDrawable("default", _ => new DefaultBox(), _ => true), new ExposedSkinnableDrawable("available", _ => new DefaultBox(), _ => true), - new ExposedSkinnableDrawable("available", _ => new DefaultBox(), _ => true, ConfineMode.ScaleToFit), new ExposedSkinnableDrawable("available", _ => new DefaultBox(), _ => true, ConfineMode.NoScaling) } }, }; }); - AddAssert("check sizes", () => fill.Children.Select(c => c.Drawable.DrawWidth).SequenceEqual(new float[] { 30, 30, 30, 50 })); + AddAssert("check sizes", () => fill.Children.Select(c => c.Drawable.DrawWidth).SequenceEqual(new float[] { 30, 30, 50 })); AddStep("adjust scale", () => fill.Scale = new Vector2(2)); - AddAssert("check sizes unchanged by scale", () => fill.Children.Select(c => c.Drawable.DrawWidth).SequenceEqual(new float[] { 30, 30, 30, 50 })); + AddAssert("check sizes unchanged by scale", () => fill.Children.Select(c => c.Drawable.DrawWidth).SequenceEqual(new float[] { 30, 30, 50 })); } [Test] @@ -74,7 +73,6 @@ namespace osu.Game.Tests.Visual.Gameplay Children = new[] { new ExposedSkinnableDrawable("default", _ => new DefaultBox(), _ => true), - new ExposedSkinnableDrawable("available", _ => new DefaultBox(), _ => true), new ExposedSkinnableDrawable("available", _ => new DefaultBox(), _ => true, ConfineMode.ScaleToFit), new ExposedSkinnableDrawable("available", _ => new DefaultBox(), _ => true, ConfineMode.NoScaling) } @@ -82,9 +80,9 @@ namespace osu.Game.Tests.Visual.Gameplay }; }); - AddAssert("check sizes", () => fill.Children.Select(c => c.Drawable.DrawWidth).SequenceEqual(new float[] { 50, 30, 50, 30 })); + AddAssert("check sizes", () => fill.Children.Select(c => c.Drawable.DrawWidth).SequenceEqual(new float[] { 50, 50, 30 })); AddStep("adjust scale", () => fill.Scale = new Vector2(2)); - AddAssert("check sizes unchanged by scale", () => fill.Children.Select(c => c.Drawable.DrawWidth).SequenceEqual(new float[] { 50, 30, 50, 30 })); + AddAssert("check sizes unchanged by scale", () => fill.Children.Select(c => c.Drawable.DrawWidth).SequenceEqual(new float[] { 50, 50, 30 })); } [Test] @@ -182,7 +180,7 @@ namespace osu.Game.Tests.Visual.Gameplay public new Drawable Drawable => base.Drawable; public ExposedSkinnableDrawable(string name, Func defaultImplementation, Func allowFallback = null, - ConfineMode confineMode = ConfineMode.ScaleDownToFit) + ConfineMode confineMode = ConfineMode.ScaleToFit) : base(new TestSkinComponent(name), defaultImplementation, allowFallback, confineMode) { } diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index c356dd246d..a4a560c8e4 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -28,10 +28,11 @@ namespace osu.Game.Scoring.Legacy { var score = new Score { - ScoreInfo = new ScoreInfo(), Replay = new Replay() }; + WorkingBeatmap workingBeatmap; + using (SerializationReader sr = new SerializationReader(stream)) { currentRuleset = GetRuleset(sr.ReadByte()); @@ -41,7 +42,7 @@ namespace osu.Game.Scoring.Legacy var version = sr.ReadInt32(); - var workingBeatmap = GetBeatmap(sr.ReadString()); + workingBeatmap = GetBeatmap(sr.ReadString()); if (workingBeatmap is DummyWorkingBeatmap) throw new BeatmapNotFoundException(); @@ -113,6 +114,10 @@ namespace osu.Game.Scoring.Legacy CalculateAccuracy(score.ScoreInfo); + // 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; + return score; } diff --git a/osu.Game/Skinning/IAnimationTimeReference.cs b/osu.Game/Skinning/IAnimationTimeReference.cs new file mode 100644 index 0000000000..bcff10a24b --- /dev/null +++ b/osu.Game/Skinning/IAnimationTimeReference.cs @@ -0,0 +1,25 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Timing; + +namespace osu.Game.Skinning +{ + /// + /// Denotes an object which provides a reference time to start animations from. + /// + [Cached] + public interface IAnimationTimeReference + { + /// + /// The reference clock. + /// + IFrameBasedClock Clock { get; } + + /// + /// The time which animations should be started from, relative to . + /// + double AnimationStartTime { get; } + } +} diff --git a/osu.Game/Skinning/LegacySkinExtensions.cs b/osu.Game/Skinning/LegacySkinExtensions.cs index 52328d43b2..8765b161d4 100644 --- a/osu.Game/Skinning/LegacySkinExtensions.cs +++ b/osu.Game/Skinning/LegacySkinExtensions.cs @@ -3,10 +3,12 @@ using System.Collections.Generic; using System.Linq; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; +using osu.Framework.Timing; namespace osu.Game.Skinning { @@ -22,7 +24,7 @@ namespace osu.Game.Skinning if (textures.Length > 0) { - var animation = new TextureAnimation + var animation = new SkinnableTextureAnimation { DefaultFrameLength = getFrameLength(source, applyConfigFrameRate, textures), Repeat = looping, @@ -53,6 +55,25 @@ namespace osu.Game.Skinning } } + public class SkinnableTextureAnimation : TextureAnimation + { + [Resolved(canBeNull: true)] + private IAnimationTimeReference timeReference { get; set; } + + public SkinnableTextureAnimation() + : base(false) + { + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + if (timeReference != null) + Clock = new FramedOffsetClock(timeReference.Clock) { Offset = -timeReference.AnimationStartTime }; + } + } + private const double default_frame_time = 1000 / 60d; private static double getFrameLength(ISkin source, bool applyConfigFrameRate, Texture[] textures) diff --git a/osu.Game/Skinning/SkinnableDrawable.cs b/osu.Game/Skinning/SkinnableDrawable.cs index f6ac6494b4..0f0d3da5aa 100644 --- a/osu.Game/Skinning/SkinnableDrawable.cs +++ b/osu.Game/Skinning/SkinnableDrawable.cs @@ -98,20 +98,13 @@ namespace osu.Game.Skinning switch (confineMode) { - case ConfineMode.NoScaling: - return; - - case ConfineMode.ScaleDownToFit: - if (Drawable.DrawSize.X <= DrawSize.X && Drawable.DrawSize.Y <= DrawSize.Y) - return; - + case ConfineMode.ScaleToFit: + Drawable.RelativeSizeAxes = Axes.Both; + Drawable.Size = Vector2.One; + Drawable.Scale = Vector2.One; + Drawable.FillMode = FillMode.Fit; break; } - - Drawable.RelativeSizeAxes = Axes.Both; - Drawable.Size = Vector2.One; - Drawable.Scale = Vector2.One; - Drawable.FillMode = FillMode.Fit; } finally { @@ -127,7 +120,6 @@ namespace osu.Game.Skinning /// Don't apply any scaling. This allows the user element to be of any size, exceeding specified bounds. /// NoScaling, - ScaleDownToFit, ScaleToFit, } } diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs index bc6e01a729..c4d796e30b 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs @@ -50,7 +50,7 @@ namespace osu.Game.Storyboards.Drawables AddInternal(Content = new Container { - Size = new Vector2(640, 480), + RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, }); diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardLayer.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardLayer.cs index def4eed2ca..2ada83c3b4 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardLayer.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardLayer.cs @@ -8,7 +8,7 @@ using osu.Framework.Graphics.Containers; namespace osu.Game.Storyboards.Drawables { - public class DrawableStoryboardLayer : LifetimeManagementContainer + public class DrawableStoryboardLayer : CompositeDrawable { public StoryboardLayer Layer { get; } public bool Enabled; @@ -23,17 +23,34 @@ namespace osu.Game.Storyboards.Drawables Origin = Anchor.Centre; Enabled = layer.VisibleWhenPassing; Masking = layer.Masking; + + InternalChild = new LayerElementContainer(layer); } - [BackgroundDependencyLoader] - private void load(CancellationToken? cancellationToken) + private class LayerElementContainer : LifetimeManagementContainer { - foreach (var element in Layer.Elements) - { - cancellationToken?.ThrowIfCancellationRequested(); + private readonly StoryboardLayer storyboardLayer; - if (element.IsDrawable) - AddInternal(element.CreateDrawable()); + public LayerElementContainer(StoryboardLayer layer) + { + storyboardLayer = layer; + + Width = 640; + Height = 480; + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + } + + [BackgroundDependencyLoader] + private void load(CancellationToken? cancellationToken) + { + foreach (var element in storyboardLayer.Elements) + { + cancellationToken?.ThrowIfCancellationRequested(); + + if (element.IsDrawable) + AddInternal(element.CreateDrawable()); + } } } } diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 781c566b5f..30c11a1cdb 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -22,8 +22,8 @@ - - + + diff --git a/osu.iOS.props b/osu.iOS.props index a2c6106931..d035f5c4d8 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,8 +70,8 @@ - - + + @@ -79,7 +79,7 @@ - + From 44fcd2613f99a9844c6a87205225710a29c9292a Mon Sep 17 00:00:00 2001 From: mcendu Date: Tue, 31 Mar 2020 22:58:04 +0800 Subject: [PATCH 0742/2376] Add support for special column --- .../Skinning/LegacyManiaColumnElement.cs | 25 +++++++++++++------ osu.Game.Rulesets.Mania/UI/ManiaStage.cs | 1 + 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs index 79e5673ff2..d479d07ad1 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs @@ -5,6 +5,7 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.UI; using osu.Game.Skinning; @@ -19,20 +20,30 @@ namespace osu.Game.Rulesets.Mania.Skinning protected Column Column { get; private set; } /// - /// The column index to use for texture lookups, in the case of no user-provided configuration. + /// The column type identifier to use for texture lookups, in the case of no user-provided configuration. /// - protected int FallbackColumnIndex { get; private set; } + protected string FallbackColumnIndex { get; private set; } [BackgroundDependencyLoader] private void load() { if (Stage == null) - FallbackColumnIndex = Column.Index % 2 + 1; + FallbackColumnIndex = (Column.Index % 2 + 1).ToString(); else - { - int dist = Math.Min(Column.Index, Stage.Columns.Count - Column.Index - 1); - FallbackColumnIndex = dist % 2 + 1; - } + switch (Column.ColumnType) + { + case ColumnType.Special: + FallbackColumnIndex = "S"; + break; + + case ColumnType.Odd: + FallbackColumnIndex = "1"; + break; + + case ColumnType.Even: + FallbackColumnIndex = "2"; + break; + } } protected override IBindable GetManiaSkinConfig(ISkin skin, LegacyManiaSkinConfigurationLookups lookup, int? index = null) diff --git a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs b/osu.Game.Rulesets.Mania/UI/ManiaStage.cs index 9edb384753..047284086e 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaStage.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; From bb5fa472dcc43d1ff37575fe5ec0332bbc73090d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 1 Apr 2020 11:59:34 +0900 Subject: [PATCH 0743/2376] Remove null-stage fallback --- .../Skinning/LegacyManiaColumnElement.cs | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs index d479d07ad1..05b731ec5d 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; @@ -27,23 +26,20 @@ namespace osu.Game.Rulesets.Mania.Skinning [BackgroundDependencyLoader] private void load() { - if (Stage == null) - FallbackColumnIndex = (Column.Index % 2 + 1).ToString(); - else - switch (Column.ColumnType) - { - case ColumnType.Special: - FallbackColumnIndex = "S"; - break; + switch (Column.ColumnType) + { + case ColumnType.Special: + FallbackColumnIndex = "S"; + break; - case ColumnType.Odd: - FallbackColumnIndex = "1"; - break; + case ColumnType.Odd: + FallbackColumnIndex = "1"; + break; - case ColumnType.Even: - FallbackColumnIndex = "2"; - break; - } + case ColumnType.Even: + FallbackColumnIndex = "2"; + break; + } } protected override IBindable GetManiaSkinConfig(ISkin skin, LegacyManiaSkinConfigurationLookups lookup, int? index = null) From 716c7fa07a6c6607b85ae6730fbf192160ec73a1 Mon Sep 17 00:00:00 2001 From: mcendu Date: Wed, 1 Apr 2020 11:04:29 +0800 Subject: [PATCH 0744/2376] Add check to detect whether mania is skinned --- .../Skinning/ManiaLegacySkinTransformer.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs index 69e6a0d238..88eb6e0d2f 100644 --- a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs @@ -18,6 +18,12 @@ namespace osu.Game.Rulesets.Mania.Skinning private Lazy isLegacySkin; + /// + /// Whether texture for the keys exists. + /// Used to determine if the mania ruleset is skinned. + /// + private Lazy hasKeyTexture; + public ManiaLegacySkinTransformer(ISkinSource source) { this.source = source; @@ -29,6 +35,10 @@ namespace osu.Game.Rulesets.Mania.Skinning private void sourceChanged() { isLegacySkin = new Lazy(() => source.GetConfig(LegacySkinConfiguration.LegacySetting.Version) != null); + hasKeyTexture = new Lazy(() => source.GetTexture( + source.GetConfig( + new LegacyManiaSkinConfigurationLookup(4, LegacyManiaSkinConfigurationLookups.KeyImage, 0))?.Value + ?? $"mania-key1") != null); } public Drawable GetDrawableComponent(ISkinComponent component) @@ -39,7 +49,7 @@ namespace osu.Game.Rulesets.Mania.Skinning return getResult(resultComponent); case ManiaSkinComponent maniaComponent: - if (!isLegacySkin.Value) + if (!isLegacySkin.Value || !hasKeyTexture.Value) return null; switch (maniaComponent.Component) From c10a91a33ed488bdc57d219224fb86355b9c6266 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 1 Apr 2020 12:04:33 +0900 Subject: [PATCH 0745/2376] Add odd/even type to test scenes --- osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs index c807e98871..ff4865c71d 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.UI; using osuTK.Graphics; @@ -26,7 +27,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning this.column = new Column(column) { Action = { Value = action }, - AccentColour = Color4.Orange + AccentColour = Color4.Orange, + ColumnType = column % 2 == 0 ? ColumnType.Even : ColumnType.Odd }; InternalChild = content = new ManiaInputManager(new ManiaRuleset().RulesetInfo, 4) From 66486b094c162625fff9bdeafe8cf1a440905913 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 1 Apr 2020 13:31:17 +0900 Subject: [PATCH 0746/2376] Remove unnecessary dependency, allow null mods --- osu.Game/Rulesets/UI/Playfield.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 8141108aef..c52183f3f2 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -10,7 +10,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics.Containers; -using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; using osuTK; @@ -62,10 +61,7 @@ namespace osu.Game.Rulesets.UI hitObjectContainerLazy = new Lazy(CreateHitObjectContainer); } - [Resolved] - private IBindable beatmap { get; set; } - - [Resolved] + [Resolved(CanBeNull = true)] private IReadOnlyList mods { get; set; } [BackgroundDependencyLoader] @@ -137,7 +133,7 @@ namespace osu.Game.Rulesets.UI { base.Update(); - if (beatmap != null) + if (mods != null) { foreach (var mod in mods) { From aac77096400c4fa48e1410e27b264409a91e71f4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 1 Apr 2020 13:31:25 +0900 Subject: [PATCH 0747/2376] Add stage test scene --- .../Skinning/TestSceneStage.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs new file mode 100644 index 0000000000..0d5ebd33e9 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs @@ -0,0 +1,27 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Rulesets.Mania.UI; + +namespace osu.Game.Rulesets.Mania.Tests.Skinning +{ + public class TestSceneStage : ManiaSkinnableTestScene + { + [BackgroundDependencyLoader] + private void load() + { + SetContents(() => + { + ManiaAction normalAction = ManiaAction.Key1; + ManiaAction specialAction = ManiaAction.Special1; + + return new ManiaInputManager(new ManiaRuleset().RulesetInfo, 4) + { + Child = new ManiaStage(0, new StageDefinition { Columns = 4 }, ref normalAction, ref specialAction) + }; + }); + } + } +} From 2d6d1a8cc6102c03ea56703c0dc1f5189bc38f69 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 1 Apr 2020 13:38:03 +0900 Subject: [PATCH 0748/2376] Implement column width and column spacing --- osu.Game.Rulesets.Mania/UI/Column.cs | 21 ++--------- osu.Game.Rulesets.Mania/UI/ManiaStage.cs | 37 ++++++++++++++++++- .../LegacyManiaSkinConfigurationLookup.cs | 2 + osu.Game/Skinning/LegacySkin.cs | 9 +++++ 4 files changed, 50 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 141718ef5e..153345dde7 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Mania.UI public class Column : ScrollingPlayfield, IKeyBindingHandler, IHasAccentColour { public const float COLUMN_WIDTH = 80; - private const float special_column_width = 70; + public const float SPECIAL_COLUMN_WIDTH = 70; /// /// The index of this column as part of the whole playfield. @@ -42,7 +42,6 @@ namespace osu.Game.Rulesets.Mania.UI Index = index; RelativeSizeAxes = Axes.Y; - Width = COLUMN_WIDTH; Drawable background = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground), _ => new DefaultColumnBackground()) { @@ -67,23 +66,9 @@ namespace osu.Game.Rulesets.Mania.UI public override Axes RelativeSizeAxes => Axes.Y; - private ColumnType columnType; + public ColumnType ColumnType { get; set; } - public ColumnType ColumnType - { - get => columnType; - set - { - if (columnType == value) - return; - - columnType = value; - - Width = IsSpecial ? special_column_width : COLUMN_WIDTH; - } - } - - public bool IsSpecial => columnType == ColumnType.Special; + public bool IsSpecial => ColumnType == ColumnType.Special; public Color4 AccentColour { get; set; } diff --git a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs b/osu.Game.Rulesets.Mania/UI/ManiaStage.cs index 047284086e..0e3fd52a13 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaStage.cs @@ -14,6 +14,7 @@ using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Skinning; using osuTK; using osuTK.Graphics; @@ -93,7 +94,6 @@ namespace osu.Game.Rulesets.Mania.UI AutoSizeAxes = Axes.X, Direction = FillDirection.Horizontal, Padding = new MarginPadding { Left = COLUMN_SPACING, Right = COLUMN_SPACING }, - Spacing = new Vector2(COLUMN_SPACING, 0) }, } }, @@ -150,6 +150,41 @@ namespace osu.Game.Rulesets.Mania.UI }, true); } + private ISkin currentSkin; + + [BackgroundDependencyLoader] + private void load(ISkinSource skin) + { + currentSkin = skin; + skin.SourceChanged += onSkinChanged; + + onSkinChanged(); + } + + private void onSkinChanged() + { + foreach (var col in columnFlow) + { + if (col.Index > 0) + { + float spacing = currentSkin.GetConfig( + new LegacyManiaSkinConfigurationLookup(Columns.Count, LegacyManiaSkinConfigurationLookups.ColumnSpacing, col.Index - 1)) + ?.Value ?? COLUMN_SPACING; + + col.Margin = new MarginPadding { Left = spacing }; + } + + float? width = currentSkin.GetConfig( + new LegacyManiaSkinConfigurationLookup(Columns.Count, LegacyManiaSkinConfigurationLookups.ColumnWidth, col.Index)) + ?.Value; + + if (width == null) + col.Width = col.IsSpecial ? Column.SPECIAL_COLUMN_WIDTH : Column.COLUMN_WIDTH; + else + col.Width = width.Value; + } + } + public void AddColumn(Column c) { topLevelContainer.Add(c.TopLevelContainer.CreateProxy()); diff --git a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs index 72556a79b4..67895a69e4 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs @@ -19,6 +19,8 @@ namespace osu.Game.Skinning public enum LegacyManiaSkinConfigurationLookups { + ColumnWidth, + ColumnSpacing, LightImage, LeftLineWidth, RightLineWidth, diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index bcab84ddd9..7d0fa2489e 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using JetBrains.Annotations; @@ -129,6 +130,14 @@ namespace osu.Game.Skinning switch (maniaLookup.Lookup) { + case LegacyManiaSkinConfigurationLookups.ColumnWidth: + Debug.Assert(maniaLookup.TargetColumn != null); + return SkinUtils.As(new Bindable(existing.ColumnWidth[maniaLookup.TargetColumn.Value])); + + case LegacyManiaSkinConfigurationLookups.ColumnSpacing: + Debug.Assert(maniaLookup.TargetColumn != null); + return SkinUtils.As(new Bindable(existing.ColumnSpacing[maniaLookup.TargetColumn.Value])); + case LegacyManiaSkinConfigurationLookups.HitPosition: return SkinUtils.As(new Bindable(existing.HitPosition)); From 87e5e98caedb8e0f5d97a91deb72d7a8b4b9d30d Mon Sep 17 00:00:00 2001 From: mcendu Date: Wed, 1 Apr 2020 14:17:23 +0800 Subject: [PATCH 0749/2376] use GetAnimation for checking --- .../Skinning/ManiaLegacySkinTransformer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs index 88eb6e0d2f..9b077fc398 100644 --- a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs @@ -35,10 +35,10 @@ namespace osu.Game.Rulesets.Mania.Skinning private void sourceChanged() { isLegacySkin = new Lazy(() => source.GetConfig(LegacySkinConfiguration.LegacySetting.Version) != null); - hasKeyTexture = new Lazy(() => source.GetTexture( + hasKeyTexture = new Lazy(() => source.GetAnimation( source.GetConfig( new LegacyManiaSkinConfigurationLookup(4, LegacyManiaSkinConfigurationLookups.KeyImage, 0))?.Value - ?? $"mania-key1") != null); + ?? $"mania-key1", true, true) != null); } public Drawable GetDrawableComponent(ISkinComponent component) From 9de348235e7df98b1d6fe17082b73b976aafe9d8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 1 Apr 2020 15:30:51 +0900 Subject: [PATCH 0750/2376] Add comment about legacy fallback widths --- osu.Game.Rulesets.Mania/UI/ManiaStage.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs b/osu.Game.Rulesets.Mania/UI/ManiaStage.cs index 0e3fd52a13..b5f2c126ae 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaStage.cs @@ -179,6 +179,7 @@ namespace osu.Game.Rulesets.Mania.UI ?.Value; if (width == null) + // only used by default skin (legacy skins get defaults set in LegacyManiaSkinConfiguration) col.Width = col.IsSpecial ? Column.SPECIAL_COLUMN_WIDTH : Column.COLUMN_WIDTH; else col.Width = width.Value; From ff2c5b446e9787494d1ad9690036904a3bf43ddf Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 1 Apr 2020 16:05:52 +0900 Subject: [PATCH 0751/2376] Fix column lights positioned incorrectly --- osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs | 5 +++++ osu.Game/Skinning/LegacyManiaSkinConfiguration.cs | 1 + osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs | 1 + osu.Game/Skinning/LegacyManiaSkinDecoder.cs | 4 ++++ osu.Game/Skinning/LegacySkin.cs | 3 +++ 5 files changed, 14 insertions(+) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs index b4bf6b1652..7e8f720e99 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs @@ -42,6 +42,10 @@ namespace osu.Game.Rulesets.Mania.Skinning bool hasRightLine = rightLineWidth > 0 && skin.GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value >= 2.4m || Stage == null || Column.Index == Stage.Columns.Count - 1; + float lightPosition = skin.GetConfig( + new LegacyManiaSkinConfigurationLookup(Stage?.Columns.Count ?? 4, LegacyManiaSkinConfigurationLookups.LightPosition))?.Value + ?? 0; + InternalChildren = new Drawable[] { new Box @@ -67,6 +71,7 @@ namespace osu.Game.Rulesets.Mania.Skinning { Origin = Anchor.BottomCentre, RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Bottom = lightPosition }, Child = light = new Sprite { Anchor = Anchor.BottomCentre, diff --git a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs index 56d2652e76..0d0c4943ef 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs @@ -14,6 +14,7 @@ namespace osu.Game.Skinning public readonly float[] ColumnWidth; public float HitPosition = 124.8f; // (480 - 402) * 1.6f + public float LightPosition = 107.2f; // (480 - 413) * 1.6f public bool ShowJudgementLine = true; public LegacyManiaSkinConfiguration(int keys) diff --git a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs index 67895a69e4..49e4faa269 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs @@ -25,6 +25,7 @@ namespace osu.Game.Skinning LeftLineWidth, RightLineWidth, HitPosition, + LightPosition, HitTargetImage, ShowJudgementLine, KeyImage, diff --git a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs index 2c6b76847d..dabdd0a980 100644 --- a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs +++ b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs @@ -91,6 +91,10 @@ namespace osu.Game.Skinning currentConfig.HitPosition = (480 - float.Parse(pair.Value, CultureInfo.InvariantCulture)) * size_scale_factor; break; + case "LightPosition": + currentConfig.LightPosition = (480 - float.Parse(pair.Value, CultureInfo.InvariantCulture)) * size_scale_factor; + break; + case "JudgementLine": currentConfig.ShowJudgementLine = pair.Value == "1"; break; diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 7d0fa2489e..eafbdd4ee5 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -141,6 +141,9 @@ namespace osu.Game.Skinning case LegacyManiaSkinConfigurationLookups.HitPosition: return SkinUtils.As(new Bindable(existing.HitPosition)); + case LegacyManiaSkinConfigurationLookups.LightPosition: + return SkinUtils.As(new Bindable(existing.LightPosition)); + case LegacyManiaSkinConfigurationLookups.ShowJudgementLine: return SkinUtils.As(new Bindable(existing.ShowJudgementLine)); } From 59eac34d82ba057f273b44f43ceee9faabae9565 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 1 Apr 2020 18:00:17 +0900 Subject: [PATCH 0752/2376] Fix barlines scrolling at different speeds in legacy skins --- .../UI/Components/ColumnHitObjectArea.cs | 52 +++++------------ .../UI/Components/HitObjectArea.cs | 57 +++++++++++++++++++ osu.Game.Rulesets.Mania/UI/ManiaStage.cs | 16 +----- 3 files changed, 73 insertions(+), 52 deletions(-) create mode 100644 osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs diff --git a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs index fb6e8a87e5..6cf08a708d 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; @@ -12,65 +10,41 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Mania.UI.Components { - public class ColumnHitObjectArea : SkinReloadableDrawable + public class ColumnHitObjectArea : HitObjectArea { public readonly Container Explosions; - - [Resolved(CanBeNull = true)] - private ManiaStage stage { get; set; } - - private readonly IBindable direction = new Bindable(); private readonly Drawable hitTarget; public ColumnHitObjectArea(HitObjectContainer hitObjectContainer) + : base(hitObjectContainer) { - InternalChildren = new[] + AddRangeInternal(new[] { hitTarget = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitTarget), _ => new DefaultHitTarget()) { RelativeSizeAxes = Axes.X, + Depth = 1 }, - hitObjectContainer, - Explosions = new Container { RelativeSizeAxes = Axes.Both } - }; + Explosions = new Container + { + RelativeSizeAxes = Axes.Both, + Depth = -1, + } + }); } - [BackgroundDependencyLoader] - private void load(IScrollingInfo scrollingInfo) + protected override void UpdateHitPosition() { - direction.BindTo(scrollingInfo.Direction); - direction.BindValueChanged(onDirectionChanged, true); - } + base.UpdateHitPosition(); - protected override void SkinChanged(ISkinSource skin, bool allowFallback) - { - base.SkinChanged(skin, allowFallback); - updateHitPosition(); - } - - private void onDirectionChanged(ValueChangedEvent direction) - { - updateHitPosition(); - } - - private void updateHitPosition() - { - float hitPosition = CurrentSkin.GetConfig( - new LegacyManiaSkinConfigurationLookup(stage?.Columns.Count ?? 4, LegacyManiaSkinConfigurationLookups.HitPosition))?.Value - ?? ManiaStage.HIT_TARGET_POSITION; - - if (direction.Value == ScrollingDirection.Up) + if (Direction.Value == ScrollingDirection.Up) { hitTarget.Anchor = hitTarget.Origin = Anchor.TopLeft; - - Padding = new MarginPadding { Top = hitPosition }; Explosions.Padding = new MarginPadding { Top = DefaultNotePiece.NOTE_HEIGHT }; } else { hitTarget.Anchor = hitTarget.Origin = Anchor.BottomLeft; - - Padding = new MarginPadding { Bottom = hitPosition }; Explosions.Padding = new MarginPadding { Bottom = DefaultNotePiece.NOTE_HEIGHT }; } } diff --git a/osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs b/osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs new file mode 100644 index 0000000000..9e62445c81 --- /dev/null +++ b/osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.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 osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Rulesets.UI; +using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Skinning; + +namespace osu.Game.Rulesets.Mania.UI.Components +{ + public class HitObjectArea : SkinReloadableDrawable + { + protected readonly IBindable Direction = new Bindable(); + + [Resolved(CanBeNull = true)] + private ManiaStage stage { get; set; } + + public HitObjectArea(HitObjectContainer hitObjectContainer) + { + InternalChildren = new[] + { + hitObjectContainer, + }; + } + + [BackgroundDependencyLoader] + private void load(IScrollingInfo scrollingInfo) + { + Direction.BindTo(scrollingInfo.Direction); + Direction.BindValueChanged(onDirectionChanged, true); + } + + protected override void SkinChanged(ISkinSource skin, bool allowFallback) + { + base.SkinChanged(skin, allowFallback); + UpdateHitPosition(); + } + + private void onDirectionChanged(ValueChangedEvent direction) + { + UpdateHitPosition(); + } + + protected virtual void UpdateHitPosition() + { + float hitPosition = CurrentSkin.GetConfig( + new LegacyManiaSkinConfigurationLookup(stage?.Columns.Count ?? 4, LegacyManiaSkinConfigurationLookups.HitPosition))?.Value + ?? ManiaStage.HIT_TARGET_POSITION; + + Padding = Direction.Value == ScrollingDirection.Up + ? new MarginPadding { Top = hitPosition } + : new MarginPadding { Bottom = hitPosition }; + } + } +} diff --git a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs b/osu.Game.Rulesets.Mania/UI/ManiaStage.cs index b5f2c126ae..c6102675a1 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaStage.cs @@ -11,6 +11,7 @@ using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects.Drawables; +using osu.Game.Rulesets.Mania.UI.Components; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; @@ -33,11 +34,10 @@ namespace osu.Game.Rulesets.Mania.UI public IReadOnlyList Columns => columnFlow.Children; private readonly FillFlowContainer columnFlow; - private readonly Container barLineContainer; - public Container Judgements => judgements; private readonly JudgementContainer judgements; + private readonly Drawable barLineContainer; private readonly Container topLevelContainer; private readonly Dictionary columnColours = new Dictionary @@ -106,13 +106,12 @@ namespace osu.Game.Rulesets.Mania.UI Width = 1366, // Bar lines should only be masked on the vertical axis BypassAutoSizeAxes = Axes.Both, Masking = true, - Child = barLineContainer = new Container + Child = barLineContainer = new HitObjectArea(HitObjectContainer) { Name = "Bar lines", Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.Y, - Child = HitObjectContainer } }, judgements = new JudgementContainer @@ -139,15 +138,6 @@ namespace osu.Game.Rulesets.Mania.UI AddColumn(column); } - - Direction.BindValueChanged(dir => - { - barLineContainer.Padding = new MarginPadding - { - Top = dir.NewValue == ScrollingDirection.Up ? HIT_TARGET_POSITION : 0, - Bottom = dir.NewValue == ScrollingDirection.Down ? HIT_TARGET_POSITION : 0, - }; - }, true); } private ISkin currentSkin; From 558feade87b1180842b9bd22a469a0f889c07dbd Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 1 Apr 2020 18:19:11 +0900 Subject: [PATCH 0753/2376] Fix ci warnings --- osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs index 9b077fc398..3e423c6b0f 100644 --- a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Mania.Skinning hasKeyTexture = new Lazy(() => source.GetAnimation( source.GetConfig( new LegacyManiaSkinConfigurationLookup(4, LegacyManiaSkinConfigurationLookups.KeyImage, 0))?.Value - ?? $"mania-key1", true, true) != null); + ?? "mania-key1", true, true) != null); } public Drawable GetDrawableComponent(ISkinComponent component) From f4d8defa48b81cd79caefd57b14d5a777c3fbd20 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 1 Apr 2020 20:01:35 +0900 Subject: [PATCH 0754/2376] Fix incorrect explosion position on default skin --- osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs index fb6e8a87e5..1b744df331 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs @@ -64,14 +64,14 @@ namespace osu.Game.Rulesets.Mania.UI.Components hitTarget.Anchor = hitTarget.Origin = Anchor.TopLeft; Padding = new MarginPadding { Top = hitPosition }; - Explosions.Padding = new MarginPadding { Top = DefaultNotePiece.NOTE_HEIGHT }; + Explosions.Padding = new MarginPadding { Top = DefaultNotePiece.NOTE_HEIGHT / 2 }; } else { hitTarget.Anchor = hitTarget.Origin = Anchor.BottomLeft; Padding = new MarginPadding { Bottom = hitPosition }; - Explosions.Padding = new MarginPadding { Bottom = DefaultNotePiece.NOTE_HEIGHT }; + Explosions.Padding = new MarginPadding { Bottom = DefaultNotePiece.NOTE_HEIGHT / 2 }; } } } From 4d8b6c47cc189a2bb4d4af49ddd8e3edaf6dc307 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 1 Apr 2020 21:23:43 +0900 Subject: [PATCH 0755/2376] 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 9e729d8705..cb848c0433 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 30c11a1cdb..4a9d2e0830 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -23,7 +23,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index d035f5c4d8..a528bd5658 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + @@ -79,7 +79,7 @@ - + From 1562612f41681da4ead59c0d220068f933ef5faf Mon Sep 17 00:00:00 2001 From: Lucas A Date: Wed, 1 Apr 2020 15:12:31 +0200 Subject: [PATCH 0756/2376] Update visual tests and remove unessecary XMLDoc tag --- .../Visual/Gameplay/TestSceneFailingLayer.cs | 30 ++++++++++++++++--- osu.Game/Screens/Play/HUD/HealthDisplay.cs | 1 - 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs index 3016890ade..97fe0ac769 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs @@ -1,7 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Testing; +using osu.Game.Configuration; using osu.Game.Screens.Play.HUD; namespace osu.Game.Tests.Visual.Gameplay @@ -10,22 +15,39 @@ namespace osu.Game.Tests.Visual.Gameplay { private readonly FailingLayer layer; + [Resolved] + private OsuConfigManager config { get; set; } + public TestSceneFailingLayer() { Child = layer = new FailingLayer(); } + [Test] + public void TestLayerConfig() + { + AddStep("enable layer", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, true)); + AddWaitStep("wait for transition to finish", 5); + AddAssert("layer is enabled", () => layer.IsPresent); + + AddStep("disable layer", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, false)); + AddWaitStep("wait for transition to finish", 5); + AddAssert("layer is disabled", () => !layer.IsPresent); + AddStep("restore layer enabling", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, true)); + } + [Test] public void TestLayerFading() { - AddSliderStep("current health", 0.0, 1.0, 1.0, val => - { - layer.Current.Value = val; - }); + AddSliderStep("current health", 0.0, 1.0, 1.0, val => layer.Current.Value = val); + var box = layer.ChildrenOfType().First(); AddStep("set health to 0.10", () => layer.Current.Value = 0.10); AddWaitStep("wait for fade to finish", 5); + AddAssert("layer fade is visible", () => box.IsPresent); AddStep("set health to 1", () => layer.Current.Value = 1f); + AddWaitStep("wait for fade to finish", 10); + AddAssert("layer fade is invisible", () => !box.IsPresent); } } } diff --git a/osu.Game/Screens/Play/HUD/HealthDisplay.cs b/osu.Game/Screens/Play/HUD/HealthDisplay.cs index 4ea08626ad..01cb64a88c 100644 --- a/osu.Game/Screens/Play/HUD/HealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/HealthDisplay.cs @@ -23,7 +23,6 @@ namespace osu.Game.Screens.Play.HUD /// /// Bind the tracked fields of to this health display. /// - /// public void BindHealthProcessor(HealthProcessor processor) { Current.BindTo(processor.Health); From c2c7ff7334ad02bd00c0d51967113a9fb7b6f8ab Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 1 Apr 2020 23:32:33 +0900 Subject: [PATCH 0757/2376] Add temporary logic to LegacySkin --- osu.Game/Skinning/LegacySkin.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index eafbdd4ee5..d915a03fd0 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -27,7 +28,13 @@ namespace osu.Game.Skinning [CanBeNull] protected IResourceStore Samples; - protected virtual bool AllowManiaSkin => true; + /// + /// Whether texture for the keys exists. + /// Used to determine if the mania ruleset is skinned. + /// + private readonly Lazy hasKeyTexture; + + protected virtual bool AllowManiaSkin => hasKeyTexture.Value; public new LegacySkinConfiguration Configuration { @@ -77,6 +84,12 @@ namespace osu.Game.Skinning (storage as ResourceStore)?.AddExtension("ogg"); } + + // todo: this shouldn't really be duplicated here (from ManiaLegacySkinTransformer). we need to come up with a better solution. + hasKeyTexture = new Lazy(() => this.GetAnimation( + GetConfig( + new LegacyManiaSkinConfigurationLookup(4, LegacyManiaSkinConfigurationLookups.KeyImage, 0))?.Value + ?? "mania-key1", true, true) != null); } protected override void Dispose(bool isDisposing) From a76428f965220c9f89dcbd349dc240b82678cedc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 1 Apr 2020 23:46:50 +0900 Subject: [PATCH 0758/2376] Move lookup to own function --- osu.Game/Skinning/LegacySkin.cs | 57 ++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index d915a03fd0..52655fd01a 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -87,9 +87,7 @@ namespace osu.Game.Skinning // todo: this shouldn't really be duplicated here (from ManiaLegacySkinTransformer). we need to come up with a better solution. hasKeyTexture = new Lazy(() => this.GetAnimation( - GetConfig( - new LegacyManiaSkinConfigurationLookup(4, LegacyManiaSkinConfigurationLookups.KeyImage, 0))?.Value - ?? "mania-key1", true, true) != null); + lookupForMania(new LegacyManiaSkinConfigurationLookup(4, LegacyManiaSkinConfigurationLookups.KeyImage, 0))?.Value ?? "mania-key1", true, true) != null); } protected override void Dispose(bool isDisposing) @@ -138,28 +136,9 @@ namespace osu.Game.Skinning if (!AllowManiaSkin) return null; - if (!maniaConfigurations.TryGetValue(maniaLookup.Keys, out var existing)) - maniaConfigurations[maniaLookup.Keys] = existing = new LegacyManiaSkinConfiguration(maniaLookup.Keys); - - switch (maniaLookup.Lookup) - { - case LegacyManiaSkinConfigurationLookups.ColumnWidth: - Debug.Assert(maniaLookup.TargetColumn != null); - return SkinUtils.As(new Bindable(existing.ColumnWidth[maniaLookup.TargetColumn.Value])); - - case LegacyManiaSkinConfigurationLookups.ColumnSpacing: - Debug.Assert(maniaLookup.TargetColumn != null); - return SkinUtils.As(new Bindable(existing.ColumnSpacing[maniaLookup.TargetColumn.Value])); - - case LegacyManiaSkinConfigurationLookups.HitPosition: - return SkinUtils.As(new Bindable(existing.HitPosition)); - - case LegacyManiaSkinConfigurationLookups.LightPosition: - return SkinUtils.As(new Bindable(existing.LightPosition)); - - case LegacyManiaSkinConfigurationLookups.ShowJudgementLine: - return SkinUtils.As(new Bindable(existing.ShowJudgementLine)); - } + var result = lookupForMania(maniaLookup); + if (result != null) + return result; break; @@ -190,6 +169,34 @@ namespace osu.Game.Skinning return null; } + private IBindable lookupForMania(LegacyManiaSkinConfigurationLookup maniaLookup) + { + if (!maniaConfigurations.TryGetValue(maniaLookup.Keys, out var existing)) + maniaConfigurations[maniaLookup.Keys] = existing = new LegacyManiaSkinConfiguration(maniaLookup.Keys); + + switch (maniaLookup.Lookup) + { + case LegacyManiaSkinConfigurationLookups.ColumnWidth: + Debug.Assert(maniaLookup.TargetColumn != null); + return SkinUtils.As(new Bindable(existing.ColumnWidth[maniaLookup.TargetColumn.Value])); + + case LegacyManiaSkinConfigurationLookups.ColumnSpacing: + Debug.Assert(maniaLookup.TargetColumn != null); + return SkinUtils.As(new Bindable(existing.ColumnSpacing[maniaLookup.TargetColumn.Value])); + + case LegacyManiaSkinConfigurationLookups.HitPosition: + return SkinUtils.As(new Bindable(existing.HitPosition)); + + case LegacyManiaSkinConfigurationLookups.LightPosition: + return SkinUtils.As(new Bindable(existing.LightPosition)); + + case LegacyManiaSkinConfigurationLookups.ShowJudgementLine: + return SkinUtils.As(new Bindable(existing.ShowJudgementLine)); + } + + return null; + } + private IBindable getCustomColour(string lookup) => Configuration.CustomColours.TryGetValue(lookup, out var col) ? new Bindable(col) : null; public override Drawable GetDrawableComponent(ISkinComponent component) From beb1f037e97a8f0ebb5c7b698f1af538e46ff167 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 2 Apr 2020 14:30:22 +0900 Subject: [PATCH 0759/2376] Add startAtCurrentTime parameter to GetAnimation() --- osu.Game/Skinning/IAnimationTimeReference.cs | 4 ++++ osu.Game/Skinning/LegacySkinExtensions.cs | 9 +++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/osu.Game/Skinning/IAnimationTimeReference.cs b/osu.Game/Skinning/IAnimationTimeReference.cs index bcff10a24b..4ed5ef64c3 100644 --- a/osu.Game/Skinning/IAnimationTimeReference.cs +++ b/osu.Game/Skinning/IAnimationTimeReference.cs @@ -9,6 +9,10 @@ namespace osu.Game.Skinning /// /// Denotes an object which provides a reference time to start animations from. /// + /// + /// This should not be used to start an animation immediately at the current time. + /// To do so, use with startAtCurrentTime = true instead. + /// [Cached] public interface IAnimationTimeReference { diff --git a/osu.Game/Skinning/LegacySkinExtensions.cs b/osu.Game/Skinning/LegacySkinExtensions.cs index 8765b161d4..a736174f13 100644 --- a/osu.Game/Skinning/LegacySkinExtensions.cs +++ b/osu.Game/Skinning/LegacySkinExtensions.cs @@ -14,7 +14,8 @@ namespace osu.Game.Skinning { public static class LegacySkinExtensions { - public static Drawable GetAnimation(this ISkin source, string componentName, bool animatable, bool looping, bool applyConfigFrameRate = false, string animationSeparator = "-") + public static Drawable GetAnimation(this ISkin source, string componentName, bool animatable, bool looping, bool applyConfigFrameRate = false, string animationSeparator = "-", + bool startAtCurrentTime = false) { Texture texture; @@ -24,7 +25,7 @@ namespace osu.Game.Skinning if (textures.Length > 0) { - var animation = new SkinnableTextureAnimation + var animation = new SkinnableTextureAnimation(startAtCurrentTime) { DefaultFrameLength = getFrameLength(source, applyConfigFrameRate, textures), Repeat = looping, @@ -60,8 +61,8 @@ namespace osu.Game.Skinning [Resolved(canBeNull: true)] private IAnimationTimeReference timeReference { get; set; } - public SkinnableTextureAnimation() - : base(false) + public SkinnableTextureAnimation(bool startAtCurrentTime = true) + : base(startAtCurrentTime) { } From 94031b57eaceb94f9b0a575dd1ea3b319d522d52 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 1 Apr 2020 19:19:32 +0900 Subject: [PATCH 0760/2376] Split hit explosion positioning from column --- .../{ => Skinning}/TestSceneHitExplosion.cs | 52 ++++++++++--------- osu.Game.Rulesets.Mania/ManiaSkinComponent.cs | 1 + osu.Game.Rulesets.Mania/UI/Column.cs | 6 +-- .../UI/Components/ColumnHitObjectArea.cs | 11 +--- ...HitExplosion.cs => DefaultHitExplosion.cs} | 32 +++++++++++- 5 files changed, 64 insertions(+), 38 deletions(-) rename osu.Game.Rulesets.Mania.Tests/{ => Skinning}/TestSceneHitExplosion.cs (54%) rename osu.Game.Rulesets.Mania/UI/{HitExplosion.cs => DefaultHitExplosion.cs} (81%) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHitExplosion.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs similarity index 54% rename from osu.Game.Rulesets.Mania.Tests/TestSceneHitExplosion.cs rename to osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs index 9a50bc3926..a8362d6048 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneHitExplosion.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs @@ -3,42 +3,32 @@ using System; using System.Collections.Generic; +using System.Linq; using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; using osu.Game.Rulesets.Mania.UI; -using osu.Game.Rulesets.UI.Scrolling; -using osu.Game.Tests.Visual; +using osu.Game.Skinning; using osuTK; using osuTK.Graphics; -namespace osu.Game.Rulesets.Mania.Tests +namespace osu.Game.Rulesets.Mania.Tests.Skinning { [TestFixture] - public class TestSceneHitExplosion : OsuTestScene + public class TestSceneHitExplosion : ManiaSkinnableTestScene { - private ScrollingTestContainer scrolling; - public override IReadOnlyList RequiredTypes => new[] { typeof(DrawableNote), typeof(DrawableManiaHitObject), }; - protected override void LoadComplete() + public TestSceneHitExplosion() { - base.LoadComplete(); - - Child = scrolling = new ScrollingTestContainer(ScrollingDirection.Down) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativePositionAxes = Axes.Y, - Y = -0.25f, - Size = new Vector2(Column.COLUMN_WIDTH, DefaultNotePiece.NOTE_HEIGHT), - }; - int runcount = 0; AddRepeatStep("explode", () => @@ -48,15 +38,29 @@ namespace osu.Game.Rulesets.Mania.Tests if (runcount % 15 > 12) return; - scrolling.AddRange(new Drawable[] + CreatedDrawables.OfType().ForEach(c => { - new HitExplosion((runcount / 15) % 2 == 0 ? new Color4(94, 0, 57, 255) : new Color4(6, 84, 0, 255), runcount % 6 != 0) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - } + c.Add(new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitExplosion), + _ => new DefaultHitExplosion((runcount / 15) % 2 == 0 ? new Color4(94, 0, 57, 255) : new Color4(6, 84, 0, 255), runcount % 6 != 0) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + })); }); }, 100); } + + [BackgroundDependencyLoader] + private void load() + { + SetContents(() => new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativePositionAxes = Axes.Y, + Y = -0.25f, + Size = new Vector2(Column.COLUMN_WIDTH, DefaultNotePiece.NOTE_HEIGHT), + }); + } } } diff --git a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs index dd1052ad0e..7d1c4ff8b3 100644 --- a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs +++ b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs @@ -26,5 +26,6 @@ namespace osu.Game.Rulesets.Mania HoldNoteHead, HoldNoteTail, HoldNoteBody, + HitExplosion } } diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 153345dde7..60cf019939 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -105,10 +105,10 @@ namespace osu.Game.Rulesets.Mania.UI if (!result.IsHit || !judgedObject.DisplayResult || !DisplayJudgements.Value) return; - hitObjectArea.Explosions.Add(new HitExplosion(judgedObject.AccentColour.Value, judgedObject is DrawableHoldNoteTick) + hitObjectArea.Explosions.Add(new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitExplosion), _ => + new DefaultHitExplosion(judgedObject.AccentColour.Value, judgedObject is DrawableHoldNoteTick)) { - Anchor = Direction.Value == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre, - Origin = Anchor.Centre + RelativeSizeAxes = Axes.Both }); } diff --git a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs index c3c69b0ff3..aa02f67c8e 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs @@ -3,7 +3,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; @@ -12,7 +11,7 @@ namespace osu.Game.Rulesets.Mania.UI.Components { public class ColumnHitObjectArea : HitObjectArea { - public readonly Container Explosions; + public readonly Container Explosions; private readonly Drawable hitTarget; public ColumnHitObjectArea(HitObjectContainer hitObjectContainer) @@ -25,7 +24,7 @@ namespace osu.Game.Rulesets.Mania.UI.Components RelativeSizeAxes = Axes.X, Depth = 1 }, - Explosions = new Container + Explosions = new Container { RelativeSizeAxes = Axes.Both, Depth = -1, @@ -38,15 +37,9 @@ namespace osu.Game.Rulesets.Mania.UI.Components base.UpdateHitPosition(); if (Direction.Value == ScrollingDirection.Up) - { hitTarget.Anchor = hitTarget.Origin = Anchor.TopLeft; - Explosions.Padding = new MarginPadding { Top = DefaultNotePiece.NOTE_HEIGHT / 2 }; - } else - { hitTarget.Anchor = hitTarget.Origin = Anchor.BottomLeft; - Explosions.Padding = new MarginPadding { Bottom = DefaultNotePiece.NOTE_HEIGHT / 2 }; - } } } } diff --git a/osu.Game.Rulesets.Mania/UI/HitExplosion.cs b/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs similarity index 81% rename from osu.Game.Rulesets.Mania/UI/HitExplosion.cs rename to osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs index 824b087cb9..a4398f6ed7 100644 --- a/osu.Game.Rulesets.Mania/UI/HitExplosion.cs +++ b/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs @@ -1,26 +1,33 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Utils; using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; +using osu.Game.Rulesets.UI.Scrolling; using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Mania.UI { - public class HitExplosion : CompositeDrawable + public class DefaultHitExplosion : CompositeDrawable { public override bool RemoveWhenNotAlive => true; + private readonly IBindable direction = new Bindable(); + private readonly CircularContainer largeFaint; private readonly CircularContainer mainGlow1; - public HitExplosion(Color4 objectColour, bool isSmall = false) + public DefaultHitExplosion(Color4 objectColour, bool isSmall = false) { + Origin = Anchor.Centre; + RelativeSizeAxes = Axes.X; Height = DefaultNotePiece.NOTE_HEIGHT; @@ -109,6 +116,13 @@ namespace osu.Game.Rulesets.Mania.UI }; } + [BackgroundDependencyLoader] + private void load(IScrollingInfo scrollingInfo) + { + direction.BindTo(scrollingInfo.Direction); + direction.BindValueChanged(onDirectionChanged, true); + } + protected override void LoadComplete() { const double duration = 200; @@ -124,5 +138,19 @@ namespace osu.Game.Rulesets.Mania.UI this.FadeOut(duration, Easing.Out); Expire(true); } + + private void onDirectionChanged(ValueChangedEvent direction) + { + if (direction.NewValue == ScrollingDirection.Up) + { + Anchor = Anchor.TopCentre; + Y = DefaultNotePiece.NOTE_HEIGHT / 2; + } + else + { + Anchor = Anchor.BottomCentre; + Y = -DefaultNotePiece.NOTE_HEIGHT / 2; + } + } } } From c8eee8d204c075174d99af7d629a042cb4ce7ee5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 1 Apr 2020 20:00:52 +0900 Subject: [PATCH 0761/2376] Add structure for legacy hit explosions --- .../Skinning/LegacyHitExplosion.cs | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs new file mode 100644 index 0000000000..404d464018 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs @@ -0,0 +1,45 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Game.Rulesets.UI.Scrolling; + +namespace osu.Game.Rulesets.Mania.Skinning +{ + public class LegacyHitExplosion : LegacyManiaColumnElement + { + private readonly IBindable direction = new Bindable(); + + private Drawable explosion; + + [BackgroundDependencyLoader] + private void load(IScrollingInfo scrollingInfo) + { + InternalChild = explosion = new Sprite + { + }; + + direction.BindTo(scrollingInfo.Direction); + direction.BindValueChanged(onDirectionChanged, true); + + // Todo: LightingN + // Todo: LightingL + } + + private void onDirectionChanged(ValueChangedEvent obj) + { + throw new System.NotImplementedException(); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + lighting.FadeInFromZero(80) + .Then().FadeOut(120); + } + } +} From 09eb9facdd6eb72543cd6c0e3ce6f7b3817966e1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 2 Apr 2020 12:01:07 +0900 Subject: [PATCH 0762/2376] Add column to test scene --- osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs index a8362d6048..718dbbea93 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs @@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning [BackgroundDependencyLoader] private void load() { - SetContents(() => new Container + SetContents(() => new ColumnTestContainer(0, ManiaAction.Key1) { Anchor = Anchor.Centre, Origin = Anchor.Centre, From b375a02cff1ea6813423a85d214677cef131599e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 2 Apr 2020 14:24:09 +0900 Subject: [PATCH 0763/2376] Cleanup positioning factor definition --- .../Skinning/LegacyHitExplosion.cs | 45 +++++++++++++------ .../Skinning/LegacyManiaSkinConfiguration.cs | 16 +++++-- osu.Game/Skinning/LegacyManiaSkinDecoder.cs | 8 ++-- 3 files changed, 48 insertions(+), 21 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs index 404d464018..688ee7e340 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs @@ -1,45 +1,64 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Animations; using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Mania.Skinning { - public class LegacyHitExplosion : LegacyManiaColumnElement + public class LegacyHitExplosion : LegacyManiaElement { private readonly IBindable direction = new Bindable(); private Drawable explosion; - [BackgroundDependencyLoader] - private void load(IScrollingInfo scrollingInfo) + public LegacyHitExplosion() { - InternalChild = explosion = new Sprite + RelativeSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load(ISkinSource skin, IScrollingInfo scrollingInfo) + { + string imageName = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ExplosionImage)?.Value + ?? "lightingN"; + + InternalChild = explosion = skin.GetAnimation(imageName, true, false, startAtCurrentTime: true).With(d => { - }; + if (d == null) + return; + + d.Origin = Anchor.Centre; + d.Blending = BlendingParameters.Additive; + + if (!(d is TextureAnimation texAnimation)) + return; + + if (texAnimation.FrameCount > 0) + texAnimation.DefaultFrameLength = Math.Max(texAnimation.DefaultFrameLength, 170.0 / texAnimation.FrameCount); + }); direction.BindTo(scrollingInfo.Direction); direction.BindValueChanged(onDirectionChanged, true); - - // Todo: LightingN - // Todo: LightingL } - private void onDirectionChanged(ValueChangedEvent obj) + private void onDirectionChanged(ValueChangedEvent direction) { - throw new System.NotImplementedException(); + if (explosion != null) + explosion.Anchor = direction.NewValue == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre; } protected override void LoadComplete() { base.LoadComplete(); - lighting.FadeInFromZero(80) - .Then().FadeOut(120); + explosion?.FadeInFromZero(80) + .Then().FadeOut(120); } } } diff --git a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs index 0d0c4943ef..ba29870ffa 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs @@ -7,14 +7,24 @@ namespace osu.Game.Skinning { public class LegacyManiaSkinConfiguration { + /// + /// Conversion factor from converting legacy positioning values (based in x480 dimensions) to x768. + /// + public const float POSITION_SCALE_FACTOR = 1.6f; + + /// + /// Size of a legacy column in the default skin, used for determining relative scale factors. + /// + public const float DEFAULT_COLUMN_SIZE = 30 * POSITION_SCALE_FACTOR; + public readonly int Keys; public readonly float[] ColumnLineWidth; public readonly float[] ColumnSpacing; public readonly float[] ColumnWidth; - public float HitPosition = 124.8f; // (480 - 402) * 1.6f - public float LightPosition = 107.2f; // (480 - 413) * 1.6f + public float HitPosition = (480 - 402) * POSITION_SCALE_FACTOR; + public float LightPosition = (480 - 413) * POSITION_SCALE_FACTOR; public bool ShowJudgementLine = true; public LegacyManiaSkinConfiguration(int keys) @@ -26,7 +36,7 @@ namespace osu.Game.Skinning ColumnWidth = new float[keys]; ColumnLineWidth.AsSpan().Fill(2); - ColumnWidth.AsSpan().Fill(48); + ColumnWidth.AsSpan().Fill(DEFAULT_COLUMN_SIZE); } } } diff --git a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs index dabdd0a980..0c9157e59b 100644 --- a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs +++ b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs @@ -11,8 +11,6 @@ namespace osu.Game.Skinning { public class LegacyManiaSkinDecoder : LegacyDecoder> { - private const float size_scale_factor = 1.6f; - public LegacyManiaSkinDecoder() : base(1) { @@ -88,11 +86,11 @@ namespace osu.Game.Skinning break; case "HitPosition": - currentConfig.HitPosition = (480 - float.Parse(pair.Value, CultureInfo.InvariantCulture)) * size_scale_factor; + currentConfig.HitPosition = (480 - float.Parse(pair.Value, CultureInfo.InvariantCulture)) * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR; break; case "LightPosition": - currentConfig.LightPosition = (480 - float.Parse(pair.Value, CultureInfo.InvariantCulture)) * size_scale_factor; + currentConfig.LightPosition = (480 - float.Parse(pair.Value, CultureInfo.InvariantCulture)) * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR; break; case "JudgementLine": @@ -111,7 +109,7 @@ namespace osu.Game.Skinning if (i >= output.Length) break; - output[i] = float.Parse(values[i], CultureInfo.InvariantCulture) * size_scale_factor; + output[i] = float.Parse(values[i], CultureInfo.InvariantCulture) * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR; } } } From fa3a449c3b11b0edfc459606e431fedcfb92a5da Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 2 Apr 2020 14:29:16 +0900 Subject: [PATCH 0764/2376] Implement legacy normal hit explosions --- .../Skinning/LegacyHitExplosion.cs | 7 ++++++- .../Skinning/ManiaLegacySkinTransformer.cs | 3 +++ osu.Game/Skinning/LegacyManiaSkinConfiguration.cs | 2 ++ .../Skinning/LegacyManiaSkinConfigurationLookup.cs | 2 ++ osu.Game/Skinning/LegacyManiaSkinDecoder.cs | 4 ++++ osu.Game/Skinning/LegacySkin.cs | 11 +++++++++++ 6 files changed, 28 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs index 688ee7e340..ca2a54aa62 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs @@ -8,10 +8,11 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; +using osuTK; namespace osu.Game.Rulesets.Mania.Skinning { - public class LegacyHitExplosion : LegacyManiaElement + public class LegacyHitExplosion : LegacyManiaColumnElement { private readonly IBindable direction = new Bindable(); @@ -28,6 +29,9 @@ namespace osu.Game.Rulesets.Mania.Skinning string imageName = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ExplosionImage)?.Value ?? "lightingN"; + float explosionScale = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ExplosionScale)?.Value + ?? 1; + InternalChild = explosion = skin.GetAnimation(imageName, true, false, startAtCurrentTime: true).With(d => { if (d == null) @@ -35,6 +39,7 @@ namespace osu.Game.Rulesets.Mania.Skinning d.Origin = Anchor.Centre; d.Blending = BlendingParameters.Additive; + d.Scale = new Vector2(explosionScale); if (!(d is TextureAnimation texAnimation)) return; diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs index 3e423c6b0f..02fd6c0572 100644 --- a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs @@ -74,6 +74,9 @@ namespace osu.Game.Rulesets.Mania.Skinning case ManiaSkinComponents.HoldNoteBody: return new LegacyBodyPiece(); + + case ManiaSkinComponents.HitExplosion: + return new LegacyHitExplosion(); } break; diff --git a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs index ba29870ffa..b5d5531e0a 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs @@ -22,6 +22,7 @@ namespace osu.Game.Skinning public readonly float[] ColumnLineWidth; public readonly float[] ColumnSpacing; public readonly float[] ColumnWidth; + public readonly float[] ExplosionWidth; public float HitPosition = (480 - 402) * POSITION_SCALE_FACTOR; public float LightPosition = (480 - 413) * POSITION_SCALE_FACTOR; @@ -34,6 +35,7 @@ namespace osu.Game.Skinning ColumnLineWidth = new float[keys + 1]; ColumnSpacing = new float[keys - 1]; ColumnWidth = new float[keys]; + ExplosionWidth = new float[keys]; ColumnLineWidth.AsSpan().Fill(2); ColumnWidth.AsSpan().Fill(DEFAULT_COLUMN_SIZE); diff --git a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs index 49e4faa269..68f402d435 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs @@ -34,5 +34,7 @@ namespace osu.Game.Skinning HoldNoteHeadImage, HoldNoteTailImage, HoldNoteBodyImage, + ExplosionImage, + ExplosionScale } } diff --git a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs index 0c9157e59b..e7b25ab267 100644 --- a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs +++ b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs @@ -96,6 +96,10 @@ namespace osu.Game.Skinning case "JudgementLine": currentConfig.ShowJudgementLine = pair.Value == "1"; break; + + case "LightingNWidth": + parseArrayValue(pair.Value, currentConfig.ExplosionWidth); + break; } } } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 52655fd01a..5af42df1de 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -192,6 +192,17 @@ namespace osu.Game.Skinning case LegacyManiaSkinConfigurationLookups.ShowJudgementLine: return SkinUtils.As(new Bindable(existing.ShowJudgementLine)); + + case LegacyManiaSkinConfigurationLookups.ExplosionScale: + Debug.Assert(maniaLookup.TargetColumn != null); + + if (GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value < 2.5m) + return SkinUtils.As(new Bindable(1)); + + if (existing.ExplosionWidth[maniaLookup.TargetColumn.Value] != 0) + return SkinUtils.As(new Bindable(existing.ExplosionWidth[maniaLookup.TargetColumn.Value] / LegacyManiaSkinConfiguration.DEFAULT_COLUMN_SIZE)); + + return SkinUtils.As(new Bindable(existing.ColumnWidth[maniaLookup.TargetColumn.Value] / LegacyManiaSkinConfiguration.DEFAULT_COLUMN_SIZE)); } return null; From de7ee571006646402a0937ef5a0c8c6f19f1aeca Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 2 Apr 2020 15:27:31 +0900 Subject: [PATCH 0765/2376] Fix adding null hit explosions --- osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs index ca2a54aa62..5cfbc1d847 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Mania.Skinning float explosionScale = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ExplosionScale)?.Value ?? 1; - InternalChild = explosion = skin.GetAnimation(imageName, true, false, startAtCurrentTime: true).With(d => + explosion = skin.GetAnimation(imageName, true, false, startAtCurrentTime: true).With(d => { if (d == null) return; @@ -48,6 +48,9 @@ namespace osu.Game.Rulesets.Mania.Skinning texAnimation.DefaultFrameLength = Math.Max(texAnimation.DefaultFrameLength, 170.0 / texAnimation.FrameCount); }); + if (explosion != null) + InternalChild = explosion; + direction.BindTo(scrollingInfo.Direction); direction.BindValueChanged(onDirectionChanged, true); } From f3b96f8f50c9dde37741578f1e13fce19d9d0ef9 Mon Sep 17 00:00:00 2001 From: mcendu Date: Thu, 2 Apr 2020 14:29:30 +0800 Subject: [PATCH 0766/2376] add fallback to normal note image --- osu.Game.Rulesets.Mania/Skinning/LegacyHoldNoteTailPiece.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyHoldNoteTailPiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyHoldNoteTailPiece.cs index 085d2bf004..cef976c7c8 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyHoldNoteTailPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyHoldNoteTailPiece.cs @@ -21,7 +21,8 @@ namespace osu.Game.Rulesets.Mania.Skinning protected override Texture GetTexture(ISkinSource skin) { return GetTextureFromLookup(skin, LegacyManiaSkinConfigurationLookups.HoldNoteTailImage) - ?? GetTextureFromLookup(skin, LegacyManiaSkinConfigurationLookups.HoldNoteHeadImage); + ?? GetTextureFromLookup(skin, LegacyManiaSkinConfigurationLookups.HoldNoteHeadImage) + ?? GetTextureFromLookup(skin, LegacyManiaSkinConfigurationLookups.NoteImage); } } } From c8d161e03aa17a542dea93e6c554ffd09cda0079 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 2 Apr 2020 15:57:02 +0900 Subject: [PATCH 0767/2376] Fix explosion expiry --- osu.Game.Rulesets.Mania/UI/Column.cs | 8 ++++++-- osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs | 1 - 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 60cf019939..5a6cd7e229 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -105,11 +105,15 @@ namespace osu.Game.Rulesets.Mania.UI if (!result.IsHit || !judgedObject.DisplayResult || !DisplayJudgements.Value) return; - hitObjectArea.Explosions.Add(new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitExplosion), _ => + var explosion = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitExplosion), _ => new DefaultHitExplosion(judgedObject.AccentColour.Value, judgedObject is DrawableHoldNoteTick)) { RelativeSizeAxes = Axes.Both - }); + }; + + hitObjectArea.Explosions.Add(explosion); + + explosion.Delay(200).Expire(true); } public bool OnPressed(ManiaAction action) diff --git a/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs b/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs index a4398f6ed7..7a047ed121 100644 --- a/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs +++ b/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs @@ -136,7 +136,6 @@ namespace osu.Game.Rulesets.Mania.UI mainGlow1.ScaleTo(1.4f, duration, Easing.OutQuint); this.FadeOut(duration, Easing.Out); - Expire(true); } private void onDirectionChanged(ValueChangedEvent direction) From 62f6683a20db2774d91226395dfe99c0864319a3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 2 Apr 2020 15:57:50 +0900 Subject: [PATCH 0768/2376] Remove unnecessary generic --- osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs index aa02f67c8e..7d280f0bea 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs @@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Mania.UI.Components { public class ColumnHitObjectArea : HitObjectArea { - public readonly Container Explosions; + public readonly Container Explosions; private readonly Drawable hitTarget; public ColumnHitObjectArea(HitObjectContainer hitObjectContainer) @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Mania.UI.Components RelativeSizeAxes = Axes.X, Depth = 1 }, - Explosions = new Container + Explosions = new Container { RelativeSizeAxes = Axes.Both, Depth = -1, From dae738d6a42bc56730d78b65d9c9e60582150bac Mon Sep 17 00:00:00 2001 From: mcendu Date: Thu, 2 Apr 2020 14:58:31 +0800 Subject: [PATCH 0769/2376] add todo entries --- osu.Game.Rulesets.Mania/Skinning/LegacyHoldNoteHeadPiece.cs | 1 + osu.Game.Rulesets.Mania/Skinning/LegacyHoldNoteTailPiece.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyHoldNoteHeadPiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyHoldNoteHeadPiece.cs index ebe7ff09b2..c5aa062d0f 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyHoldNoteHeadPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyHoldNoteHeadPiece.cs @@ -10,6 +10,7 @@ namespace osu.Game.Rulesets.Mania.Skinning { protected override Texture GetTexture(ISkinSource skin) { + // TODO: Should fallback to the head from default legacy skin instead of note. return GetTextureFromLookup(skin, LegacyManiaSkinConfigurationLookups.HoldNoteHeadImage) ?? GetTextureFromLookup(skin, LegacyManiaSkinConfigurationLookups.NoteImage); } diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyHoldNoteTailPiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyHoldNoteTailPiece.cs index cef976c7c8..2e8259f10a 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyHoldNoteTailPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyHoldNoteTailPiece.cs @@ -20,6 +20,7 @@ namespace osu.Game.Rulesets.Mania.Skinning protected override Texture GetTexture(ISkinSource skin) { + // TODO: Should fallback to the head from default legacy skin instead of note. return GetTextureFromLookup(skin, LegacyManiaSkinConfigurationLookups.HoldNoteTailImage) ?? GetTextureFromLookup(skin, LegacyManiaSkinConfigurationLookups.HoldNoteHeadImage) ?? GetTextureFromLookup(skin, LegacyManiaSkinConfigurationLookups.NoteImage); From 7ba533b7a4a47cf7b2d61b4453b1cd329e7fef51 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 2 Apr 2020 16:04:09 +0900 Subject: [PATCH 0770/2376] Expand mania to fit vertical screen bounds --- .../UI/ManiaPlayfieldAdjustmentContainer.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs index d893a3fdde..30e0aafb7d 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfieldAdjustmentContainer.cs @@ -3,7 +3,6 @@ using osu.Framework.Graphics; using osu.Game.Rulesets.UI; -using osuTK; namespace osu.Game.Rulesets.Mania.UI { @@ -13,8 +12,6 @@ namespace osu.Game.Rulesets.Mania.UI { Anchor = Anchor.Centre; Origin = Anchor.Centre; - - Size = new Vector2(1, 0.8f); } } } From 5aa4c4f3cbb651ed5ca44d2806b876bcf910c06e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 2 Apr 2020 16:10:09 +0900 Subject: [PATCH 0771/2376] Remove corner radius --- osu.Game.Rulesets.Mania/UI/ManiaStage.cs | 31 ++++++++---------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs b/osu.Game.Rulesets.Mania/UI/ManiaStage.cs index c6102675a1..1e190f4857 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaStage.cs @@ -72,30 +72,19 @@ namespace osu.Game.Rulesets.Mania.UI AutoSizeAxes = Axes.X, Children = new Drawable[] { - new Container + new Box { - Name = "Columns mask", + Name = "Background", + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black + }, + columnFlow = new FillFlowContainer + { + Name = "Columns", RelativeSizeAxes = Axes.Y, AutoSizeAxes = Axes.X, - Masking = true, - CornerRadius = 5, - Children = new Drawable[] - { - new Box - { - Name = "Background", - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black - }, - columnFlow = new FillFlowContainer - { - Name = "Columns", - RelativeSizeAxes = Axes.Y, - AutoSizeAxes = Axes.X, - Direction = FillDirection.Horizontal, - Padding = new MarginPadding { Left = COLUMN_SPACING, Right = COLUMN_SPACING }, - }, - } + Direction = FillDirection.Horizontal, + Padding = new MarginPadding { Left = COLUMN_SPACING, Right = COLUMN_SPACING }, }, new Container { From 63708532a17dbcf186c505025ad5805bfeb7d76d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 2 Apr 2020 16:36:57 +0900 Subject: [PATCH 0772/2376] Remove frozen clock from test scenes --- .../Skinning/ManiaHitObjectTestScene.cs | 15 ++++++++---- .../Skinning/ManiaSkinnableTestScene.cs | 23 ++++++++++++++++++- .../Skinning/TestSceneHoldNote.cs | 5 +--- 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs index e65982b240..18eebada00 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs @@ -4,7 +4,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Timing; using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.UI.Scrolling; using osuTK.Graphics; @@ -37,10 +36,13 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning Child = new ScrollingHitObjectContainer { RelativeSizeAxes = Axes.Both, - Clock = new FramedClock(new StopwatchClock()), }.With(c => { - c.Add(CreateHitObject().With(h => h.AccentColour.Value = Color4.Orange)); + c.Add(CreateHitObject().With(h => + { + h.HitObject.StartTime = START_TIME; + h.AccentColour.Value = Color4.Orange; + })); }) }, new ColumnTestContainer(1, ManiaAction.Key2) @@ -52,10 +54,13 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning Child = new ScrollingHitObjectContainer { RelativeSizeAxes = Axes.Both, - Clock = new FramedClock(new StopwatchClock()), }.With(c => { - c.Add(CreateHitObject().With(h => h.AccentColour.Value = Color4.Orange)); + c.Add(CreateHitObject().With(h => + { + h.HitObject.StartTime = START_TIME; + h.AccentColour.Value = Color4.Orange; + })); }) }, } diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs index 41fb7c727e..eaa2a56e36 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs @@ -19,6 +19,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning /// public abstract class ManiaSkinnableTestScene : SkinnableTestScene { + protected const double START_TIME = 1000000000; + [Cached(Type = typeof(IScrollingInfo))] private readonly TestScrollingInfo scrollingInfo = new TestScrollingInfo(); @@ -52,7 +54,26 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning IBindable IScrollingInfo.Direction => Direction; IBindable IScrollingInfo.TimeRange { get; } = new Bindable(1000); - IScrollAlgorithm IScrollingInfo.Algorithm { get; } = new ConstantScrollAlgorithm(); + IScrollAlgorithm IScrollingInfo.Algorithm { get; } = new ZeroScrollAlgorithm(); + } + + private class ZeroScrollAlgorithm : IScrollAlgorithm + { + public double GetDisplayStartTime(double originTime, float offset, double timeRange, float scrollLength) + => double.MinValue; + + public float GetLength(double startTime, double endTime, double timeRange, float scrollLength) + => scrollLength; + + public float PositionAt(double time, double currentTime, double timeRange, float scrollLength) + => (float)((time - START_TIME) / timeRange) * scrollLength; + + public double TimeAt(float position, double currentTime, double timeRange, float scrollLength) + => 0; + + public void Reset() + { + } } } } diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs index 19623a5705..91a0a06552 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs @@ -15,10 +15,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning var note = new HoldNote { Duration = 1000 }; note.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - return new DrawableHoldNote(note) - { - Height = 200, - }; + return new DrawableHoldNote(note); } } } From 95523197324a259c637c4b1e908fb86562b4ddb1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 2 Apr 2020 17:09:08 +0900 Subject: [PATCH 0773/2376] Fix hold note animation not being reset --- .../Skinning/TestSceneHoldNote.cs | 14 ++++++++++++++ .../Skinning/LegacyBodyPiece.cs | 1 + 2 files changed, 15 insertions(+) diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs index 91a0a06552..95e86de884 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs @@ -1,6 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; +using osu.Framework.Bindables; +using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Mania.Objects; @@ -10,6 +13,17 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning { public class TestSceneHoldNote : ManiaHitObjectTestScene { + public TestSceneHoldNote() + { + AddToggleStep("toggle hitting", v => + { + foreach (var holdNote in CreatedDrawables.SelectMany(d => d.ChildrenOfType())) + { + ((Bindable)holdNote.IsHitting).Value = v; + } + }); + } + protected override DrawableManiaHitObject CreateHitObject() { var note = new HoldNote { Duration = 1000 }; diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs index 643d92ff41..1ffee98a6c 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs @@ -69,6 +69,7 @@ namespace osu.Game.Rulesets.Mania.Skinning if (!(sprite is TextureAnimation animation)) return; + animation.GotoFrame(0); animation.IsPlaying = isHitting.NewValue; } From a77933f5e007904b7d423c62dc7747b308a03a24 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 2 Apr 2020 17:56:12 +0900 Subject: [PATCH 0774/2376] Add support for parsing mania skin colours --- osu.Game.Tests/Resources/mania-skin-colours.ini | 3 +++ .../Skins/LegacyManiaSkinDecoderTest.cs | 16 ++++++++++++++++ osu.Game/Beatmaps/Formats/LegacyDecoder.cs | 4 ++-- .../Skinning/LegacyManiaSkinConfiguration.cs | 7 ++++++- osu.Game/Skinning/LegacyManiaSkinDecoder.cs | 6 ++++++ osu.Game/Skinning/LegacySkin.cs | 8 +++++--- 6 files changed, 38 insertions(+), 6 deletions(-) create mode 100644 osu.Game.Tests/Resources/mania-skin-colours.ini diff --git a/osu.Game.Tests/Resources/mania-skin-colours.ini b/osu.Game.Tests/Resources/mania-skin-colours.ini new file mode 100644 index 0000000000..91d9696e0c --- /dev/null +++ b/osu.Game.Tests/Resources/mania-skin-colours.ini @@ -0,0 +1,3 @@ +[Mania] +Keys: 4 +ColourBarline: 50,50,50,50 \ No newline at end of file diff --git a/osu.Game.Tests/Skins/LegacyManiaSkinDecoderTest.cs b/osu.Game.Tests/Skins/LegacyManiaSkinDecoderTest.cs index 736f97f39f..83fd4878aa 100644 --- a/osu.Game.Tests/Skins/LegacyManiaSkinDecoderTest.cs +++ b/osu.Game.Tests/Skins/LegacyManiaSkinDecoderTest.cs @@ -5,6 +5,7 @@ using NUnit.Framework; using osu.Game.IO; using osu.Game.Skinning; using osu.Game.Tests.Resources; +using osuTK.Graphics; namespace osu.Game.Tests.Skins { @@ -83,5 +84,20 @@ namespace osu.Game.Tests.Skins Assert.That(configs[0].HitPosition, Is.EqualTo(16)); } } + + [Test] + public void TestParseColours() + { + var decoder = new LegacyManiaSkinDecoder(); + + using (var resStream = TestResources.OpenResource("mania-skin-colours.ini")) + using (var stream = new LineBufferedReader(resStream)) + { + var configs = decoder.Decode(stream); + + Assert.That(configs.Count, Is.EqualTo(1)); + Assert.That(configs[0].CustomColours, Contains.Key("ColourBarline").And.ContainValue(new Color4(50, 50, 50, 50))); + } + } } } diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index bbc0aad467..561707f9ef 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -73,7 +73,7 @@ namespace osu.Game.Beatmaps.Formats switch (section) { case Section.Colours: - handleColours(output, line); + HandleColours(output, line); return; } } @@ -87,7 +87,7 @@ namespace osu.Game.Beatmaps.Formats return line; } - private void handleColours(T output, string line) + protected void HandleColours(TModel output, string line) { var pair = SplitKeyVal(line); diff --git a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs index 0d0c4943ef..95886fa97f 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs @@ -2,13 +2,18 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using osu.Game.Beatmaps.Formats; +using osuTK.Graphics; namespace osu.Game.Skinning { - public class LegacyManiaSkinConfiguration + public class LegacyManiaSkinConfiguration : IHasCustomColours { public readonly int Keys; + public Dictionary CustomColours { get; set; } = new Dictionary(); + public readonly float[] ColumnLineWidth; public readonly float[] ColumnSpacing; public readonly float[] ColumnWidth; diff --git a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs index dabdd0a980..f290e705fa 100644 --- a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs +++ b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs @@ -73,6 +73,12 @@ namespace osu.Game.Skinning { var pair = SplitKeyVal(line); + if (pair.Key.StartsWith("Colour")) + { + HandleColours(currentConfig, line); + continue; + } + switch (pair.Key) { case "ColumnLineWidth": diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 52655fd01a..9585768bab 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -14,6 +14,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; using osu.Game.Audio; +using osu.Game.Beatmaps.Formats; using osu.Game.IO; using osu.Game.Rulesets.Scoring; using osuTK.Graphics; @@ -112,7 +113,7 @@ namespace osu.Game.Skinning break; default: - return SkinUtils.As(getCustomColour(colour.ToString())); + return SkinUtils.As(getCustomColour(Configuration, colour.ToString())); } break; @@ -130,7 +131,7 @@ namespace osu.Game.Skinning break; case SkinCustomColourLookup customColour: - return SkinUtils.As(getCustomColour(customColour.Lookup.ToString())); + return SkinUtils.As(getCustomColour(Configuration, customColour.Lookup.ToString())); case LegacyManiaSkinConfigurationLookup maniaLookup: if (!AllowManiaSkin) @@ -197,7 +198,8 @@ namespace osu.Game.Skinning return null; } - private IBindable getCustomColour(string lookup) => Configuration.CustomColours.TryGetValue(lookup, out var col) ? new Bindable(col) : null; + private IBindable getCustomColour(IHasCustomColours source, string lookup) + => source.CustomColours.TryGetValue(lookup, out var col) ? new Bindable(col) : null; public override Drawable GetDrawableComponent(ISkinComponent component) { From 62f1bc276d14c3b706835807f706bc40573da892 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 2 Apr 2020 18:10:17 +0900 Subject: [PATCH 0775/2376] Add skinning support for column line colour --- osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs | 5 +++++ osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs | 1 + osu.Game/Skinning/LegacySkin.cs | 3 +++ 3 files changed, 9 insertions(+) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs index 7e8f720e99..27845fca4a 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs @@ -46,6 +46,9 @@ namespace osu.Game.Rulesets.Mania.Skinning new LegacyManiaSkinConfigurationLookup(Stage?.Columns.Count ?? 4, LegacyManiaSkinConfigurationLookups.LightPosition))?.Value ?? 0; + Color4 lineColour = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ColumnLineColour)?.Value + ?? Color4.White; + InternalChildren = new Drawable[] { new Box @@ -57,6 +60,7 @@ namespace osu.Game.Rulesets.Mania.Skinning { RelativeSizeAxes = Axes.Y, Width = leftLineWidth, + Colour = lineColour, Alpha = hasLeftLine ? 1 : 0 }, new Box @@ -65,6 +69,7 @@ namespace osu.Game.Rulesets.Mania.Skinning Origin = Anchor.TopRight, RelativeSizeAxes = Axes.Y, Width = rightLineWidth, + Colour = lineColour, Alpha = hasRightLine ? 1 : 0 }, lightContainer = new Container diff --git a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs index 49e4faa269..3cccb71745 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs @@ -34,5 +34,6 @@ namespace osu.Game.Skinning HoldNoteHeadImage, HoldNoteTailImage, HoldNoteBodyImage, + ColumnLineColour } } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 9585768bab..a51556fa77 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -193,6 +193,9 @@ namespace osu.Game.Skinning case LegacyManiaSkinConfigurationLookups.ShowJudgementLine: return SkinUtils.As(new Bindable(existing.ShowJudgementLine)); + + case LegacyManiaSkinConfigurationLookups.ColumnLineColour: + return SkinUtils.As(getCustomColour(existing, "ColourColumnLine")); } return null; From c18248c82736794faf87e70bb842a43c6337cac8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 2 Apr 2020 18:46:09 +0900 Subject: [PATCH 0776/2376] Fix crash caused by user json order changing --- osu.Game/Users/User.cs | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/osu.Game/Users/User.cs b/osu.Game/Users/User.cs index 2a6f7844a2..f8bb8f4c6a 100644 --- a/osu.Game/Users/User.cs +++ b/osu.Game/Users/User.cs @@ -173,8 +173,27 @@ namespace osu.Game.Users public int Available; } + private UserStatistics statistics; + [JsonProperty(@"statistics")] - public UserStatistics Statistics; + public UserStatistics Statistics + { + get => statistics ??= new UserStatistics(); + set + { + if (statistics != null) + // we may already have rank history populated + value.RankHistory = statistics.RankHistory; + + statistics = value; + } + } + + [JsonProperty(@"rankHistory")] + private RankHistoryData rankHistory + { + set => statistics.RankHistory = value; + } public class RankHistoryData { @@ -185,12 +204,6 @@ namespace osu.Game.Users public int[] Data; } - [JsonProperty(@"rankHistory")] - private RankHistoryData rankHistory - { - set => Statistics.RankHistory = value; - } - [JsonProperty("badges")] public Badge[] Badges; From a3d4212462794230d3d2f9ce7128ae0399616ef1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 2 Apr 2020 19:30:58 +0900 Subject: [PATCH 0777/2376] Fix weird slider ball sizing --- .../Resources/special-skin/sliderb0.png | Bin 0 -> 10899 bytes .../Resources/special-skin/sliderb0@2x.png | Bin 0 -> 23267 bytes .../Skinning/LegacySliderBall.cs | 2 ++ .../Skinning/OsuLegacySkinTransformer.cs | 12 +----------- 4 files changed, 3 insertions(+), 11 deletions(-) create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/special-skin/sliderb0.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/special-skin/sliderb0@2x.png diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/sliderb0.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/sliderb0.png new file mode 100644 index 0000000000000000000000000000000000000000..316d52c685d1169bf9bb991d6b2d8bd4b3797c61 GIT binary patch literal 10899 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 Ml9pnX{L}FN0ew%+2><{9 literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/sliderb0@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/sliderb0@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..e6f6b3c23942e012da9dcb30317ffd0590d88d20 GIT binary patch literal 23267 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 O%h(WaP;|^G;=ch*f89R- literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs index 81c02199d0..b4ed75d97c 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs @@ -18,6 +18,8 @@ namespace osu.Game.Rulesets.Osu.Skinning public LegacySliderBall(Drawable animationContent) { this.animationContent = animationContent; + + AutoSizeAxes = Axes.Both; } [BackgroundDependencyLoader] diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs index 075c536b4c..0d67846b8e 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs @@ -62,17 +62,7 @@ namespace osu.Game.Rulesets.Osu.Skinning // Math.Max((150 / Velocity) * GameBase.SIXTY_FRAME_TIME, GameBase.SIXTY_FRAME_TIME); if (sliderBallContent != null) - { - var size = sliderBallContent.Size; - - sliderBallContent.RelativeSizeAxes = Axes.Both; - sliderBallContent.Size = Vector2.One; - - return new LegacySliderBall(sliderBallContent) - { - Size = size - }; - } + return new LegacySliderBall(sliderBallContent); return null; From a6d6bab0ccb5e5af9d600546089a240915f1dc7a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 2 Apr 2020 21:21:29 +0900 Subject: [PATCH 0778/2376] 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 cb848c0433..067431596c 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 4a9d2e0830..4597d212f3 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -23,7 +23,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index a528bd5658..27e485709b 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + @@ -79,7 +79,7 @@ - + From 23b53bee563d30bae925a9b0ce86f3688932f4e0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 2 Apr 2020 22:05:32 +0900 Subject: [PATCH 0779/2376] 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 067431596c..3e10e6cc4d 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 4597d212f3..073799f08f 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -22,7 +22,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 27e485709b..6578aec69f 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + From 2a6c0de225b7fa64bfad2d02e747ea3d3806c31d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 2 Apr 2020 22:55:42 +0900 Subject: [PATCH 0780/2376] Add frameLength parameter to GetAnimation --- osu.Game/Skinning/LegacySkinExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/LegacySkinExtensions.cs b/osu.Game/Skinning/LegacySkinExtensions.cs index a736174f13..ea3d180ef8 100644 --- a/osu.Game/Skinning/LegacySkinExtensions.cs +++ b/osu.Game/Skinning/LegacySkinExtensions.cs @@ -15,7 +15,7 @@ namespace osu.Game.Skinning public static class LegacySkinExtensions { public static Drawable GetAnimation(this ISkin source, string componentName, bool animatable, bool looping, bool applyConfigFrameRate = false, string animationSeparator = "-", - bool startAtCurrentTime = false) + bool startAtCurrentTime = false, double? frameLength = null) { Texture texture; @@ -27,7 +27,7 @@ namespace osu.Game.Skinning { var animation = new SkinnableTextureAnimation(startAtCurrentTime) { - DefaultFrameLength = getFrameLength(source, applyConfigFrameRate, textures), + DefaultFrameLength = frameLength ?? getFrameLength(source, applyConfigFrameRate, textures), Repeat = looping, }; From 47e2ff5ce61a9b96d72f51c79620c892bc91d5f6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 2 Apr 2020 22:55:54 +0900 Subject: [PATCH 0781/2376] Fix incorrect frame length for hit explosions --- .../Skinning/LegacyHitExplosion.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs index 5cfbc1d847..4868dd87ef 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs @@ -32,7 +32,14 @@ namespace osu.Game.Rulesets.Mania.Skinning float explosionScale = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ExplosionScale)?.Value ?? 1; - explosion = skin.GetAnimation(imageName, true, false, startAtCurrentTime: true).With(d => + // Create a temporary animation to retrieve the number of frames, in an effort to calculate the intended frame length. + // This animation is discarded and re-queried with the appropriate frame length afterwards. + var tmp = skin.GetAnimation(imageName, true, false); + double frameLength = 0; + if (tmp is IAnimation tmpAnimation && tmpAnimation.FrameCount > 0) + frameLength = Math.Max(1000 / 60.0, 170.0 / tmpAnimation.FrameCount); + + explosion = skin.GetAnimation(imageName, true, false, startAtCurrentTime: true, frameLength: frameLength).With(d => { if (d == null) return; @@ -40,12 +47,6 @@ namespace osu.Game.Rulesets.Mania.Skinning d.Origin = Anchor.Centre; d.Blending = BlendingParameters.Additive; d.Scale = new Vector2(explosionScale); - - if (!(d is TextureAnimation texAnimation)) - return; - - if (texAnimation.FrameCount > 0) - texAnimation.DefaultFrameLength = Math.Max(texAnimation.DefaultFrameLength, 170.0 / texAnimation.FrameCount); }); if (explosion != null) From 24a7b5f0d69438034f406e0ff27bf5d7578d3ad0 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 2 Apr 2020 23:59:53 +0900 Subject: [PATCH 0782/2376] Fix missing comma --- osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs index 6239b69b4d..853d07c060 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs @@ -35,7 +35,7 @@ namespace osu.Game.Skinning HoldNoteTailImage, HoldNoteBodyImage, ExplosionImage, - ExplosionScale + ExplosionScale, ColumnLineColour } } From c042e709a59f874918b4aa6dcc771421d19085cc Mon Sep 17 00:00:00 2001 From: Will Kennedy Date: Thu, 2 Apr 2020 20:43:54 -0400 Subject: [PATCH 0783/2376] Fix GetDecoder getting fallback decoder too often --- .../Beatmaps/Formats/OsuJsonDecoderTest.cs | 25 +++++++++++++++++++ osu.Game/Beatmaps/Formats/Decoder.cs | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs index 63346b8c9d..c3771302ca 100644 --- a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs @@ -127,6 +127,31 @@ namespace osu.Game.Tests.Beatmaps.Formats .Assert(); } + [Test] + public void TestGetJsonDecoder() + { + Decoder decoder; + + using (var stream = TestResources.OpenResource(normal)) + using (var sr = new LineBufferedReader(stream)) + { + var legacyDecoded = new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(sr); + + using (var ms = new MemoryStream()) + using (var sw = new StreamWriter(ms)) + using (var sr2 = new LineBufferedReader(ms)) + { + sw.Write(legacyDecoded.Serialize()); + sw.Flush(); + + ms.Position = 0; + decoder = Decoder.GetDecoder(sr2); + } + } + + Assert.IsInstanceOf(typeof(JsonBeatmapDecoder), decoder); + } + /// /// Reads a .osu file first with a , serializes the resulting to JSON /// and then deserializes the result back into a through an . diff --git a/osu.Game/Beatmaps/Formats/Decoder.cs b/osu.Game/Beatmaps/Formats/Decoder.cs index 45122f6312..46a1ed1967 100644 --- a/osu.Game/Beatmaps/Formats/Decoder.cs +++ b/osu.Game/Beatmaps/Formats/Decoder.cs @@ -63,7 +63,7 @@ namespace osu.Game.Beatmaps.Formats if (line == null) throw new IOException("Unknown file format (null)"); - var decoder = typedDecoders.Select(d => line.StartsWith(d.Key, StringComparison.InvariantCulture) ? d.Value : null).FirstOrDefault(); + var decoder = typedDecoders.Where(d => line.StartsWith(d.Key, StringComparison.InvariantCulture)).FirstOrDefault().Value; // it's important the magic does NOT get consumed here, since sometimes it's part of the structure // (see JsonBeatmapDecoder - the magic string is the opening brace) From 57944bd335a8f94469ec8383dcd980f01f7cd083 Mon Sep 17 00:00:00 2001 From: Will Kennedy Date: Thu, 2 Apr 2020 21:36:31 -0400 Subject: [PATCH 0784/2376] fix(?) InspectCode warnings --- osu.Game/Beatmaps/Formats/Decoder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Formats/Decoder.cs b/osu.Game/Beatmaps/Formats/Decoder.cs index 46a1ed1967..845ac20db0 100644 --- a/osu.Game/Beatmaps/Formats/Decoder.cs +++ b/osu.Game/Beatmaps/Formats/Decoder.cs @@ -63,7 +63,7 @@ namespace osu.Game.Beatmaps.Formats if (line == null) throw new IOException("Unknown file format (null)"); - var decoder = typedDecoders.Where(d => line.StartsWith(d.Key, StringComparison.InvariantCulture)).FirstOrDefault().Value; + var decoder = typedDecoders.Where(d => line.StartsWith(d.Key, StringComparison.InvariantCulture)).Select(d => d.Value).FirstOrDefault(); // it's important the magic does NOT get consumed here, since sometimes it's part of the structure // (see JsonBeatmapDecoder - the magic string is the opening brace) From 877bd7837a7c3aae64164d6bfed4cfca9524e06a Mon Sep 17 00:00:00 2001 From: Will Kennedy Date: Thu, 2 Apr 2020 22:02:57 -0400 Subject: [PATCH 0785/2376] Changed variable names --- .../Beatmaps/Formats/OsuJsonDecoderTest.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs index c3771302ca..b034e66616 100644 --- a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs @@ -137,15 +137,15 @@ namespace osu.Game.Tests.Beatmaps.Formats { var legacyDecoded = new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(sr); - using (var ms = new MemoryStream()) - using (var sw = new StreamWriter(ms)) - using (var sr2 = new LineBufferedReader(ms)) + using (var memStream = new MemoryStream()) + using (var memWriter = new StreamWriter(memStream)) + using (var memReader = new LineBufferedReader(memStream)) { - sw.Write(legacyDecoded.Serialize()); - sw.Flush(); + memWriter.Write(legacyDecoded.Serialize()); + memWriter.Flush(); - ms.Position = 0; - decoder = Decoder.GetDecoder(sr2); + memStream.Position = 0; + decoder = Decoder.GetDecoder(memReader); } } From 1f797207f7c0502a5e1f6a0f324d74e4b5bc130a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 2 Apr 2020 18:39:49 +0900 Subject: [PATCH 0786/2376] Rework lookups to not require total playfield columns --- osu.Game.Rulesets.Catch/CatchRuleset.cs | 2 +- .../Skinning/TestSceneColumnBackground.cs | 4 +- .../Skinning/TestSceneColumnHitObjectArea.cs | 4 +- .../Skinning/TestSceneHitExplosion.cs | 2 +- .../Skinning/TestSceneKeyArea.cs | 4 +- .../Skinning/TestScenePlayfield.cs | 52 +++++++++++++++++++ osu.Game.Rulesets.Mania/ManiaRuleset.cs | 2 +- osu.Game.Rulesets.Mania/ManiaSkinComponent.cs | 5 +- .../Objects/Drawables/DrawableHoldNote.cs | 2 +- .../Objects/Drawables/DrawableNote.cs | 2 +- .../Skinning/LegacyBodyPiece.cs | 7 --- .../Skinning/LegacyColumnBackground.cs | 9 ++-- .../Skinning/LegacyKeyArea.cs | 3 -- .../Skinning/LegacyManiaElement.cs | 11 +--- .../Skinning/ManiaLegacySkinTransformer.cs | 21 +++++--- .../Skinning/ManiaSkinConfigurationLookup.cs | 19 +++++++ osu.Game.Rulesets.Mania/UI/Column.cs | 8 +-- .../UI/Components/ColumnHitObjectArea.cs | 4 +- .../UI/Components/HitObjectArea.cs | 8 ++- osu.Game.Rulesets.Mania/UI/ManiaStage.cs | 10 ++-- osu.Game.Rulesets.Osu/OsuRuleset.cs | 2 +- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 2 +- osu.Game/Rulesets/Ruleset.cs | 2 +- .../Screens/Edit/Compose/ComposeScreen.cs | 2 +- osu.Game/Screens/Play/Player.cs | 6 +-- osu.Game/Tests/Visual/SkinnableTestScene.cs | 19 ++++--- 26 files changed, 141 insertions(+), 71 deletions(-) create mode 100644 osu.Game.Rulesets.Mania.Tests/Skinning/TestScenePlayfield.cs create mode 100644 osu.Game.Rulesets.Mania/Skinning/ManiaSkinConfigurationLookup.cs diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index 212365caad..ca75a816f1 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -143,7 +143,7 @@ namespace osu.Game.Rulesets.Catch public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new CatchDifficultyCalculator(this, beatmap); - public override ISkin CreateLegacySkinProvider(ISkinSource source) => new CatchLegacySkinTransformer(source); + public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new CatchLegacySkinTransformer(source); public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => new CatchPerformanceCalculator(this, beatmap, score); diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnBackground.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnBackground.cs index ca323b5911..d6bacbe59e 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnBackground.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnBackground.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning { RelativeSizeAxes = Axes.Both, Width = 0.5f, - Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground), _ => new DefaultColumnBackground()) + Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground, 0), _ => new DefaultColumnBackground()) { RelativeSizeAxes = Axes.Both } @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning { RelativeSizeAxes = Axes.Both, Width = 0.5f, - Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground), _ => new DefaultColumnBackground()) + Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground, 0), _ => new DefaultColumnBackground()) { RelativeSizeAxes = Axes.Both } diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnHitObjectArea.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnHitObjectArea.cs index 5d05bca03e..4392666cb7 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnHitObjectArea.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnHitObjectArea.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning { RelativeSizeAxes = Axes.Both, Width = 0.5f, - Child = new ColumnHitObjectArea(new HitObjectContainer()) + Child = new ColumnHitObjectArea(0, new HitObjectContainer()) { RelativeSizeAxes = Axes.Both } @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning { RelativeSizeAxes = Axes.Both, Width = 0.5f, - Child = new ColumnHitObjectArea(new HitObjectContainer()) + Child = new ColumnHitObjectArea(1, new HitObjectContainer()) { RelativeSizeAxes = Axes.Both } diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs index 718dbbea93..5f046574ba 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning CreatedDrawables.OfType().ForEach(c => { - c.Add(new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitExplosion), + c.Add(new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitExplosion, 0), _ => new DefaultHitExplosion((runcount / 15) % 2 == 0 ? new Color4(94, 0, 57, 255) : new Color4(6, 84, 0, 255), runcount % 6 != 0) { Anchor = Anchor.Centre, diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneKeyArea.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneKeyArea.cs index 1e6f00205a..c8f901285a 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneKeyArea.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneKeyArea.cs @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning { RelativeSizeAxes = Axes.Both, Width = 0.5f, - Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea), _ => new DefaultKeyArea()) + Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea, 0), _ => new DefaultKeyArea()) { RelativeSizeAxes = Axes.Both }, @@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning { RelativeSizeAxes = Axes.Both, Width = 0.5f, - Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea), _ => new DefaultKeyArea()) + Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea, 1), _ => new DefaultKeyArea()) { RelativeSizeAxes = Axes.Both }, diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestScenePlayfield.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestScenePlayfield.cs new file mode 100644 index 0000000000..161eda650e --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestScenePlayfield.cs @@ -0,0 +1,52 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Rulesets.Mania.UI; + +namespace osu.Game.Rulesets.Mania.Tests.Skinning +{ + public class TestScenePlayfield : ManiaSkinnableTestScene + { + private List stageDefinitions = new List(); + + [Test] + public void TestSingleStage() + { + AddStep("create stage", () => + { + stageDefinitions = new List + { + new StageDefinition { Columns = 2 } + }; + + SetContents(() => new ManiaPlayfield(stageDefinitions)); + }); + } + + [Test] + public void TestDualStages() + { + AddStep("create stage", () => + { + stageDefinitions = new List + { + new StageDefinition { Columns = 2 }, + new StageDefinition { Columns = 2 } + }; + + SetContents(() => new ManiaPlayfield(stageDefinitions)); + }); + } + + protected override IBeatmap CreateBeatmapForSkinProvider() + { + var maniaBeatmap = (ManiaBeatmap)base.CreateBeatmapForSkinProvider(); + maniaBeatmap.Stages = stageDefinitions; + return maniaBeatmap; + } + } +} diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 9d06bd7c25..2bd88fee90 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Mania public override HitObjectComposer CreateHitObjectComposer() => new ManiaHitObjectComposer(this); - public override ISkin CreateLegacySkinProvider(ISkinSource source) => new ManiaLegacySkinTransformer(source); + public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new ManiaLegacySkinTransformer(source, beatmap); public override IEnumerable ConvertFromLegacyMods(LegacyMods mods) { diff --git a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs index 7d1c4ff8b3..89eb203309 100644 --- a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs +++ b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs @@ -7,9 +7,12 @@ namespace osu.Game.Rulesets.Mania { public class ManiaSkinComponent : GameplaySkinComponent { - public ManiaSkinComponent(ManiaSkinComponents component) + public readonly int TargetColumn; + + public ManiaSkinComponent(ManiaSkinComponents component, int targetColumn) : base(component) { + TargetColumn = targetColumn; } protected override string RulesetPrefix => ManiaRuleset.SHORT_NAME; diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 7cacaf35a6..a9ef661aaa 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables AddRangeInternal(new[] { - bodyPiece = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HoldNoteBody), _ => new DefaultBodyPiece()) + bodyPiece = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HoldNoteBody, hitObject.Column), _ => new DefaultBodyPiece()) { RelativeSizeAxes = Axes.X }, diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs index fdc50048fe..9451bc4430 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - AddInternal(headPiece = new SkinnableDrawable(new ManiaSkinComponent(Component), _ => new DefaultNotePiece()) + AddInternal(headPiece = new SkinnableDrawable(new ManiaSkinComponent(Component, hitObject.Column), _ => new DefaultNotePiece()) { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs index 1ffee98a6c..0c9bc97ba9 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs @@ -6,7 +6,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; using osu.Game.Rulesets.Mania.Objects.Drawables; -using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; @@ -21,12 +20,6 @@ namespace osu.Game.Rulesets.Mania.Skinning private Drawable sprite; - [Resolved(CanBeNull = true)] - private ManiaStage stage { get; set; } - - [Resolved] - private Column column { get; set; } - public LegacyBodyPiece() { RelativeSizeAxes = Axes.Both; diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs index 27845fca4a..8cd0272b52 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs @@ -18,12 +18,14 @@ namespace osu.Game.Rulesets.Mania.Skinning public class LegacyColumnBackground : LegacyManiaColumnElement, IKeyBindingHandler { private readonly IBindable direction = new Bindable(); + private readonly bool isLastColumn; private Container lightContainer; private Sprite light; - public LegacyColumnBackground() + public LegacyColumnBackground(bool isLastColumn) { + this.isLastColumn = isLastColumn; RelativeSizeAxes = Axes.Both; } @@ -40,10 +42,9 @@ namespace osu.Game.Rulesets.Mania.Skinning bool hasLeftLine = leftLineWidth > 0; bool hasRightLine = rightLineWidth > 0 && skin.GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value >= 2.4m - || Stage == null || Column.Index == Stage.Columns.Count - 1; + || isLastColumn; - float lightPosition = skin.GetConfig( - new LegacyManiaSkinConfigurationLookup(Stage?.Columns.Count ?? 4, LegacyManiaSkinConfigurationLookups.LightPosition))?.Value + float lightPosition = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.LightPosition)?.Value ?? 0; Color4 lineColour = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ColumnLineColour)?.Value diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs index d2541772cc..7c8d1cd303 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs @@ -22,9 +22,6 @@ namespace osu.Game.Rulesets.Mania.Skinning private Sprite upSprite; private Sprite downSprite; - [Resolved(CanBeNull = true)] - private ManiaStage stage { get; set; } - [Resolved] private Column column { get; set; } diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyManiaElement.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyManiaElement.cs index 2fb229862f..11fdd663a1 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyManiaElement.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyManiaElement.cs @@ -1,11 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using JetBrains.Annotations; -using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; -using osu.Game.Rulesets.Mania.UI; using osu.Game.Skinning; namespace osu.Game.Rulesets.Mania.Skinning @@ -15,10 +12,6 @@ namespace osu.Game.Rulesets.Mania.Skinning /// public class LegacyManiaElement : CompositeDrawable { - [Resolved(CanBeNull = true)] - [CanBeNull] - protected ManiaStage Stage { get; private set; } - /// /// Retrieve a per-column-count skin configuration. /// @@ -26,7 +19,7 @@ namespace osu.Game.Rulesets.Mania.Skinning /// The value to retrieve. /// If not null, denotes the index of the column to which the entry applies. protected virtual IBindable GetManiaSkinConfig(ISkin skin, LegacyManiaSkinConfigurationLookups lookup, int? index = null) - => skin.GetConfig( - new LegacyManiaSkinConfigurationLookup(Stage?.Columns.Count ?? 4, lookup, index)); + => skin.GetConfig( + new ManiaSkinConfigurationLookup(lookup, index)); } } diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs index 02fd6c0572..cbe2036343 100644 --- a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs @@ -8,6 +8,8 @@ using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Game.Rulesets.Scoring; using osu.Game.Audio; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Skinning; namespace osu.Game.Rulesets.Mania.Skinning @@ -15,6 +17,7 @@ namespace osu.Game.Rulesets.Mania.Skinning public class ManiaLegacySkinTransformer : ISkin { private readonly ISkin source; + private readonly ManiaBeatmap beatmap; private Lazy isLegacySkin; @@ -24,9 +27,10 @@ namespace osu.Game.Rulesets.Mania.Skinning /// private Lazy hasKeyTexture; - public ManiaLegacySkinTransformer(ISkinSource source) + public ManiaLegacySkinTransformer(ISkinSource source, IBeatmap beatmap) { this.source = source; + this.beatmap = (ManiaBeatmap)beatmap; source.SourceChanged += sourceChanged; sourceChanged(); @@ -36,8 +40,8 @@ namespace osu.Game.Rulesets.Mania.Skinning { isLegacySkin = new Lazy(() => source.GetConfig(LegacySkinConfiguration.LegacySetting.Version) != null); hasKeyTexture = new Lazy(() => source.GetAnimation( - source.GetConfig( - new LegacyManiaSkinConfigurationLookup(4, LegacyManiaSkinConfigurationLookups.KeyImage, 0))?.Value + source.GetConfig( + new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.KeyImage, 0))?.Value ?? "mania-key1", true, true) != null); } @@ -55,7 +59,7 @@ namespace osu.Game.Rulesets.Mania.Skinning switch (maniaComponent.Component) { case ManiaSkinComponents.ColumnBackground: - return new LegacyColumnBackground(); + return new LegacyColumnBackground(maniaComponent.TargetColumn == beatmap.TotalColumns - 1); case ManiaSkinComponents.HitTarget: return new LegacyHitTarget(); @@ -115,7 +119,12 @@ namespace osu.Game.Rulesets.Mania.Skinning public SampleChannel GetSample(ISampleInfo sample) => source.GetSample(sample); - public IBindable GetConfig(TLookup lookup) => - source.GetConfig(lookup); + public IBindable GetConfig(TLookup lookup) + { + if (lookup is ManiaSkinConfigurationLookup maniaLookup) + return source.GetConfig(new LegacyManiaSkinConfigurationLookup(beatmap.TotalColumns, maniaLookup.Lookup, maniaLookup.TargetColumn)); + + return source.GetConfig(lookup); + } } } diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaSkinConfigurationLookup.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaSkinConfigurationLookup.cs new file mode 100644 index 0000000000..7e5a2aa7ed --- /dev/null +++ b/osu.Game.Rulesets.Mania/Skinning/ManiaSkinConfigurationLookup.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.Game.Skinning; + +namespace osu.Game.Rulesets.Mania.Skinning +{ + public class ManiaSkinConfigurationLookup + { + public readonly LegacyManiaSkinConfigurationLookups Lookup; + public readonly int? TargetColumn; + + public ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups lookup, int? targetColumn = null) + { + Lookup = lookup; + TargetColumn = targetColumn; + } + } +} diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 5a6cd7e229..d2f58d7255 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Mania.UI RelativeSizeAxes = Axes.Y; - Drawable background = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground), _ => new DefaultColumnBackground()) + Drawable background = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground, Index), _ => new DefaultColumnBackground()) { RelativeSizeAxes = Axes.Both }; @@ -52,8 +52,8 @@ namespace osu.Game.Rulesets.Mania.UI { // For input purposes, the background is added at the highest depth, but is then proxied back below all other elements background.CreateProxy(), - hitObjectArea = new ColumnHitObjectArea(HitObjectContainer) { RelativeSizeAxes = Axes.Both }, - new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea), _ => new DefaultKeyArea()) + hitObjectArea = new ColumnHitObjectArea(Index, HitObjectContainer) { RelativeSizeAxes = Axes.Both }, + new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea, Index), _ => new DefaultKeyArea()) { RelativeSizeAxes = Axes.Both }, @@ -105,7 +105,7 @@ namespace osu.Game.Rulesets.Mania.UI if (!result.IsHit || !judgedObject.DisplayResult || !DisplayJudgements.Value) return; - var explosion = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitExplosion), _ => + var explosion = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitExplosion, Index), _ => new DefaultHitExplosion(judgedObject.AccentColour.Value, judgedObject is DrawableHoldNoteTick)) { RelativeSizeAxes = Axes.Both diff --git a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs index 7d280f0bea..cb79bf7f43 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs @@ -14,12 +14,12 @@ namespace osu.Game.Rulesets.Mania.UI.Components public readonly Container Explosions; private readonly Drawable hitTarget; - public ColumnHitObjectArea(HitObjectContainer hitObjectContainer) + public ColumnHitObjectArea(int columnIndex, HitObjectContainer hitObjectContainer) : base(hitObjectContainer) { AddRangeInternal(new[] { - hitTarget = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitTarget), _ => new DefaultHitTarget()) + hitTarget = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitTarget, columnIndex), _ => new DefaultHitTarget()) { RelativeSizeAxes = Axes.X, Depth = 1 diff --git a/osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs b/osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs index 9e62445c81..bca7c3ff08 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Game.Rulesets.Mania.Skinning; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; @@ -14,9 +15,6 @@ namespace osu.Game.Rulesets.Mania.UI.Components { protected readonly IBindable Direction = new Bindable(); - [Resolved(CanBeNull = true)] - private ManiaStage stage { get; set; } - public HitObjectArea(HitObjectContainer hitObjectContainer) { InternalChildren = new[] @@ -45,8 +43,8 @@ namespace osu.Game.Rulesets.Mania.UI.Components protected virtual void UpdateHitPosition() { - float hitPosition = CurrentSkin.GetConfig( - new LegacyManiaSkinConfigurationLookup(stage?.Columns.Count ?? 4, LegacyManiaSkinConfigurationLookups.HitPosition))?.Value + float hitPosition = CurrentSkin.GetConfig( + new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.HitPosition))?.Value ?? ManiaStage.HIT_TARGET_POSITION; Padding = Direction.Value == ScrollingDirection.Up diff --git a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs b/osu.Game.Rulesets.Mania/UI/ManiaStage.cs index 1e190f4857..adab08eb06 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaStage.cs @@ -11,6 +11,7 @@ using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects.Drawables; +using osu.Game.Rulesets.Mania.Skinning; using osu.Game.Rulesets.Mania.UI.Components; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI; @@ -24,7 +25,6 @@ namespace osu.Game.Rulesets.Mania.UI /// /// A collection of s. /// - [Cached] public class ManiaStage : ScrollingPlayfield { public const float COLUMN_SPACING = 1; @@ -146,15 +146,15 @@ namespace osu.Game.Rulesets.Mania.UI { if (col.Index > 0) { - float spacing = currentSkin.GetConfig( - new LegacyManiaSkinConfigurationLookup(Columns.Count, LegacyManiaSkinConfigurationLookups.ColumnSpacing, col.Index - 1)) + float spacing = currentSkin.GetConfig( + new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnSpacing, col.Index - 1)) ?.Value ?? COLUMN_SPACING; col.Margin = new MarginPadding { Left = spacing }; } - float? width = currentSkin.GetConfig( - new LegacyManiaSkinConfigurationLookup(Columns.Count, LegacyManiaSkinConfigurationLookups.ColumnWidth, col.Index)) + float? width = currentSkin.GetConfig( + new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnWidth, col.Index)) ?.Value; if (width == null) diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index a0f5b8fe01..689a7b35ea 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -179,7 +179,7 @@ namespace osu.Game.Rulesets.Osu public override RulesetSettingsSubsection CreateSettings() => new OsuSettingsSubsection(this); - public override ISkin CreateLegacySkinProvider(ISkinSource source) => new OsuLegacySkinTransformer(source); + public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new OsuLegacySkinTransformer(source); public int LegacyID => 0; diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index a6c9a33569..74d9e68ad3 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Taiko public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new TaikoBeatmapConverter(beatmap, this); - public override ISkin CreateLegacySkinProvider(ISkinSource source) => new TaikoLegacySkinTransformer(source); + public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new TaikoLegacySkinTransformer(source); public const string SHORT_NAME = "taiko"; diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 58f598a203..bee11accca 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -102,7 +102,7 @@ namespace osu.Game.Rulesets public ModAutoplay GetAutoplayMod() => GetAllMods().OfType().First(); - public virtual ISkin CreateLegacySkinProvider(ISkinSource source) => null; + public virtual ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => null; protected Ruleset() { diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs index cdea200e10..04983ca597 100644 --- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs +++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs @@ -25,7 +25,7 @@ namespace osu.Game.Screens.Edit.Compose // the beatmapSkinProvider is used as the fallback source here to allow the ruleset-specific skin implementation // full access to all skin sources. - var rulesetSkinProvider = new SkinProvidingContainer(ruleset.CreateLegacySkinProvider(beatmapSkinProvider)); + var rulesetSkinProvider = new SkinProvidingContainer(ruleset.CreateLegacySkinProvider(beatmapSkinProvider, EditorBeatmap.PlayableBeatmap)); // 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. diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 5da53ad2c9..4597ae760c 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -176,7 +176,7 @@ namespace osu.Game.Screens.Play dependencies.CacheAs(gameplayBeatmap); addUnderlayComponents(GameplayClockContainer); - addGameplayComponents(GameplayClockContainer, Beatmap.Value); + addGameplayComponents(GameplayClockContainer, Beatmap.Value, playableBeatmap); addOverlayComponents(GameplayClockContainer, Beatmap.Value); DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updatePauseOnFocusLostState(), true); @@ -214,13 +214,13 @@ namespace osu.Game.Screens.Play target.Add(DimmableStoryboard = new DimmableStoryboard(Beatmap.Value.Storyboard) { RelativeSizeAxes = Axes.Both }); } - private void addGameplayComponents(Container target, WorkingBeatmap working) + private void addGameplayComponents(Container target, WorkingBeatmap working, IBeatmap playableBeatmap) { var beatmapSkinProvider = new BeatmapSkinProvidingContainer(working.Skin); // the beatmapSkinProvider is used as the fallback source here to allow the ruleset-specific skin implementation // full access to all skin sources. - var rulesetSkinProvider = new SkinProvidingContainer(ruleset.CreateLegacySkinProvider(beatmapSkinProvider)); + var rulesetSkinProvider = new SkinProvidingContainer(ruleset.CreateLegacySkinProvider(beatmapSkinProvider, playableBeatmap)); // 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. diff --git a/osu.Game/Tests/Visual/SkinnableTestScene.cs b/osu.Game/Tests/Visual/SkinnableTestScene.cs index 7a5328d30c..d0113b3096 100644 --- a/osu.Game/Tests/Visual/SkinnableTestScene.cs +++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; +using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; using osu.Game.Skinning; using osuTK; @@ -47,16 +48,18 @@ namespace osu.Game.Tests.Visual { createdDrawables.Clear(); - Cell(0).Child = createProvider(null, creationFunction); - Cell(1).Child = createProvider(metricsSkin, creationFunction); - Cell(2).Child = createProvider(defaultSkin, creationFunction); - Cell(3).Child = createProvider(specialSkin, creationFunction); - Cell(4).Child = createProvider(oldSkin, creationFunction); + var beatmap = CreateBeatmapForSkinProvider(); + + Cell(0).Child = createProvider(null, creationFunction, beatmap); + Cell(1).Child = createProvider(metricsSkin, creationFunction, beatmap); + Cell(2).Child = createProvider(defaultSkin, creationFunction, beatmap); + Cell(3).Child = createProvider(specialSkin, creationFunction, beatmap); + Cell(4).Child = createProvider(oldSkin, creationFunction, beatmap); } protected IEnumerable CreatedDrawables => createdDrawables; - private Drawable createProvider(Skin skin, Func creationFunction) + private Drawable createProvider(Skin skin, Func creationFunction, IBeatmap beatmap) { var created = creationFunction(); createdDrawables.Add(created); @@ -100,7 +103,7 @@ namespace osu.Game.Tests.Visual { new OutlineBox { Alpha = autoSize ? 1 : 0 }, mainProvider.WithChild( - new SkinProvidingContainer(Ruleset.Value.CreateInstance().CreateLegacySkinProvider(mainProvider)) + new SkinProvidingContainer(Ruleset.Value.CreateInstance().CreateLegacySkinProvider(mainProvider, beatmap)) { Child = created, RelativeSizeAxes = !autoSize ? Axes.Both : Axes.None, @@ -113,6 +116,8 @@ namespace osu.Game.Tests.Visual }; } + protected virtual IBeatmap CreateBeatmapForSkinProvider() => CreateWorkingBeatmap(Ruleset.Value).GetPlayableBeatmap(Ruleset.Value); + private class OutlineBox : CompositeDrawable { public OutlineBox() From 571748d10528e5cabad54a4c4dccd3b439235c48 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 3 Apr 2020 11:55:52 +0900 Subject: [PATCH 0787/2376] Add some xmldocs + nullable parameter --- osu.Game.Rulesets.Mania/ManiaSkinComponent.cs | 14 ++++++++++++-- .../Skinning/ManiaSkinConfigurationLookup.cs | 14 ++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs index 89eb203309..2371d74a2b 100644 --- a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs +++ b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs @@ -1,15 +1,25 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Game.Rulesets.Mania.UI; using osu.Game.Skinning; namespace osu.Game.Rulesets.Mania { public class ManiaSkinComponent : GameplaySkinComponent { - public readonly int TargetColumn; + /// + /// The intended index for this component. + /// May be null if the component does not exist in a . + /// + public readonly int? TargetColumn; - public ManiaSkinComponent(ManiaSkinComponents component, int targetColumn) + /// + /// Creates a new . + /// + /// The component. + /// The intended index for this component. May be null if the component does not exist in a . + public ManiaSkinComponent(ManiaSkinComponents component, int? targetColumn = null) : base(component) { TargetColumn = targetColumn; diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaSkinConfigurationLookup.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaSkinConfigurationLookup.cs index 7e5a2aa7ed..f07a5518b7 100644 --- a/osu.Game.Rulesets.Mania/Skinning/ManiaSkinConfigurationLookup.cs +++ b/osu.Game.Rulesets.Mania/Skinning/ManiaSkinConfigurationLookup.cs @@ -1,15 +1,29 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Game.Rulesets.Mania.UI; using osu.Game.Skinning; namespace osu.Game.Rulesets.Mania.Skinning { public class ManiaSkinConfigurationLookup { + /// + /// The configuration lookup value. + /// public readonly LegacyManiaSkinConfigurationLookups Lookup; + + /// + /// The intended index for the configuration. + /// May be null if the configuration does not apply to a . + /// public readonly int? TargetColumn; + /// + /// Creates a new . + /// + /// The lookup value. + /// The intended index for the configuration. May be null if the configuration does not apply to a . public ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups lookup, int? targetColumn = null) { Lookup = lookup; From b42d1104b7270a5946090dc563ad439dd32478ef Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 3 Apr 2020 13:16:01 +0900 Subject: [PATCH 0788/2376] Fix mania converts scrolling at incorrect speeds --- .../UI/DrawableManiaRuleset.cs | 13 ++++++++ .../UI/Scrolling/DrawableScrollingRuleset.cs | 32 +++++++++---------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index e5ec054fa7..796d083c32 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -7,6 +7,7 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Input; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Input.Handlers; using osu.Game.Replays; using osu.Game.Rulesets.Mania.Beatmaps; @@ -46,6 +47,18 @@ namespace osu.Game.Rulesets.Mania.UI [BackgroundDependencyLoader] private void load() { + bool isForCurrentRuleset = Beatmap.BeatmapInfo.Ruleset.Equals(Ruleset.RulesetInfo); + + foreach (var p in ControlPoints) + { + // Mania doesn't care about global velocity + p.Velocity = 1; + + // For non-mania beatmap, speed changes should only happen through timing points + if (!isForCurrentRuleset) + p.DifficultyPoint = new DifficultyControlPoint(); + } + BarLines.ForEach(Playfield.Add); Config.BindWith(ManiaRulesetSetting.ScrollDirection, configDirection); diff --git a/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs b/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs index 8bcdfff2fd..f3d2c5bdcb 100644 --- a/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs +++ b/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs @@ -74,11 +74,9 @@ namespace osu.Game.Rulesets.UI.Scrolling protected virtual bool RelativeScaleBeatLengths => false; /// - /// Provides the default s that adjust the scrolling rate of s - /// inside this . + /// The s that adjust the scrolling rate of s inside this . /// - /// - private readonly SortedList controlPoints = new SortedList(Comparer.Default); + protected readonly SortedList ControlPoints = new SortedList(Comparer.Default); protected IScrollingInfo ScrollingInfo => scrollingInfo; @@ -95,11 +93,11 @@ namespace osu.Game.Rulesets.UI.Scrolling switch (VisualisationMethod) { case ScrollVisualisationMethod.Sequential: - scrollingInfo.Algorithm = new SequentialScrollAlgorithm(controlPoints); + scrollingInfo.Algorithm = new SequentialScrollAlgorithm(ControlPoints); break; case ScrollVisualisationMethod.Overlapping: - scrollingInfo.Algorithm = new OverlappingScrollAlgorithm(controlPoints); + scrollingInfo.Algorithm = new OverlappingScrollAlgorithm(ControlPoints); break; case ScrollVisualisationMethod.Constant: @@ -168,10 +166,18 @@ namespace osu.Game.Rulesets.UI.Scrolling // Collapse sections with the same start time .GroupBy(s => s.StartTime).Select(g => g.Last()).OrderBy(s => s.StartTime); - controlPoints.AddRange(timingChanges); + ControlPoints.AddRange(timingChanges); - if (controlPoints.Count == 0) - controlPoints.Add(new MultiplierControlPoint { Velocity = Beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier }); + if (ControlPoints.Count == 0) + ControlPoints.Add(new MultiplierControlPoint { Velocity = Beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier }); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + if (!(Playfield is ScrollingPlayfield)) + throw new ArgumentException($"{nameof(Playfield)} must be a {nameof(ScrollingPlayfield)} when using {nameof(DrawableScrollingRuleset)}."); } public bool OnPressed(GlobalAction action) @@ -193,14 +199,6 @@ namespace osu.Game.Rulesets.UI.Scrolling return false; } - protected override void LoadComplete() - { - base.LoadComplete(); - - if (!(Playfield is ScrollingPlayfield)) - throw new ArgumentException($"{nameof(Playfield)} must be a {nameof(ScrollingPlayfield)} when using {nameof(DrawableScrollingRuleset)}."); - } - public void OnReleased(GlobalAction action) { } From 8cb0eb9b1251169e4433fe42948239cda76da607 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 3 Apr 2020 15:08:06 +0900 Subject: [PATCH 0789/2376] Fix dynamic recompilation in intro test scenes --- osu.Game.Tests/Visual/Menus/IntroTestScene.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Menus/IntroTestScene.cs b/osu.Game.Tests/Visual/Menus/IntroTestScene.cs index 1ad4d9dca9..33811f9529 100644 --- a/osu.Game.Tests/Visual/Menus/IntroTestScene.cs +++ b/osu.Game.Tests/Visual/Menus/IntroTestScene.cs @@ -22,7 +22,6 @@ namespace osu.Game.Tests.Visual.Menus { typeof(StartupScreen), typeof(IntroScreen), - typeof(OsuScreen), typeof(IntroTestScene), }; From 51db361c32c2c1a3a97599ff5ea47490d1e8369c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 3 Apr 2020 15:59:56 +0900 Subject: [PATCH 0790/2376] Update usages of Animation and Video in line with framework changes --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 2 +- .../Skinning/LegacyHitExplosion.cs | 2 +- osu.Game.Tournament/Components/TourneyVideo.cs | 4 ++-- osu.Game/Screens/Menu/IntroTriangles.cs | 3 +-- osu.Game/Skinning/LegacySkinExtensions.cs | 8 +++++--- .../Drawables/DrawableStoryboardAnimation.cs | 2 +- .../Drawables/DrawableStoryboardVideo.cs | 13 ++++++------- 7 files changed, 17 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 13935e036b..7c815370c8 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -384,7 +384,7 @@ namespace osu.Game.Rulesets.Catch.UI } currentCatcher.Show(); - (currentCatcher.Drawable as IAnimation)?.GotoFrame(0); + (currentCatcher.Drawable as IFramedAnimation)?.GotoFrame(0); } private void beginTrail() diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs index 4868dd87ef..c87a1d438b 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs @@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Mania.Skinning // This animation is discarded and re-queried with the appropriate frame length afterwards. var tmp = skin.GetAnimation(imageName, true, false); double frameLength = 0; - if (tmp is IAnimation tmpAnimation && tmpAnimation.FrameCount > 0) + if (tmp is IFramedAnimation tmpAnimation && tmpAnimation.FrameCount > 0) frameLength = Math.Max(1000 / 60.0, 170.0 / tmpAnimation.FrameCount); explosion = skin.GetAnimation(imageName, true, false, startAtCurrentTime: true, frameLength: frameLength).With(d => diff --git a/osu.Game.Tournament/Components/TourneyVideo.cs b/osu.Game.Tournament/Components/TourneyVideo.cs index bc66fad8c1..317c5f6a56 100644 --- a/osu.Game.Tournament/Components/TourneyVideo.cs +++ b/osu.Game.Tournament/Components/TourneyVideo.cs @@ -16,7 +16,7 @@ namespace osu.Game.Tournament.Components { private readonly string filename; private readonly bool drawFallbackGradient; - private VideoSprite video; + private Video video; private ManualClock manualClock; @@ -33,7 +33,7 @@ namespace osu.Game.Tournament.Components if (stream != null) { - InternalChild = video = new VideoSprite(stream, false) + InternalChild = video = new Video(stream, false) { RelativeSizeAxes = Axes.Both, FillMode = FillMode.Fit, diff --git a/osu.Game/Screens/Menu/IntroTriangles.cs b/osu.Game/Screens/Menu/IntroTriangles.cs index be5762e68d..b44b6ea993 100644 --- a/osu.Game/Screens/Menu/IntroTriangles.cs +++ b/osu.Game/Screens/Menu/IntroTriangles.cs @@ -270,10 +270,9 @@ namespace osu.Game.Screens.Menu [BackgroundDependencyLoader] private void load() { - InternalChild = new VideoSprite(videoStream, false) + InternalChild = new Video(videoStream, false) { RelativeSizeAxes = Axes.Both, - Clock = new FramedOffsetClock(Clock) { Offset = -logo_1 } }; } } diff --git a/osu.Game/Skinning/LegacySkinExtensions.cs b/osu.Game/Skinning/LegacySkinExtensions.cs index ea3d180ef8..9bfde4fdcb 100644 --- a/osu.Game/Skinning/LegacySkinExtensions.cs +++ b/osu.Game/Skinning/LegacySkinExtensions.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; -using osu.Framework.Timing; namespace osu.Game.Skinning { @@ -28,7 +27,7 @@ namespace osu.Game.Skinning var animation = new SkinnableTextureAnimation(startAtCurrentTime) { DefaultFrameLength = frameLength ?? getFrameLength(source, applyConfigFrameRate, textures), - Repeat = looping, + Loop = looping, }; foreach (var t in textures) @@ -71,7 +70,10 @@ namespace osu.Game.Skinning base.LoadComplete(); if (timeReference != null) - Clock = new FramedOffsetClock(timeReference.Clock) { Offset = -timeReference.AnimationStartTime }; + { + Clock = timeReference.Clock; + PlaybackPosition = timeReference.AnimationStartTime - timeReference.Clock.CurrentTime; + } } } diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs index eabb78bac5..72e52f6106 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs @@ -108,7 +108,7 @@ namespace osu.Game.Storyboards.Drawables Animation = animation; Origin = animation.Origin; Position = animation.InitialPosition; - Repeat = animation.LoopType == AnimationLoopType.LoopForever; + Loop = animation.LoopType == AnimationLoopType.LoopForever; LifetimeStart = animation.StartTime; LifetimeEnd = animation.EndTime; diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs index d4dbdf1ea8..2e7b66ea4f 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Video; -using osu.Framework.Timing; using osu.Game.Beatmaps; namespace osu.Game.Storyboards.Drawables @@ -16,7 +15,7 @@ namespace osu.Game.Storyboards.Drawables public class DrawableStoryboardVideo : CompositeDrawable { public readonly StoryboardVideo Video; - private VideoSprite videoSprite; + private Video video; public override bool RemoveWhenNotAlive => false; @@ -40,14 +39,14 @@ namespace osu.Game.Storyboards.Drawables if (stream == null) return; - InternalChild = videoSprite = new VideoSprite(stream, false) + InternalChild = video = new Video(stream, false) { RelativeSizeAxes = Axes.Both, FillMode = FillMode.Fill, Anchor = Anchor.Centre, Origin = Anchor.Centre, Alpha = 0, - Clock = new FramedOffsetClock(Clock) { Offset = -Video.StartTime } + PlaybackPosition = Video.StartTime }; } @@ -55,10 +54,10 @@ namespace osu.Game.Storyboards.Drawables { base.LoadComplete(); - if (videoSprite == null) return; + if (video == null) return; - using (videoSprite.BeginAbsoluteSequence(0)) - videoSprite.FadeIn(500); + using (video.BeginAbsoluteSequence(0)) + video.FadeIn(500); } } } From b1268a73f1c0a0de1260e57a34aa20ec2a3bc64b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 3 Apr 2020 18:15:24 +0900 Subject: [PATCH 0791/2376] Add keybinding repeat extension method --- osu.Game/Extensions/DrawableExtensions.cs | 32 +++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 osu.Game/Extensions/DrawableExtensions.cs diff --git a/osu.Game/Extensions/DrawableExtensions.cs b/osu.Game/Extensions/DrawableExtensions.cs new file mode 100644 index 0000000000..1790eb608e --- /dev/null +++ b/osu.Game/Extensions/DrawableExtensions.cs @@ -0,0 +1,32 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Input.Bindings; +using osu.Framework.Threading; + +namespace osu.Game.Extensions +{ + public static class DrawableExtensions + { + /// + /// Helper method that is used while doesn't support repetitions of . + /// Simulates repetitions by continually invoking a delegate according to the default key repeat rate. + /// + /// + /// The returned delegate can be cancelled to stop repeat events from firing (usually in ). + /// + /// The which is handling the repeat. + /// The to schedule repetitions on. + /// The to be invoked once immediately and with every repetition. + /// A which can be cancelled to stop the repeat events from firing. + public static ScheduledDelegate BeginKeyRepeat(this IKeyBindingHandler handler, Scheduler scheduler, Action action) + { + action(); + + ScheduledDelegate repeatDelegate = new ScheduledDelegate(action, handler.Time.Current + 250, 70); + scheduler.Add(repeatDelegate); + return repeatDelegate; + } + } +} From 0a7d9b930c76ca3c224abe79d0d395334e777d9d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 3 Apr 2020 18:23:03 +0900 Subject: [PATCH 0792/2376] Add osu!taiko legacy drum skinning support --- .../metrics-skin/taiko-bar-left@2x.png | Bin 0 -> 78533 bytes .../metrics-skin/taiko-drum-inner@2x.png | Bin 0 -> 4829 bytes .../metrics-skin/taiko-drum-outer@2x.png | Bin 0 -> 7818 bytes .../TestSceneInputDrum.cs | 13 +- .../Skinning/LegacyTaikoDrum.cs | 144 ++++++++++++++++++ .../Skinning/TaikoLegacySkinTransformer.cs | 17 ++- .../TaikoSkinComponents.cs | 1 + osu.Game.Rulesets.Taiko/UI/InputDrum.cs | 65 ++++---- 8 files changed, 200 insertions(+), 40 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taiko-bar-left@2x.png create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taiko-drum-inner@2x.png create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taiko-drum-outer@2x.png create mode 100644 osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoDrum.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taiko-bar-left@2x.png b/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taiko-bar-left@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..dc3d7f4c702b7e94d1c53e3434b61d028bd803e3 GIT binary patch literal 78533 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 literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taiko-drum-inner@2x.png b/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taiko-drum-inner@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..15a89ade1bb6e63efb49b48fee95d7c664ed5ff0 GIT binary patch literal 4829 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 RequiredTypes => new[] { typeof(InputDrum), - typeof(DrumSampleMapping), - typeof(HitSampleInfo), - typeof(SampleControlPoint) }; - public TestSceneInputDrum() + [BackgroundDependencyLoader] + private void load() { - Add(new TaikoInputManager(new RulesetInfo { ID = 1 }) + SetContents(() => new TaikoInputManager(new RulesetInfo { ID = 1 }) { RelativeSizeAxes = Axes.Both, Child = new Container diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoDrum.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoDrum.cs new file mode 100644 index 0000000000..8fe7c5e566 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoDrum.cs @@ -0,0 +1,144 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Bindings; +using osu.Game.Rulesets.Taiko.Audio; +using osu.Game.Skinning; +using osuTK; + +namespace osu.Game.Rulesets.Taiko.Skinning +{ + /// + /// A component of the playfield that captures input and displays input as a drum. + /// + internal class LegacyInputDrum : Container + { + public LegacyInputDrum() + { + AutoSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load(ISkinSource skin) + { + Children = new Drawable[] + { + new Sprite + { + Texture = skin.GetTexture("taiko-bar-left") + }, + new LegacyHalfDrum(false) + { + Name = "Left Half", + RelativeSizeAxes = Axes.Both, + Width = 0.5f, + RimAction = TaikoAction.LeftRim, + CentreAction = TaikoAction.LeftCentre + }, + new LegacyHalfDrum(true) + { + Name = "Right Half", + Anchor = Anchor.TopRight, + RelativeSizeAxes = Axes.Both, + Width = 0.5f, + Scale = new Vector2(-1, 1), + RimAction = TaikoAction.RightRim, + CentreAction = TaikoAction.RightCentre + } + }; + } + + /// + /// A half-drum. Contains one centre and one rim hit. + /// + private class LegacyHalfDrum : Container, IKeyBindingHandler + { + /// + /// The key to be used for the rim of the half-drum. + /// + public TaikoAction RimAction; + + /// + /// The key to be used for the centre of the half-drum. + /// + public TaikoAction CentreAction; + + private readonly Sprite rimHit; + private readonly Sprite centreHit; + + [Resolved] + private DrumSampleMapping sampleMappings { get; set; } + + public LegacyHalfDrum(bool flipped) + { + Masking = true; + + Children = new Drawable[] + { + rimHit = new Sprite + { + Anchor = flipped ? Anchor.CentreRight : Anchor.CentreLeft, + Origin = flipped ? Anchor.CentreLeft : Anchor.CentreRight, + Scale = new Vector2(-1, 1), + Alpha = 0, + }, + centreHit = new Sprite + { + Anchor = flipped ? Anchor.CentreRight : Anchor.CentreLeft, + Origin = flipped ? Anchor.CentreRight : Anchor.CentreLeft, + Alpha = 0, + } + }; + } + + [BackgroundDependencyLoader] + private void load(ISkinSource skin) + { + rimHit.Texture = skin.GetTexture(@"taiko-drum-outer"); + centreHit.Texture = skin.GetTexture(@"taiko-drum-inner"); + } + + public bool OnPressed(TaikoAction action) + { + Drawable target = null; + var drumSample = sampleMappings.SampleAt(Time.Current); + + if (action == CentreAction) + { + target = centreHit; + drumSample.Centre?.Play(); + } + else if (action == RimAction) + { + target = rimHit; + drumSample.Rim?.Play(); + } + + if (target != null) + { + const float alpha_amount = 1; + + const float down_time = 80; + const float up_time = 50; + + target.Animate( + t => t.FadeTo(Math.Min(target.Alpha + alpha_amount, 1), down_time) + ).Then( + t => t.FadeOut(up_time) + ); + } + + return false; + } + + public void OnReleased(TaikoAction action) + { + } + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs index 381cd14cd4..78eec94590 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs @@ -20,7 +20,22 @@ namespace osu.Game.Rulesets.Taiko.Skinning this.source = source; } - public Drawable GetDrawableComponent(ISkinComponent component) => source.GetDrawableComponent(component); + public Drawable GetDrawableComponent(ISkinComponent component) + { + if (!(component is TaikoSkinComponent taikoComponent)) + return null; + + switch (taikoComponent.Component) + { + case TaikoSkinComponents.InputDrum: + if (GetTexture("taiko-bar-left") != null) + return new LegacyInputDrum(); + + return null; + } + + return source.GetDrawableComponent(component); + } public Texture GetTexture(string componentName) => source.GetTexture(componentName); diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs index 04aca534c6..6d4581db80 100644 --- a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs +++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs @@ -5,5 +5,6 @@ namespace osu.Game.Rulesets.Taiko { public enum TaikoSkinComponents { + InputDrum, } } diff --git a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs index d26ccfe867..422ea2f929 100644 --- a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs +++ b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs @@ -12,6 +12,7 @@ using osu.Framework.Input.Bindings; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Rulesets.Taiko.Audio; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.UI { @@ -22,11 +23,12 @@ namespace osu.Game.Rulesets.Taiko.UI { private const float middle_split = 0.025f; - private readonly ControlPointInfo controlPoints; + [Cached] + private DrumSampleMapping sampleMapping; public InputDrum(ControlPointInfo controlPoints) { - this.controlPoints = controlPoints; + sampleMapping = new DrumSampleMapping(controlPoints); RelativeSizeAxes = Axes.Both; FillMode = FillMode.Fit; @@ -35,35 +37,37 @@ namespace osu.Game.Rulesets.Taiko.UI [BackgroundDependencyLoader] private void load() { - var sampleMappings = new DrumSampleMapping(controlPoints); - - Children = new Drawable[] + Child = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.InputDrum), _ => new Container { - new TaikoHalfDrum(false, sampleMappings) + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { - Name = "Left Half", - Anchor = Anchor.Centre, - Origin = Anchor.CentreRight, - RelativeSizeAxes = Axes.Both, - RelativePositionAxes = Axes.X, - X = -middle_split / 2, - RimAction = TaikoAction.LeftRim, - CentreAction = TaikoAction.LeftCentre - }, - new TaikoHalfDrum(true, sampleMappings) - { - Name = "Right Half", - Anchor = Anchor.Centre, - Origin = Anchor.CentreLeft, - RelativeSizeAxes = Axes.Both, - RelativePositionAxes = Axes.X, - X = middle_split / 2, - RimAction = TaikoAction.RightRim, - CentreAction = TaikoAction.RightCentre + new TaikoHalfDrum(false) + { + Name = "Left Half", + Anchor = Anchor.Centre, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.Both, + RelativePositionAxes = Axes.X, + X = -middle_split / 2, + RimAction = TaikoAction.LeftRim, + CentreAction = TaikoAction.LeftCentre + }, + new TaikoHalfDrum(true) + { + Name = "Right Half", + Anchor = Anchor.Centre, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Both, + RelativePositionAxes = Axes.X, + X = middle_split / 2, + RimAction = TaikoAction.RightRim, + CentreAction = TaikoAction.RightCentre + } } - }; + }); - AddRangeInternal(sampleMappings.Sounds); + AddRangeInternal(sampleMapping.Sounds); } /// @@ -86,12 +90,11 @@ namespace osu.Game.Rulesets.Taiko.UI private readonly Sprite centre; private readonly Sprite centreHit; - private readonly DrumSampleMapping sampleMappings; + [Resolved] + private DrumSampleMapping sampleMappings { get; set; } - public TaikoHalfDrum(bool flipped, DrumSampleMapping sampleMappings) + public TaikoHalfDrum(bool flipped) { - this.sampleMappings = sampleMappings; - Masking = true; Children = new Drawable[] From 1ff2cc31d113ea02c02802c1d720411ceb9f6bbb Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 3 Apr 2020 18:25:01 +0900 Subject: [PATCH 0793/2376] Implement more familiar scroll speed options in mania --- .../ManiaRulesetConfigManager.cs | 5 +- .../UI/DrawableManiaRuleset.cs | 22 ++++++++ .../UI/Scrolling/DrawableScrollingRuleset.cs | 56 ++++++++++++------- 3 files changed, 62 insertions(+), 21 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs index f5412dcfc5..4926f448ee 100644 --- a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs +++ b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.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.Configuration.Tracking; using osu.Game.Configuration; using osu.Game.Rulesets.Configuration; @@ -19,13 +20,13 @@ namespace osu.Game.Rulesets.Mania.Configuration { base.InitialiseDefaults(); - Set(ManiaRulesetSetting.ScrollTime, 1500.0, 50.0, 5000.0, 50.0); + Set(ManiaRulesetSetting.ScrollTime, 1500.0, DrawableManiaRuleset.MIN_TIME_RANGE, DrawableManiaRuleset.MAX_TIME_RANGE, 1); Set(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down); } public override TrackedSettings CreateTrackedSettings() => new TrackedSettings { - new TrackedSetting(ManiaRulesetSetting.ScrollTime, v => new SettingDescription(v, "Scroll Time", $"{v}ms")) + new TrackedSetting(ManiaRulesetSetting.ScrollTime, v => new SettingDescription(v, "Scroll Speed", $"{(int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / v)}")) }; } diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index e5ec054fa7..f4e67b0793 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Graphics; using osu.Framework.Input; using osu.Game.Beatmaps; using osu.Game.Input.Handlers; @@ -25,6 +26,16 @@ namespace osu.Game.Rulesets.Mania.UI { public class DrawableManiaRuleset : DrawableScrollingRuleset { + /// + /// The minimum time range. This occurs at a of 40. + /// + public const double MIN_TIME_RANGE = 150; + + /// + /// The maximum time range. This occurs at a of 1. + /// + public const double MAX_TIME_RANGE = 6000; + protected new ManiaPlayfield Playfield => (ManiaPlayfield)base.Playfield; public new ManiaBeatmap Beatmap => (ManiaBeatmap)base.Beatmap; @@ -54,6 +65,17 @@ namespace osu.Game.Rulesets.Mania.UI Config.BindWith(ManiaRulesetSetting.ScrollTime, TimeRange); } + protected override void AdjustScrollSpeed(int amount) + { + this.TransformTo(nameof(relativeTimeRange), relativeTimeRange + amount, 200, Easing.OutQuint); + } + + private double relativeTimeRange + { + get => MAX_TIME_RANGE / TimeRange.Value; + set => TimeRange.Value = MAX_TIME_RANGE / value; + } + /// /// Retrieves the column that intersects a screen-space position. /// diff --git a/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs b/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs index 8bcdfff2fd..e9fe52cd3b 100644 --- a/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs +++ b/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs @@ -9,9 +9,11 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Input.Bindings; using osu.Framework.Lists; +using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Configuration; +using osu.Game.Extensions; using osu.Game.Input.Bindings; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; @@ -174,25 +176,6 @@ namespace osu.Game.Rulesets.UI.Scrolling controlPoints.Add(new MultiplierControlPoint { Velocity = Beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier }); } - public bool OnPressed(GlobalAction action) - { - if (!UserScrollSpeedAdjustment) - return false; - - switch (action) - { - case GlobalAction.IncreaseScrollSpeed: - this.TransformBindableTo(TimeRange, TimeRange.Value - time_span_step, 200, Easing.OutQuint); - return true; - - case GlobalAction.DecreaseScrollSpeed: - this.TransformBindableTo(TimeRange, TimeRange.Value + time_span_step, 200, Easing.OutQuint); - return true; - } - - return false; - } - protected override void LoadComplete() { base.LoadComplete(); @@ -201,8 +184,43 @@ namespace osu.Game.Rulesets.UI.Scrolling throw new ArgumentException($"{nameof(Playfield)} must be a {nameof(ScrollingPlayfield)} when using {nameof(DrawableScrollingRuleset)}."); } + /// + /// Adjusts the scroll speed of the . + /// + /// The amount to adjust by. Greater than 0 if the scroll speed should be increased, less than 0 if it should be decreased. + protected virtual void AdjustScrollSpeed(int amount) => this.TransformBindableTo(TimeRange, TimeRange.Value - amount * time_span_step, 200, Easing.OutQuint); + + public bool OnPressed(GlobalAction action) + { + if (!UserScrollSpeedAdjustment) + return false; + + switch (action) + { + case GlobalAction.IncreaseScrollSpeed: + scheduleScrollSpeedAdjustment(1); + return true; + + case GlobalAction.DecreaseScrollSpeed: + scheduleScrollSpeedAdjustment(-1); + return true; + } + + return false; + } + + private ScheduledDelegate scheduledScrollSpeedAdjustment; + public void OnReleased(GlobalAction action) { + scheduledScrollSpeedAdjustment?.Cancel(); + scheduledScrollSpeedAdjustment = null; + } + + private void scheduleScrollSpeedAdjustment(int amount) + { + scheduledScrollSpeedAdjustment?.Cancel(); + scheduledScrollSpeedAdjustment = this.BeginKeyRepeat(Scheduler, () => AdjustScrollSpeed(amount)); } private class LocalScrollingInfo : IScrollingInfo From fd9d4a8d322cc0576537ef0a4e6aa1e86cc4ae7b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 3 Apr 2020 18:29:32 +0900 Subject: [PATCH 0794/2376] 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 3e10e6cc4d..68528d5688 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 073799f08f..ad9a835cdb 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -22,7 +22,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 6578aec69f..6a32359ebe 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + From d90db5649dc049d9bb1b00bf1a79266562fd4782 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 3 Apr 2020 18:32:07 +0900 Subject: [PATCH 0795/2376] Improve comment slightly --- osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs b/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs index e9fe52cd3b..a7eb78e3ae 100644 --- a/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs +++ b/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs @@ -185,7 +185,7 @@ namespace osu.Game.Rulesets.UI.Scrolling } /// - /// Adjusts the scroll speed of the . + /// Adjusts the scroll speed of s. /// /// The amount to adjust by. Greater than 0 if the scroll speed should be increased, less than 0 if it should be decreased. protected virtual void AdjustScrollSpeed(int amount) => this.TransformBindableTo(TimeRange, TimeRange.Value - amount * time_span_step, 200, Easing.OutQuint); From 23b7cde941495bdc43944cdc66b634600370060a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 3 Apr 2020 18:38:04 +0900 Subject: [PATCH 0796/2376] Add milliseconds value alongside --- .../Configuration/ManiaRulesetConfigManager.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs index 4926f448ee..7e84f17809 100644 --- a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs +++ b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs @@ -26,7 +26,8 @@ namespace osu.Game.Rulesets.Mania.Configuration public override TrackedSettings CreateTrackedSettings() => new TrackedSettings { - new TrackedSetting(ManiaRulesetSetting.ScrollTime, v => new SettingDescription(v, "Scroll Speed", $"{(int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / v)}")) + new TrackedSetting(ManiaRulesetSetting.ScrollTime, + v => new SettingDescription(v, "Scroll Speed", $"{(int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / v)} ({v}ms)")) }; } From d896d5a231bd22a6964a57997e2eae836c62daaa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 3 Apr 2020 18:51:34 +0900 Subject: [PATCH 0797/2376] Rename filename to match class --- .../Skinning/{LegacyTaikoDrum.cs => LegacyInputDrum.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename osu.Game.Rulesets.Taiko/Skinning/{LegacyTaikoDrum.cs => LegacyInputDrum.cs} (100%) diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoDrum.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs similarity index 100% rename from osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoDrum.cs rename to osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs From f59479fa0719b1e4408d013c0da8ee1f64292ed1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 3 Apr 2020 21:09:33 +0900 Subject: [PATCH 0798/2376] Update framework --- .idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml | 2 +- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml b/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml index 7515e76054..4bb9f4d2a0 100644 --- a/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml +++ b/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/osu.Android.props b/osu.Android.props index 3e10e6cc4d..db68a3052a 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 073799f08f..edccb56cd1 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -23,7 +23,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 6578aec69f..f8449be037 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + @@ -79,7 +79,7 @@ - + From 7b2144a1a71c748ea7e42434265ad6890e35604c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 3 Apr 2020 23:31:46 +0900 Subject: [PATCH 0799/2376] Fix merge mishap --- .../Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs b/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs index 36fb64bfef..0955f32790 100644 --- a/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs +++ b/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs @@ -182,14 +182,6 @@ namespace osu.Game.Rulesets.UI.Scrolling throw new ArgumentException($"{nameof(Playfield)} must be a {nameof(ScrollingPlayfield)} when using {nameof(DrawableScrollingRuleset)}."); } - protected override void LoadComplete() - { - base.LoadComplete(); - - if (!(Playfield is ScrollingPlayfield)) - throw new ArgumentException($"{nameof(Playfield)} must be a {nameof(ScrollingPlayfield)} when using {nameof(DrawableScrollingRuleset)}."); - } - /// /// Adjusts the scroll speed of s. /// From 7e82f5740b0668e1f21321cd257be9928026ad54 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 3 Apr 2020 19:35:50 +0300 Subject: [PATCH 0800/2376] Add a skin extension for simplifying falling back on hyper-dash colours --- .../Objects/Drawables/FruitPiece.cs | 3 +-- .../Skinning/CatchSkinExtensions.cs | 16 ++++++++++++++++ .../Skinning/LegacyFruitPiece.cs | 7 +------ 3 files changed, 18 insertions(+), 8 deletions(-) create mode 100644 osu.Game.Rulesets.Catch/Skinning/CatchSkinExtensions.cs diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs index 16818746b5..2437958916 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs @@ -64,8 +64,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables }); var hyperDashColour = - skin.GetConfig(CatchSkinColour.HyperDashFruit)?.Value ?? - skin.GetConfig(CatchSkinColour.HyperDash)?.Value ?? + skin.GetHyperDashFruitColour()?.Value ?? Catcher.DefaultHyperDashColour; if (hitObject.HyperDash) diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchSkinExtensions.cs b/osu.Game.Rulesets.Catch/Skinning/CatchSkinExtensions.cs new file mode 100644 index 0000000000..8fc0831918 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Skinning/CatchSkinExtensions.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 osu.Framework.Bindables; +using osu.Game.Skinning; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Catch.Skinning +{ + internal static class CatchSkinExtensions + { + public static IBindable GetHyperDashFruitColour(this ISkin skin) + => skin.GetConfig(CatchSkinColour.HyperDashFruit) ?? + skin.GetConfig(CatchSkinColour.HyperDash); + } +} diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs index 5235058c52..d8489399d2 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs @@ -54,15 +54,10 @@ namespace osu.Game.Rulesets.Catch.Skinning if (drawableCatchObject.HitObject.HyperDash) { - var hyperDashColour = - skin.GetConfig(CatchSkinColour.HyperDashFruit)?.Value ?? - skin.GetConfig(CatchSkinColour.HyperDash)?.Value ?? - Catcher.DefaultHyperDashColour; - var hyperDash = new Sprite { Texture = skin.GetTexture(lookupName), - Colour = hyperDashColour, + Colour = skin.GetHyperDashFruitColour()?.Value ?? Catcher.DefaultHyperDashColour, Anchor = Anchor.Centre, Origin = Anchor.Centre, Blending = BlendingParameters.Additive, From 0340b6db51f88120511ac243cc0872bc8527eb32 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 3 Apr 2020 19:50:32 +0300 Subject: [PATCH 0801/2376] Describe step names more --- .../TestSceneHyperDashColouring.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs index ebc3d3bff1..066b399f13 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Catch.Tests { DrawableFruit drawableFruit = null; - AddStep("setup fruit", () => + AddStep("setup hyper-dash fruit", () => { var fruit = new Fruit { HyperDashTarget = new Banana() }; fruit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); @@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Catch.Tests }, false, false, false, legacyFruit); }); - AddAssert("default colour", () => + AddAssert("hyper-dash fruit has default colour", () => legacyFruit ? checkLegacyFruitHyperDashColour(drawableFruit, Catcher.DefaultHyperDashColour) : checkFruitHyperDashColour(drawableFruit, Catcher.DefaultHyperDashColour)); @@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Catch.Tests { DrawableFruit drawableFruit = null; - AddStep("setup fruit", () => + AddStep("setup hyper-dash fruit", () => { var fruit = new Fruit { HyperDashTarget = new Banana() }; fruit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); @@ -77,7 +77,7 @@ namespace osu.Game.Rulesets.Catch.Tests }, customCatcherHyperDashColour, false, true, legacyFruit); }); - AddAssert("custom colour", () => + AddAssert("hyper-dash fruit use fruit colour from skin", () => legacyFruit ? checkLegacyFruitHyperDashColour(drawableFruit, TestSkin.CustomHyperDashFruitColour) : checkFruitHyperDashColour(drawableFruit, TestSkin.CustomHyperDashFruitColour)); @@ -89,7 +89,7 @@ namespace osu.Game.Rulesets.Catch.Tests { DrawableFruit drawableFruit = null; - AddStep("setup fruit", () => + AddStep("setup hyper-dash fruit", () => { var fruit = new Fruit { HyperDashTarget = new Banana() }; fruit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); @@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Catch.Tests }, true, false, false, legacyFruit); }); - AddAssert("catcher custom colour", () => + AddAssert("hyper-dash fruit colour falls back to catcher colour from skin", () => legacyFruit ? checkLegacyFruitHyperDashColour(drawableFruit, TestSkin.CustomHyperDashColour) : checkFruitHyperDashColour(drawableFruit, TestSkin.CustomHyperDashColour)); From dd684b68d9cef47cc6e5a61a730343536428dc3b Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 3 Apr 2020 19:53:38 +0300 Subject: [PATCH 0802/2376] Make parameters required --- osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs index 066b399f13..2009099a61 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs @@ -109,7 +109,7 @@ namespace osu.Game.Rulesets.Catch.Tests : checkFruitHyperDashColour(drawableFruit, TestSkin.CustomHyperDashColour)); } - private Drawable setupSkinHierarchy(Drawable child, bool customCatcherColour = false, bool customAfterColour = false, bool customFruitColour = false, bool legacySkin = true) + private Drawable setupSkinHierarchy(Drawable child, bool customCatcherColour, bool customAfterColour, bool customFruitColour, bool legacySkin = true) { var testSkinProvider = new SkinProvidingContainer(new TestSkin(customCatcherColour, customAfterColour, customFruitColour)); @@ -143,7 +143,7 @@ namespace osu.Game.Rulesets.Catch.Tests private readonly bool customAfterColour; private readonly bool customFruitColour; - public TestSkin(bool customCatcherColour = false, bool customAfterColour = false, bool customFruitColour = false) + public TestSkin(bool customCatcherColour, bool customAfterColour, bool customFruitColour) { this.customCatcherColour = customCatcherColour; this.customAfterColour = customAfterColour; From d73c791a108fe0bc349535242ecd60250073679c Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Fri, 3 Apr 2020 20:56:52 +0300 Subject: [PATCH 0803/2376] Support this typo for old skins --- osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs | 4 +++- osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs index e7486ef9b0..8a9ce79dd4 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs @@ -62,7 +62,9 @@ namespace osu.Game.Rulesets.Osu.Skinning } }; - bool overlayAboveNumber = skin.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumber)?.Value ?? true; + bool? numberSetting = skin.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumber)?.Value; + bool? numerSetting = skin.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumer)?.Value; + bool overlayAboveNumber = numberSetting ?? numerSetting ?? true; if (!overlayAboveNumber) ChangeInternalChildDepth(hitCircleText, -float.MaxValue); diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs index c6920bd03e..154160fdb5 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs @@ -12,6 +12,7 @@ namespace osu.Game.Rulesets.Osu.Skinning AllowSliderBallTint, CursorExpand, CursorRotate, - HitCircleOverlayAboveNumber + HitCircleOverlayAboveNumber, + HitCircleOverlayAboveNumer // Some old skins will have this typo } } From 493b6540116687b1d031963d30a30c5b7b90f06a Mon Sep 17 00:00:00 2001 From: Joehu Date: Fri, 3 Apr 2020 11:30:02 -0700 Subject: [PATCH 0804/2376] Remove horizontal margin from mod display Can skew center alignment on fill flow containers. Fixes affected areas. Vector2(5, 0) is similar to MarginPadding { Left = 10 }. --- .../Visual/UserInterface/TestSceneModSelectOverlay.cs | 2 +- osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs | 2 +- osu.Game/Screens/Play/HUD/ModDisplay.cs | 1 - osu.Game/Screens/Play/HUDOverlay.cs | 2 +- osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs | 1 + osu.Game/Screens/Select/FooterButtonMods.cs | 1 - 6 files changed, 4 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 03a19b6690..2294cd6966 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -70,7 +70,7 @@ namespace osu.Game.Tests.Visual.UserInterface Anchor = Anchor.TopRight, Origin = Anchor.TopRight, AutoSizeAxes = Axes.Both, - Position = new Vector2(0, 25), + Position = new Vector2(-5, 25), Current = { BindTarget = modSelect.SelectedMods } } }; diff --git a/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs index 6cd1aa912f..ed3f9af8e2 100644 --- a/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs @@ -161,7 +161,7 @@ namespace osu.Game.Screens.Multi { AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, - Spacing = new Vector2(10, 0), + Spacing = new Vector2(15, 0), Children = new Drawable[] { authorText = new LinkFlowContainer { AutoSizeAxes = Axes.Both }, diff --git a/osu.Game/Screens/Play/HUD/ModDisplay.cs b/osu.Game/Screens/Play/HUD/ModDisplay.cs index 336b03544f..cd15886c0b 100644 --- a/osu.Game/Screens/Play/HUD/ModDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ModDisplay.cs @@ -56,7 +56,6 @@ namespace osu.Game.Screens.Play.HUD Origin = Anchor.TopCentre, AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, - Margin = new MarginPadding { Left = 10, Right = 10 }, }, unrankedText = new OsuSpriteText { diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index a5f8051557..e06f6d19c2 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -285,7 +285,7 @@ namespace osu.Game.Screens.Play Anchor = Anchor.TopRight, Origin = Anchor.TopRight, AutoSizeAxes = Axes.Both, - Margin = new MarginPadding { Top = 20, Right = 10 }, + Margin = new MarginPadding { Top = 20, Right = 20 }, }; protected virtual HitErrorDisplay CreateHitErrorDisplayOverlay() => new HitErrorDisplay(scoreProcessor, drawableRuleset?.FirstAvailableHitWindows); diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index df7eed9a02..8ef0920d19 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -124,6 +124,7 @@ namespace osu.Game.Screens.Ranking.Expanded Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, AutoSizeAxes = Axes.Both, + Spacing = new Vector2(5, 0), Children = new Drawable[] { new StarRatingDisplay(beatmap) diff --git a/osu.Game/Screens/Select/FooterButtonMods.cs b/osu.Game/Screens/Select/FooterButtonMods.cs index 2411cf26f9..b18301c082 100644 --- a/osu.Game/Screens/Select/FooterButtonMods.cs +++ b/osu.Game/Screens/Select/FooterButtonMods.cs @@ -92,7 +92,6 @@ namespace osu.Game.Screens.Select public FooterModDisplay() { ExpansionMode = ExpansionMode.AlwaysContracted; - IconsContainer.Margin = new MarginPadding(); } } } From 88cc552534043ec8c715a45635fe7bf581a9ba11 Mon Sep 17 00:00:00 2001 From: Joehu Date: Fri, 3 Apr 2020 11:30:22 -0700 Subject: [PATCH 0805/2376] Fix results star rating display not being centered when no mods are present Needed or the spacing will apply to the fill flow container, causing alignment issues. --- .../Expanded/ExpandedPanelMiddleContent.cs | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index 8ef0920d19..b058cc142b 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -30,6 +30,9 @@ namespace osu.Game.Screens.Ranking.Expanded private readonly ScoreInfo score; private readonly List statisticDisplays = new List(); + + private FillFlowContainer starAndModDisplay; + private RollingCounter scoreCounter; /// @@ -119,7 +122,7 @@ namespace osu.Game.Screens.Ranking.Expanded Alpha = 0, AlwaysPresent = true }, - new FillFlowContainer + starAndModDisplay = new FillFlowContainer { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, @@ -132,15 +135,6 @@ namespace osu.Game.Screens.Ranking.Expanded Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft }, - new ModDisplay - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - DisplayUnrankedText = false, - ExpansionMode = ExpansionMode.AlwaysExpanded, - Scale = new Vector2(0.5f), - Current = { Value = score.Mods } - } } }, new FillFlowContainer @@ -215,6 +209,19 @@ namespace osu.Game.Screens.Ranking.Expanded } } }; + + if (score.Mods.Any()) + { + starAndModDisplay.Add(new ModDisplay + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + DisplayUnrankedText = false, + ExpansionMode = ExpansionMode.AlwaysExpanded, + Scale = new Vector2(0.5f), + Current = { Value = score.Mods } + }); + } } protected override void LoadComplete() From 8cdae790c3b0fc90996ae473ffe998206f9af51a Mon Sep 17 00:00:00 2001 From: Lucas A Date: Fri, 3 Apr 2020 17:32:37 +0200 Subject: [PATCH 0806/2376] Load user rulesets from the game data directory --- osu.Game/OsuGameBase.cs | 2 +- osu.Game/Rulesets/RulesetStore.cs | 40 +++++++++++++++++++++++++++---- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 5487bd9320..609b6ce98e 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -168,7 +168,7 @@ namespace osu.Game var defaultBeatmap = new DummyWorkingBeatmap(Audio, Textures); - dependencies.Cache(RulesetStore = new RulesetStore(contextFactory)); + dependencies.Cache(RulesetStore = new RulesetStore(contextFactory, Storage)); dependencies.Cache(FileStore = new FileStore(contextFactory, Storage)); // ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup() diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index a389d4ff75..c3c7b653da 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using System.Reflection; using osu.Framework.Logging; +using osu.Framework.Platform; using osu.Game.Database; namespace osu.Game.Rulesets @@ -17,16 +18,20 @@ namespace osu.Game.Rulesets private readonly Dictionary loadedAssemblies = new Dictionary(); - public RulesetStore(IDatabaseContextFactory factory) + private readonly Storage rulesetStorage; + + public RulesetStore(IDatabaseContextFactory factory, Storage storage = null) : base(factory) { + rulesetStorage = storage?.GetStorageForDirectory("rulesets"); + AppDomain.CurrentDomain.AssemblyResolve += resolveRulesetDependencyAssembly; + // On android in release configuration assemblies are loaded from the apk directly into memory. // We cannot read assemblies from cwd, so should check loaded assemblies instead. loadFromAppDomain(); loadFromDisk(); + loadUserRulesets(); addMissingRulesets(); - - AppDomain.CurrentDomain.AssemblyResolve += resolveRulesetAssembly; } /// @@ -48,7 +53,17 @@ namespace osu.Game.Rulesets /// public IEnumerable AvailableRulesets { get; private set; } - private Assembly resolveRulesetAssembly(object sender, ResolveEventArgs args) => loadedAssemblies.Keys.FirstOrDefault(a => a.FullName == args.Name); + private Assembly resolveRulesetDependencyAssembly(object sender, ResolveEventArgs args) + { + var asm = new AssemblyName(args.Name); + + // this assumes the only explicit dependency of the ruleset is the game core assembly. + // the ruleset dependency on the game core assembly requires manual resolving, transient dependencies should be resolved automatically + if (asm.Name.Equals(typeof(OsuGame).Assembly.GetName().Name, StringComparison.Ordinal)) + return Assembly.GetExecutingAssembly(); + + return null; + } private void addMissingRulesets() { @@ -120,6 +135,21 @@ namespace osu.Game.Rulesets } } + private void loadUserRulesets() + { + try + { + var rulesets = rulesetStorage?.GetFiles(".", $"{ruleset_library_prefix}.*.dll"); + + foreach (var ruleset in rulesets.Where(f => !f.Contains("Tests"))) + loadRulesetFromFile(rulesetStorage?.GetFullPath(ruleset)); + } + catch (Exception e) + { + Logger.Error(e, "Couldn't load user rulesets"); + } + } + private void loadFromDisk() { try @@ -175,7 +205,7 @@ namespace osu.Game.Rulesets protected virtual void Dispose(bool disposing) { - AppDomain.CurrentDomain.AssemblyResolve -= resolveRulesetAssembly; + AppDomain.CurrentDomain.AssemblyResolve -= resolveRulesetDependencyAssembly; } } } From e1a67bdb96d7af8fe76fd7f483061d74deba1652 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sat, 4 Apr 2020 11:13:25 +0300 Subject: [PATCH 0807/2376] Move implementation to transformer --- osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs | 4 +--- osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs | 6 ++++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs index 8a9ce79dd4..e7486ef9b0 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs @@ -62,9 +62,7 @@ namespace osu.Game.Rulesets.Osu.Skinning } }; - bool? numberSetting = skin.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumber)?.Value; - bool? numerSetting = skin.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumer)?.Value; - bool overlayAboveNumber = numberSetting ?? numerSetting ?? true; + bool overlayAboveNumber = skin.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumber)?.Value ?? true; if (!overlayAboveNumber) ChangeInternalChildDepth(hitCircleText, -float.MaxValue); diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs index 0d67846b8e..d4bc651414 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs @@ -132,6 +132,12 @@ namespace osu.Game.Rulesets.Osu.Skinning return SkinUtils.As(new BindableFloat(LEGACY_CIRCLE_RADIUS)); break; + + case OsuSkinConfiguration.HitCircleOverlayAboveNumber: + // Quote from https://osu.ppy.sh/help/wiki/Skinning/skin.ini#%5Bgeneral%5D + // Old command: HitCircleOverlayAboveNumer (with typo) still works for legacy support + var rv = source.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumber); + return rv ?? source.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumer); } break; From 6700ef910f3a7d218a0e1b91f1692e4089d518ac Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sat, 4 Apr 2020 11:35:15 +0300 Subject: [PATCH 0808/2376] use startAtCurrentTime --- osu.Game/Skinning/LegacySkinExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/LegacySkinExtensions.cs b/osu.Game/Skinning/LegacySkinExtensions.cs index 9bfde4fdcb..476e53bdaa 100644 --- a/osu.Game/Skinning/LegacySkinExtensions.cs +++ b/osu.Game/Skinning/LegacySkinExtensions.cs @@ -14,7 +14,7 @@ namespace osu.Game.Skinning public static class LegacySkinExtensions { public static Drawable GetAnimation(this ISkin source, string componentName, bool animatable, bool looping, bool applyConfigFrameRate = false, string animationSeparator = "-", - bool startAtCurrentTime = false, double? frameLength = null) + bool startAtCurrentTime = true, double? frameLength = null) { Texture texture; From c3f0ef1bd4a13888c51e48ff4f409e7c05aafbe0 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sat, 4 Apr 2020 15:10:54 +0300 Subject: [PATCH 0809/2376] Major DRYing of code --- .../TestSceneSliderSnaking.cs | 201 +++++------------- 1 file changed, 56 insertions(+), 145 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs index a53e06dc0f..04f00122dc 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs @@ -1,8 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; +using Humanizer; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -38,6 +40,9 @@ namespace osu.Game.Rulesets.Osu.Tests private readonly Bindable snakingIn = new Bindable(); private readonly Bindable snakingOut = new Bindable(); + private const double duration_of_span = 3605; + private const double fade_in_modifier = -1200; + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) { var working = new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager); @@ -55,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Tests private DrawableSlider slider; private DrawableSliderRepeat repeat; - private Vector2 vector; + private Vector2 savedVector; [SetUpSteps] public override void SetUpSteps() { } @@ -67,25 +72,18 @@ namespace osu.Game.Rulesets.Osu.Tests base.SetUpSteps(); AddUntilStep("wait for track to start running", () => track.IsRunning); - AddStep("retrieve 1st slider", () => slider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.First()); - testLinear(true); - testLinear(false); - AddStep("retrieve 2nd slider", () => slider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.Skip(1).First()); - testRepeating(true); - testRepeating(false); - AddStep("retrieve 3rd slider", () => slider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.Skip(2).First()); - testDoubleRepeating(true); - testDoubleRepeating(false); + for (int i = 0; i < 3; i++) + { + testSlider(i, true); + testSlider(i, false); + } } [TestCase(true)] [TestCase(false)] public void TestArrowStays(bool isHit) { - var isSame = isHit ? "is same" : "decreased"; - var enable = isHit ? "enable" : "disable"; - - AddStep($"{enable} autoplay", () => autoplay = isHit); + AddStep($"{(isHit ? "enable" : "disable")} autoplay", () => autoplay = isHit); setSnaking(true); base.SetUpSteps(); @@ -95,154 +93,67 @@ namespace osu.Game.Rulesets.Osu.Tests var drawable = Player.DrawableRuleset.Playfield.AllHitObjects.Skip(1).First(); repeat = drawable.ChildrenOfType>().First().Children.First(); }); - AddStep("Save repeat vector", () => vector = repeat.Position); + AddStep("Save repeat vector", () => savedVector = repeat.Position); addSeekStep(13700); - AddAssert($"Repeat vector {isSame}", () => isHit ? Precision.AlmostEquals(vector.X, repeat.X, 1) && Precision.AlmostEquals(vector.Y, repeat.Y, 1) : repeat.X < vector.X && repeat.Y < vector.Y); + // Precision.AlmostEquals is used because repeat might have a chance to update its position depending on where in the frame its hit + AddAssert($"Repeat vector {(isHit ? "is same" : "decreased")}", () => isHit ? Precision.AlmostEquals(savedVector.X, repeat.X, 1) && Precision.AlmostEquals(savedVector.Y, repeat.Y, 1) : repeat.X < savedVector.X && repeat.Y < savedVector.Y); } - private void testLinear(bool snaking) + private void testSlider(int index, bool snaking) { - var increased = snaking ? "increased" : "is same"; - + double startTime = index * 10000 + 3000; + int repeats = index; + AddStep($"retrieve {(index + 1).ToOrdinalWords()} slider", () => + { + slider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.Skip(index).First(); + }); setSnaking(snaking); - addSeekStep(1800); - AddStep("Save end vector", () => + testSnakingIn(startTime + fade_in_modifier, snaking); + for (int i = 0; i < repeats + 1; i++) { - var body = (PlaySliderBody)slider.Body.Drawable; - vector = body.CurrentCurve.Last(); - }); - addSeekStep(1900); - AddAssert($"End vector {increased}", () => + testSnakingOut(startTime + 100 + duration_of_span * i, snaking && i == repeats, i%2 == 1); + } + } + + private void testSnakingIn(double startTime, bool isSnakingExpected) + { + addSeekStep(startTime); + AddStep("Save end vector", () => savedVector = getCurrentSliderVector(true)); + addSeekStep(startTime + 100); + AddAssert($"End vector increased", () => { - var body = (PlaySliderBody)slider.Body.Drawable; - var last = body.CurrentCurve.Last(); - return snaking ? last.X > vector.X && last.Y > vector.Y : last == vector; - }); - addSeekStep(3100); - AddStep("Save start vector", () => - { - var body = (PlaySliderBody)slider.Body.Drawable; - vector = body.CurrentCurve.First(); - }); - addSeekStep(3200); - AddAssert($"Start vector {increased}", () => - { - var body = (PlaySliderBody)slider.Body.Drawable; - var first = body.CurrentCurve.First(); - return snaking ? first.X > vector.X && first.Y > vector.Y : first == vector; + var currentVector = getCurrentSliderVector(true); + return isSnakingExpected ? currentVector.X > savedVector.X && currentVector.Y > savedVector.Y : currentVector == savedVector; }); } - private void testRepeating(bool snaking) + private void testSnakingOut(double startTime, bool isSnakingExpected, bool testSliderEnd) { - var increased = snaking ? "increased" : "is same"; - var decreased = snaking ? "decreased" : "is same"; - - setSnaking(snaking); - addSeekStep(8800); - AddStep("Save end vector", () => + addSeekStep(startTime); + AddStep($"Save {(testSliderEnd ? "end" : "start")} vector", () => savedVector = getCurrentSliderVector(testSliderEnd)); + addSeekStep(startTime + 100); + AddAssert($"{(testSliderEnd ? "End" : "Start")} vector {(isSnakingExpected ? (testSliderEnd ? "decreased" : "increased") : "is same")}", () => { - var body = (PlaySliderBody)slider.Body.Drawable; - vector = body.CurrentCurve.Last(); - }); - addSeekStep(8900); - AddAssert($"End vector {increased}", () => - { - var body = (PlaySliderBody)slider.Body.Drawable; - var last = body.CurrentCurve.Last(); - return snaking ? last.X > vector.X && last.Y > vector.Y : last == vector; - }); - addSeekStep(10100); - AddStep("Save start vector", () => - { - var body = (PlaySliderBody)slider.Body.Drawable; - vector = body.CurrentCurve.First(); - }); - addSeekStep(10200); - AddAssert("Start vector is same", () => - { - var body = (PlaySliderBody)slider.Body.Drawable; - var first = body.CurrentCurve.First(); - return first == vector; - }); - addSeekStep(13700); - AddStep("Save end vector", () => - { - var body = (PlaySliderBody)slider.Body.Drawable; - vector = body.CurrentCurve.Last(); - }); - addSeekStep(13800); - AddAssert($"End vector {decreased}", () => - { - var body = (PlaySliderBody)slider.Body.Drawable; - var last = body.CurrentCurve.Last(); - return snaking ? last.X < vector.X && last.Y < vector.Y : last == vector; + var currentVector = getCurrentSliderVector(testSliderEnd); + bool check(Vector2 a, Vector2 b) + { + if (testSliderEnd) + return a.X < b.X && a.Y < b.Y; + return a.X > b.X && a.Y > b.Y; + } + return isSnakingExpected ? check(currentVector, savedVector) : currentVector == savedVector; }); } - private void testDoubleRepeating(bool snaking) + private Vector2 getCurrentSliderVector(bool getEndOne) { - var increased = snaking ? "increased" : "is same"; - - setSnaking(snaking); - addSeekStep(18800); - AddStep("Save end vector", () => - { - var body = (PlaySliderBody)slider.Body.Drawable; - vector = body.CurrentCurve.Last(); - }); - addSeekStep(18900); - AddAssert($"End vector {increased}", () => - { - var body = (PlaySliderBody)slider.Body.Drawable; - var last = body.CurrentCurve.Last(); - return snaking ? last.X > vector.X && last.Y > vector.Y : last == vector; - }); - addSeekStep(20100); - AddStep("Save start vector", () => - { - var body = (PlaySliderBody)slider.Body.Drawable; - vector = body.CurrentCurve.First(); - }); - addSeekStep(20200); - AddAssert("Start vector is same", () => - { - var body = (PlaySliderBody)slider.Body.Drawable; - var first = body.CurrentCurve.First(); - return first == vector; - }); - addSeekStep(23700); - AddStep("Save end vector", () => - { - var body = (PlaySliderBody)slider.Body.Drawable; - vector = body.CurrentCurve.Last(); - }); - addSeekStep(23800); - AddAssert("End vector is same", () => - { - var body = (PlaySliderBody)slider.Body.Drawable; - var last = body.CurrentCurve.Last(); - return last == vector; - }); - addSeekStep(27300); - AddStep("Save start vector", () => - { - var body = (PlaySliderBody)slider.Body.Drawable; - vector = body.CurrentCurve.First(); - }); - addSeekStep(27400); - AddAssert($"Start vector {increased}", () => - { - var body = (PlaySliderBody)slider.Body.Drawable; - var first = body.CurrentCurve.First(); - return snaking ? first.X > vector.X && first.Y > vector.Y : first == vector; - }); + var body = (PlaySliderBody)slider.Body.Drawable; + return getEndOne ? body.CurrentCurve.Last() : body.CurrentCurve.First(); } private void setSnaking(bool value) { - var text = value ? "Enable" : "Disable"; - AddStep($"{text} snaking", () => + AddStep($"{(value ? "Enable" : "Disable")} snaking", () => { snakingIn.Value = value; snakingOut.Value = value; @@ -272,7 +183,7 @@ namespace osu.Game.Rulesets.Osu.Tests }, new Slider { - StartTime = 10000, + StartTime = 13000, Position = new Vector2(100, 100), Path = new SliderPath(PathType.PerfectCurve, new[] { @@ -284,7 +195,7 @@ namespace osu.Game.Rulesets.Osu.Tests new Slider { - StartTime = 20000, + StartTime = 23000, Position = new Vector2(100, 100), Path = new SliderPath(PathType.PerfectCurve, new[] { @@ -296,7 +207,7 @@ namespace osu.Game.Rulesets.Osu.Tests new HitCircle { - StartTime = 99999, + StartTime = 199999, } } }; From a8a52e506dfd9aaba5b19f9c9294718ff12ee39e Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sat, 4 Apr 2020 15:35:35 +0300 Subject: [PATCH 0810/2376] Review and style changes --- .../TestSceneSliderSnaking.cs | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs index 04f00122dc..99b2f7d46e 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using System.Linq; using Humanizer; @@ -37,8 +36,8 @@ namespace osu.Game.Rulesets.Osu.Tests protected override bool Autoplay => autoplay; private bool autoplay; - private readonly Bindable snakingIn = new Bindable(); - private readonly Bindable snakingOut = new Bindable(); + private readonly BindableBool snakingIn = new BindableBool(); + private readonly BindableBool snakingOut = new BindableBool(); private const double duration_of_span = 3605; private const double fade_in_modifier = -1200; @@ -95,8 +94,15 @@ namespace osu.Game.Rulesets.Osu.Tests }); AddStep("Save repeat vector", () => savedVector = repeat.Position); addSeekStep(13700); - // Precision.AlmostEquals is used because repeat might have a chance to update its position depending on where in the frame its hit - AddAssert($"Repeat vector {(isHit ? "is same" : "decreased")}", () => isHit ? Precision.AlmostEquals(savedVector.X, repeat.X, 1) && Precision.AlmostEquals(savedVector.Y, repeat.Y, 1) : repeat.X < savedVector.X && repeat.Y < savedVector.Y); + + AddAssert($"Repeat vector {(isHit ? "is same" : "decreased")}", () => + { + if (isHit) + // Precision.AlmostEquals is used because repeat might have a chance to update its position depending on where in the frame its hit + return Precision.AlmostEquals(savedVector, repeat.Position, 1); + + return repeat.X < savedVector.X && repeat.Y < savedVector.Y; + }); } private void testSlider(int index, bool snaking) @@ -109,9 +115,10 @@ namespace osu.Game.Rulesets.Osu.Tests }); setSnaking(snaking); testSnakingIn(startTime + fade_in_modifier, snaking); + for (int i = 0; i < repeats + 1; i++) { - testSnakingOut(startTime + 100 + duration_of_span * i, snaking && i == repeats, i%2 == 1); + testSnakingOut(startTime + 100 + duration_of_span * i, snaking && i == repeats, i % 2 == 1); } } @@ -120,7 +127,7 @@ namespace osu.Game.Rulesets.Osu.Tests addSeekStep(startTime); AddStep("Save end vector", () => savedVector = getCurrentSliderVector(true)); addSeekStep(startTime + 100); - AddAssert($"End vector increased", () => + AddAssert($"End vector {(isSnakingExpected ? "increased" : "is same")}", () => { var currentVector = getCurrentSliderVector(true); return isSnakingExpected ? currentVector.X > savedVector.X && currentVector.Y > savedVector.Y : currentVector == savedVector; @@ -135,12 +142,15 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert($"{(testSliderEnd ? "End" : "Start")} vector {(isSnakingExpected ? (testSliderEnd ? "decreased" : "increased") : "is same")}", () => { var currentVector = getCurrentSliderVector(testSliderEnd); + bool check(Vector2 a, Vector2 b) { if (testSliderEnd) return a.X < b.X && a.Y < b.Y; + return a.X > b.X && a.Y > b.Y; } + return isSnakingExpected ? check(currentVector, savedVector) : currentVector == savedVector; }); } From 0ebb5a81f937601a9008a4f9cb94779f6d9b4861 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sat, 4 Apr 2020 15:59:39 +0300 Subject: [PATCH 0811/2376] Fix oversight in testing --- osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs index 99b2f7d46e..51d4d1c008 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs @@ -86,14 +86,14 @@ namespace osu.Game.Rulesets.Osu.Tests setSnaking(true); base.SetUpSteps(); - addSeekStep(13500); + addSeekStep(16500); AddStep("retrieve 2nd slider repeat", () => { var drawable = Player.DrawableRuleset.Playfield.AllHitObjects.Skip(1).First(); repeat = drawable.ChildrenOfType>().First().Children.First(); }); AddStep("Save repeat vector", () => savedVector = repeat.Position); - addSeekStep(13700); + addSeekStep(16700); AddAssert($"Repeat vector {(isHit ? "is same" : "decreased")}", () => { From f3bcb0628c828b12484c4b6f46be8b44c3599ccd Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 4 Apr 2020 19:09:52 +0300 Subject: [PATCH 0812/2376] Add helper methods for retrieving other skin hyper-dash colours --- osu.Game.Rulesets.Catch/Skinning/CatchSkinExtensions.cs | 7 +++++++ osu.Game.Rulesets.Catch/UI/Catcher.cs | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchSkinExtensions.cs b/osu.Game.Rulesets.Catch/Skinning/CatchSkinExtensions.cs index 8fc0831918..48e11121ea 100644 --- a/osu.Game.Rulesets.Catch/Skinning/CatchSkinExtensions.cs +++ b/osu.Game.Rulesets.Catch/Skinning/CatchSkinExtensions.cs @@ -9,6 +9,13 @@ namespace osu.Game.Rulesets.Catch.Skinning { internal static class CatchSkinExtensions { + public static IBindable GetHyperDashCatcherColour(this ISkin skin) + => skin.GetConfig(CatchSkinColour.HyperDash); + + public static IBindable GetHyperDashEndGlowColour(this ISkin skin) + => skin.GetConfig(CatchSkinColour.HyperDashAfterImage) ?? + skin.GetConfig(CatchSkinColour.HyperDash); + public static IBindable GetHyperDashFruitColour(this ISkin skin) => skin.GetConfig(CatchSkinColour.HyperDashFruit) ?? skin.GetConfig(CatchSkinColour.HyperDash); diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 98cc10aa31..49c9a77277 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -389,9 +389,9 @@ namespace osu.Game.Rulesets.Catch.UI { base.SkinChanged(skin, allowFallback); - hyperDashColour = skin.GetConfig(CatchSkinColour.HyperDash)?.Value ?? DefaultHyperDashColour; - hyperDashEndGlowColour = skin.GetConfig(CatchSkinColour.HyperDashAfterImage)?.Value ?? hyperDashColour; updateCatcherColour(); + hyperDashColour = skin.GetHyperDashCatcherColour()?.Value ?? DefaultHyperDashColour; + hyperDashEndGlowColour = skin.GetHyperDashEndGlowColour()?.Value ?? DefaultHyperDashColour; } protected override void Update() From 50604dc7b22624632f015fcb55c0cb44bd3f4080 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 4 Apr 2020 19:29:06 +0300 Subject: [PATCH 0813/2376] Update catcher hyper-dashing colours on changing hyper-dash state only --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 49c9a77277..0b73c510d9 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -254,7 +254,10 @@ namespace osu.Game.Rulesets.Catch.UI hyperDashDirection = 0; if (wasHyperDashing) + { + updateCatcherColour(false); Trail &= Dashing; + } } else { @@ -264,6 +267,7 @@ namespace osu.Game.Rulesets.Catch.UI if (!wasHyperDashing) { + updateCatcherColour(true); Trail = true; var hyperDashEndGlow = createAdditiveSprite(endGlowSprites); @@ -273,15 +277,13 @@ namespace osu.Game.Rulesets.Catch.UI hyperDashEndGlow.Expire(true); } } - - updateCatcherColour(); } - private void updateCatcherColour() + private void updateCatcherColour(bool hyperDashing) { const float hyper_dash_transition_length = 180; - if (HyperDashing) + if (hyperDashing) { this.FadeColour(hyperDashColour == DefaultHyperDashColour ? Color4.OrangeRed : hyperDashColour, hyper_dash_transition_length, Easing.OutQuint); this.FadeTo(0.2f, hyper_dash_transition_length, Easing.OutQuint); @@ -389,9 +391,9 @@ namespace osu.Game.Rulesets.Catch.UI { base.SkinChanged(skin, allowFallback); - updateCatcherColour(); hyperDashColour = skin.GetHyperDashCatcherColour()?.Value ?? DefaultHyperDashColour; hyperDashEndGlowColour = skin.GetHyperDashEndGlowColour()?.Value ?? DefaultHyperDashColour; + updateCatcherColour(HyperDashing); } protected override void Update() From fbe95a52e3b068d37c49dafa6057a744d0d0df9c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 4 Apr 2020 19:29:41 +0300 Subject: [PATCH 0814/2376] Remove unnecessary restating comment --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 0b73c510d9..1cb6987397 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -294,7 +294,6 @@ namespace osu.Game.Rulesets.Catch.UI this.FadeTo(1f, hyper_dash_transition_length, Easing.OutQuint); } - // update hyper-dash colour of the hyper-dashing catcher sprites containers. hyperDashTrails?.FadeColour(hyperDashColour, hyper_dash_transition_length, Easing.OutQuint); endGlowSprites?.FadeColour(hyperDashEndGlowColour, hyper_dash_transition_length, Easing.OutQuint); } From 19f39fe6327ffd3b3bdf27cde0da4d5b1a2801a7 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 4 Apr 2020 19:33:52 +0300 Subject: [PATCH 0815/2376] Change AdditiveTarget into a set method --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 39 ++++++++++++----------- osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 5 ++- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 1cb6987397..0e42c19455 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -42,25 +42,6 @@ namespace osu.Game.Rulesets.Catch.UI private Container hyperDashTrails; private Container endGlowSprites; - public Container AdditiveTarget - { - get => additiveTarget; - set - { - if (additiveTarget == value) - return; - - additiveTarget?.RemoveRange(new[] { dashTrails, hyperDashTrails, endGlowSprites }); - - additiveTarget = value; - additiveTarget?.AddRange(new[] - { - dashTrails ??= new Container { RelativeSizeAxes = Axes.Both, Colour = Color4.White }, - hyperDashTrails ??= new Container { RelativeSizeAxes = Axes.Both, Colour = hyperDashColour }, - endGlowSprites ??= new Container { RelativeSizeAxes = Axes.Both, Colour = hyperDashEndGlowColour }, - }); - } - } public CatcherAnimationState CurrentState { get; private set; } @@ -167,6 +148,26 @@ namespace osu.Game.Rulesets.Catch.UI updateCatcher(); } + /// + /// Sets container target to provide catcher additive trails content in. + /// + /// The container to add catcher trails in. + public void SetAdditiveTarget(Container target) + { + if (additiveTarget == target) + return; + + additiveTarget?.RemoveRange(new[] { dashTrails, hyperDashTrails, endGlowSprites }); + + additiveTarget = target; + additiveTarget?.AddRange(new[] + { + dashTrails ??= new Container { RelativeSizeAxes = Axes.Both, Colour = Color4.White }, + hyperDashTrails ??= new Container { RelativeSizeAxes = Axes.Both, Colour = hyperDashColour }, + endGlowSprites ??= new Container { RelativeSizeAxes = Axes.Both, Colour = hyperDashEndGlowColour }, + }); + } + /// /// Add a caught fruit to the catcher's stack. /// diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 37501736ff..641b81599e 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -33,10 +33,9 @@ namespace osu.Game.Rulesets.Catch.UI { RelativeSizeAxes = Axes.X; Height = CATCHER_SIZE; - Child = MovableCatcher = new Catcher(difficulty); - // this property adds containers to 'this' so it must not be set in the object initializer. - MovableCatcher.AdditiveTarget = this; + Child = MovableCatcher = new Catcher(difficulty); + MovableCatcher.SetAdditiveTarget(this); } public static float GetCatcherSize(BeatmapDifficulty difficulty) From 0e45a4d54e1dd372f120dbfb926234065f4158af Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sat, 4 Apr 2020 20:13:46 +0200 Subject: [PATCH 0816/2376] Add back cached ruleset assembly lookup --- osu.Game/Rulesets/RulesetStore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index c3c7b653da..7b4c0302aa 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -62,7 +62,7 @@ namespace osu.Game.Rulesets if (asm.Name.Equals(typeof(OsuGame).Assembly.GetName().Name, StringComparison.Ordinal)) return Assembly.GetExecutingAssembly(); - return null; + return loadedAssemblies.Keys.FirstOrDefault(a => a.FullName == asm.FullName); } private void addMissingRulesets() From e340d2628b36a9ce935e8d75931f414416d683e6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 5 Apr 2020 03:17:11 +0900 Subject: [PATCH 0817/2376] Fix sliderball accent colour not being set correctly --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 5c7f4a42b3..9b6f39d91d 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -186,7 +186,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables base.ApplySkin(skin, allowFallback); bool allowBallTint = skin.GetConfig(OsuSkinConfiguration.AllowSliderBallTint)?.Value ?? false; - Ball.Colour = allowBallTint ? AccentColour.Value : Color4.White; + Ball.AccentColour = allowBallTint ? AccentColour.Value : Color4.White; } protected override void CheckForResult(bool userTriggered, double timeOffset) From 1e8badb14a9ebdb4733af916d847fc10e0505f7c Mon Sep 17 00:00:00 2001 From: Santeri Nogelainen Date: Sat, 4 Apr 2020 22:28:36 +0300 Subject: [PATCH 0818/2376] Move all logic to TopLocalRank and remove CarouselBeatmapRank --- osu.Game/Online/Leaderboards/TopLocalRank.cs | 52 +++++++------- .../Select/Carousel/CarouselBeatmapRank.cs | 68 ------------------- .../Carousel/DrawableCarouselBeatmap.cs | 6 +- 3 files changed, 32 insertions(+), 94 deletions(-) delete mode 100644 osu.Game/Screens/Select/Carousel/CarouselBeatmapRank.cs diff --git a/osu.Game/Online/Leaderboards/TopLocalRank.cs b/osu.Game/Online/Leaderboards/TopLocalRank.cs index 83d92f8ffa..51c171a176 100644 --- a/osu.Game/Online/Leaderboards/TopLocalRank.cs +++ b/osu.Game/Online/Leaderboards/TopLocalRank.cs @@ -1,12 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Rulesets; @@ -14,43 +11,44 @@ using osu.Game.Scoring; namespace osu.Game.Online.Leaderboards { - public class TopLocalRank : Container + public class TopLocalRank : UpdateableRank { private readonly BeatmapInfo beatmap; - private readonly UpdateableRank rank; private ScoreManager scores; private IBindable ruleset; private IAPIProvider api; - /// - /// Raised when the top score is loaded - /// - public Action ScoreLoaded; + protected override double LoadDelay => 250; - public TopLocalRank(BeatmapInfo beatmap) + public TopLocalRank(BeatmapInfo beatmap) : base(null) { this.beatmap = beatmap; - - RelativeSizeAxes = Axes.Both; - - InternalChild = rank = new UpdateableRank(null) - { - RelativeSizeAxes = Axes.Both - }; } [BackgroundDependencyLoader] private void load(ScoreManager scores, IBindable ruleset, IAPIProvider api) { + scores.ItemAdded += scoreChanged; + scores.ItemRemoved += scoreChanged; + ruleset.ValueChanged += _ => fetchAndLoadTopScore(); + + this.ruleset = ruleset.GetBoundCopy(); this.scores = scores; - this.ruleset = ruleset; this.api = api; - FetchAndLoadTopScore(); + fetchAndLoadTopScore(); } - public void FetchAndLoadTopScore() + private void scoreChanged(ScoreInfo score) + { + if (score.BeatmapInfoID == beatmap.ID) + { + fetchAndLoadTopScore(); + } + } + + private void fetchAndLoadTopScore() { var score = fetchTopScore(); @@ -59,9 +57,16 @@ namespace osu.Game.Online.Leaderboards private void loadTopScore(ScoreInfo score) { - Schedule(() => rank.Rank = score?.Rank); + var rank = score?.Rank; - ScoreLoaded?.Invoke(score); + // toggle the display of this drawable + // we do not want empty space if there is no rank to be displayed + if (rank.HasValue) + Show(); + else + Hide(); + + Schedule(() => Rank = rank); } private ScoreInfo fetchTopScore() @@ -69,8 +74,7 @@ namespace osu.Game.Online.Leaderboards if (scores == null || beatmap == null || ruleset?.Value == null || api?.LocalUser.Value == null) return null; - return scores.GetAllUsableScores() - .Where(s => s.UserID == api.LocalUser.Value.Id && s.BeatmapInfoID == beatmap.ID && s.RulesetID == ruleset.Value.ID) + return scores.QueryScores(s => s.UserID == api.LocalUser.Value.Id && s.BeatmapInfoID == beatmap.ID && s.RulesetID == ruleset.Value.ID && !s.DeletePending) .OrderByDescending(s => s.TotalScore) .FirstOrDefault(); } diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmapRank.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmapRank.cs deleted file mode 100644 index fbd4292138..0000000000 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmapRank.cs +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Beatmaps; -using osu.Game.Online.Leaderboards; -using osu.Game.Rulesets; -using osu.Game.Scoring; - -namespace osu.Game.Screens.Select.Carousel -{ - public class CarouselBeatmapRank : Container - { - private const int rank_size = 20; - private readonly BeatmapInfo beatmap; - - private TopLocalRank rank; - - public CarouselBeatmapRank(BeatmapInfo beatmap) - { - this.beatmap = beatmap; - - Height = rank_size; - } - - [BackgroundDependencyLoader] - private void load(ScoreManager scores, IBindable ruleset) - { - scores.ItemAdded += scoreChanged; - scores.ItemRemoved += scoreChanged; - ruleset.ValueChanged += _ => rulesetChanged(); - - rank = new TopLocalRank(beatmap) - { - ScoreLoaded = scaleDisplay - }; - - InternalChild = new DelayedLoadWrapper(rank) - { - RelativeSizeAxes = Axes.Both - }; - } - - private void rulesetChanged() - { - rank.FetchAndLoadTopScore(); - } - - private void scoreChanged(ScoreInfo score) - { - if (score.BeatmapInfoID == beatmap.ID) - { - rank.FetchAndLoadTopScore(); - } - } - - private void scaleDisplay(ScoreInfo score) - { - if (score != null) - Width = rank_size * 2; - else - Width = 0; - } - } -} diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index 4b42d818f5..5357f9a652 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -19,6 +19,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Leaderboards; using osu.Game.Overlays; using osuTK; using osuTK.Graphics; @@ -129,9 +130,10 @@ namespace osu.Game.Screens.Select.Carousel AutoSizeAxes = Axes.Both, Children = new Drawable[] { - new CarouselBeatmapRank(beatmap) + new TopLocalRank(beatmap) { - Scale = new Vector2(0.8f) + Scale = new Vector2(0.8f), + Size = new Vector2(40, 20) }, starCounter = new StarCounter { From da59baa7798fc9f9851e0bd716a97ae1179a812e Mon Sep 17 00:00:00 2001 From: Santeri Nogelainen Date: Sat, 4 Apr 2020 22:42:13 +0300 Subject: [PATCH 0819/2376] Add line break --- osu.Game/Online/Leaderboards/TopLocalRank.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Leaderboards/TopLocalRank.cs b/osu.Game/Online/Leaderboards/TopLocalRank.cs index 51c171a176..be014dafc3 100644 --- a/osu.Game/Online/Leaderboards/TopLocalRank.cs +++ b/osu.Game/Online/Leaderboards/TopLocalRank.cs @@ -21,7 +21,8 @@ namespace osu.Game.Online.Leaderboards protected override double LoadDelay => 250; - public TopLocalRank(BeatmapInfo beatmap) : base(null) + public TopLocalRank(BeatmapInfo beatmap) + : base(null) { this.beatmap = beatmap; } From 634a8f9ff49bbf0bf85e4834df444e70463cec76 Mon Sep 17 00:00:00 2001 From: Endrik Date: Sat, 4 Apr 2020 23:05:10 +0300 Subject: [PATCH 0820/2376] Return inline --- osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs index d4bc651414..30ed37a966 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs @@ -136,8 +136,7 @@ namespace osu.Game.Rulesets.Osu.Skinning case OsuSkinConfiguration.HitCircleOverlayAboveNumber: // Quote from https://osu.ppy.sh/help/wiki/Skinning/skin.ini#%5Bgeneral%5D // Old command: HitCircleOverlayAboveNumer (with typo) still works for legacy support - var rv = source.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumber); - return rv ?? source.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumer); + return source.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumber) ?? source.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumer); } break; From 0014a8404e17d196d3aebf386346efd20790d023 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 4 Apr 2020 23:12:42 +0300 Subject: [PATCH 0821/2376] GetHyperDashEndGlowColour() -> GetHyperDashCatcherAfterImageColour() --- osu.Game.Rulesets.Catch/Skinning/CatchSkinExtensions.cs | 2 +- osu.Game.Rulesets.Catch/UI/Catcher.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchSkinExtensions.cs b/osu.Game.Rulesets.Catch/Skinning/CatchSkinExtensions.cs index 48e11121ea..06d21f8c5e 100644 --- a/osu.Game.Rulesets.Catch/Skinning/CatchSkinExtensions.cs +++ b/osu.Game.Rulesets.Catch/Skinning/CatchSkinExtensions.cs @@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Catch.Skinning public static IBindable GetHyperDashCatcherColour(this ISkin skin) => skin.GetConfig(CatchSkinColour.HyperDash); - public static IBindable GetHyperDashEndGlowColour(this ISkin skin) + public static IBindable GetHyperDashCatcherAfterImageColour(this ISkin skin) => skin.GetConfig(CatchSkinColour.HyperDashAfterImage) ?? skin.GetConfig(CatchSkinColour.HyperDash); diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 0e42c19455..0d5b454a9d 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -392,7 +392,7 @@ namespace osu.Game.Rulesets.Catch.UI base.SkinChanged(skin, allowFallback); hyperDashColour = skin.GetHyperDashCatcherColour()?.Value ?? DefaultHyperDashColour; - hyperDashEndGlowColour = skin.GetHyperDashEndGlowColour()?.Value ?? DefaultHyperDashColour; + hyperDashEndGlowColour = skin.GetHyperDashCatcherAfterImageColour()?.Value ?? DefaultHyperDashColour; updateCatcherColour(HyperDashing); } From 36ad1cbd79854ffc4b8193e776ae8b6f253e9f39 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sat, 4 Apr 2020 23:17:55 +0300 Subject: [PATCH 0822/2376] Format the code --- osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs index 30ed37a966..0a697d1fde 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs @@ -136,7 +136,7 @@ namespace osu.Game.Rulesets.Osu.Skinning case OsuSkinConfiguration.HitCircleOverlayAboveNumber: // Quote from https://osu.ppy.sh/help/wiki/Skinning/skin.ini#%5Bgeneral%5D // Old command: HitCircleOverlayAboveNumer (with typo) still works for legacy support - return source.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumber) ?? source.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumer); + return source.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumber) ?? source.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumer); } break; From c4f7b4576848da614959c8a427f7886e7a2f66f3 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 5 Apr 2020 00:02:33 +0300 Subject: [PATCH 0823/2376] Revert "Add support for custom hyper-dash fruit colouring" This reverts commit 6f2cc5471adabc4392fcf1f63a5de32266016c10 and also its testing cases. This became dead code after actual correct osu!catch skin colouring, we don't support modern skinning (non-legacy skinning) at the moment, so for what it's worth this can be reverted to default red-coloured --- .../TestSceneHyperDashColouring.cs | 67 ++++++------------- .../Objects/Drawables/FruitPiece.cs | 12 +--- 2 files changed, 23 insertions(+), 56 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs index 2009099a61..10739a3131 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs @@ -4,15 +4,10 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Audio.Sample; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Textures; using osu.Framework.Testing; -using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Catch.Objects; @@ -31,9 +26,8 @@ namespace osu.Game.Rulesets.Catch.Tests [Resolved] private SkinManager skins { get; set; } - [TestCase(false)] - [TestCase(true)] - public void TestHyperDashFruitColour(bool legacyFruit) + [Test] + public void TestHyperDashFruitColour() { DrawableFruit drawableFruit = null; @@ -47,20 +41,15 @@ namespace osu.Game.Rulesets.Catch.Tests Anchor = Anchor.Centre, Origin = Anchor.Centre, Scale = new Vector2(4f), - }, false, false, false, legacyFruit); + }, false, false, false); }); - AddAssert("hyper-dash fruit has default colour", () => - legacyFruit - ? checkLegacyFruitHyperDashColour(drawableFruit, Catcher.DefaultHyperDashColour) - : checkFruitHyperDashColour(drawableFruit, Catcher.DefaultHyperDashColour)); + AddAssert("hyper-dash fruit has default colour", () => checkLegacyFruitHyperDashColour(drawableFruit, Catcher.DefaultHyperDashColour)); } - [TestCase(false, true)] - [TestCase(false, false)] - [TestCase(true, true)] - [TestCase(true, false)] - public void TestCustomHyperDashFruitColour(bool legacyFruit, bool customCatcherHyperDashColour) + [TestCase(true)] + [TestCase(false)] + public void TestCustomHyperDashFruitColour(bool customCatcherHyperDashColour) { DrawableFruit drawableFruit = null; @@ -74,18 +63,14 @@ namespace osu.Game.Rulesets.Catch.Tests Anchor = Anchor.Centre, Origin = Anchor.Centre, Scale = new Vector2(4f), - }, customCatcherHyperDashColour, false, true, legacyFruit); + }, customCatcherHyperDashColour, false, true); }); - AddAssert("hyper-dash fruit use fruit colour from skin", () => - legacyFruit - ? checkLegacyFruitHyperDashColour(drawableFruit, TestSkin.CustomHyperDashFruitColour) - : checkFruitHyperDashColour(drawableFruit, TestSkin.CustomHyperDashFruitColour)); + AddAssert("hyper-dash fruit use fruit colour from skin", () => checkLegacyFruitHyperDashColour(drawableFruit, TestSkin.CustomHyperDashFruitColour)); } - [TestCase(false)] - [TestCase(true)] - public void TestCustomHyperDashFruitColourFallback(bool legacyFruit) + [Test] + public void TestCustomHyperDashFruitColourFallback() { DrawableFruit drawableFruit = null; @@ -100,36 +85,24 @@ namespace osu.Game.Rulesets.Catch.Tests Anchor = Anchor.Centre, Origin = Anchor.Centre, Scale = new Vector2(4f), - }, true, false, false, legacyFruit); + }, true, false, false); }); - AddAssert("hyper-dash fruit colour falls back to catcher colour from skin", () => - legacyFruit - ? checkLegacyFruitHyperDashColour(drawableFruit, TestSkin.CustomHyperDashColour) - : checkFruitHyperDashColour(drawableFruit, TestSkin.CustomHyperDashColour)); + AddAssert("hyper-dash fruit colour falls back to catcher colour from skin", () => checkLegacyFruitHyperDashColour(drawableFruit, TestSkin.CustomHyperDashColour)); } - private Drawable setupSkinHierarchy(Drawable child, bool customCatcherColour, bool customAfterColour, bool customFruitColour, bool legacySkin = true) + private Drawable setupSkinHierarchy(Drawable child, bool customCatcherColour, bool customAfterColour, bool customFruitColour) { + var legacySkinProvider = new SkinProvidingContainer(skins.GetSkin(DefaultLegacySkin.Info)); var testSkinProvider = new SkinProvidingContainer(new TestSkin(customCatcherColour, customAfterColour, customFruitColour)); + var legacySkinTransformer = new SkinProvidingContainer(new CatchLegacySkinTransformer(testSkinProvider)); - if (legacySkin) - { - var legacySkinProvider = new SkinProvidingContainer(skins.GetSkin(DefaultLegacySkin.Info)); - var legacySkinTransformer = new SkinProvidingContainer(new CatchLegacySkinTransformer(testSkinProvider)); - - return legacySkinProvider - .WithChild(testSkinProvider - .WithChild(legacySkinTransformer - .WithChild(child))); - } - - return testSkinProvider.WithChild(child); + return legacySkinProvider + .WithChild(testSkinProvider + .WithChild(legacySkinTransformer + .WithChild(child))); } - private bool checkFruitHyperDashColour(DrawableFruit fruit, Color4 expectedColour) => - fruit.ChildrenOfType().First().Drawable.ChildrenOfType().Single(c => c.BorderColour == expectedColour).Any(d => d.Colour == expectedColour); - private bool checkLegacyFruitHyperDashColour(DrawableFruit fruit, Color4 expectedColour) => fruit.ChildrenOfType().First().Drawable.ChildrenOfType().Any(c => c.Colour == expectedColour); diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs index 2437958916..359329885c 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs @@ -7,10 +7,8 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Game.Rulesets.Catch.Skinning; using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Skinning; using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.Objects.Drawables @@ -34,7 +32,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables } [BackgroundDependencyLoader] - private void load(DrawableHitObject drawableObject, ISkinSource skin) + private void load(DrawableHitObject drawableObject) { DrawableCatchHitObject drawableCatchObject = (DrawableCatchHitObject)drawableObject; hitObject = drawableCatchObject.HitObject; @@ -63,10 +61,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables }, }); - var hyperDashColour = - skin.GetHyperDashFruitColour()?.Value ?? - Catcher.DefaultHyperDashColour; - if (hitObject.HyperDash) { AddInternal(new Circle @@ -74,7 +68,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, - BorderColour = hyperDashColour, + BorderColour = Catcher.DefaultHyperDashColour, BorderThickness = 12f * RADIUS_ADJUST, Children = new Drawable[] { @@ -84,7 +78,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables Alpha = 0.3f, Blending = BlendingParameters.Additive, RelativeSizeAxes = Axes.Both, - Colour = hyperDashColour, + Colour = Catcher.DefaultHyperDashColour, } } }); From 10e65c4f53929cf477042c58aa302fd0f6e3b076 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 5 Apr 2020 00:10:12 +0300 Subject: [PATCH 0824/2376] Add handling for legacy CatchTheBeat section in LegacyDecoder --- osu.Game/Beatmaps/Formats/LegacyDecoder.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index 561707f9ef..743a470e6e 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -73,6 +73,9 @@ namespace osu.Game.Beatmaps.Formats switch (section) { case Section.Colours: + // osu!catch section only has colour settings + // so no harm in handling the entire section + case Section.CatchTheBeat: HandleColours(output, line); return; } @@ -149,7 +152,8 @@ namespace osu.Game.Beatmaps.Formats HitObjects, Variables, Fonts, - Mania + CatchTheBeat, + Mania, } internal class LegacyDifficultyControlPoint : DifficultyControlPoint From 55d076d6f359ed50edd3cf371195b656effd9929 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 5 Apr 2020 00:10:25 +0300 Subject: [PATCH 0825/2376] Transform CatchSkinColour lookup to skin configuration custom colours lookup --- .../Skinning/CatchLegacySkinTransformer.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs index 65e6e6f209..4a87eb95e7 100644 --- a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs @@ -65,6 +65,15 @@ namespace osu.Game.Rulesets.Catch.Skinning public SampleChannel GetSample(ISampleInfo sample) => source.GetSample(sample); - public IBindable GetConfig(TLookup lookup) => source.GetConfig(lookup); + public IBindable GetConfig(TLookup lookup) + { + switch (lookup) + { + case CatchSkinColour colour: + return source.GetConfig(new SkinCustomColourLookup(colour)); + } + + return source.GetConfig(lookup); + } } } From b100230538707ffcbed2a70fc17b0981b618b3eb Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 5 Apr 2020 00:13:23 +0300 Subject: [PATCH 0826/2376] Test CatchSkinColour transformation on colour retrieval implicitly --- .../TestSceneHyperDashColouring.cs | 37 ++++--------------- 1 file changed, 8 insertions(+), 29 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs index 10739a3131..c8d28dbaeb 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs @@ -106,44 +106,23 @@ namespace osu.Game.Rulesets.Catch.Tests private bool checkLegacyFruitHyperDashColour(DrawableFruit fruit, Color4 expectedColour) => fruit.ChildrenOfType().First().Drawable.ChildrenOfType().Any(c => c.Colour == expectedColour); - private class TestSkin : ISkin + private class TestSkin : LegacySkin { public static Color4 CustomHyperDashColour { get; } = Color4.Goldenrod; public static Color4 CustomHyperDashFruitColour { get; } = Color4.Cyan; public static Color4 CustomHyperDashAfterColour { get; } = Color4.Lime; - private readonly bool customCatcherColour; - private readonly bool customAfterColour; - private readonly bool customFruitColour; - public TestSkin(bool customCatcherColour, bool customAfterColour, bool customFruitColour) + : base(new SkinInfo(), null, null, string.Empty) { - this.customCatcherColour = customCatcherColour; - this.customAfterColour = customAfterColour; - this.customFruitColour = customFruitColour; - } + if (customCatcherColour) + Configuration.CustomColours[CatchSkinColour.HyperDash.ToString()] = CustomHyperDashColour; - public Drawable GetDrawableComponent(ISkinComponent component) => null; + if (customAfterColour) + Configuration.CustomColours[CatchSkinColour.HyperDashAfterImage.ToString()] = CustomHyperDashAfterColour; - public Texture GetTexture(string componentName) => null; - - public SampleChannel GetSample(ISampleInfo sampleInfo) => null; - - public IBindable GetConfig(TLookup lookup) - { - if (lookup is CatchSkinColour config) - { - if (config == CatchSkinColour.HyperDash && customCatcherColour) - return SkinUtils.As(new Bindable(CustomHyperDashColour)); - - if (config == CatchSkinColour.HyperDashFruit && customFruitColour) - return SkinUtils.As(new Bindable(CustomHyperDashFruitColour)); - - if (config == CatchSkinColour.HyperDashAfterImage && customAfterColour) - return SkinUtils.As(new Bindable(CustomHyperDashAfterColour)); - } - - return null; + if (customFruitColour) + Configuration.CustomColours[CatchSkinColour.HyperDashFruit.ToString()] = CustomHyperDashFruitColour; } } } From dfd86e643bd415e5653a942e2432d56d224c6a1b Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 5 Apr 2020 00:14:07 +0300 Subject: [PATCH 0827/2376] Add custom-coloured osu!catch skin configuration to 'Resources/special-skin' --- osu.Game.Rulesets.Catch.Tests/Resources/special-skin/skin.ini | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/special-skin/skin.ini diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/skin.ini b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/skin.ini new file mode 100644 index 0000000000..36515f33c5 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/skin.ini @@ -0,0 +1,4 @@ +[CatchTheBeat] +HyperDash: 232,185,35 +HyperDashFruit: 0,255,255 +HyperDashAfterImage: 232,74,35 \ No newline at end of file From 42ccee5e6c8a2eedc459a369e129cccab853133d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 5 Apr 2020 00:15:42 +0300 Subject: [PATCH 0828/2376] Fix CI issue --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 0d5b454a9d..7971a17e68 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -42,7 +42,6 @@ namespace osu.Game.Rulesets.Catch.UI private Container hyperDashTrails; private Container endGlowSprites; - public CatcherAnimationState CurrentState { get; private set; } /// From f6bbec72bfd664f1a29f27de192496bba08df6ca Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 5 Apr 2020 00:20:21 +0300 Subject: [PATCH 0829/2376] Revert "Add custom-coloured osu!catch skin configuration to 'Resources/special-skin'" This reverts commit dfd86e643bd415e5653a942e2432d56d224c6a1b. --- osu.Game.Rulesets.Catch.Tests/Resources/special-skin/skin.ini | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/special-skin/skin.ini diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/skin.ini b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/skin.ini deleted file mode 100644 index 36515f33c5..0000000000 --- a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/skin.ini +++ /dev/null @@ -1,4 +0,0 @@ -[CatchTheBeat] -HyperDash: 232,185,35 -HyperDashFruit: 0,255,255 -HyperDashAfterImage: 232,74,35 \ No newline at end of file From b8327ed877b61ac867210f7b611240c28063fdc7 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 5 Apr 2020 00:30:10 +0300 Subject: [PATCH 0830/2376] Add test for osu!catch skin colour decoding Tests the skin configuration CatchTheBeat section's colours decoding part --- .../CatchSkinColourDecodingTest.cs | 36 +++++++++++++++++++ .../Resources/special-skin/skin.ini | 4 +++ .../Skinning/CatchLegacySkinTransformer.cs | 2 +- 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/special-skin/skin.ini diff --git a/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs new file mode 100644 index 0000000000..57228210d6 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs @@ -0,0 +1,36 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.IO.Stores; +using osu.Game.Rulesets.Catch.Skinning; +using osu.Game.Skinning; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Catch.Tests +{ + [TestFixture] + public class CatchSkinColourDecodingTest + { + [Test] + public void TestCatchSkinColourDecoding() + { + var store = new NamespacedResourceStore(new DllResourceStore(GetType().Assembly), "Resources/special-skin"); + var rawSkin = new TestLegacySkin(new SkinInfo { Name = "special-skin" }, store); + var skin = new CatchLegacySkinTransformer(rawSkin); + + Assert.AreEqual(new Color4(232, 185, 35, 255), skin.GetHyperDashCatcherColour()?.Value); + Assert.AreEqual(new Color4(232, 74, 35, 255), skin.GetHyperDashCatcherAfterImageColour()?.Value); + Assert.AreEqual(new Color4(0, 255, 255, 255), skin.GetHyperDashFruitColour()?.Value); + } + + private class TestLegacySkin : LegacySkin + { + public TestLegacySkin(SkinInfo skin, IResourceStore storage) + // Bypass LegacySkinResourceStore to avoid returning null for retrieving files due to bad skin info (SkinInfo.Files = null). + : base(skin, storage, null, "skin.ini") + { + } + } + } +} diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/skin.ini b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/skin.ini new file mode 100644 index 0000000000..96d50f1451 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/skin.ini @@ -0,0 +1,4 @@ +[CatchTheBeat] +HyperDash: 232,185,35 +HyperDashFruit: 0,255,255 +HyperDashAfterImage: 232,74,35 diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs index 65e6e6f209..ba939157ea 100644 --- a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Catch.Skinning { private readonly ISkin source; - public CatchLegacySkinTransformer(ISkinSource source) + public CatchLegacySkinTransformer(ISkin source) { this.source = source; } From 42ac0c72eac12ea39415f9bc9926adcc5a1a6ff6 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 5 Apr 2020 00:46:52 +0300 Subject: [PATCH 0831/2376] Fix grammer issue and more rewording MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game.Rulesets.Catch/Skinning/CatchSkinColour.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchSkinColour.cs b/osu.Game.Rulesets.Catch/Skinning/CatchSkinColour.cs index 2ad8f89739..4506111498 100644 --- a/osu.Game.Rulesets.Catch/Skinning/CatchSkinColour.cs +++ b/osu.Game.Rulesets.Catch/Skinning/CatchSkinColour.cs @@ -6,12 +6,12 @@ namespace osu.Game.Rulesets.Catch.Skinning public enum CatchSkinColour { /// - /// The colour to be used for the catcher while on hyper-dashing state. + /// The colour to be used for the catcher while in hyper-dashing state. /// HyperDash, /// - /// The colour to be used for hyper-dash fruits. + /// The colour to be used for fruits that grant the catcher the ability to hyper-dash. /// HyperDashFruit, From 2fec8b7b8555101200250846e9ea0958f77b6b1c Mon Sep 17 00:00:00 2001 From: Shane Woolcock Date: Sun, 5 Apr 2020 13:01:10 +0930 Subject: [PATCH 0832/2376] Use DisplayModes rather than AvailableResolutions --- .../Settings/Sections/Graphics/LayoutSettings.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs index b73c8f7622..00b7643332 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs @@ -209,15 +209,16 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics private IReadOnlyList getResolutions() { var resolutions = new List { new Size(9999, 9999) }; + var currentDisplay = game.Window?.CurrentDisplay.Value; - if (game.Window != null) + if (currentDisplay != null) { - resolutions.AddRange(game.Window.AvailableResolutions - .Where(r => r.Width >= 800 && r.Height >= 600) - .OrderByDescending(r => r.Width) - .ThenByDescending(r => r.Height) - .Select(res => new Size(res.Width, res.Height)) - .Distinct()); + resolutions.AddRange(currentDisplay.DisplayModes + .Where(m => m.Size.Width >= 800 && m.Size.Height >= 600) + .OrderByDescending(m => m.Size.Width) + .ThenByDescending(m => m.Size.Height) + .Select(m => m.Size) + .Distinct()); } return resolutions; From bc6c6228ace9161032f22cc004478c3f59179604 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 5 Apr 2020 14:13:06 +0900 Subject: [PATCH 0833/2376] Tidy up a touch --- osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs index 0a697d1fde..487401c939 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs @@ -134,9 +134,10 @@ namespace osu.Game.Rulesets.Osu.Skinning break; case OsuSkinConfiguration.HitCircleOverlayAboveNumber: - // Quote from https://osu.ppy.sh/help/wiki/Skinning/skin.ini#%5Bgeneral%5D - // Old command: HitCircleOverlayAboveNumer (with typo) still works for legacy support - return source.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumber) ?? source.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumer); + // See https://osu.ppy.sh/help/wiki/Skinning/skin.ini#%5Bgeneral%5D + // HitCircleOverlayAboveNumer (with typo) should still be supported for now. + return source.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumber) ?? + source.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumer); } break; From 8d3e228f78b5b16f31e9fd250ca82d4ad0c5a770 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sun, 5 Apr 2020 11:22:52 +0300 Subject: [PATCH 0834/2376] Split and rename tests --- .../TestSceneSliderSnaking.cs | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs index 51d4d1c008..c282314be7 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs @@ -64,23 +64,33 @@ namespace osu.Game.Rulesets.Osu.Tests [SetUpSteps] public override void SetUpSteps() { } - [Test] - public void TestSnaking() + [TestCase(0)] + [TestCase(1)] + [TestCase(2)] + public void TestSnakingEnabled(int repeatAmount) { AddStep("have autoplay", () => autoplay = true); base.SetUpSteps(); AddUntilStep("wait for track to start running", () => track.IsRunning); - for (int i = 0; i < 3; i++) - { - testSlider(i, true); - testSlider(i, false); - } + testSlider(repeatAmount, true); + } + + [TestCase(0)] + [TestCase(1)] + [TestCase(2)] + public void TestSnakingDisabled(int repeatAmount) + { + AddStep("have autoplay", () => autoplay = true); + base.SetUpSteps(); + AddUntilStep("wait for track to start running", () => track.IsRunning); + + testSlider(repeatAmount, false); } [TestCase(true)] [TestCase(false)] - public void TestArrowStays(bool isHit) + public void TestArrowMovement(bool isHit) { AddStep($"{(isHit ? "enable" : "disable")} autoplay", () => autoplay = isHit); setSnaking(true); From 1f6a4fa4b812752767b3aa7b36ef3d1e90ee052e Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sun, 5 Apr 2020 12:45:10 +0300 Subject: [PATCH 0835/2376] Remove transformations --- .../Objects/Drawables/DrawableSliderRepeat.cs | 5 ++++- .../Objects/Drawables/Pieces/ReverseArrowPiece.cs | 9 +++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs index b04d484195..6c818f4a3e 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Utils; using osu.Game.Rulesets.Objects.Drawables; @@ -21,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private double animDuration; - private readonly Drawable scaleContainer; + private readonly ReverseArrowPiece scaleContainer; public DrawableSliderRepeat(SliderRepeat sliderRepeat, DrawableSlider drawableSlider) : base(sliderRepeat) @@ -79,6 +80,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables case ArmedState.Hit: this.FadeOut(animDuration, Easing.Out) .ScaleTo(Scale * 1.5f, animDuration, Easing.Out); + scaleContainer.ShouldFollowBeats = false; + scaleContainer.Transforms.ForEach(t => scaleContainer.RemoveTransform(t)); break; } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ReverseArrowPiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ReverseArrowPiece.cs index 35a27bb0a6..73f02aa59c 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ReverseArrowPiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ReverseArrowPiece.cs @@ -13,6 +13,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { public class ReverseArrowPiece : BeatSyncedContainer { + public bool ShouldFollowBeats = true; + public ReverseArrowPiece() { Divisor = 2; @@ -37,7 +39,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces }; } - protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) => - Child.ScaleTo(1.3f).ScaleTo(1f, timingPoint.BeatLength, Easing.Out); + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) + { + if (ShouldFollowBeats) + Child.ScaleTo(1.3f).ScaleTo(1f, timingPoint.BeatLength, Easing.Out); + } } } From a3626333bebb580005c353466f6e130ef9c1f798 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sun, 5 Apr 2020 13:36:52 +0300 Subject: [PATCH 0836/2376] Use DI instead --- .../Objects/Drawables/DrawableSliderRepeat.cs | 3 +-- .../Objects/Drawables/Pieces/ReverseArrowPiece.cs | 7 +++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs index 6c818f4a3e..517af630fc 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private double animDuration; - private readonly ReverseArrowPiece scaleContainer; + private readonly Drawable scaleContainer; public DrawableSliderRepeat(SliderRepeat sliderRepeat, DrawableSlider drawableSlider) : base(sliderRepeat) @@ -80,7 +80,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables case ArmedState.Hit: this.FadeOut(animDuration, Easing.Out) .ScaleTo(Scale * 1.5f, animDuration, Easing.Out); - scaleContainer.ShouldFollowBeats = false; scaleContainer.Transforms.ForEach(t => scaleContainer.RemoveTransform(t)); break; } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ReverseArrowPiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ReverseArrowPiece.cs index 73f02aa59c..d792665d9d 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ReverseArrowPiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ReverseArrowPiece.cs @@ -8,12 +8,15 @@ using osu.Framework.Graphics.Sprites; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics.Containers; using osu.Game.Skinning; +using osu.Framework.Allocation; +using osu.Game.Rulesets.Objects.Drawables; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { public class ReverseArrowPiece : BeatSyncedContainer { - public bool ShouldFollowBeats = true; + [Resolved] + private DrawableHitObject drawableSlider { get; set; } public ReverseArrowPiece() { @@ -41,7 +44,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) { - if (ShouldFollowBeats) + if (!drawableSlider.IsHit) Child.ScaleTo(1.3f).ScaleTo(1f, timingPoint.BeatLength, Easing.Out); } } From 23c3be0969b3c240d53844221f71928fdaa20520 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sun, 5 Apr 2020 13:39:31 +0300 Subject: [PATCH 0837/2376] Rename variable --- .../Objects/Drawables/Pieces/ReverseArrowPiece.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ReverseArrowPiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ReverseArrowPiece.cs index d792665d9d..6f3b2b6890 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ReverseArrowPiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ReverseArrowPiece.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces public class ReverseArrowPiece : BeatSyncedContainer { [Resolved] - private DrawableHitObject drawableSlider { get; set; } + private DrawableHitObject drawableRepeat { get; set; } public ReverseArrowPiece() { @@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) { - if (!drawableSlider.IsHit) + if (!drawableRepeat.IsHit) Child.ScaleTo(1.3f).ScaleTo(1f, timingPoint.BeatLength, Easing.Out); } } From d68c45e22b38173d4c30b21d9eef8c0f48b71a46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 5 Apr 2020 13:47:30 +0200 Subject: [PATCH 0838/2376] Use ElementAt() where applicable --- osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs index c282314be7..98b039c9b4 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs @@ -99,7 +99,7 @@ namespace osu.Game.Rulesets.Osu.Tests addSeekStep(16500); AddStep("retrieve 2nd slider repeat", () => { - var drawable = Player.DrawableRuleset.Playfield.AllHitObjects.Skip(1).First(); + var drawable = Player.DrawableRuleset.Playfield.AllHitObjects.ElementAt(1); repeat = drawable.ChildrenOfType>().First().Children.First(); }); AddStep("Save repeat vector", () => savedVector = repeat.Position); @@ -121,7 +121,7 @@ namespace osu.Game.Rulesets.Osu.Tests int repeats = index; AddStep($"retrieve {(index + 1).ToOrdinalWords()} slider", () => { - slider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.Skip(index).First(); + slider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.ElementAt(index); }); setSnaking(snaking); testSnakingIn(startTime + fade_in_modifier, snaking); From 4170c210b29872b1edcb0072cd42b037245f34bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 5 Apr 2020 13:50:27 +0200 Subject: [PATCH 0839/2376] Centralise hitobject start time calculation --- .../TestSceneSliderSnaking.cs | 72 ++++++++++--------- 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs index 98b039c9b4..287da2d25c 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs @@ -117,7 +117,7 @@ namespace osu.Game.Rulesets.Osu.Tests private void testSlider(int index, bool snaking) { - double startTime = index * 10000 + 3000; + double startTime = hitObjects[index].StartTime; int repeats = index; AddStep($"retrieve {(index + 1).ToOrdinalWords()} slider", () => { @@ -189,46 +189,48 @@ namespace osu.Game.Rulesets.Osu.Tests protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap { - HitObjects = new List + HitObjects = hitObjects + }; + + private readonly List hitObjects = new List + { + new Slider { - new Slider + StartTime = 3000, + Position = new Vector2(100, 100), + Path = new SliderPath(PathType.PerfectCurve, new[] { - StartTime = 3000, - Position = new Vector2(100, 100), - Path = new SliderPath(PathType.PerfectCurve, new[] - { - Vector2.Zero, - new Vector2(300, 200) - }), - }, - new Slider + Vector2.Zero, + new Vector2(300, 200) + }), + }, + new Slider + { + StartTime = 13000, + Position = new Vector2(100, 100), + Path = new SliderPath(PathType.PerfectCurve, new[] { - StartTime = 13000, - Position = new Vector2(100, 100), - Path = new SliderPath(PathType.PerfectCurve, new[] - { - Vector2.Zero, - new Vector2(300, 200) - }), - RepeatCount = 1, - }, + Vector2.Zero, + new Vector2(300, 200) + }), + RepeatCount = 1, + }, - new Slider + new Slider + { + StartTime = 23000, + Position = new Vector2(100, 100), + Path = new SliderPath(PathType.PerfectCurve, new[] { - StartTime = 23000, - Position = new Vector2(100, 100), - Path = new SliderPath(PathType.PerfectCurve, new[] - { - Vector2.Zero, - new Vector2(300, 200) - }), - RepeatCount = 2, - }, + Vector2.Zero, + new Vector2(300, 200) + }), + RepeatCount = 2, + }, - new HitCircle - { - StartTime = 199999, - } + new HitCircle + { + StartTime = 199999, } }; } From cbc546905ff103ace3583a9446b1764849228392 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 5 Apr 2020 15:26:32 +0200 Subject: [PATCH 0840/2376] Rewrite snaking tests --- .../TestSceneSliderSnaking.cs | 117 ++++++++++-------- 1 file changed, 67 insertions(+), 50 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs index 287da2d25c..19b05f6b51 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; using Humanizer; @@ -67,25 +68,48 @@ namespace osu.Game.Rulesets.Osu.Tests [TestCase(0)] [TestCase(1)] [TestCase(2)] - public void TestSnakingEnabled(int repeatAmount) + public void TestSnakingEnabled(int sliderIndex) { - AddStep("have autoplay", () => autoplay = true); + AddStep("enable autoplay", () => autoplay = true); base.SetUpSteps(); AddUntilStep("wait for track to start running", () => track.IsRunning); - testSlider(repeatAmount, true); + double startTime = hitObjects[sliderIndex].StartTime; + retrieveSlider(sliderIndex); + setSnaking(true); + + ensureSnakingIn(startTime + fade_in_modifier); + + for (int i = 0; i < sliderIndex; i++) + { + // non-final repeats should not snake out + ensureNoSnakingOut(startTime, i); + } + + // final repeat should snake out + ensureSnakingOut(startTime, sliderIndex); } [TestCase(0)] [TestCase(1)] [TestCase(2)] - public void TestSnakingDisabled(int repeatAmount) + public void TestSnakingDisabled(int sliderIndex) { AddStep("have autoplay", () => autoplay = true); base.SetUpSteps(); AddUntilStep("wait for track to start running", () => track.IsRunning); - testSlider(repeatAmount, false); + double startTime = hitObjects[sliderIndex].StartTime; + retrieveSlider(sliderIndex); + setSnaking(false); + + ensureNoSnakingIn(startTime + fade_in_modifier); + + for (int i = 0; i <= sliderIndex; i++) + { + // no snaking out ever, including final repeat + ensureNoSnakingOut(startTime, i); + } } [TestCase(true)] @@ -115,62 +139,55 @@ namespace osu.Game.Rulesets.Osu.Tests }); } - private void testSlider(int index, bool snaking) + private void retrieveSlider(int index) => AddStep($"retrieve {(index + 1).ToOrdinalWords()} slider", () => { - double startTime = hitObjects[index].StartTime; - int repeats = index; - AddStep($"retrieve {(index + 1).ToOrdinalWords()} slider", () => - { - slider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.ElementAt(index); - }); - setSnaking(snaking); - testSnakingIn(startTime + fade_in_modifier, snaking); + slider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.ElementAt(index); + }); - for (int i = 0; i < repeats + 1; i++) - { - testSnakingOut(startTime + 100 + duration_of_span * i, snaking && i == repeats, i % 2 == 1); - } + private void ensureSnakingIn(double startTime) => checkPositionChange(startTime, sliderEnd, positionIncreased); + private void ensureNoSnakingIn(double startTime) => checkPositionChange(startTime, sliderEnd, positionRemainsSame); + + private void ensureSnakingOut(double startTime, int repeatIndex) + { + var repeatTime = timeAtRepeat(startTime, repeatIndex); + + if (repeatIndex % 2 == 0) + checkPositionChange(repeatTime, sliderStart, positionIncreased); + else + checkPositionChange(repeatTime, sliderEnd, positionDecreased); } - private void testSnakingIn(double startTime, bool isSnakingExpected) + private void ensureNoSnakingOut(double startTime, int repeatIndex) => + checkPositionChange(timeAtRepeat(startTime, repeatIndex), positionAtRepeat(repeatIndex), positionRemainsSame); + + private double timeAtRepeat(double startTime, int repeatIndex) => startTime + 100 + duration_of_span * repeatIndex; + private Func positionAtRepeat(int repeatIndex) => repeatIndex % 2 == 0 ? (Func)sliderStart : sliderEnd; + + private List sliderCurve => ((PlaySliderBody)slider.Body.Drawable).CurrentCurve; + private Vector2 sliderStart() => sliderCurve.First(); + private Vector2 sliderEnd() => sliderCurve.Last(); + + private bool positionRemainsSame(Vector2 previous, Vector2 current) => previous == current; + private bool positionIncreased(Vector2 previous, Vector2 current) => current.X > previous.X && current.Y > previous.Y; + private bool positionDecreased(Vector2 previous, Vector2 current) => current.X < previous.X && current.Y < previous.Y; + + private void checkPositionChange(double startTime, Func positionToCheck, Func positionAssertion) { + Vector2 previousPosition = Vector2.Zero; + + string positionDescription = positionToCheck.Method.Name.Humanize(LetterCasing.LowerCase); + string assertionDescription = positionAssertion.Method.Name.Humanize(LetterCasing.LowerCase); + addSeekStep(startTime); - AddStep("Save end vector", () => savedVector = getCurrentSliderVector(true)); + AddStep($"save {positionDescription} position", () => previousPosition = positionToCheck.Invoke()); addSeekStep(startTime + 100); - AddAssert($"End vector {(isSnakingExpected ? "increased" : "is same")}", () => + AddAssert($"{positionDescription} {assertionDescription}", () => { - var currentVector = getCurrentSliderVector(true); - return isSnakingExpected ? currentVector.X > savedVector.X && currentVector.Y > savedVector.Y : currentVector == savedVector; + var currentPosition = positionToCheck.Invoke(); + return positionAssertion.Invoke(previousPosition, currentPosition); }); } - private void testSnakingOut(double startTime, bool isSnakingExpected, bool testSliderEnd) - { - addSeekStep(startTime); - AddStep($"Save {(testSliderEnd ? "end" : "start")} vector", () => savedVector = getCurrentSliderVector(testSliderEnd)); - addSeekStep(startTime + 100); - AddAssert($"{(testSliderEnd ? "End" : "Start")} vector {(isSnakingExpected ? (testSliderEnd ? "decreased" : "increased") : "is same")}", () => - { - var currentVector = getCurrentSliderVector(testSliderEnd); - - bool check(Vector2 a, Vector2 b) - { - if (testSliderEnd) - return a.X < b.X && a.Y < b.Y; - - return a.X > b.X && a.Y > b.Y; - } - - return isSnakingExpected ? check(currentVector, savedVector) : currentVector == savedVector; - }); - } - - private Vector2 getCurrentSliderVector(bool getEndOne) - { - var body = (PlaySliderBody)slider.Body.Drawable; - return getEndOne ? body.CurrentCurve.Last() : body.CurrentCurve.First(); - } - private void setSnaking(bool value) { AddStep($"{(value ? "Enable" : "Disable")} snaking", () => From c817cc726afdddd759dd4ee4379b2bf8b5548bb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 5 Apr 2020 15:37:31 +0200 Subject: [PATCH 0841/2376] Rewrite repeat arrow test --- .../TestSceneSliderSnaking.cs | 40 +++++++++---------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs index 19b05f6b51..e26a91eb0e 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs @@ -59,8 +59,6 @@ namespace osu.Game.Rulesets.Osu.Tests } private DrawableSlider slider; - private DrawableSliderRepeat repeat; - private Vector2 savedVector; [SetUpSteps] public override void SetUpSteps() { } @@ -112,31 +110,24 @@ namespace osu.Game.Rulesets.Osu.Tests } } - [TestCase(true)] - [TestCase(false)] - public void TestArrowMovement(bool isHit) + [Test] + public void TestRepeatArrowDoesNotMoveWhenHit() { - AddStep($"{(isHit ? "enable" : "disable")} autoplay", () => autoplay = isHit); + AddStep("enable autoplay", () => autoplay = true); setSnaking(true); base.SetUpSteps(); - addSeekStep(16500); - AddStep("retrieve 2nd slider repeat", () => - { - var drawable = Player.DrawableRuleset.Playfield.AllHitObjects.ElementAt(1); - repeat = drawable.ChildrenOfType>().First().Children.First(); - }); - AddStep("Save repeat vector", () => savedVector = repeat.Position); - addSeekStep(16700); + checkPositionChange(16600, sliderRepeat, positionAlmostSame); + } - AddAssert($"Repeat vector {(isHit ? "is same" : "decreased")}", () => - { - if (isHit) - // Precision.AlmostEquals is used because repeat might have a chance to update its position depending on where in the frame its hit - return Precision.AlmostEquals(savedVector, repeat.Position, 1); + [Test] + public void TestRepeatArrowMovesWhenNotHit() + { + AddStep("disable autoplay", () => autoplay = false); + setSnaking(true); + base.SetUpSteps(); - return repeat.X < savedVector.X && repeat.Y < savedVector.Y; - }); + checkPositionChange(16600, sliderRepeat, positionDecreased); } private void retrieveSlider(int index) => AddStep($"retrieve {(index + 1).ToOrdinalWords()} slider", () => @@ -166,10 +157,17 @@ namespace osu.Game.Rulesets.Osu.Tests private List sliderCurve => ((PlaySliderBody)slider.Body.Drawable).CurrentCurve; private Vector2 sliderStart() => sliderCurve.First(); private Vector2 sliderEnd() => sliderCurve.Last(); + private Vector2 sliderRepeat() + { + var drawable = Player.DrawableRuleset.Playfield.AllHitObjects.ElementAt(1); + var repeat = drawable.ChildrenOfType>().First().Children.First(); + return repeat.Position; + } private bool positionRemainsSame(Vector2 previous, Vector2 current) => previous == current; private bool positionIncreased(Vector2 previous, Vector2 current) => current.X > previous.X && current.Y > previous.Y; private bool positionDecreased(Vector2 previous, Vector2 current) => current.X < previous.X && current.Y < previous.Y; + private bool positionAlmostSame(Vector2 previous, Vector2 current) => Precision.AlmostEquals(previous, current, 1); private void checkPositionChange(double startTime, Func positionToCheck, Func positionAssertion) { From 7135c997466f0ed207390a8f96604b28fcf0d464 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 5 Apr 2020 15:39:32 +0200 Subject: [PATCH 0842/2376] Final cleanups --- osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs index e26a91eb0e..2eee5c4825 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs @@ -157,6 +157,7 @@ namespace osu.Game.Rulesets.Osu.Tests private List sliderCurve => ((PlaySliderBody)slider.Body.Drawable).CurrentCurve; private Vector2 sliderStart() => sliderCurve.First(); private Vector2 sliderEnd() => sliderCurve.Last(); + private Vector2 sliderRepeat() { var drawable = Player.DrawableRuleset.Playfield.AllHitObjects.ElementAt(1); @@ -188,7 +189,7 @@ namespace osu.Game.Rulesets.Osu.Tests private void setSnaking(bool value) { - AddStep($"{(value ? "Enable" : "Disable")} snaking", () => + AddStep($"{(value ? "enable" : "disable")} snaking", () => { snakingIn.Value = value; snakingOut.Value = value; From f9e44ae53ee8b7116a87fc4fa43b18ccaf5f2a96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 5 Apr 2020 15:53:56 +0200 Subject: [PATCH 0843/2376] Bring back comment about AlmostEquals --- osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs index 2eee5c4825..75adbd0987 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; @@ -117,6 +117,8 @@ namespace osu.Game.Rulesets.Osu.Tests setSnaking(true); base.SetUpSteps(); + // repeat might have a chance to update its position depending on where in the frame its hit, + // so some leniency is allowed here instead of checking strict equality checkPositionChange(16600, sliderRepeat, positionAlmostSame); } From 25c96744870547b4e0297c4b41495d73cd891e24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 5 Apr 2020 15:54:15 +0200 Subject: [PATCH 0844/2376] Rename method to justify its existence better --- osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs index 75adbd0987..e320cfff45 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; @@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Osu.Tests AddUntilStep("wait for track to start running", () => track.IsRunning); double startTime = hitObjects[sliderIndex].StartTime; - retrieveSlider(sliderIndex); + retrieveDrawableSlider(sliderIndex); setSnaking(true); ensureSnakingIn(startTime + fade_in_modifier); @@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.Osu.Tests AddUntilStep("wait for track to start running", () => track.IsRunning); double startTime = hitObjects[sliderIndex].StartTime; - retrieveSlider(sliderIndex); + retrieveDrawableSlider(sliderIndex); setSnaking(false); ensureNoSnakingIn(startTime + fade_in_modifier); @@ -132,7 +132,7 @@ namespace osu.Game.Rulesets.Osu.Tests checkPositionChange(16600, sliderRepeat, positionDecreased); } - private void retrieveSlider(int index) => AddStep($"retrieve {(index + 1).ToOrdinalWords()} slider", () => + private void retrieveDrawableSlider(int index) => AddStep($"retrieve {(index + 1).ToOrdinalWords()} slider", () => { slider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.ElementAt(index); }); From 3ff27816be8ac5d98bbbd4ee6aa90469271bc130 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 5 Apr 2020 15:54:50 +0200 Subject: [PATCH 0845/2376] Trim excess newlines --- osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs index e320cfff45..f5b20fd1c5 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs @@ -233,7 +233,6 @@ namespace osu.Game.Rulesets.Osu.Tests }), RepeatCount = 1, }, - new Slider { StartTime = 23000, @@ -245,7 +244,6 @@ namespace osu.Game.Rulesets.Osu.Tests }), RepeatCount = 2, }, - new HitCircle { StartTime = 199999, From 0eaea8ef9da45538ba1439de7c54157bba942673 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 5 Apr 2020 21:29:03 +0300 Subject: [PATCH 0846/2376] Create a constructor for break period For simple construction of break periods (e.g. filling a method with an array of break periods inside a test case) --- .../Visual/Gameplay/TestSceneBreakTracker.cs | 23 ++++++------------- .../Beatmaps/Formats/LegacyBeatmapDecoder.cs | 7 ++---- osu.Game/Beatmaps/Timing/BreakPeriod.cs | 11 +++++++++ 3 files changed, 20 insertions(+), 21 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs index ff25e609c1..91d6f2f143 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs @@ -26,16 +26,8 @@ namespace osu.Game.Tests.Visual.Gameplay private readonly IReadOnlyList testBreaks = new List { - new BreakPeriod - { - StartTime = 1000, - EndTime = 5000, - }, - new BreakPeriod - { - StartTime = 6000, - EndTime = 13500, - }, + new BreakPeriod(1000, 5000), + new BreakPeriod(6000, 13500), }; public TestSceneBreakTracker() @@ -70,7 +62,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestNoEffectsBreak() { - var shortBreak = new BreakPeriod { EndTime = 500 }; + var shortBreak = new BreakPeriod(0, 500); setClock(true); loadBreaksStep("short break", new[] { shortBreak }); @@ -127,13 +119,12 @@ namespace osu.Game.Tests.Visual.Gameplay private void addShowBreakStep(double seconds) { - AddStep($"show '{seconds}s' break", () => breakOverlay.Breaks = breakTracker.Breaks = new List + AddStep($"show '{seconds}s' break", () => { - new BreakPeriod + breakOverlay.Breaks = breakTracker.Breaks = new List { - StartTime = Clock.CurrentTime, - EndTime = Clock.CurrentTime + seconds * 1000, - } + new BreakPeriod(Clock.CurrentTime, Clock.CurrentTime + seconds * 1000) + }; }); } diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index f5b27eddd2..33bb9774df 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -305,12 +305,9 @@ namespace osu.Game.Beatmaps.Formats case LegacyEventType.Break: double start = getOffsetTime(Parsing.ParseDouble(split[1])); + double end = Math.Max(start, getOffsetTime(Parsing.ParseDouble(split[2]))); - var breakEvent = new BreakPeriod - { - StartTime = start, - EndTime = Math.Max(start, getOffsetTime(Parsing.ParseDouble(split[2]))) - }; + var breakEvent = new BreakPeriod(start, end); if (!breakEvent.HasEffect) return; diff --git a/osu.Game/Beatmaps/Timing/BreakPeriod.cs b/osu.Game/Beatmaps/Timing/BreakPeriod.cs index 5d79c7a86b..bb8ae4a66a 100644 --- a/osu.Game/Beatmaps/Timing/BreakPeriod.cs +++ b/osu.Game/Beatmaps/Timing/BreakPeriod.cs @@ -32,6 +32,17 @@ namespace osu.Game.Beatmaps.Timing /// public bool HasEffect => Duration >= MIN_BREAK_DURATION; + /// + /// Constructs a new break period. + /// + /// The start time of the break period. + /// The end time of the break period. + public BreakPeriod(double startTime, double endTime) + { + StartTime = startTime; + EndTime = endTime; + } + /// /// Whether this break contains a specified time. /// From e71a9668a537b2616b71b8f192c37671e2447553 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 5 Apr 2020 21:32:55 +0300 Subject: [PATCH 0847/2376] Disallow draining in non-draining sections --- .../Scoring/DrainingHealthProcessor.cs | 57 ++++++++++++++++--- 1 file changed, 50 insertions(+), 7 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs index fffcbb3c9f..b36e42326c 100644 --- a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs @@ -3,7 +3,9 @@ using System; using System.Collections.Generic; +using System.Linq; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; @@ -47,6 +49,34 @@ namespace osu.Game.Rulesets.Scoring private double targetMinimumHealth; private double drainRate = 1; + private readonly List<(double startTime, double endTime)> nonDrainSections = new List<(double, double)>(); + private int currentNonDrainSection; + + private bool isInNonDrainSection + { + get + { + if (nonDrainSections.Count == 0) + return false; + + var time = Time.Current; + + if (time > nonDrainSections[currentNonDrainSection].endTime) + { + while (time > nonDrainSections[currentNonDrainSection].endTime && currentNonDrainSection < nonDrainSections.Count - 1) + currentNonDrainSection++; + } + else + { + while (time < nonDrainSections[currentNonDrainSection].startTime && currentNonDrainSection > 0) + currentNonDrainSection--; + } + + var closestSection = nonDrainSections[currentNonDrainSection]; + return time >= closestSection.startTime && time <= closestSection.endTime; + } + } + /// /// Creates a new . /// @@ -60,23 +90,36 @@ namespace osu.Game.Rulesets.Scoring { base.Update(); - if (!IsBreakTime.Value) - { - // When jumping in and out of gameplay time within a single frame, health should only be drained for the period within the gameplay time - double lastGameplayTime = Math.Clamp(Time.Current - Time.Elapsed, drainStartTime, gameplayEndTime); - double currentGameplayTime = Math.Clamp(Time.Current, drainStartTime, gameplayEndTime); + if (isInNonDrainSection) + return; - Health.Value -= drainRate * (currentGameplayTime - lastGameplayTime); - } + // When jumping in and out of gameplay time within a single frame, health should only be drained for the period within the gameplay time + double lastGameplayTime = Math.Clamp(Time.Current - Time.Elapsed, drainStartTime, gameplayEndTime); + double currentGameplayTime = Math.Clamp(Time.Current, drainStartTime, gameplayEndTime); + + Health.Value -= drainRate * (currentGameplayTime - lastGameplayTime); } public override void ApplyBeatmap(IBeatmap beatmap) { + nonDrainSections.Clear(); + this.beatmap = beatmap; if (beatmap.HitObjects.Count > 0) gameplayEndTime = beatmap.HitObjects[^1].GetEndTime(); + // Ranges between the end of last hit object before a break + // and the start of first hit object after a break should + // not allow HP draining. (with break periods in) + foreach (BreakPeriod b in beatmap.Breaks) + { + var startTime = beatmap.HitObjects.LastOrDefault(h => h.GetEndTime() < b.StartTime)?.GetEndTime() ?? double.MinValue; + var endTime = beatmap.HitObjects.FirstOrDefault(h => h.StartTime > b.EndTime)?.StartTime ?? double.MaxValue; + + nonDrainSections.Add((startTime, endTime)); + } + targetMinimumHealth = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, min_health_target, mid_health_target, max_health_target); base.ApplyBeatmap(beatmap); From 7fab07670ed8ff6725bc4b8c1c543e0101af9664 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 5 Apr 2020 21:35:09 +0300 Subject: [PATCH 0848/2376] Remove no longer necessary usage of IsBreakTime --- osu.Game/Rulesets/Scoring/HealthProcessor.cs | 5 ----- osu.Game/Screens/Play/Player.cs | 2 -- 2 files changed, 7 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/HealthProcessor.cs b/osu.Game/Rulesets/Scoring/HealthProcessor.cs index 45edc0f4a3..1535fe4d00 100644 --- a/osu.Game/Rulesets/Scoring/HealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/HealthProcessor.cs @@ -26,11 +26,6 @@ namespace osu.Game.Rulesets.Scoring /// public readonly BindableDouble Health = new BindableDouble(1) { MinValue = 0, MaxValue = 1 }; - /// - /// Whether gameplay is currently in a break. - /// - public readonly IBindable IsBreakTime = new Bindable(); - /// /// Whether this ScoreProcessor has already triggered the failed state. /// diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 4597ae760c..f2051790db 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -243,8 +243,6 @@ namespace osu.Game.Screens.Play Breaks = working.Beatmap.Breaks } }); - - HealthProcessor.IsBreakTime.BindTo(breakTracker.IsBreakTime); } private void addOverlayComponents(Container target, WorkingBeatmap working) From c902ba40860b2757ea960925322bf9731b4eeefc Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 5 Apr 2020 21:46:07 +0300 Subject: [PATCH 0849/2376] Add test cases for HP draining not applied before a break and after it --- .../TestSceneDrainingHealthProcessor.cs | 87 ++++++++++++++----- 1 file changed, 66 insertions(+), 21 deletions(-) diff --git a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs index 885abb61b5..2f83ea4832 100644 --- a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs +++ b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs @@ -1,13 +1,15 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; +using System.Linq; using NUnit.Framework; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Utils; using osu.Framework.Testing; using osu.Framework.Timing; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; @@ -18,7 +20,6 @@ namespace osu.Game.Tests.Gameplay [HeadlessTest] public class TestSceneDrainingHealthProcessor : OsuTestScene { - private Bindable breakTime; private HealthProcessor processor; private ManualClock clock; @@ -41,6 +42,55 @@ namespace osu.Game.Tests.Gameplay assertHealthEqualTo(1); } + [Test] + public void TestHealthNotDrainedBeforeBreak() + { + createProcessor(createBeatmap(0, 2000, + new BreakPeriod(400, 600), new BreakPeriod(1200, 1400))); + + setTime(300); + setHealth(1); + + setTime(400); + assertHealthEqualTo(1); + + setTime(1100); + setHealth(1); + + setTime(1200); + assertHealthEqualTo(1); + } + + [Test] + public void TestHealthNotDrainedDuringBreak() + { + createProcessor(createBeatmap(0, 2000, new BreakPeriod(0, 1200))); + + setTime(700); + assertHealthEqualTo(1); + setTime(900); + assertHealthEqualTo(1); + } + + [Test] + public void TestHealthNotDrainedAfterBreak() + { + createProcessor(createBeatmap(0, 2000, + new BreakPeriod(400, 600), new BreakPeriod(1200, 1400))); + + setTime(600); + setHealth(1); + + setTime(700); + assertHealthEqualTo(1); + + setTime(1400); + setHealth(1); + + setTime(1500); + assertHealthEqualTo(1); + } + [Test] public void TestHealthNotDrainedAfterGameplayEnd() { @@ -54,18 +104,6 @@ namespace osu.Game.Tests.Gameplay assertHealthEqualTo(1); } - [Test] - public void TestHealthNotDrainedDuringBreak() - { - createProcessor(createBeatmap(0, 2000)); - setBreak(true); - - setTime(700); - assertHealthEqualTo(1); - setTime(900); - assertHealthEqualTo(1); - } - [Test] public void TestHealthDrainedDuringGameplay() { @@ -112,30 +150,39 @@ namespace osu.Game.Tests.Gameplay assertHealthNotEqualTo(1); } - private Beatmap createBeatmap(double startTime, double endTime) + private Beatmap createBeatmap(double startTime, double endTime, params BreakPeriod[] breaks) { var beatmap = new Beatmap { BeatmapInfo = { BaseDifficulty = { DrainRate = 5 } }, }; - for (double time = startTime; time <= endTime; time += 100) + double time = startTime; + + while (time <= endTime) + { beatmap.HitObjects.Add(new JudgeableHitObject { StartTime = time }); + // leave a 100ms gap between the start and end of a break period. + time += (getCurrentBreak(breaks, time)?.Duration ?? 0) + 100; + } + + beatmap.Breaks.AddRange(breaks); + + static BreakPeriod getCurrentBreak(IEnumerable breaks, double time) => + breaks?.FirstOrDefault(b => time >= b.StartTime && time <= b.EndTime); + return beatmap; } private void createProcessor(Beatmap beatmap) => AddStep("create processor", () => { - breakTime = new Bindable(); - Child = processor = new DrainingHealthProcessor(beatmap.HitObjects[0].StartTime).With(d => { d.RelativeSizeAxes = Axes.Both; d.Clock = new FramedClock(clock = new ManualClock()); }); - processor.IsBreakTime.BindTo(breakTime); processor.ApplyBeatmap(beatmap); }); @@ -143,8 +190,6 @@ namespace osu.Game.Tests.Gameplay private void setHealth(double health) => AddStep($"set health = {health}", () => processor.Health.Value = health); - private void setBreak(bool enabled) => AddStep($"{(enabled ? "enable" : "disable")} break", () => breakTime.Value = enabled); - private void assertHealthEqualTo(double value) => AddAssert($"health = {value}", () => Precision.AlmostEquals(value, processor.Health.Value, 0.0001f)); From 1b76a53d329acbe4c3dc73800e867b69277e5a0a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 5 Apr 2020 22:10:35 +0300 Subject: [PATCH 0850/2376] Move CatchTheBeat section handling to LegacySkinDecoder Best place to reside at --- osu.Game/Beatmaps/Formats/LegacyDecoder.cs | 3 --- osu.Game/Skinning/LegacySkinDecoder.cs | 6 ++++++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index 743a470e6e..113526f9dd 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -73,9 +73,6 @@ namespace osu.Game.Beatmaps.Formats switch (section) { case Section.Colours: - // osu!catch section only has colour settings - // so no harm in handling the entire section - case Section.CatchTheBeat: HandleColours(output, line); return; } diff --git a/osu.Game/Skinning/LegacySkinDecoder.cs b/osu.Game/Skinning/LegacySkinDecoder.cs index 88ba7b23b7..b5734edacf 100644 --- a/osu.Game/Skinning/LegacySkinDecoder.cs +++ b/osu.Game/Skinning/LegacySkinDecoder.cs @@ -44,6 +44,12 @@ namespace osu.Game.Skinning } break; + + // osu!catch section only has colour settings + // so no harm in handling the entire section + case Section.CatchTheBeat: + HandleColours(skin, line); + return; } if (!string.IsNullOrEmpty(pair.Key)) From 7f3ad6d5be7b79358f6c1d9ed2c44c3e60a30dda Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 5 Apr 2020 22:15:11 +0300 Subject: [PATCH 0851/2376] Move default colour fallback to the extension methods itself --- osu.Game.Rulesets.Catch/Skinning/CatchSkinExtensions.cs | 6 +++++- osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs | 3 +-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchSkinExtensions.cs b/osu.Game.Rulesets.Catch/Skinning/CatchSkinExtensions.cs index 8fc0831918..623f87bf11 100644 --- a/osu.Game.Rulesets.Catch/Skinning/CatchSkinExtensions.cs +++ b/osu.Game.Rulesets.Catch/Skinning/CatchSkinExtensions.cs @@ -1,7 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using JetBrains.Annotations; using osu.Framework.Bindables; +using osu.Game.Rulesets.Catch.UI; using osu.Game.Skinning; using osuTK.Graphics; @@ -9,8 +11,10 @@ namespace osu.Game.Rulesets.Catch.Skinning { internal static class CatchSkinExtensions { + [NotNull] public static IBindable GetHyperDashFruitColour(this ISkin skin) => skin.GetConfig(CatchSkinColour.HyperDashFruit) ?? - skin.GetConfig(CatchSkinColour.HyperDash); + skin.GetConfig(CatchSkinColour.HyperDash) ?? + new Bindable(Catcher.DefaultHyperDashColour); } } diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs index d8489399d2..470c12559e 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs @@ -7,7 +7,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Catch.Objects.Drawables; -using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Skinning; using osuTK; @@ -57,7 +56,7 @@ namespace osu.Game.Rulesets.Catch.Skinning var hyperDash = new Sprite { Texture = skin.GetTexture(lookupName), - Colour = skin.GetHyperDashFruitColour()?.Value ?? Catcher.DefaultHyperDashColour, + Colour = skin.GetHyperDashFruitColour().Value, Anchor = Anchor.Centre, Origin = Anchor.Centre, Blending = BlendingParameters.Additive, From 0f11ecce018984ccac589d126052e35b7a500b3b Mon Sep 17 00:00:00 2001 From: Joehu Date: Sun, 5 Apr 2020 14:53:49 -0700 Subject: [PATCH 0852/2376] Make icons container private --- osu.Game/Screens/Play/HUD/ModDisplay.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ModDisplay.cs b/osu.Game/Screens/Play/HUD/ModDisplay.cs index cd15886c0b..99c31241f1 100644 --- a/osu.Game/Screens/Play/HUD/ModDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ModDisplay.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.Play.HUD } } - protected readonly FillFlowContainer IconsContainer; + private readonly FillFlowContainer iconsContainer; private readonly OsuSpriteText unrankedText; public ModDisplay() @@ -50,7 +50,7 @@ namespace osu.Game.Screens.Play.HUD Children = new Drawable[] { - IconsContainer = new ReverseChildIDFillFlowContainer + iconsContainer = new ReverseChildIDFillFlowContainer { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, @@ -68,11 +68,11 @@ namespace osu.Game.Screens.Play.HUD Current.ValueChanged += mods => { - IconsContainer.Clear(); + iconsContainer.Clear(); foreach (Mod mod in mods.NewValue) { - IconsContainer.Add(new ModIcon(mod) { Scale = new Vector2(0.6f) }); + iconsContainer.Add(new ModIcon(mod) { Scale = new Vector2(0.6f) }); } if (IsLoaded) @@ -91,7 +91,7 @@ namespace osu.Game.Screens.Play.HUD base.LoadComplete(); appearTransform(); - IconsContainer.FadeInFromZero(fade_duration, Easing.OutQuint); + iconsContainer.FadeInFromZero(fade_duration, Easing.OutQuint); } private void appearTransform() @@ -103,20 +103,20 @@ namespace osu.Game.Screens.Play.HUD expand(); - using (IconsContainer.BeginDelayedSequence(1200)) + using (iconsContainer.BeginDelayedSequence(1200)) contract(); } private void expand() { if (ExpansionMode != ExpansionMode.AlwaysContracted) - IconsContainer.TransformSpacingTo(new Vector2(5, 0), 500, Easing.OutQuint); + iconsContainer.TransformSpacingTo(new Vector2(5, 0), 500, Easing.OutQuint); } private void contract() { if (ExpansionMode != ExpansionMode.AlwaysExpanded) - IconsContainer.TransformSpacingTo(new Vector2(-25, 0), 500, Easing.OutQuint); + iconsContainer.TransformSpacingTo(new Vector2(-25, 0), 500, Easing.OutQuint); } protected override bool OnHover(HoverEvent e) From 57b6a91449bd557530baee213fe82c2ecc5f9692 Mon Sep 17 00:00:00 2001 From: Joehu Date: Sun, 5 Apr 2020 14:57:44 -0700 Subject: [PATCH 0853/2376] Remove unnecessary input override on footer button mods Was used when it expanded on hover, but doesn't anymore. --- osu.Game/Screens/Select/FooterButtonMods.cs | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/Select/FooterButtonMods.cs b/osu.Game/Screens/Select/FooterButtonMods.cs index b18301c082..02333da0dc 100644 --- a/osu.Game/Screens/Select/FooterButtonMods.cs +++ b/osu.Game/Screens/Select/FooterButtonMods.cs @@ -27,18 +27,19 @@ namespace osu.Game.Screens.Select } protected readonly OsuSpriteText MultiplierText; - private readonly FooterModDisplay modDisplay; + private readonly ModDisplay modDisplay; private Color4 lowMultiplierColour; private Color4 highMultiplierColour; public FooterButtonMods() { - ButtonContentContainer.Add(modDisplay = new FooterModDisplay + ButtonContentContainer.Add(modDisplay = new ModDisplay { Anchor = Anchor.Centre, Origin = Anchor.Centre, DisplayUnrankedText = false, - Scale = new Vector2(0.8f) + Scale = new Vector2(0.8f), + ExpansionMode = ExpansionMode.AlwaysContracted, }); ButtonContentContainer.Add(MultiplierText = new OsuSpriteText { @@ -84,15 +85,5 @@ namespace osu.Game.Screens.Select else modDisplay.FadeOut(); } - - private class FooterModDisplay : ModDisplay - { - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Parent?.Parent?.ReceivePositionalInputAt(screenSpacePos) ?? false; - - public FooterModDisplay() - { - ExpansionMode = ExpansionMode.AlwaysContracted; - } - } } } From cfa2404626674b5ba37673bb8fe8007f1c016ad9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Apr 2020 12:39:49 +0900 Subject: [PATCH 0854/2376] Remove explicit specification of new default --- osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs index c87a1d438b..ce0b9fe4b6 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Mania.Skinning if (tmp is IFramedAnimation tmpAnimation && tmpAnimation.FrameCount > 0) frameLength = Math.Max(1000 / 60.0, 170.0 / tmpAnimation.FrameCount); - explosion = skin.GetAnimation(imageName, true, false, startAtCurrentTime: true, frameLength: frameLength).With(d => + explosion = skin.GetAnimation(imageName, true, false, frameLength: frameLength).With(d => { if (d == null) return; From 66b8a8ad2eadf04b6dcd88b9ba9740044f4cf92e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Apr 2020 12:45:58 +0900 Subject: [PATCH 0855/2376] Remove stray default value specification --- .../Objects/Drawables/Connections/FollowPoint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs index 8bb324d02e..a981648444 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs @@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections Anchor = Anchor.Centre, Alpha = 0.5f, } - }, confineMode: ConfineMode.NoScaling); + }); } public double AnimationStartTime { get; set; } From 33c64428a891bb01b85a2e88fb2111edd454acea Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Apr 2020 13:04:32 +0900 Subject: [PATCH 0856/2376] Fix playback position being set incorrectly for IAnimationTimeReference --- osu.Game/Skinning/LegacySkinExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/LegacySkinExtensions.cs b/osu.Game/Skinning/LegacySkinExtensions.cs index 476e53bdaa..549571dec4 100644 --- a/osu.Game/Skinning/LegacySkinExtensions.cs +++ b/osu.Game/Skinning/LegacySkinExtensions.cs @@ -72,7 +72,7 @@ namespace osu.Game.Skinning if (timeReference != null) { Clock = timeReference.Clock; - PlaybackPosition = timeReference.AnimationStartTime - timeReference.Clock.CurrentTime; + PlaybackPosition = timeReference.Clock.CurrentTime - timeReference.AnimationStartTime; } } } From a4b4b7df211d2f2ddb80241c45dca36059513f11 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Apr 2020 13:04:46 +0900 Subject: [PATCH 0857/2376] Fix follow points not starting at correct time --- osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs index 487401c939..ba0003b5cd 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs @@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Skinning switch (osuComponent.Component) { case OsuSkinComponents.FollowPoint: - return this.GetAnimation(component.LookupName, true, false, true); + return this.GetAnimation(component.LookupName, true, false, true, startAtCurrentTime: false); case OsuSkinComponents.SliderFollowCircle: var followCircle = this.GetAnimation("sliderfollowcircle", true, true, true); From 018244826221080e9c0bba050787001c3aa0f107 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 6 Apr 2020 18:35:39 +0900 Subject: [PATCH 0858/2376] Fix performance when parsing mania skins --- osu.Game/Skinning/LegacyManiaSkinDecoder.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs index 3393fe09b3..eb90225d1c 100644 --- a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs +++ b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs @@ -108,6 +108,8 @@ namespace osu.Game.Skinning break; } } + + pendingLines.Clear(); } private void parseArrayValue(string value, float[] output) From eff17c2da57f26fdafff02fc979e949abf7611c6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 6 Apr 2020 19:02:50 +0900 Subject: [PATCH 0859/2376] Allow legacy skin textures from subpaths --- osu.Game/Skinning/LegacySkin.cs | 35 ++++++++++++-------- osu.Game/Skinning/LegacySkinResourceStore.cs | 3 +- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 3d3eac97f6..5e2d0fb25f 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -243,21 +243,24 @@ namespace osu.Game.Skinning public override Texture GetTexture(string componentName) { - componentName = getFallbackName(componentName); - - float ratio = 2; - var texture = Textures?.Get($"{componentName}@2x"); - - if (texture == null) + foreach (var name in getFallbackNames(componentName)) { - ratio = 1; - texture = Textures?.Get(componentName); + float ratio = 2; + var texture = Textures?.Get($"{name}@2x"); + + if (texture == null) + { + ratio = 1; + texture = Textures?.Get(name); + } + + if (texture != null) + texture.ScaleAdjust = ratio; + + return texture; } - if (texture != null) - texture.ScaleAdjust = ratio; - - return texture; + return null; } public override SampleChannel GetSample(ISampleInfo sampleInfo) @@ -277,10 +280,14 @@ namespace osu.Game.Skinning return null; } - private string getFallbackName(string componentName) + private IEnumerable getFallbackNames(string componentName) { + // May be something like "Gameplay/osu/approachcircle" from lazer, or "Arrows/note1" from a user skin. + yield return componentName; + + // Fall back to using the last piece for components coming from lazer (e.g. "Gameplay/osu/approachcircle" -> "approachcircle"). string lastPiece = componentName.Split('/').Last(); - return componentName.StartsWith("Gameplay/taiko/") ? "taiko-" + lastPiece : lastPiece; + yield return componentName.StartsWith("Gameplay/taiko/") ? "taiko-" + lastPiece : lastPiece; } } } diff --git a/osu.Game/Skinning/LegacySkinResourceStore.cs b/osu.Game/Skinning/LegacySkinResourceStore.cs index 249d48b34b..05d0dee05f 100644 --- a/osu.Game/Skinning/LegacySkinResourceStore.cs +++ b/osu.Game/Skinning/LegacySkinResourceStore.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using osu.Framework.Extensions; using osu.Framework.IO.Stores; using osu.Game.Database; @@ -27,7 +28,7 @@ namespace osu.Game.Skinning foreach (var filename in base.GetFilenames(name)) { - var path = getPathForFile(filename); + var path = getPathForFile(filename.ToStandardisedPath()); if (path != null) yield return path; } From 707a6269b3f4f153a1baf3b5272770b97475205e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 6 Apr 2020 19:03:37 +0900 Subject: [PATCH 0860/2376] Fix incorrect key texture lookup --- osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs index cbe2036343..78ea4b68ae 100644 --- a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Mania.Skinning { isLegacySkin = new Lazy(() => source.GetConfig(LegacySkinConfiguration.LegacySetting.Version) != null); hasKeyTexture = new Lazy(() => source.GetAnimation( - source.GetConfig( + GetConfig( new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.KeyImage, 0))?.Value ?? "mania-key1", true, true) != null); } From db6db861c069256915643494766c172dc3925cc9 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 6 Apr 2020 19:04:02 +0900 Subject: [PATCH 0861/2376] Implement mania note + key image configs --- .../Skinning/LegacyManiaSkinConfiguration.cs | 2 ++ osu.Game/Skinning/LegacyManiaSkinDecoder.cs | 18 ++++++++----- osu.Game/Skinning/LegacySkin.cs | 27 +++++++++++++++++++ 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs index ac257b8c80..603487a603 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs @@ -24,6 +24,8 @@ namespace osu.Game.Skinning public Dictionary CustomColours { get; set; } = new Dictionary(); + public Dictionary ImageLookups = new Dictionary(); + public readonly float[] ColumnLineWidth; public readonly float[] ColumnSpacing; public readonly float[] ColumnWidth; diff --git a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs index 3393fe09b3..b23b115c14 100644 --- a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs +++ b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs @@ -71,12 +71,6 @@ namespace osu.Game.Skinning { var pair = SplitKeyVal(line); - if (pair.Key.StartsWith("Colour")) - { - HandleColours(currentConfig, line); - continue; - } - switch (pair.Key) { case "ColumnLineWidth": @@ -106,6 +100,18 @@ namespace osu.Game.Skinning case "LightingNWidth": parseArrayValue(pair.Value, currentConfig.ExplosionWidth); break; + + case string _ when pair.Key.StartsWith("Colour"): + HandleColours(currentConfig, line); + break; + + case string _ when pair.Key.StartsWith("NoteImage"): + currentConfig.ImageLookups[pair.Key] = pair.Value; + break; + + case string _ when pair.Key.StartsWith("KeyImage"): + currentConfig.ImageLookups[pair.Key] = pair.Value; + break; } } } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 5e2d0fb25f..ff050d90e2 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -207,6 +207,30 @@ namespace osu.Game.Skinning case LegacyManiaSkinConfigurationLookups.ColumnLineColour: return SkinUtils.As(getCustomColour(existing, "ColourColumnLine")); + + case LegacyManiaSkinConfigurationLookups.NoteImage: + Debug.Assert(maniaLookup.TargetColumn != null); + return SkinUtils.As(getManiaImage(existing, $"NoteImage{maniaLookup.TargetColumn}")); + + case LegacyManiaSkinConfigurationLookups.HoldNoteHeadImage: + Debug.Assert(maniaLookup.TargetColumn != null); + return SkinUtils.As(getManiaImage(existing, $"NoteImage{maniaLookup.TargetColumn}H")); + + case LegacyManiaSkinConfigurationLookups.HoldNoteTailImage: + Debug.Assert(maniaLookup.TargetColumn != null); + return SkinUtils.As(getManiaImage(existing, $"NoteImage{maniaLookup.TargetColumn}T")); + + case LegacyManiaSkinConfigurationLookups.HoldNoteBodyImage: + Debug.Assert(maniaLookup.TargetColumn != null); + return SkinUtils.As(getManiaImage(existing, $"NoteImage{maniaLookup.TargetColumn}L")); + + case LegacyManiaSkinConfigurationLookups.KeyImage: + Debug.Assert(maniaLookup.TargetColumn != null); + return SkinUtils.As(getManiaImage(existing, $"KeyImage{maniaLookup.TargetColumn}")); + + case LegacyManiaSkinConfigurationLookups.KeyImageDown: + Debug.Assert(maniaLookup.TargetColumn != null); + return SkinUtils.As(getManiaImage(existing, $"KeyImage{maniaLookup.TargetColumn}D")); } return null; @@ -215,6 +239,9 @@ namespace osu.Game.Skinning private IBindable getCustomColour(IHasCustomColours source, string lookup) => source.CustomColours.TryGetValue(lookup, out var col) ? new Bindable(col) : null; + private IBindable getManiaImage(LegacyManiaSkinConfiguration source, string lookup) + => source.ImageLookups.TryGetValue(lookup, out var image) ? new Bindable(image) : null; + public override Drawable GetDrawableComponent(ISkinComponent component) { switch (component) From 8438ee7e0787fe472199571566482b3a21265b99 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 6 Apr 2020 19:35:27 +0900 Subject: [PATCH 0862/2376] Improve testing --- .../Skins/TestSceneSkinConfigurationLookup.cs | 77 ++++++++++++++----- 1 file changed, 57 insertions(+), 20 deletions(-) diff --git a/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs b/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs index 35313ee858..9c1a6a1346 100644 --- a/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs +++ b/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.IO; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -12,7 +13,10 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Textures; using osu.Framework.Testing; using osu.Game.Audio; +using osu.Game.IO; +using osu.Game.Rulesets.Osu; using osu.Game.Skinning; +using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Visual; using osuTK.Graphics; @@ -22,15 +26,15 @@ namespace osu.Game.Tests.Skins [HeadlessTest] public class TestSceneSkinConfigurationLookup : OsuTestScene { - private SkinSource source1; - private SkinSource source2; + private UserSkinSource userSource; + private BeatmapSkinSource beatmapSource; private SkinRequester requester; [SetUp] public void SetUp() => Schedule(() => { - Add(new SkinProvidingContainer(source1 = new SkinSource()) - .WithChild(new SkinProvidingContainer(source2 = new SkinSource()) + Add(new SkinProvidingContainer(userSource = new UserSkinSource()) + .WithChild(new SkinProvidingContainer(beatmapSource = new BeatmapSkinSource()) .WithChild(requester = new SkinRequester()))); }); @@ -39,8 +43,8 @@ namespace osu.Game.Tests.Skins { AddStep("Add config values", () => { - source1.Configuration.ConfigDictionary["Lookup"] = "source1"; - source2.Configuration.ConfigDictionary["Lookup"] = "source2"; + userSource.Configuration.ConfigDictionary["Lookup"] = "source1"; + beatmapSource.Configuration.ConfigDictionary["Lookup"] = "source2"; }); AddAssert("Check lookup finds source2", () => requester.GetConfig("Lookup")?.Value == "source2"); @@ -49,21 +53,21 @@ namespace osu.Game.Tests.Skins [Test] public void TestFloatLookup() { - AddStep("Add config values", () => source1.Configuration.ConfigDictionary["FloatTest"] = "1.1"); + AddStep("Add config values", () => userSource.Configuration.ConfigDictionary["FloatTest"] = "1.1"); AddAssert("Check float parse lookup", () => requester.GetConfig("FloatTest")?.Value == 1.1f); } [Test] public void TestBoolLookup() { - AddStep("Add config values", () => source1.Configuration.ConfigDictionary["BoolTest"] = "1"); + AddStep("Add config values", () => userSource.Configuration.ConfigDictionary["BoolTest"] = "1"); AddAssert("Check bool parse lookup", () => requester.GetConfig("BoolTest")?.Value == true); } [Test] public void TestEnumLookup() { - AddStep("Add config values", () => source1.Configuration.ConfigDictionary["Test"] = "Test2"); + AddStep("Add config values", () => userSource.Configuration.ConfigDictionary["Test"] = "Test2"); AddAssert("Check enum parse lookup", () => requester.GetConfig(LookupType.Test)?.Value == ValueType.Test2); } @@ -76,7 +80,7 @@ namespace osu.Game.Tests.Skins [Test] public void TestLookupNull() { - AddStep("Add config values", () => source1.Configuration.ConfigDictionary["Lookup"] = null); + AddStep("Add config values", () => userSource.Configuration.ConfigDictionary["Lookup"] = null); AddAssert("Check lookup null", () => { @@ -88,7 +92,7 @@ namespace osu.Game.Tests.Skins [Test] public void TestColourLookup() { - AddStep("Add config colour", () => source1.Configuration.CustomColours["Lookup"] = Color4.Red); + AddStep("Add config colour", () => userSource.Configuration.CustomColours["Lookup"] = Color4.Red); AddAssert("Check colour lookup", () => requester.GetConfig(new SkinCustomColourLookup("Lookup"))?.Value == Color4.Red); } @@ -101,7 +105,7 @@ namespace osu.Game.Tests.Skins [Test] public void TestWrongColourType() { - AddStep("Add config colour", () => source1.Configuration.CustomColours["Lookup"] = Color4.Red); + AddStep("Add config colour", () => userSource.Configuration.CustomColours["Lookup"] = Color4.Red); AddAssert("perform incorrect lookup", () => { @@ -127,26 +131,51 @@ namespace osu.Game.Tests.Skins [Test] public void TestEmptyComboColoursNoFallback() { - AddStep("Add custom combo colours to source1", () => source1.Configuration.AddComboColours( + AddStep("Add custom combo colours to source1", () => userSource.Configuration.AddComboColours( new Color4(100, 150, 200, 255), new Color4(55, 110, 166, 255), new Color4(75, 125, 175, 255) )); - AddStep("Disallow default colours fallback in source2", () => source2.Configuration.AllowDefaultComboColoursFallback = false); + AddStep("Disallow default colours fallback in source2", () => beatmapSource.Configuration.AllowDefaultComboColoursFallback = false); AddAssert("Check retrieved combo colours from source1", () => - requester.GetConfig>(GlobalSkinColours.ComboColours)?.Value?.SequenceEqual(source1.Configuration.ComboColours) ?? false); + requester.GetConfig>(GlobalSkinColours.ComboColours)?.Value?.SequenceEqual(userSource.Configuration.ComboColours) ?? false); } [Test] - public void TestLegacyVersionLookup() + public void TestNullBeatmapVersionFallsBackToUserSkin() { - AddStep("Set source1 version 2.3", () => source1.Configuration.LegacyVersion = 2.3m); - AddStep("Set source2 version null", () => source2.Configuration.LegacyVersion = null); + AddStep("Set source1 version 2.3", () => userSource.Configuration.LegacyVersion = 2.3m); + AddStep("Set source2 version null", () => beatmapSource.Configuration.LegacyVersion = null); AddAssert("Check legacy version lookup", () => requester.GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value == 2.3m); } + [Test] + public void TestSetBeatmapVersionNoFallback() + { + AddStep("Set source1 version 2.3", () => userSource.Configuration.LegacyVersion = 2.3m); + AddStep("Set source2 version null", () => beatmapSource.Configuration.LegacyVersion = 1.7m); + AddAssert("Check legacy version lookup", () => requester.GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value == 1.7m); + } + + [Test] + public void TestNullBeatmapAndUserVersionFallsBackToLatest() + { + AddStep("Set source1 version 2.3", () => userSource.Configuration.LegacyVersion = null); + AddStep("Set source2 version null", () => beatmapSource.Configuration.LegacyVersion = null); + AddAssert("Check legacy version lookup", + () => requester.GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value == LegacySkinConfiguration.LATEST_VERSION); + } + + [Test] + public void TestIniWithNoVersionFallsBackTo1() + { + AddStep("Parse skin with no version", () => userSource.Configuration = new LegacySkinDecoder().Decode(new LineBufferedReader(new MemoryStream()))); + AddStep("Set source2 version null", () => beatmapSource.Configuration.LegacyVersion = null); + AddAssert("Check legacy version lookup", () => requester.GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value == 1.0m); + } + public enum LookupType { Test @@ -159,14 +188,22 @@ namespace osu.Game.Tests.Skins Test3 } - public class SkinSource : LegacySkin + public class UserSkinSource : LegacySkin { - public SkinSource() + public UserSkinSource() : base(new SkinInfo(), null, null, string.Empty) { } } + public class BeatmapSkinSource : LegacyBeatmapSkin + { + public BeatmapSkinSource() + : base(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, null, null) + { + } + } + public class SkinRequester : Drawable, ISkin { private ISkinSource skin; From a4208f35c49c55ea1bbe4907d374e17a497cde30 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 6 Apr 2020 19:36:04 +0900 Subject: [PATCH 0863/2376] Make versionless skins fallback to version 1.0 --- osu.Game.Tests/Skins/LegacySkinDecoderTest.cs | 2 +- osu.Game/Skinning/LegacyBeatmapSkin.cs | 15 +++++++++++++++ osu.Game/Skinning/LegacySkin.cs | 7 ++----- osu.Game/Skinning/LegacySkinDecoder.cs | 7 +++++++ 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs b/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs index cef38bbbb8..aedf26ee75 100644 --- a/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs +++ b/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs @@ -106,7 +106,7 @@ namespace osu.Game.Tests.Skins var decoder = new LegacySkinDecoder(); using (var resStream = TestResources.OpenResource("skin-empty.ini")) using (var stream = new LineBufferedReader(resStream)) - Assert.IsNull(decoder.Decode(stream).LegacyVersion); + Assert.That(decoder.Decode(stream).LegacyVersion, Is.EqualTo(1.0m)); } } } diff --git a/osu.Game/Skinning/LegacyBeatmapSkin.cs b/osu.Game/Skinning/LegacyBeatmapSkin.cs index 1c39fc41bb..1190a330fe 100644 --- a/osu.Game/Skinning/LegacyBeatmapSkin.cs +++ b/osu.Game/Skinning/LegacyBeatmapSkin.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Audio; +using osu.Framework.Bindables; using osu.Framework.IO.Stores; using osu.Game.Beatmaps; @@ -18,6 +19,20 @@ namespace osu.Game.Skinning Configuration.AllowDefaultComboColoursFallback = false; } + public override IBindable GetConfig(TLookup lookup) + { + switch (lookup) + { + case LegacySkinConfiguration.LegacySetting s when s == LegacySkinConfiguration.LegacySetting.Version: + if (Configuration.LegacyVersion is decimal version) + return SkinUtils.As(new Bindable(version)); + + return null; + } + + return base.GetConfig(lookup); + } + private static SkinInfo createSkinInfo(BeatmapInfo beatmap) => new SkinInfo { Name = beatmap.ToString(), Creator = beatmap.Metadata.Author.ToString() }; } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 3d3eac97f6..a68ae11288 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -71,7 +71,7 @@ namespace osu.Game.Skinning } } else - Configuration = new LegacySkinConfiguration { LegacyVersion = LegacySkinConfiguration.LATEST_VERSION }; + Configuration = new LegacySkinConfiguration(); } if (storage != null) @@ -122,10 +122,7 @@ namespace osu.Game.Skinning switch (legacy) { case LegacySkinConfiguration.LegacySetting.Version: - if (Configuration.LegacyVersion is decimal version) - return SkinUtils.As(new Bindable(version)); - - break; + return SkinUtils.As(new Bindable(Configuration.LegacyVersion ?? LegacySkinConfiguration.LATEST_VERSION)); } break; diff --git a/osu.Game/Skinning/LegacySkinDecoder.cs b/osu.Game/Skinning/LegacySkinDecoder.cs index 88ba7b23b7..5d4b8de7ac 100644 --- a/osu.Game/Skinning/LegacySkinDecoder.cs +++ b/osu.Game/Skinning/LegacySkinDecoder.cs @@ -52,5 +52,12 @@ namespace osu.Game.Skinning base.ParseLine(skin, section, line); } + + protected override LegacySkinConfiguration CreateTemplateObject() + { + var config = base.CreateTemplateObject(); + config.LegacyVersion = 1.0m; + return config; + } } } From 00f390c850e31f86c3fbe5df759524bfb9e4bb5e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Apr 2020 20:13:53 +0900 Subject: [PATCH 0864/2376] 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 77365b51a9..161a15fa4e 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 8d31fbf280..84c3c0ec8d 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -22,7 +22,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index e2b98720be..7a894facce 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -79,7 +79,7 @@ - + From 9ed0560da30f8af0f729b48395316899fe3e413b Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2020 11:37:56 +0000 Subject: [PATCH 0865/2376] Bump SharpCompress from 0.24.0 to 0.25.0 Bumps [SharpCompress](https://github.com/adamhathcock/sharpcompress) from 0.24.0 to 0.25.0. - [Release notes](https://github.com/adamhathcock/sharpcompress/releases) - [Commits](https://github.com/adamhathcock/sharpcompress/compare/0.24...0.25) Signed-off-by: dependabot-preview[bot] --- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 84c3c0ec8d..c62aec7250 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -25,7 +25,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 7a894facce..834c0ee956 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -80,7 +80,7 @@ - + From 678ac0f9e1f7e3a5378aa65386ee7cc428d11825 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2020 12:08:26 +0000 Subject: [PATCH 0866/2376] Bump Microsoft.Build.Traversal from 2.0.32 to 2.0.34 Bumps [Microsoft.Build.Traversal](https://github.com/Microsoft/MSBuildSdks) from 2.0.32 to 2.0.34. - [Release notes](https://github.com/Microsoft/MSBuildSdks/releases) - [Changelog](https://github.com/microsoft/MSBuildSdks/blob/master/RELEASE.md) - [Commits](https://github.com/Microsoft/MSBuildSdks/compare/Microsoft.Build.Traversal.2.0.32...Microsoft.Build.Traversal.2.0.34) Signed-off-by: dependabot-preview[bot] --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index 0223dc7330..6c793a3f1d 100644 --- a/global.json +++ b/global.json @@ -5,6 +5,6 @@ "version": "3.1.100" }, "msbuild-sdks": { - "Microsoft.Build.Traversal": "2.0.32" + "Microsoft.Build.Traversal": "2.0.34" } } \ No newline at end of file From b7308f5ed479623e67150e9886450436617410d3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 7 Apr 2020 00:26:38 +0900 Subject: [PATCH 0867/2376] Fix storyboard videos being offset incorrectly --- osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs index 2e7b66ea4f..a85936edf7 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs @@ -46,7 +46,6 @@ namespace osu.Game.Storyboards.Drawables Anchor = Anchor.Centre, Origin = Anchor.Centre, Alpha = 0, - PlaybackPosition = Video.StartTime }; } @@ -56,6 +55,8 @@ namespace osu.Game.Storyboards.Drawables if (video == null) return; + video.PlaybackPosition = Clock.CurrentTime - Video.StartTime; + using (video.BeginAbsoluteSequence(0)) video.FadeIn(500); } From 5dfa2a2bad5121104a5ada46c148c6bb139ebd6c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 7 Apr 2020 11:50:40 +0900 Subject: [PATCH 0868/2376] Fix step namings --- .../Skins/TestSceneSkinConfigurationLookup.cs | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs b/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs index 9c1a6a1346..685decf097 100644 --- a/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs +++ b/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs @@ -43,11 +43,11 @@ namespace osu.Game.Tests.Skins { AddStep("Add config values", () => { - userSource.Configuration.ConfigDictionary["Lookup"] = "source1"; - beatmapSource.Configuration.ConfigDictionary["Lookup"] = "source2"; + userSource.Configuration.ConfigDictionary["Lookup"] = "user skin"; + beatmapSource.Configuration.ConfigDictionary["Lookup"] = "beatmap skin"; }); - AddAssert("Check lookup finds source2", () => requester.GetConfig("Lookup")?.Value == "source2"); + AddAssert("Check lookup finds beatmap skin", () => requester.GetConfig("Lookup")?.Value == "beatmap skin"); } [Test] @@ -131,39 +131,39 @@ namespace osu.Game.Tests.Skins [Test] public void TestEmptyComboColoursNoFallback() { - AddStep("Add custom combo colours to source1", () => userSource.Configuration.AddComboColours( + AddStep("Add custom combo colours to user skin", () => userSource.Configuration.AddComboColours( new Color4(100, 150, 200, 255), new Color4(55, 110, 166, 255), new Color4(75, 125, 175, 255) )); - AddStep("Disallow default colours fallback in source2", () => beatmapSource.Configuration.AllowDefaultComboColoursFallback = false); + AddStep("Disallow default colours fallback in beatmap skin", () => beatmapSource.Configuration.AllowDefaultComboColoursFallback = false); - AddAssert("Check retrieved combo colours from source1", () => + AddAssert("Check retrieved combo colours from user skin", () => requester.GetConfig>(GlobalSkinColours.ComboColours)?.Value?.SequenceEqual(userSource.Configuration.ComboColours) ?? false); } [Test] public void TestNullBeatmapVersionFallsBackToUserSkin() { - AddStep("Set source1 version 2.3", () => userSource.Configuration.LegacyVersion = 2.3m); - AddStep("Set source2 version null", () => beatmapSource.Configuration.LegacyVersion = null); + AddStep("Set user skin version 2.3", () => userSource.Configuration.LegacyVersion = 2.3m); + AddStep("Set beatmap skin version null", () => beatmapSource.Configuration.LegacyVersion = null); AddAssert("Check legacy version lookup", () => requester.GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value == 2.3m); } [Test] public void TestSetBeatmapVersionNoFallback() { - AddStep("Set source1 version 2.3", () => userSource.Configuration.LegacyVersion = 2.3m); - AddStep("Set source2 version null", () => beatmapSource.Configuration.LegacyVersion = 1.7m); + AddStep("Set user skin version 2.3", () => userSource.Configuration.LegacyVersion = 2.3m); + AddStep("Set beatmap skin version null", () => beatmapSource.Configuration.LegacyVersion = 1.7m); AddAssert("Check legacy version lookup", () => requester.GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value == 1.7m); } [Test] public void TestNullBeatmapAndUserVersionFallsBackToLatest() { - AddStep("Set source1 version 2.3", () => userSource.Configuration.LegacyVersion = null); - AddStep("Set source2 version null", () => beatmapSource.Configuration.LegacyVersion = null); + AddStep("Set user skin version 2.3", () => userSource.Configuration.LegacyVersion = null); + AddStep("Set beatmap skin version null", () => beatmapSource.Configuration.LegacyVersion = null); AddAssert("Check legacy version lookup", () => requester.GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value == LegacySkinConfiguration.LATEST_VERSION); } @@ -172,7 +172,7 @@ namespace osu.Game.Tests.Skins public void TestIniWithNoVersionFallsBackTo1() { AddStep("Parse skin with no version", () => userSource.Configuration = new LegacySkinDecoder().Decode(new LineBufferedReader(new MemoryStream()))); - AddStep("Set source2 version null", () => beatmapSource.Configuration.LegacyVersion = null); + AddStep("Set beatmap skin version null", () => beatmapSource.Configuration.LegacyVersion = null); AddAssert("Check legacy version lookup", () => requester.GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value == 1.0m); } From 8506029237cc57cb8f5b9f457daf5185eb271325 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 7 Apr 2020 13:46:37 +0900 Subject: [PATCH 0869/2376] Fix SkinnableTestScene losing test resources on dynamic recompilation --- osu.Game/Tests/Visual/SkinnableTestScene.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Tests/Visual/SkinnableTestScene.cs b/osu.Game/Tests/Visual/SkinnableTestScene.cs index d0113b3096..71d3266d18 100644 --- a/osu.Game/Tests/Visual/SkinnableTestScene.cs +++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs @@ -26,6 +26,9 @@ namespace osu.Game.Tests.Visual private Skin specialSkin; private Skin oldSkin; + // Keep a static reference to ensure we don't use a dynamically recompiled DLL as a source (resources will be missing). + private static DllResourceStore dllStore; + protected SkinnableTestScene() : base(2, 3) { @@ -34,7 +37,7 @@ namespace osu.Game.Tests.Visual [BackgroundDependencyLoader] private void load(AudioManager audio, SkinManager skinManager) { - var dllStore = new DllResourceStore(GetType().Assembly); + dllStore ??= new DllResourceStore(GetType().Assembly); metricsSkin = new TestLegacySkin(new SkinInfo { Name = "metrics-skin" }, new NamespacedResourceStore(dllStore, "Resources/metrics_skin"), audio, true); defaultSkin = skinManager.GetSkin(DefaultLegacySkin.Info); From c46ea7bdef8a42c0f06a8920e60df8a6a4d8dea5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 7 Apr 2020 14:49:24 +0900 Subject: [PATCH 0870/2376] Add disposal, prevent memory leaks --- osu.Game/Online/Leaderboards/TopLocalRank.cs | 30 +++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/osu.Game/Online/Leaderboards/TopLocalRank.cs b/osu.Game/Online/Leaderboards/TopLocalRank.cs index be014dafc3..f355a907af 100644 --- a/osu.Game/Online/Leaderboards/TopLocalRank.cs +++ b/osu.Game/Online/Leaderboards/TopLocalRank.cs @@ -15,9 +15,14 @@ namespace osu.Game.Online.Leaderboards { private readonly BeatmapInfo beatmap; - private ScoreManager scores; - private IBindable ruleset; - private IAPIProvider api; + [Resolved] + private ScoreManager scores { get; set; } + + [Resolved] + private IBindable ruleset { get; set; } + + [Resolved] + private IAPIProvider api { get; set; } protected override double LoadDelay => 250; @@ -28,25 +33,19 @@ namespace osu.Game.Online.Leaderboards } [BackgroundDependencyLoader] - private void load(ScoreManager scores, IBindable ruleset, IAPIProvider api) + private void load() { scores.ItemAdded += scoreChanged; scores.ItemRemoved += scoreChanged; ruleset.ValueChanged += _ => fetchAndLoadTopScore(); - this.ruleset = ruleset.GetBoundCopy(); - this.scores = scores; - this.api = api; - fetchAndLoadTopScore(); } private void scoreChanged(ScoreInfo score) { if (score.BeatmapInfoID == beatmap.ID) - { fetchAndLoadTopScore(); - } } private void fetchAndLoadTopScore() @@ -79,5 +78,16 @@ namespace osu.Game.Online.Leaderboards .OrderByDescending(s => s.TotalScore) .FirstOrDefault(); } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (scores != null) + { + scores.ItemAdded -= scoreChanged; + scores.ItemRemoved -= scoreChanged; + } + } } } From 933314d724169677f5ec39a071dc009134d8b355 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 7 Apr 2020 14:50:11 +0900 Subject: [PATCH 0871/2376] Remove unnecessary method --- osu.Game/Online/Leaderboards/TopLocalRank.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/osu.Game/Online/Leaderboards/TopLocalRank.cs b/osu.Game/Online/Leaderboards/TopLocalRank.cs index f355a907af..3e77549851 100644 --- a/osu.Game/Online/Leaderboards/TopLocalRank.cs +++ b/osu.Game/Online/Leaderboards/TopLocalRank.cs @@ -50,14 +50,7 @@ namespace osu.Game.Online.Leaderboards private void fetchAndLoadTopScore() { - var score = fetchTopScore(); - - loadTopScore(score); - } - - private void loadTopScore(ScoreInfo score) - { - var rank = score?.Rank; + var rank = fetchTopScore()?.Rank; // toggle the display of this drawable // we do not want empty space if there is no rank to be displayed From ed17a1c99016cfd1668d2e8be9158a95ea3bcf7e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 7 Apr 2020 15:30:06 +0900 Subject: [PATCH 0872/2376] Improve visual display --- osu.Game/Online/Leaderboards/TopLocalRank.cs | 23 +++++++++++--------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/osu.Game/Online/Leaderboards/TopLocalRank.cs b/osu.Game/Online/Leaderboards/TopLocalRank.cs index 3e77549851..345e8cb221 100644 --- a/osu.Game/Online/Leaderboards/TopLocalRank.cs +++ b/osu.Game/Online/Leaderboards/TopLocalRank.cs @@ -4,6 +4,8 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Rulesets; @@ -24,8 +26,6 @@ namespace osu.Game.Online.Leaderboards [Resolved] private IAPIProvider api { get; set; } - protected override double LoadDelay => 250; - public TopLocalRank(BeatmapInfo beatmap) : base(null) { @@ -48,20 +48,23 @@ namespace osu.Game.Online.Leaderboards fetchAndLoadTopScore(); } + private ScheduledDelegate scheduledRankUpdate; + private void fetchAndLoadTopScore() { var rank = fetchTopScore()?.Rank; + scheduledRankUpdate = Schedule(() => + { + Rank = rank; - // toggle the display of this drawable - // we do not want empty space if there is no rank to be displayed - if (rank.HasValue) - Show(); - else - Hide(); - - Schedule(() => Rank = rank); + // Required since presence is changed via IsPresent override + Invalidate(Invalidation.Presence); + }); } + // We're present if a rank is set, or if there is a pending rank update (IsPresent = true is required for the scheduler to run). + public override bool IsPresent => base.IsPresent && (Rank != null || scheduledRankUpdate?.Completed == false); + private ScoreInfo fetchTopScore() { if (scores == null || beatmap == null || ruleset?.Value == null || api?.LocalUser.Value == null) From ed3e0a01e162720cf3c40482085f11cd1263179f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 7 Apr 2020 15:31:22 +0900 Subject: [PATCH 0873/2376] Re-namespace into song select --- osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs | 1 - .../Leaderboards => Screens/Select/Carousel}/TopLocalRank.cs | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) rename osu.Game/{Online/Leaderboards => Screens/Select/Carousel}/TopLocalRank.cs (97%) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index 5357f9a652..2520c70989 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -19,7 +19,6 @@ using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; -using osu.Game.Online.Leaderboards; using osu.Game.Overlays; using osuTK; using osuTK.Graphics; diff --git a/osu.Game/Online/Leaderboards/TopLocalRank.cs b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs similarity index 97% rename from osu.Game/Online/Leaderboards/TopLocalRank.cs rename to osu.Game/Screens/Select/Carousel/TopLocalRank.cs index 345e8cb221..e981550c84 100644 --- a/osu.Game/Online/Leaderboards/TopLocalRank.cs +++ b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs @@ -8,10 +8,11 @@ using osu.Framework.Graphics; using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Online.API; +using osu.Game.Online.Leaderboards; using osu.Game.Rulesets; using osu.Game.Scoring; -namespace osu.Game.Online.Leaderboards +namespace osu.Game.Screens.Select.Carousel { public class TopLocalRank : UpdateableRank { From 3ecb99462fce3ac20e5e3aefd5bc909de6c99a81 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 7 Apr 2020 16:07:18 +0900 Subject: [PATCH 0874/2376] Make note height scale by minimum column width --- osu.Game.Rulesets.Mania/Skinning/LegacyNotePiece.cs | 10 ++++++++-- osu.Game/Skinning/LegacyManiaSkinConfiguration.cs | 9 +++++++++ .../Skinning/LegacyManiaSkinConfigurationLookup.cs | 3 ++- osu.Game/Skinning/LegacyManiaSkinDecoder.cs | 4 ++++ osu.Game/Skinning/LegacySkin.cs | 3 +++ 5 files changed, 26 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyNotePiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyNotePiece.cs index d2ceb06d0b..85523ae3c0 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyNotePiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyNotePiece.cs @@ -20,6 +20,8 @@ namespace osu.Game.Rulesets.Mania.Skinning private Container directionContainer; private Sprite noteSprite; + private float? minimumColumnWidth; + public LegacyNotePiece() { RelativeSizeAxes = Axes.X; @@ -29,6 +31,8 @@ namespace osu.Game.Rulesets.Mania.Skinning [BackgroundDependencyLoader] private void load(ISkinSource skin, IScrollingInfo scrollingInfo) { + minimumColumnWidth = skin.GetConfig(new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.MinimumColumnWidth))?.Value; + InternalChild = directionContainer = new Container { Origin = Anchor.BottomCentre, @@ -47,8 +51,10 @@ namespace osu.Game.Rulesets.Mania.Skinning if (noteSprite.Texture != null) { - var scale = DrawWidth / noteSprite.Texture.DisplayWidth; - noteSprite.Scale = new Vector2(scale); + // The height is scaled to the minimum column width, if provided. + float minimumWidth = minimumColumnWidth ?? DrawWidth; + + noteSprite.Scale = Vector2.Divide(new Vector2(DrawWidth, minimumWidth), noteSprite.Texture.DisplayWidth); } } diff --git a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs index ac257b8c80..08b3b8ff5a 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using osu.Game.Beatmaps.Formats; using osuTK.Graphics; @@ -45,5 +46,13 @@ namespace osu.Game.Skinning ColumnLineWidth.AsSpan().Fill(2); ColumnWidth.AsSpan().Fill(DEFAULT_COLUMN_SIZE); } + + private float? minimumColumnWidth; + + public float MinimumColumnWidth + { + get => minimumColumnWidth ?? ColumnWidth.Min(); + set => minimumColumnWidth = value; + } } } diff --git a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs index 853d07c060..588e9e3ee2 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs @@ -36,6 +36,7 @@ namespace osu.Game.Skinning HoldNoteBodyImage, ExplosionImage, ExplosionScale, - ColumnLineColour + ColumnLineColour, + MinimumColumnWidth } } diff --git a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs index eb90225d1c..4fe36c2239 100644 --- a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs +++ b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs @@ -106,6 +106,10 @@ namespace osu.Game.Skinning case "LightingNWidth": parseArrayValue(pair.Value, currentConfig.ExplosionWidth); break; + + case "WidthForNoteHeightScale": + currentConfig.MinimumColumnWidth = float.Parse(pair.Value, CultureInfo.InvariantCulture) * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR; + break; } } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 3d3eac97f6..d5ef5220cf 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -207,6 +207,9 @@ namespace osu.Game.Skinning case LegacyManiaSkinConfigurationLookups.ColumnLineColour: return SkinUtils.As(getCustomColour(existing, "ColourColumnLine")); + + case LegacyManiaSkinConfigurationLookups.MinimumColumnWidth: + return SkinUtils.As(new Bindable(existing.MinimumColumnWidth)); } return null; From 2c840c52a3dfe841e4e77dcf6b4579e9165c65a7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 7 Apr 2020 15:38:29 +0900 Subject: [PATCH 0875/2376] Add skinnable test scene per ruleset to better allow dynamic compilation --- .../CatchSkinnableTestScene.cs | 19 +++++++++++++++++ .../TestSceneCatcher.cs | 8 +++---- .../TestSceneCatcherArea.cs | 3 +-- .../TestSceneFruitObjects.cs | 8 +++---- .../Skinning/ManiaSkinnableTestScene.cs | 9 ++++++++ .../OsuSkinnableTestScene.cs | 19 +++++++++++++++++ .../TestSceneDrawableJudgement.cs | 7 +++---- .../TestSceneGameplayCursor.cs | 14 +++++++++---- .../TestSceneHitCircle.cs | 3 +-- .../TestSceneSlider.cs | 3 +-- .../TaikoSkinnableTestScene.cs | 21 +++++++++++++++++++ .../TestSceneInputDrum.cs | 10 +++++---- 12 files changed, 98 insertions(+), 26 deletions(-) create mode 100644 osu.Game.Rulesets.Catch.Tests/CatchSkinnableTestScene.cs create mode 100644 osu.Game.Rulesets.Osu.Tests/OsuSkinnableTestScene.cs create mode 100644 osu.Game.Rulesets.Taiko.Tests/TaikoSkinnableTestScene.cs diff --git a/osu.Game.Rulesets.Catch.Tests/CatchSkinnableTestScene.cs b/osu.Game.Rulesets.Catch.Tests/CatchSkinnableTestScene.cs new file mode 100644 index 0000000000..f7f1a8d58f --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/CatchSkinnableTestScene.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 System; +using System.Collections.Generic; +using osu.Game.Rulesets.Catch.Skinning; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Catch.Tests +{ + public abstract class CatchSkinnableTestScene : SkinnableTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(CatchRuleset), + typeof(CatchLegacySkinTransformer), + }; + } +} diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs index fe0d512166..acc5f4e428 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs @@ -4,21 +4,21 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Game.Rulesets.Catch.UI; -using osu.Game.Tests.Visual; using System; using System.Collections.Generic; +using System.Linq; using osu.Framework.Graphics; namespace osu.Game.Rulesets.Catch.Tests { [TestFixture] - public class TestSceneCatcher : SkinnableTestScene + public class TestSceneCatcher : CatchSkinnableTestScene { - public override IReadOnlyList RequiredTypes => new[] + public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] { typeof(CatcherArea), typeof(CatcherSprite) - }; + }).ToList(); [BackgroundDependencyLoader] private void load() diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs index cf68c5424d..2b30edb70b 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs @@ -17,12 +17,11 @@ using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; -using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Catch.Tests { [TestFixture] - public class TestSceneCatcherArea : SkinnableTestScene + public class TestSceneCatcherArea : CatchSkinnableTestScene { private RulesetInfo catchRuleset; diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs index 82d5aa936f..cd674bb754 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs @@ -3,20 +3,20 @@ using System; using System.Collections.Generic; +using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces; -using osu.Game.Tests.Visual; using osuTK; namespace osu.Game.Rulesets.Catch.Tests { [TestFixture] - public class TestSceneFruitObjects : SkinnableTestScene + public class TestSceneFruitObjects : CatchSkinnableTestScene { - public override IReadOnlyList RequiredTypes => new[] + public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] { typeof(CatchHitObject), typeof(Fruit), @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Tests typeof(DrawableBanana), typeof(DrawableBananaShower), typeof(Pulp), - }; + }).ToList(); protected override void LoadComplete() { diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs index eaa2a56e36..009e609c56 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs @@ -1,12 +1,15 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; +using osu.Game.Rulesets.Mania.Skinning; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling.Algorithms; using osu.Game.Tests.Visual; @@ -24,6 +27,12 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning [Cached(Type = typeof(IScrollingInfo))] private readonly TestScrollingInfo scrollingInfo = new TestScrollingInfo(); + public override IReadOnlyList RequiredTypes => new[] + { + typeof(ManiaRuleset), + typeof(ManiaLegacySkinTransformer), + }; + protected ManiaSkinnableTestScene() { scrollingInfo.Direction.Value = ScrollingDirection.Down; diff --git a/osu.Game.Rulesets.Osu.Tests/OsuSkinnableTestScene.cs b/osu.Game.Rulesets.Osu.Tests/OsuSkinnableTestScene.cs new file mode 100644 index 0000000000..929ce5dcc0 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/OsuSkinnableTestScene.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 System; +using System.Collections.Generic; +using osu.Game.Rulesets.Osu.Skinning; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Osu.Tests +{ + public abstract class OsuSkinnableTestScene : SkinnableTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(OsuRuleset), + typeof(OsuLegacySkinTransformer), + }; + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs index 02d4406809..f867630df6 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs @@ -10,17 +10,16 @@ using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Scoring; -using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Osu.Tests { - public class TestSceneDrawableJudgement : SkinnableTestScene + public class TestSceneDrawableJudgement : OsuSkinnableTestScene { - public override IReadOnlyList RequiredTypes => new[] + public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] { typeof(DrawableJudgement), typeof(DrawableOsuJudgement) - }; + }).ToList(); public TestSceneDrawableJudgement() { diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs index 7b96e2ec6a..22dacc6f5e 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs @@ -3,26 +3,32 @@ using System; using System.Collections.Generic; +using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Testing.Input; using osu.Game.Configuration; +using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Rulesets.Osu.UI.Cursor; +using osu.Game.Rulesets.UI; using osu.Game.Screens.Play; -using osu.Game.Tests.Visual; using osuTK; namespace osu.Game.Rulesets.Osu.Tests { [TestFixture] - public class TestSceneGameplayCursor : SkinnableTestScene + public class TestSceneGameplayCursor : OsuSkinnableTestScene { - public override IReadOnlyList RequiredTypes => new[] + public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] { + typeof(GameplayCursorContainer), typeof(OsuCursorContainer), + typeof(OsuCursor), + typeof(LegacyCursor), + typeof(LegacyCursorTrail), typeof(CursorTrail) - }; + }).ToList(); [Cached] private GameplayBeatmap gameplayBeatmap; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs index ae5a28217c..e117729f01 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs @@ -14,12 +14,11 @@ using osu.Game.Rulesets.Mods; using System.Linq; using NUnit.Framework; using osu.Game.Rulesets.Scoring; -using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Osu.Tests { [TestFixture] - public class TestSceneHitCircle : SkinnableTestScene + public class TestSceneHitCircle : OsuSkinnableTestScene { public override IReadOnlyList RequiredTypes => new[] { diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs index a201364de4..eb6130c8a6 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs @@ -22,12 +22,11 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; -using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Osu.Tests { [TestFixture] - public class TestSceneSlider : SkinnableTestScene + public class TestSceneSlider : OsuSkinnableTestScene { public override IReadOnlyList RequiredTypes => new[] { diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoSkinnableTestScene.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoSkinnableTestScene.cs new file mode 100644 index 0000000000..6db2a6907f --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoSkinnableTestScene.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 System; +using System.Collections.Generic; +using osu.Game.Rulesets.Taiko.Skinning; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Taiko.Tests +{ + public abstract class TaikoSkinnableTestScene : SkinnableTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(TaikoRuleset), + typeof(TaikoLegacySkinTransformer), + }; + + protected override Ruleset CreateRulesetForSkinProvider() => new TaikoRuleset(); + } +} diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneInputDrum.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneInputDrum.cs index c79088056f..1928e9f66f 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneInputDrum.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneInputDrum.cs @@ -3,24 +3,26 @@ using System; using System.Collections.Generic; +using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osuTK; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Taiko.Skinning; using osu.Game.Rulesets.Taiko.UI; -using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Taiko.Tests { [TestFixture] - public class TestSceneInputDrum : SkinnableTestScene + public class TestSceneInputDrum : TaikoSkinnableTestScene { - public override IReadOnlyList RequiredTypes => new[] + public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] { typeof(InputDrum), - }; + typeof(LegacyInputDrum), + }).ToList(); [BackgroundDependencyLoader] private void load() From 0a340bac5a48abce060e0ee88b9ed86a3bd9c5c4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 7 Apr 2020 16:18:24 +0900 Subject: [PATCH 0876/2376] Ensure the correct (up-to-date) ruleset is retrieved --- osu.Game.Rulesets.Catch.Tests/CatchSkinnableTestScene.cs | 2 ++ .../Skinning/ManiaSkinnableTestScene.cs | 2 ++ osu.Game.Rulesets.Osu.Tests/OsuSkinnableTestScene.cs | 2 ++ osu.Game/Tests/Visual/SkinnableTestScene.cs | 6 +++++- 4 files changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch.Tests/CatchSkinnableTestScene.cs b/osu.Game.Rulesets.Catch.Tests/CatchSkinnableTestScene.cs index f7f1a8d58f..0c46b078b5 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchSkinnableTestScene.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchSkinnableTestScene.cs @@ -15,5 +15,7 @@ namespace osu.Game.Rulesets.Catch.Tests typeof(CatchRuleset), typeof(CatchLegacySkinTransformer), }; + + protected override Ruleset CreateRulesetForSkinProvider() => new CatchRuleset(); } } diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs index 009e609c56..7f0503913f 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs @@ -33,6 +33,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning typeof(ManiaLegacySkinTransformer), }; + protected override Ruleset CreateRulesetForSkinProvider() => new ManiaRuleset(); + protected ManiaSkinnableTestScene() { scrollingInfo.Direction.Value = ScrollingDirection.Down; diff --git a/osu.Game.Rulesets.Osu.Tests/OsuSkinnableTestScene.cs b/osu.Game.Rulesets.Osu.Tests/OsuSkinnableTestScene.cs index 929ce5dcc0..90ebbd9f04 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuSkinnableTestScene.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuSkinnableTestScene.cs @@ -15,5 +15,7 @@ namespace osu.Game.Rulesets.Osu.Tests typeof(OsuRuleset), typeof(OsuLegacySkinTransformer), }; + + protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset(); } } diff --git a/osu.Game/Tests/Visual/SkinnableTestScene.cs b/osu.Game/Tests/Visual/SkinnableTestScene.cs index 71d3266d18..50cc5b6c5c 100644 --- a/osu.Game/Tests/Visual/SkinnableTestScene.cs +++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets; using osu.Game.Skinning; using osuTK; using osuTK.Graphics; @@ -34,6 +35,9 @@ namespace osu.Game.Tests.Visual { } + // Required to be part of the per-ruleset implementation to construct the newer version of the Ruleset. + protected abstract Ruleset CreateRulesetForSkinProvider(); + [BackgroundDependencyLoader] private void load(AudioManager audio, SkinManager skinManager) { @@ -106,7 +110,7 @@ namespace osu.Game.Tests.Visual { new OutlineBox { Alpha = autoSize ? 1 : 0 }, mainProvider.WithChild( - new SkinProvidingContainer(Ruleset.Value.CreateInstance().CreateLegacySkinProvider(mainProvider, beatmap)) + new SkinProvidingContainer(CreateRulesetForSkinProvider().CreateLegacySkinProvider(mainProvider, beatmap)) { Child = created, RelativeSizeAxes = !autoSize ? Axes.Both : Axes.None, From 9071bf5cbb72bdc02bffd822620ddfa19efc75c0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 7 Apr 2020 16:18:34 +0900 Subject: [PATCH 0877/2376] Fix mania test scene not using mania skinnable test scene --- .../Skinning/TestSceneDrawableJudgement.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs index a6bc64550f..6ab8a68176 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs @@ -10,11 +10,10 @@ using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; -using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Mania.Tests.Skinning { - public class TestSceneDrawableJudgement : SkinnableTestScene + public class TestSceneDrawableJudgement : ManiaSkinnableTestScene { public override IReadOnlyList RequiredTypes => new[] { From 9cfeb60afc96f1dbdb9abc6a026cad59436aebd9 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 7 Apr 2020 16:30:58 +0900 Subject: [PATCH 0878/2376] Fix missed speed removal in mania --- osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index c8c537964f..14cad39b04 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -64,6 +64,7 @@ namespace osu.Game.Rulesets.Mania.UI { // Mania doesn't care about global velocity p.Velocity = 1; + p.BaseBeatLength *= Beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier; // For non-mania beatmap, speed changes should only happen through timing points if (!isForCurrentRuleset) From 9fd73492ca7c354668699620744d22bfdc0f36e4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 7 Apr 2020 16:50:08 +0900 Subject: [PATCH 0879/2376] Implement judgement line colour --- osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs | 5 +++++ osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs | 3 ++- osu.Game/Skinning/LegacySkin.cs | 3 +++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs index 53e4f3cd14..40752d3f4b 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; using osuTK; +using osuTK.Graphics; namespace osu.Game.Rulesets.Mania.Skinning { @@ -33,6 +34,9 @@ namespace osu.Game.Rulesets.Mania.Skinning bool showJudgementLine = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ShowJudgementLine)?.Value ?? true; + Color4 lineColour = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.JudgementLineColour)?.Value + ?? Color4.White; + InternalChild = directionContainer = new Container { Origin = Anchor.CentreLeft, @@ -52,6 +56,7 @@ namespace osu.Game.Rulesets.Mania.Skinning Anchor = Anchor.CentreLeft, RelativeSizeAxes = Axes.X, Height = 1, + Colour = lineColour, Alpha = showJudgementLine ? 0.9f : 0 } } diff --git a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs index 853d07c060..ee5db2a77f 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs @@ -36,6 +36,7 @@ namespace osu.Game.Skinning HoldNoteBodyImage, ExplosionImage, ExplosionScale, - ColumnLineColour + ColumnLineColour, + JudgementLineColour, } } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 3d3eac97f6..f206bc792d 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -207,6 +207,9 @@ namespace osu.Game.Skinning case LegacyManiaSkinConfigurationLookups.ColumnLineColour: return SkinUtils.As(getCustomColour(existing, "ColourColumnLine")); + + case LegacyManiaSkinConfigurationLookups.JudgementLineColour: + return SkinUtils.As(getCustomColour(existing, "ColourJudgementLine")); } return null; From 11d58fb7f61ccfd17bd96d080886cd2853feeb53 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 7 Apr 2020 16:53:29 +0900 Subject: [PATCH 0880/2376] Implement column background and light colours --- .../Skinning/LegacyColumnBackground.cs | 9 ++++++++- .../Skinning/LegacyManiaSkinConfigurationLookup.cs | 2 ++ osu.Game/Skinning/LegacySkin.cs | 11 ++++++++++- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs index 8cd0272b52..6504321bb2 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs @@ -50,12 +50,18 @@ namespace osu.Game.Rulesets.Mania.Skinning Color4 lineColour = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ColumnLineColour)?.Value ?? Color4.White; + Color4 backgroundColour = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour)?.Value + ?? Color4.Black; + + Color4 lightColour = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ColumnLightColour)?.Value + ?? Color4.White; + InternalChildren = new Drawable[] { new Box { RelativeSizeAxes = Axes.Both, - Colour = Color4.Black + Colour = backgroundColour }, new Box { @@ -82,6 +88,7 @@ namespace osu.Game.Rulesets.Mania.Skinning { Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, + Colour = lightColour, Texture = skin.GetTexture(lightImage), RelativeSizeAxes = Axes.X, Width = 1, diff --git a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs index ee5db2a77f..7d3614bf83 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs @@ -38,5 +38,7 @@ namespace osu.Game.Skinning ExplosionScale, ColumnLineColour, JudgementLineColour, + ColumnBackgroundColour, + ColumnLightColour } } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index f206bc792d..f1a911e652 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -88,7 +88,8 @@ namespace osu.Game.Skinning // todo: this shouldn't really be duplicated here (from ManiaLegacySkinTransformer). we need to come up with a better solution. hasKeyTexture = new Lazy(() => this.GetAnimation( - lookupForMania(new LegacyManiaSkinConfigurationLookup(4, LegacyManiaSkinConfigurationLookups.KeyImage, 0))?.Value ?? "mania-key1", true, true) != null); + lookupForMania(new LegacyManiaSkinConfigurationLookup(4, LegacyManiaSkinConfigurationLookups.KeyImage, 0))?.Value ?? "mania-key1", true, + true) != null); } protected override void Dispose(bool isDisposing) @@ -210,6 +211,14 @@ namespace osu.Game.Skinning case LegacyManiaSkinConfigurationLookups.JudgementLineColour: return SkinUtils.As(getCustomColour(existing, "ColourJudgementLine")); + + case LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour: + Debug.Assert(maniaLookup.TargetColumn != null); + return SkinUtils.As(getCustomColour(existing, $"Colour{maniaLookup.TargetColumn}")); + + case LegacyManiaSkinConfigurationLookups.ColumnLightColour: + Debug.Assert(maniaLookup.TargetColumn != null); + return SkinUtils.As(getCustomColour(existing, $"ColourLight{maniaLookup.TargetColumn}")); } return null; From 2568f3f5886fd537c52ca0877d7edd51dd1d08b1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 7 Apr 2020 17:11:32 +0900 Subject: [PATCH 0881/2376] Fix off-by-one indexing --- osu.Game/Skinning/LegacySkin.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index f1a911e652..9e0f4007a1 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -214,11 +214,11 @@ namespace osu.Game.Skinning case LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour: Debug.Assert(maniaLookup.TargetColumn != null); - return SkinUtils.As(getCustomColour(existing, $"Colour{maniaLookup.TargetColumn}")); + return SkinUtils.As(getCustomColour(existing, $"Colour{maniaLookup.TargetColumn + 1}")); case LegacyManiaSkinConfigurationLookups.ColumnLightColour: Debug.Assert(maniaLookup.TargetColumn != null); - return SkinUtils.As(getCustomColour(existing, $"ColourLight{maniaLookup.TargetColumn}")); + return SkinUtils.As(getCustomColour(existing, $"ColourLight{maniaLookup.TargetColumn + 1}")); } return null; From ccc764eace14444e4421d83eb4ee1c842f46f2f1 Mon Sep 17 00:00:00 2001 From: Tim Oliver Date: Tue, 7 Apr 2020 16:39:41 +0800 Subject: [PATCH 0882/2376] =?UTF-8?q?Added=20=E2=80=9Cinstant=20fly?= =?UTF-8?q?=E2=80=9D=20variant=20of=20hit=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Objects/Drawables/DrawableCentreHit.cs | 17 +++++++++++++++++ .../Objects/Drawables/DrawableRimHit.cs | 17 +++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs index 4979135f50..08df05e719 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs @@ -2,7 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; namespace osu.Game.Rulesets.Taiko.Objects.Drawables @@ -23,4 +26,18 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables MainPiece.AccentColour = colours.PinkDarker; } } + + public class DrawableFlyingCentreHit : DrawableCentreHit + { + protected override void CheckForResult(bool userTriggered, double timeOffset) + { + ApplyResult(r => r.Type = HitResult.Good); + } + + public DrawableFlyingCentreHit(double time) + : base(new Hit { StartTime = time }) + { + HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + } + } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs index 5a12d71cea..0c2c9fbdef 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs @@ -2,7 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; namespace osu.Game.Rulesets.Taiko.Objects.Drawables @@ -23,4 +26,18 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables MainPiece.AccentColour = colours.BlueDarker; } } + + public class DrawableFlyingRimHit : DrawableRimHit + { + protected override void CheckForResult(bool userTriggered, double timeOffset) + { + ApplyResult(r => r.Type = HitResult.Good); + } + + public DrawableFlyingRimHit(double time) + : base(new Hit { StartTime = time }) + { + HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + } + } } From 2705de70a206ad71d8db84d70c4af6cbd259b4ca Mon Sep 17 00:00:00 2001 From: Tim Oliver Date: Tue, 7 Apr 2020 16:40:01 +0800 Subject: [PATCH 0883/2376] Added arbitrary hit handler to drum roll object --- .../Objects/Drawables/DrawableDrumRoll.cs | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index 5806c90115..3e7b6dfd31 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -34,6 +34,14 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private Color4 colourIdle; private Color4 colourEngaged; + private bool judgingStarted; + + /// + /// A handler action for when the drumroll has been hit, + /// regardless of any judgement. + /// + public Action OnHit; + public DrawableDrumRoll(DrumRoll drumRoll) : base(drumRoll) { @@ -86,15 +94,27 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected override TaikoPiece CreateMainPiece() => new ElongatedCirclePiece(); - public override bool OnPressed(TaikoAction action) => false; + public override bool OnPressed(TaikoAction action) + { + if (judgingStarted) + OnHit.Invoke(action); + + return false; + } private void onNewResult(DrawableHitObject obj, JudgementResult result) { if (!(obj is DrawableDrumRollTick)) return; + DrawableDrumRollTick drumRollTick = (DrawableDrumRollTick)obj; + if (result.Type > HitResult.Miss) + { + OnHit.Invoke(drumRollTick.JudgedAction); + judgingStarted = true; rollingHits++; + } else rollingHits--; @@ -113,8 +133,11 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables return; int countHit = NestedHitObjects.Count(o => o.IsHit); + if (countHit >= HitObject.RequiredGoodHits) + { ApplyResult(r => r.Type = countHit >= HitObject.RequiredGreatHits ? HitResult.Great : HitResult.Good); + } else ApplyResult(r => r.Type = HitResult.Miss); } From 7c3c198212d70205ee4897eafc7cff815e55c929 Mon Sep 17 00:00:00 2001 From: Tim Oliver Date: Tue, 7 Apr 2020 16:40:18 +0800 Subject: [PATCH 0884/2376] Added judgement forwarder to drumroll tick object --- .../Objects/Drawables/DrawableDrumRollTick.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs index 25b6141a0e..9961cb6ea2 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs @@ -11,6 +11,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { public class DrawableDrumRollTick : DrawableTaikoHitObject { + /// + /// The action type that the user took which caused this tick to + /// have been judged as "hit" + /// + public TaikoAction JudgedAction; + public DrawableDrumRollTick(DrumRollTick tick) : base(tick) { @@ -49,7 +55,11 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables } } - public override bool OnPressed(TaikoAction action) => UpdateResult(true); + public override bool OnPressed(TaikoAction action) + { + JudgedAction = action; + return UpdateResult(true); + } protected override DrawableStrongNestedHit CreateStrongHit(StrongHitObject hitObject) => new StrongNestedHit(hitObject, this); From 3fec213c928db64bb7dcb07bf6b468fb9d9e39c8 Mon Sep 17 00:00:00 2001 From: Tim Oliver Date: Tue, 7 Apr 2020 16:40:32 +0800 Subject: [PATCH 0885/2376] Added separate scrolling track to display drum roll notes --- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 25 ++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index bde9085c23..b32e7b53da 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -42,6 +42,7 @@ namespace osu.Game.Rulesets.Taiko.UI private readonly Container hitExplosionContainer; private readonly Container kiaiExplosionContainer; private readonly JudgementContainer judgementContainer; + private readonly ScrollingHitObjectContainer drumRollHitContainer; internal readonly HitTarget HitTarget; private readonly ProxyContainer topLevelHitContainer; @@ -135,6 +136,14 @@ namespace osu.Game.Rulesets.Taiko.UI Margin = new MarginPadding { Left = HIT_TARGET_OFFSET }, Blending = BlendingParameters.Additive }, + drumRollHitContainer = new ScrollingHitObjectContainer + { + Name = "Drumroll hit", + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Stretch, + Margin = new MarginPadding { Left = HIT_TARGET_OFFSET }, + Width = 1.0f + } } }, overlayBackgroundContainer = new Container @@ -212,12 +221,28 @@ namespace osu.Game.Rulesets.Taiko.UI barlineContainer.Add(barline.CreateProxy()); break; + case DrawableDrumRoll drumRoll: + drumRoll.OnHit += onDrumrollArbitraryHit; + break; + case DrawableTaikoHitObject taikoObject: topLevelHitContainer.Add(taikoObject.CreateProxiedContent()); break; } } + private void onDrumrollArbitraryHit(TaikoAction action) + { + DrawableHit drawableHit; + + if (action == TaikoAction.LeftRim || action == TaikoAction.RightRim) + drawableHit = new DrawableFlyingRimHit(Time.Current); + else + drawableHit = new DrawableFlyingCentreHit(Time.Current); + + drumRollHitContainer.Add(drawableHit); + } + internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result) { if (!DisplayJudgements.Value) From a1e215888eda520a432e63465e5a6c4c33bc111c Mon Sep 17 00:00:00 2001 From: Tim Oliver Date: Tue, 7 Apr 2020 17:25:47 +0800 Subject: [PATCH 0886/2376] Added logic to allow strong notes --- .../Objects/Drawables/DrawableCentreHit.cs | 4 ++-- .../Objects/Drawables/DrawableDrumRoll.cs | 6 +++--- osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs | 4 ++-- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs index 08df05e719..86e885239f 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs @@ -34,8 +34,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables ApplyResult(r => r.Type = HitResult.Good); } - public DrawableFlyingCentreHit(double time) - : base(new Hit { StartTime = time }) + public DrawableFlyingCentreHit(double time, bool isStrong = false) + : base(new Hit { StartTime = time, IsStrong = isStrong }) { HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index 3e7b6dfd31..64be870262 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables /// A handler action for when the drumroll has been hit, /// regardless of any judgement. /// - public Action OnHit; + public Action OnHit; public DrawableDrumRoll(DrumRoll drumRoll) : base(drumRoll) @@ -97,7 +97,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables public override bool OnPressed(TaikoAction action) { if (judgingStarted) - OnHit.Invoke(action); + OnHit.Invoke(action, HitObject.IsStrong); return false; } @@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (result.Type > HitResult.Miss) { - OnHit.Invoke(drumRollTick.JudgedAction); + OnHit.Invoke(drumRollTick.JudgedAction, HitObject.IsStrong); judgingStarted = true; rollingHits++; } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs index 0c2c9fbdef..ad9872b21f 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs @@ -34,8 +34,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables ApplyResult(r => r.Type = HitResult.Good); } - public DrawableFlyingRimHit(double time) - : base(new Hit { StartTime = time }) + public DrawableFlyingRimHit(double time, bool isStrong = false) + : base(new Hit { StartTime = time, IsStrong = isStrong }) { HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); } diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index b32e7b53da..59cf9193b5 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -231,14 +231,14 @@ namespace osu.Game.Rulesets.Taiko.UI } } - private void onDrumrollArbitraryHit(TaikoAction action) + private void onDrumrollArbitraryHit(TaikoAction action, bool isStrong) { DrawableHit drawableHit; if (action == TaikoAction.LeftRim || action == TaikoAction.RightRim) - drawableHit = new DrawableFlyingRimHit(Time.Current); + drawableHit = new DrawableFlyingRimHit(Time.Current, isStrong); else - drawableHit = new DrawableFlyingCentreHit(Time.Current); + drawableHit = new DrawableFlyingCentreHit(Time.Current, isStrong); drumRollHitContainer.Add(drawableHit); } From c9872f1d93369601dd5787994772673790a904e1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 7 Apr 2020 18:55:03 +0900 Subject: [PATCH 0887/2376] Retrieve dll resources using a more reliable method --- osu.Game/Tests/Visual/SkinnableTestScene.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Tests/Visual/SkinnableTestScene.cs b/osu.Game/Tests/Visual/SkinnableTestScene.cs index 71d3266d18..69e17af01b 100644 --- a/osu.Game/Tests/Visual/SkinnableTestScene.cs +++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs @@ -26,9 +26,6 @@ namespace osu.Game.Tests.Visual private Skin specialSkin; private Skin oldSkin; - // Keep a static reference to ensure we don't use a dynamically recompiled DLL as a source (resources will be missing). - private static DllResourceStore dllStore; - protected SkinnableTestScene() : base(2, 3) { @@ -37,7 +34,7 @@ namespace osu.Game.Tests.Visual [BackgroundDependencyLoader] private void load(AudioManager audio, SkinManager skinManager) { - dllStore ??= new DllResourceStore(GetType().Assembly); + var dllStore = new DllResourceStore(DynamicCompilationOriginal.GetType().Assembly); metricsSkin = new TestLegacySkin(new SkinInfo { Name = "metrics-skin" }, new NamespacedResourceStore(dllStore, "Resources/metrics_skin"), audio, true); defaultSkin = skinManager.GetSkin(DefaultLegacySkin.Info); From 08308e07e7a1ea594214594400f3cb784936a9a9 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Tue, 7 Apr 2020 12:20:54 +0200 Subject: [PATCH 0888/2376] Apply review suggestions --- osu.Game/Rulesets/RulesetStore.cs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index 7b4c0302aa..ef72f187c3 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -24,12 +24,12 @@ namespace osu.Game.Rulesets : base(factory) { rulesetStorage = storage?.GetStorageForDirectory("rulesets"); - AppDomain.CurrentDomain.AssemblyResolve += resolveRulesetDependencyAssembly; // On android in release configuration assemblies are loaded from the apk directly into memory. // We cannot read assemblies from cwd, so should check loaded assemblies instead. loadFromAppDomain(); loadFromDisk(); + AppDomain.CurrentDomain.AssemblyResolve += resolveRulesetDependencyAssembly; loadUserRulesets(); addMissingRulesets(); } @@ -57,6 +57,7 @@ namespace osu.Game.Rulesets { var asm = new AssemblyName(args.Name); + // the requesting assembly may be located out of the executable's base directory, thus requiring manual resolving of its dependencies. // this assumes the only explicit dependency of the ruleset is the game core assembly. // the ruleset dependency on the game core assembly requires manual resolving, transient dependencies should be resolved automatically if (asm.Name.Equals(typeof(OsuGame).Assembly.GetName().Name, StringComparison.Ordinal)) @@ -137,17 +138,10 @@ namespace osu.Game.Rulesets private void loadUserRulesets() { - try - { - var rulesets = rulesetStorage?.GetFiles(".", $"{ruleset_library_prefix}.*.dll"); + var rulesets = rulesetStorage?.GetFiles(".", $"{ruleset_library_prefix}.*.dll"); - foreach (var ruleset in rulesets.Where(f => !f.Contains("Tests"))) - loadRulesetFromFile(rulesetStorage?.GetFullPath(ruleset)); - } - catch (Exception e) - { - Logger.Error(e, "Couldn't load user rulesets"); - } + foreach (var ruleset in rulesets.Where(f => !f.Contains("Tests"))) + loadRulesetFromFile(rulesetStorage?.GetFullPath(ruleset)); } private void loadFromDisk() From e597ee9ffd47628c6a18e238f3cbad1e93e5af1b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 7 Apr 2020 21:52:15 +0900 Subject: [PATCH 0889/2376] 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 161a15fa4e..aaac6ec427 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index c62aec7250..3e2c2b1599 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -22,7 +22,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 834c0ee956..7903d964ce 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -79,7 +79,7 @@ - + From 2087d8d09e92c4662a9cc228e25476801624539a Mon Sep 17 00:00:00 2001 From: Lucas A Date: Tue, 7 Apr 2020 16:01:47 +0200 Subject: [PATCH 0890/2376] Don't search for user rulesets if rulesetsStorage isn't set (Testing environnment) --- osu.Game/Rulesets/RulesetStore.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index ef72f187c3..34da2dc2db 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -138,10 +138,12 @@ namespace osu.Game.Rulesets private void loadUserRulesets() { - var rulesets = rulesetStorage?.GetFiles(".", $"{ruleset_library_prefix}.*.dll"); + if (rulesetStorage == null) return; + + var rulesets = rulesetStorage.GetFiles(".", $"{ruleset_library_prefix}.*.dll"); foreach (var ruleset in rulesets.Where(f => !f.Contains("Tests"))) - loadRulesetFromFile(rulesetStorage?.GetFullPath(ruleset)); + loadRulesetFromFile(rulesetStorage.GetFullPath(ruleset)); } private void loadFromDisk() From 16d906d769b576e1678c983f37e2ff0ac114e6b9 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Tue, 7 Apr 2020 17:16:06 +0300 Subject: [PATCH 0891/2376] Get rid of unnecessary removal --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs index 517af630fc..b04d484195 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Utils; using osu.Game.Rulesets.Objects.Drawables; @@ -80,7 +79,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables case ArmedState.Hit: this.FadeOut(animDuration, Easing.Out) .ScaleTo(Scale * 1.5f, animDuration, Easing.Out); - scaleContainer.Transforms.ForEach(t => scaleContainer.RemoveTransform(t)); break; } } From 35d66c3c1df447370eca1baaf5f1281b60c015df Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 7 Apr 2020 23:37:30 +0900 Subject: [PATCH 0892/2376] Fix missing comma --- osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs index f8089a9590..9a2f9f2fe5 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs @@ -39,7 +39,7 @@ namespace osu.Game.Skinning ColumnLineColour, JudgementLineColour, ColumnBackgroundColour, - ColumnLightColour + ColumnLightColour, MinimumColumnWidth } } From 65db64e13e615de506c07d9c71816c294e414361 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 7 Apr 2020 22:41:20 +0200 Subject: [PATCH 0893/2376] Add failing test cases --- .../Skinning/LegacySkinTextureFallbackTest.cs | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs diff --git a/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs b/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs new file mode 100644 index 0000000000..867af9c1b8 --- /dev/null +++ b/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs @@ -0,0 +1,109 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Graphics.Textures; +using osu.Game.Skinning; + +namespace osu.Game.Tests.NonVisual.Skinning +{ + [TestFixture] + public sealed class LegacySkinTextureFallbackTest + { + private static object[][] fallbackTestCases = + { + new object[] + { + // textures in store + new[] { "Gameplay/osu/followpoint@2x", "Gameplay/osu/followpoint" }, + // requested component + "Gameplay/osu/followpoint", + // returned texture name & scale + "Gameplay/osu/followpoint@2x", 2 + }, + new object[] + { + new[] { "Gameplay/osu/followpoint@2x" }, + "Gameplay/osu/followpoint", + "Gameplay/osu/followpoint@2x", 2 + }, + new object[] + { + new[] { "Gameplay/osu/followpoint" }, + "Gameplay/osu/followpoint", + "Gameplay/osu/followpoint", 1 + }, + new object[] + { + new[] { "Gameplay/osu/followpoint", "followpoint@2x" }, + "Gameplay/osu/followpoint", + "Gameplay/osu/followpoint", 1 + }, + new object[] + { + new[] { "followpoint@2x", "followpoint" }, + "Gameplay/osu/followpoint", + "followpoint@2x", 2 + }, + new object[] + { + new[] { "followpoint@2x" }, + "Gameplay/osu/followpoint", + "followpoint@2x", 2 + }, + new object[] + { + new[] { "followpoint" }, + "Gameplay/osu/followpoint", + "followpoint", 1 + }, + }; + + [TestCaseSource(nameof(fallbackTestCases))] + public void TestFallbackOrder(string[] filesInStore, string requestedComponent, string expectedTexture, float expectedScale) + { + var textureStore = new TestTextureStore(filesInStore); + var legacySkin = new TestLegacySkin(textureStore); + + var texture = legacySkin.GetTexture(requestedComponent); + + Assert.IsNotNull(texture); + Assert.AreEqual(textureStore.Textures[expectedTexture], texture); + Assert.AreEqual(expectedScale, texture.ScaleAdjust); + } + + [Test] + public void TestReturnNullOnFallbackFailure() + { + var textureStore = new TestTextureStore("sliderb", "hit100"); + var legacySkin = new TestLegacySkin(textureStore); + + var texture = legacySkin.GetTexture("Gameplay/osu/followpoint"); + + Assert.IsNull(texture); + } + + private class TestLegacySkin : LegacySkin + { + public TestLegacySkin(TextureStore textureStore) + : base(new SkinInfo(), null, null, string.Empty) + { + Textures = textureStore; + } + } + + private class TestTextureStore : TextureStore + { + public readonly Dictionary Textures; + + public TestTextureStore(params string[] fileNames) + { + Textures = fileNames.ToDictionary(fileName => fileName, fileName => new Texture(1, 1)); + } + + public override Texture Get(string name) => Textures.GetValueOrDefault(name); + } + } +} From f5f0b94944af1f1455859420aba1ed4f2fed083c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 7 Apr 2020 22:50:25 +0200 Subject: [PATCH 0894/2376] Fix incorrect fallback logic The recently-modified skin texture fallback logic was very subtly incorrect. If at the end of the first loop no texture was found, it would be checked for null to avoid setting scale adjust on a null texture, but then returned anyway, bypassing the fallback logic for subsequent possible paths entirely. Invert the check and explicitly continue to the next fallback path if neither a 2x, nor 1x texture with the given name is found in the store. --- osu.Game/Skinning/LegacySkin.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 60eb3d8e51..ea1cc203d7 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -293,9 +293,10 @@ namespace osu.Game.Skinning texture = Textures?.Get(name); } - if (texture != null) - texture.ScaleAdjust = ratio; + if (texture == null) + continue; + texture.ScaleAdjust = ratio; return texture; } From 737a3b608a925aff03cbe91214a2f8f3ecc2df7d Mon Sep 17 00:00:00 2001 From: Alchyr Date: Tue, 7 Apr 2020 17:34:18 -0700 Subject: [PATCH 0895/2376] Correct spelling --- osu.Game/Screens/Menu/MainMenu.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index dcee5e83b7..174eadfe26 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -294,7 +294,7 @@ namespace osu.Game.Screens.Menu { new PopupDialogOkButton { - Text = @"Good bye", + Text = @"Goodbye", Action = confirm }, new PopupDialogCancelButton From 66a474619ce2f56d127d92c2c0ca2429f845ab10 Mon Sep 17 00:00:00 2001 From: Alchyr Date: Tue, 7 Apr 2020 18:13:26 -0700 Subject: [PATCH 0896/2376] Adjust TimingControlPoint equivalency --- osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs index 51b3377394..158788964b 100644 --- a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs @@ -50,6 +50,6 @@ namespace osu.Game.Beatmaps.ControlPoints public override bool EquivalentTo(ControlPoint other) => other is TimingControlPoint otherTyped - && TimeSignature == otherTyped.TimeSignature && BeatLength.Equals(otherTyped.BeatLength); + && Time == otherTyped.Time && TimeSignature == otherTyped.TimeSignature && BeatLength.Equals(otherTyped.BeatLength); } } From c5aae9b757ef7c726b513a7b0dfd2e1b55d2cda2 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 8 Apr 2020 12:19:09 +0900 Subject: [PATCH 0897/2376] Fix post-merge errors --- .../Difficulty/CatchDifficultyCalculator.cs | 14 +++++--------- .../Preprocessing/CatchDifficultyHitObject.cs | 1 + 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 7d763f1792..ee3b410780 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -50,15 +50,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) { - float halfCatchWidth; - - using (var catcher = new CatcherArea.Catcher(beatmap.BeatmapInfo.BaseDifficulty)) - { - halfCatchWidth = catcher.CatchWidth * 0.5f; - // We're only using 80% of the catcher's width to simulate imperfect gameplay, reduced further at circle sizes above 5.5 - halfCatchWidth *= Math.Min(1.075f - (0.05f * beatmap.BeatmapInfo.BaseDifficulty.CircleSize), 0.8f); - } - CatchHitObject lastObject = null; // In 2B beatmaps, it is possible that a normal Fruit is placed in the middle of a JuiceStream. @@ -81,8 +72,13 @@ namespace osu.Game.Rulesets.Catch.Difficulty protected override Skill[] CreateSkills(IBeatmap beatmap) { using (var catcher = new Catcher(beatmap.BeatmapInfo.BaseDifficulty)) + { halfCatcherWidth = catcher.CatchWidth * 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); + } + return new Skill[] { new Movement(halfCatcherWidth), diff --git a/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs b/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs index b2b4129c8a..360af1a8c9 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs @@ -24,6 +24,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing /// Milliseconds elapsed since the start time of the previous , with a minimum of 40ms. /// public readonly double StrainTime; + public readonly double ClockRate; public CatchDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate, float halfCatcherWidth) From fd51bbb9ecd1e7e381c481d50c85ba392a63bc75 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 8 Apr 2020 12:20:46 +0900 Subject: [PATCH 0898/2376] Apply latest changes --- .../Difficulty/Skills/Movement.cs | 25 ++++--------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs index 3ab4ae63a1..5cd2f1f581 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs @@ -46,29 +46,29 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills float distanceMoved = playerPosition - lastPlayerPosition.Value; - double weightedStrainTime = catchCurrent.StrainTime + 10 + (8 / catchCurrent.ClockRate); + double weightedStrainTime = catchCurrent.StrainTime + 13 + (3 / catchCurrent.ClockRate); double distanceAddition = (Math.Pow(Math.Abs(distanceMoved), 1.3) / 510); double sqrtStrain = Math.Sqrt(weightedStrainTime); double edgeDashBonus = 0; - // Direction changes give an extra point! + // Direction change bonus. if (Math.Abs(distanceMoved) > 0.1) { if (Math.Abs(lastDistanceMoved) > 0.1 && Math.Sign(distanceMoved) != Math.Sign(lastDistanceMoved)) { double bonusFactor = Math.Min(50, Math.Abs(distanceMoved)) / 50; - double antiflowFactor = Math.Max(Math.Min(70, Math.Abs(lastDistanceMoved)) / 70, 0.3); + double antiflowFactor = Math.Max(Math.Min(70, Math.Abs(lastDistanceMoved)) / 70, 0.38); - distanceAddition += direction_change_bonus / Math.Sqrt(lastStrainTime + 18) * bonusFactor * antiflowFactor * Math.Max(1 - Math.Pow(weightedStrainTime / 1000, 3), 0); + distanceAddition += direction_change_bonus / Math.Sqrt(lastStrainTime + 16) * bonusFactor * antiflowFactor * Math.Max(1 - Math.Pow(weightedStrainTime / 1000, 3), 0); } // Base bonus for every movement, giving some weight to streams. distanceAddition += 12.5 * Math.Min(Math.Abs(distanceMoved), normalized_hitobject_radius * 2) / (normalized_hitobject_radius * 6) / sqrtStrain; } - // Bonus for "almost" hyperdashes at corner points + // Bonus for edge dashes. if (catchCurrent.LastObject.DistanceToHyperDash <= 20.0f / CatchPlayfield.BASE_WIDTH) { if (!catchCurrent.LastObject.HyperDash) @@ -82,21 +82,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills distanceAddition *= 1.0 + edgeDashBonus * ((20 - catchCurrent.LastObject.DistanceToHyperDash * CatchPlayfield.BASE_WIDTH) / 20) * Math.Pow((Math.Min(catchCurrent.StrainTime * catchCurrent.ClockRate, 265) / 265), 1.5); // Edge Dashes are easier at lower ms values } - // Prevent wide dense stacks of notes which fit on the catcher from greatly increasing SR - if (Math.Abs(distanceMoved) > 0.1) - { - if (Math.Abs(lastDistanceMoved) > 0.1 && Math.Sign(distanceMoved) != Math.Sign(lastDistanceMoved)) - { - if (Math.Abs(distanceMoved) <= (CatcherArea.CATCHER_SIZE) && Math.Abs(lastDistanceMoved) == Math.Abs(distanceMoved)) - { - if (catchCurrent.StrainTime <= 80 && lastStrainTime == catchCurrent.StrainTime) - { - distanceAddition *= Math.Max(((catchCurrent.StrainTime / 80) - 0.75) * 4, 0); - } - } - } - } - lastPlayerPosition = playerPosition; lastDistanceMoved = distanceMoved; lastStrainTime = catchCurrent.StrainTime; From a7d1eed3f5d938608eb9581ccde72072efd014b9 Mon Sep 17 00:00:00 2001 From: Tim Oliver Date: Wed, 8 Apr 2020 12:12:59 +0800 Subject: [PATCH 0899/2376] Added content proxying to drull roll elements --- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 59cf9193b5..e947795fe5 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -241,6 +241,7 @@ namespace osu.Game.Rulesets.Taiko.UI drawableHit = new DrawableFlyingCentreHit(Time.Current, isStrong); drumRollHitContainer.Add(drawableHit); + topLevelHitContainer.Add(drawableHit.CreateProxiedContent()); } internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result) From f4dc604dbf5928e8142573562d18274030bbdd0f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 8 Apr 2020 13:32:37 +0900 Subject: [PATCH 0900/2376] Fix dragging tournament ladder too far causing it to disappear --- osu.Game.Tournament/Screens/Ladder/LadderDragContainer.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tournament/Screens/Ladder/LadderDragContainer.cs b/osu.Game.Tournament/Screens/Ladder/LadderDragContainer.cs index bdaa1ae7fd..fa03518c47 100644 --- a/osu.Game.Tournament/Screens/Ladder/LadderDragContainer.cs +++ b/osu.Game.Tournament/Screens/Ladder/LadderDragContainer.cs @@ -22,6 +22,8 @@ namespace osu.Game.Tournament.Screens.Ladder protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false; + public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) => false; + protected override void OnDrag(DragEvent e) { this.MoveTo(target += e.Delta, 1000, Easing.OutQuint); From 6e12f1b69b95e0d9b0fbe84b4666b6a87f8b5caa Mon Sep 17 00:00:00 2001 From: Tim Oliver Date: Tue, 7 Apr 2020 16:39:41 +0800 Subject: [PATCH 0901/2376] =?UTF-8?q?Added=20=E2=80=9Cinstant=20fly?= =?UTF-8?q?=E2=80=9D=20variant=20of=20hit=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Objects/Drawables/DrawableCentreHit.cs | 17 +++++++++++++++++ .../Objects/Drawables/DrawableRimHit.cs | 17 +++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs index 4979135f50..08df05e719 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs @@ -2,7 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; namespace osu.Game.Rulesets.Taiko.Objects.Drawables @@ -23,4 +26,18 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables MainPiece.AccentColour = colours.PinkDarker; } } + + public class DrawableFlyingCentreHit : DrawableCentreHit + { + protected override void CheckForResult(bool userTriggered, double timeOffset) + { + ApplyResult(r => r.Type = HitResult.Good); + } + + public DrawableFlyingCentreHit(double time) + : base(new Hit { StartTime = time }) + { + HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + } + } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs index 5a12d71cea..0c2c9fbdef 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs @@ -2,7 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; namespace osu.Game.Rulesets.Taiko.Objects.Drawables @@ -23,4 +26,18 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables MainPiece.AccentColour = colours.BlueDarker; } } + + public class DrawableFlyingRimHit : DrawableRimHit + { + protected override void CheckForResult(bool userTriggered, double timeOffset) + { + ApplyResult(r => r.Type = HitResult.Good); + } + + public DrawableFlyingRimHit(double time) + : base(new Hit { StartTime = time }) + { + HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + } + } } From 1057981c793dfabd262123cacfb47f36df57a844 Mon Sep 17 00:00:00 2001 From: Tim Oliver Date: Tue, 7 Apr 2020 16:40:01 +0800 Subject: [PATCH 0902/2376] Added arbitrary hit handler to drum roll object --- .../Objects/Drawables/DrawableDrumRoll.cs | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index 5806c90115..3e7b6dfd31 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -34,6 +34,14 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private Color4 colourIdle; private Color4 colourEngaged; + private bool judgingStarted; + + /// + /// A handler action for when the drumroll has been hit, + /// regardless of any judgement. + /// + public Action OnHit; + public DrawableDrumRoll(DrumRoll drumRoll) : base(drumRoll) { @@ -86,15 +94,27 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected override TaikoPiece CreateMainPiece() => new ElongatedCirclePiece(); - public override bool OnPressed(TaikoAction action) => false; + public override bool OnPressed(TaikoAction action) + { + if (judgingStarted) + OnHit.Invoke(action); + + return false; + } private void onNewResult(DrawableHitObject obj, JudgementResult result) { if (!(obj is DrawableDrumRollTick)) return; + DrawableDrumRollTick drumRollTick = (DrawableDrumRollTick)obj; + if (result.Type > HitResult.Miss) + { + OnHit.Invoke(drumRollTick.JudgedAction); + judgingStarted = true; rollingHits++; + } else rollingHits--; @@ -113,8 +133,11 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables return; int countHit = NestedHitObjects.Count(o => o.IsHit); + if (countHit >= HitObject.RequiredGoodHits) + { ApplyResult(r => r.Type = countHit >= HitObject.RequiredGreatHits ? HitResult.Great : HitResult.Good); + } else ApplyResult(r => r.Type = HitResult.Miss); } From 9d5a9775017f7ffa67207419b28b5cf3e6fdca61 Mon Sep 17 00:00:00 2001 From: Tim Oliver Date: Tue, 7 Apr 2020 16:40:18 +0800 Subject: [PATCH 0903/2376] Added judgement forwarder to drumroll tick object --- .../Objects/Drawables/DrawableDrumRollTick.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs index 25b6141a0e..9961cb6ea2 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs @@ -11,6 +11,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { public class DrawableDrumRollTick : DrawableTaikoHitObject { + /// + /// The action type that the user took which caused this tick to + /// have been judged as "hit" + /// + public TaikoAction JudgedAction; + public DrawableDrumRollTick(DrumRollTick tick) : base(tick) { @@ -49,7 +55,11 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables } } - public override bool OnPressed(TaikoAction action) => UpdateResult(true); + public override bool OnPressed(TaikoAction action) + { + JudgedAction = action; + return UpdateResult(true); + } protected override DrawableStrongNestedHit CreateStrongHit(StrongHitObject hitObject) => new StrongNestedHit(hitObject, this); From 7751c5e3aa30fc530b4d9c8973c307765b5f7b16 Mon Sep 17 00:00:00 2001 From: Tim Oliver Date: Tue, 7 Apr 2020 16:40:32 +0800 Subject: [PATCH 0904/2376] Added separate scrolling track to display drum roll notes --- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 25 ++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index bde9085c23..b32e7b53da 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -42,6 +42,7 @@ namespace osu.Game.Rulesets.Taiko.UI private readonly Container hitExplosionContainer; private readonly Container kiaiExplosionContainer; private readonly JudgementContainer judgementContainer; + private readonly ScrollingHitObjectContainer drumRollHitContainer; internal readonly HitTarget HitTarget; private readonly ProxyContainer topLevelHitContainer; @@ -135,6 +136,14 @@ namespace osu.Game.Rulesets.Taiko.UI Margin = new MarginPadding { Left = HIT_TARGET_OFFSET }, Blending = BlendingParameters.Additive }, + drumRollHitContainer = new ScrollingHitObjectContainer + { + Name = "Drumroll hit", + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Stretch, + Margin = new MarginPadding { Left = HIT_TARGET_OFFSET }, + Width = 1.0f + } } }, overlayBackgroundContainer = new Container @@ -212,12 +221,28 @@ namespace osu.Game.Rulesets.Taiko.UI barlineContainer.Add(barline.CreateProxy()); break; + case DrawableDrumRoll drumRoll: + drumRoll.OnHit += onDrumrollArbitraryHit; + break; + case DrawableTaikoHitObject taikoObject: topLevelHitContainer.Add(taikoObject.CreateProxiedContent()); break; } } + private void onDrumrollArbitraryHit(TaikoAction action) + { + DrawableHit drawableHit; + + if (action == TaikoAction.LeftRim || action == TaikoAction.RightRim) + drawableHit = new DrawableFlyingRimHit(Time.Current); + else + drawableHit = new DrawableFlyingCentreHit(Time.Current); + + drumRollHitContainer.Add(drawableHit); + } + internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result) { if (!DisplayJudgements.Value) From b883586addd31c3933c8549af61a071ad65a143d Mon Sep 17 00:00:00 2001 From: Tim Oliver Date: Tue, 7 Apr 2020 17:25:47 +0800 Subject: [PATCH 0905/2376] Added logic to allow strong notes --- .../Objects/Drawables/DrawableCentreHit.cs | 4 ++-- .../Objects/Drawables/DrawableDrumRoll.cs | 6 +++--- osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs | 4 ++-- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs index 08df05e719..86e885239f 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs @@ -34,8 +34,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables ApplyResult(r => r.Type = HitResult.Good); } - public DrawableFlyingCentreHit(double time) - : base(new Hit { StartTime = time }) + public DrawableFlyingCentreHit(double time, bool isStrong = false) + : base(new Hit { StartTime = time, IsStrong = isStrong }) { HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index 3e7b6dfd31..64be870262 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables /// A handler action for when the drumroll has been hit, /// regardless of any judgement. /// - public Action OnHit; + public Action OnHit; public DrawableDrumRoll(DrumRoll drumRoll) : base(drumRoll) @@ -97,7 +97,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables public override bool OnPressed(TaikoAction action) { if (judgingStarted) - OnHit.Invoke(action); + OnHit.Invoke(action, HitObject.IsStrong); return false; } @@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (result.Type > HitResult.Miss) { - OnHit.Invoke(drumRollTick.JudgedAction); + OnHit.Invoke(drumRollTick.JudgedAction, HitObject.IsStrong); judgingStarted = true; rollingHits++; } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs index 0c2c9fbdef..ad9872b21f 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs @@ -34,8 +34,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables ApplyResult(r => r.Type = HitResult.Good); } - public DrawableFlyingRimHit(double time) - : base(new Hit { StartTime = time }) + public DrawableFlyingRimHit(double time, bool isStrong = false) + : base(new Hit { StartTime = time, IsStrong = isStrong }) { HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); } diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index b32e7b53da..59cf9193b5 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -231,14 +231,14 @@ namespace osu.Game.Rulesets.Taiko.UI } } - private void onDrumrollArbitraryHit(TaikoAction action) + private void onDrumrollArbitraryHit(TaikoAction action, bool isStrong) { DrawableHit drawableHit; if (action == TaikoAction.LeftRim || action == TaikoAction.RightRim) - drawableHit = new DrawableFlyingRimHit(Time.Current); + drawableHit = new DrawableFlyingRimHit(Time.Current, isStrong); else - drawableHit = new DrawableFlyingCentreHit(Time.Current); + drawableHit = new DrawableFlyingCentreHit(Time.Current, isStrong); drumRollHitContainer.Add(drawableHit); } From c30ea2ec2916d2f5852a63b0e21f599bb34b6ef0 Mon Sep 17 00:00:00 2001 From: Tim Oliver Date: Wed, 8 Apr 2020 12:12:59 +0800 Subject: [PATCH 0906/2376] Added content proxying to drull roll elements --- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 59cf9193b5..e947795fe5 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -241,6 +241,7 @@ namespace osu.Game.Rulesets.Taiko.UI drawableHit = new DrawableFlyingCentreHit(Time.Current, isStrong); drumRollHitContainer.Add(drawableHit); + topLevelHitContainer.Add(drawableHit.CreateProxiedContent()); } internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result) From 3794b55eef5eed19a8acd0656f3af1ccd3f01380 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 8 Apr 2020 14:08:34 +0900 Subject: [PATCH 0907/2376] Rename ManiaStage to Stage --- osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs | 2 +- osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs | 8 ++++---- osu.Game.Rulesets.Mania/UI/Column.cs | 2 +- osu.Game.Rulesets.Mania/UI/Components/DefaultKeyArea.cs | 2 +- osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs | 2 +- osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs | 6 +++--- osu.Game.Rulesets.Mania/UI/{ManiaStage.cs => Stage.cs} | 4 ++-- 7 files changed, 13 insertions(+), 13 deletions(-) rename osu.Game.Rulesets.Mania/UI/{ManiaStage.cs => Stage.cs} (97%) diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs index 0d5ebd33e9..37b97a444a 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning return new ManiaInputManager(new ManiaRuleset().RulesetInfo, 4) { - Child = new ManiaStage(0, new StageDefinition { Columns = 4 }, ref normalAction, ref specialAction) + Child = new Stage(0, new StageDefinition { Columns = 4 }, ref normalAction, ref specialAction) }; }); } diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs index d5fd2808b8..7376a90f17 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs @@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Mania.Tests [Cached(typeof(IReadOnlyList))] private IReadOnlyList mods { get; set; } = Array.Empty(); - private readonly List stages = new List(); + private readonly List stages = new List(); private FillFlowContainer fill; @@ -81,9 +81,9 @@ namespace osu.Game.Rulesets.Mania.Tests AddAssert("check bar anchors", () => barsInStageAreAnchored(stages[1], Anchor.TopCentre)); } - private bool notesInStageAreAnchored(ManiaStage stage, Anchor anchor) => stage.Columns.SelectMany(c => c.AllHitObjects).All(o => o.Anchor == anchor); + private bool notesInStageAreAnchored(Stage stage, Anchor anchor) => stage.Columns.SelectMany(c => c.AllHitObjects).All(o => o.Anchor == anchor); - private bool barsInStageAreAnchored(ManiaStage stage, Anchor anchor) => stage.AllHitObjects.Where(obj => obj is DrawableBarLine).All(o => o.Anchor == anchor); + private bool barsInStageAreAnchored(Stage stage, Anchor anchor) => stage.AllHitObjects.Where(obj => obj is DrawableBarLine).All(o => o.Anchor == anchor); private void createNote() { @@ -133,7 +133,7 @@ namespace osu.Game.Rulesets.Mania.Tests { var specialAction = ManiaAction.Special1; - var stage = new ManiaStage(0, new StageDefinition { Columns = 2 }, ref action, ref specialAction); + var stage = new Stage(0, new StageDefinition { Columns = 2 }, ref action, ref specialAction); stages.Add(stage); return new ScrollingTestContainer(direction) diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index d2f58d7255..d1da102be5 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -138,6 +138,6 @@ namespace osu.Game.Rulesets.Mania.UI public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) // This probably shouldn't exist as is, but the columns in the stage are separated by a 1px border - => DrawRectangle.Inflate(new Vector2(ManiaStage.COLUMN_SPACING / 2, 0)).Contains(ToLocalSpace(screenSpacePos)); + => DrawRectangle.Inflate(new Vector2(Stage.COLUMN_SPACING / 2, 0)).Contains(ToLocalSpace(screenSpacePos)); } } diff --git a/osu.Game.Rulesets.Mania/UI/Components/DefaultKeyArea.cs b/osu.Game.Rulesets.Mania/UI/Components/DefaultKeyArea.cs index 982a18cb60..a5de09ca75 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/DefaultKeyArea.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/DefaultKeyArea.cs @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Mania.UI.Components InternalChild = directionContainer = new Container { RelativeSizeAxes = Axes.X, - Height = ManiaStage.HIT_TARGET_POSITION, + Height = Stage.HIT_TARGET_POSITION, Children = new[] { gradient = new Box diff --git a/osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs b/osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs index bca7c3ff08..ba5281a1a2 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs @@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Mania.UI.Components { float hitPosition = CurrentSkin.GetConfig( new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.HitPosition))?.Value - ?? ManiaStage.HIT_TARGET_POSITION; + ?? Stage.HIT_TARGET_POSITION; Padding = Direction.Value == ScrollingDirection.Up ? new MarginPadding { Top = hitPosition } diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs index 08f6049782..c2eb48b774 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Mania.UI { public class ManiaPlayfield : ScrollingPlayfield { - private readonly List stages = new List(); + private readonly List stages = new List(); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => stages.Any(s => s.ReceivePositionalInputAt(screenSpacePos)); @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Mania.UI for (int i = 0; i < stageDefinitions.Count; i++) { - var newStage = new ManiaStage(firstColumnIndex, stageDefinitions[i], ref normalColumnAction, ref specialColumnAction); + var newStage = new Stage(firstColumnIndex, stageDefinitions[i], ref normalColumnAction, ref specialColumnAction); playfieldGrid.Content[0][i] = newStage; @@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Mania.UI /// public int TotalColumns => stages.Sum(s => s.Columns.Count); - private ManiaStage getStageByColumn(int column) + private Stage getStageByColumn(int column) { int sum = 0; diff --git a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs similarity index 97% rename from osu.Game.Rulesets.Mania/UI/ManiaStage.cs rename to osu.Game.Rulesets.Mania/UI/Stage.cs index adab08eb06..1d64672035 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs +++ b/osu.Game.Rulesets.Mania/UI/Stage.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Mania.UI /// /// A collection of s. /// - public class ManiaStage : ScrollingPlayfield + public class Stage : ScrollingPlayfield { public const float COLUMN_SPACING = 1; @@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Mania.UI private readonly int firstColumnIndex; - public ManiaStage(int firstColumnIndex, StageDefinition definition, ref ManiaAction normalColumnStartAction, ref ManiaAction specialColumnStartAction) + public Stage(int firstColumnIndex, StageDefinition definition, ref ManiaAction normalColumnStartAction, ref ManiaAction specialColumnStartAction) { this.firstColumnIndex = firstColumnIndex; From 9db996a91f171c4618477eadea2db9dcb6b7e3a6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 8 Apr 2020 14:08:58 +0900 Subject: [PATCH 0908/2376] Increase size of default osu!mania skin's keys to allow clearance with HUD --- osu.Game.Rulesets.Mania/UI/Components/DefaultKeyArea.cs | 7 +++++-- osu.Game.Rulesets.Mania/UI/Stage.cs | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/Components/DefaultKeyArea.cs b/osu.Game.Rulesets.Mania/UI/Components/DefaultKeyArea.cs index a5de09ca75..47cb9bd45a 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/DefaultKeyArea.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/DefaultKeyArea.cs @@ -53,9 +53,8 @@ namespace osu.Game.Rulesets.Mania.UI.Components keyIcon = new Container { Name = "Key icon", - Anchor = Anchor.Centre, - Origin = Anchor.Centre, Size = new Vector2(key_icon_size), + Origin = Anchor.Centre, Masking = true, CornerRadius = key_icon_corner_radius, BorderThickness = 2, @@ -88,11 +87,15 @@ namespace osu.Game.Rulesets.Mania.UI.Components { if (direction.NewValue == ScrollingDirection.Up) { + keyIcon.Anchor = Anchor.BottomCentre; + keyIcon.Y = -20; directionContainer.Anchor = directionContainer.Origin = Anchor.TopLeft; gradient.Colour = ColourInfo.GradientVertical(Color4.Black, Color4.Black.Opacity(0)); } else { + keyIcon.Anchor = Anchor.TopCentre; + keyIcon.Y = 20; directionContainer.Anchor = directionContainer.Origin = Anchor.BottomLeft; gradient.Colour = ColourInfo.GradientVertical(Color4.Black.Opacity(0), Color4.Black); } diff --git a/osu.Game.Rulesets.Mania/UI/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs index 1d64672035..58e7fba4df 100644 --- a/osu.Game.Rulesets.Mania/UI/Stage.cs +++ b/osu.Game.Rulesets.Mania/UI/Stage.cs @@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Mania.UI { public const float COLUMN_SPACING = 1; - public const float HIT_TARGET_POSITION = 50; + public const float HIT_TARGET_POSITION = 110; public IReadOnlyList Columns => columnFlow.Children; private readonly FillFlowContainer columnFlow; From e429c274a99ac0b0d6428468d0b680a55e7efbb9 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 8 Apr 2020 15:08:13 +0900 Subject: [PATCH 0909/2376] Initial structure --- .../Skinning/ManiaSkinnableTestScene.cs | 1 + .../Skinning/TestSceneStageBackground.cs | 35 +++++++++++++++++++ osu.Game.Rulesets.Mania/ManiaSkinComponent.cs | 3 +- .../Skinning/LegacyStageBackground.cs | 11 ++++++ .../Skinning/ManiaLegacySkinTransformer.cs | 3 ++ .../UI/Components/DefaultStageBackground.cs | 11 ++++++ 6 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs create mode 100644 osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs create mode 100644 osu.Game.Rulesets.Mania/UI/Components/DefaultStageBackground.cs diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs index 7f0503913f..a3c1d518c5 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs @@ -31,6 +31,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning { typeof(ManiaRuleset), typeof(ManiaLegacySkinTransformer), + typeof(ManiaSettingsSubsection) }; protected override Ruleset CreateRulesetForSkinProvider() => new ManiaRuleset(); diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs new file mode 100644 index 0000000000..a8fc68188a --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs @@ -0,0 +1,35 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Rulesets.Mania.Skinning; +using osu.Game.Rulesets.Mania.UI.Components; +using osu.Game.Skinning; + +namespace osu.Game.Rulesets.Mania.Tests.Skinning +{ + public class TestSceneStageBackground : ManiaSkinnableTestScene + { + public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] + { + typeof(DefaultStageBackground), + typeof(LegacyStageBackground), + }).ToList(); + + [BackgroundDependencyLoader] + private void load() + { + SetContents(() => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground), _ => new DefaultStageBackground()) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Width = 0.5f, + }); + } + } +} diff --git a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs index 2371d74a2b..a7252a348a 100644 --- a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs +++ b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs @@ -39,6 +39,7 @@ namespace osu.Game.Rulesets.Mania HoldNoteHead, HoldNoteTail, HoldNoteBody, - HitExplosion + HitExplosion, + StageBackground } } diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs new file mode 100644 index 0000000000..d2ea47cfeb --- /dev/null +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs @@ -0,0 +1,11 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics.Containers; + +namespace osu.Game.Rulesets.Mania.Skinning +{ + public class LegacyStageBackground : CompositeDrawable + { + } +} diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs index 78ea4b68ae..27df534ddd 100644 --- a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs @@ -81,6 +81,9 @@ namespace osu.Game.Rulesets.Mania.Skinning case ManiaSkinComponents.HitExplosion: return new LegacyHitExplosion(); + + case ManiaSkinComponents.StageBackground: + return new LegacyStageBackground(); } break; diff --git a/osu.Game.Rulesets.Mania/UI/Components/DefaultStageBackground.cs b/osu.Game.Rulesets.Mania/UI/Components/DefaultStageBackground.cs new file mode 100644 index 0000000000..1e10cd8d59 --- /dev/null +++ b/osu.Game.Rulesets.Mania/UI/Components/DefaultStageBackground.cs @@ -0,0 +1,11 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics.Containers; + +namespace osu.Game.Rulesets.Mania.UI.Components +{ + public class DefaultStageBackground : CompositeDrawable + { + } +} From cd15b672eba7c97ed90b58d61f936d058d20df14 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 8 Apr 2020 15:36:07 +0900 Subject: [PATCH 0910/2376] Implement left and right stage images --- .../Skinning/LegacyStageBackground.cs | 54 ++++++++++++++++++- .../UI/Components/DefaultStageBackground.cs | 19 +++++++ osu.Game.Rulesets.Mania/UI/Stage.cs | 7 +-- .../LegacyManiaSkinConfigurationLookup.cs | 4 +- osu.Game/Skinning/LegacySkin.cs | 6 +++ 5 files changed, 82 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs index d2ea47cfeb..7680526ac4 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs @@ -1,11 +1,61 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Graphics.Containers; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Game.Skinning; +using osuTK; namespace osu.Game.Rulesets.Mania.Skinning { - public class LegacyStageBackground : CompositeDrawable + public class LegacyStageBackground : LegacyManiaElement { + private Drawable leftSprite; + private Drawable rightSprite; + + public LegacyStageBackground() + { + RelativeSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load(ISkinSource skin) + { + string leftImage = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.LeftStageImage)?.Value + ?? "mania-stage-left"; + + string rightImage = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.RightStageImage)?.Value + ?? "mania-stage-right"; + + InternalChildren = new[] + { + leftSprite = new Sprite + { + Anchor = Anchor.TopLeft, + Origin = Anchor.TopRight, + X = 0.05f, + Texture = skin.GetTexture(leftImage), + }, + rightSprite = new Sprite + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopLeft, + X = -0.05f, + Texture = skin.GetTexture(rightImage) + } + }; + } + + protected override void Update() + { + base.Update(); + + if (leftSprite?.Height > 0) + leftSprite.Scale = new Vector2(DrawHeight / leftSprite.Height); + + if (rightSprite?.Height > 0) + rightSprite.Scale = new Vector2(DrawHeight / rightSprite.Height); + } } } diff --git a/osu.Game.Rulesets.Mania/UI/Components/DefaultStageBackground.cs b/osu.Game.Rulesets.Mania/UI/Components/DefaultStageBackground.cs index 1e10cd8d59..f5b542d085 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/DefaultStageBackground.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/DefaultStageBackground.cs @@ -1,11 +1,30 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; +using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osuTK.Graphics; namespace osu.Game.Rulesets.Mania.UI.Components { public class DefaultStageBackground : CompositeDrawable { + public DefaultStageBackground() + { + RelativeSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new Box + { + Name = "Background", + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black + }; + } } } diff --git a/osu.Game.Rulesets.Mania/UI/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs index 58e7fba4df..91839bd043 100644 --- a/osu.Game.Rulesets.Mania/UI/Stage.cs +++ b/osu.Game.Rulesets.Mania/UI/Stage.cs @@ -6,7 +6,6 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects; @@ -72,11 +71,9 @@ namespace osu.Game.Rulesets.Mania.UI AutoSizeAxes = Axes.X, Children = new Drawable[] { - new Box + new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground), _ => new DefaultStageBackground()) { - Name = "Background", - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black + RelativeSizeAxes = Axes.Both }, columnFlow = new FillFlowContainer { diff --git a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs index 9a2f9f2fe5..59847017ec 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs @@ -40,6 +40,8 @@ namespace osu.Game.Skinning JudgementLineColour, ColumnBackgroundColour, ColumnLightColour, - MinimumColumnWidth + MinimumColumnWidth, + LeftStageImage, + RightStageImage, } } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index ea1cc203d7..91f970d19f 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -243,6 +243,12 @@ namespace osu.Game.Skinning case LegacyManiaSkinConfigurationLookups.KeyImageDown: Debug.Assert(maniaLookup.TargetColumn != null); return SkinUtils.As(getManiaImage(existing, $"KeyImage{maniaLookup.TargetColumn}D")); + + case LegacyManiaSkinConfigurationLookups.LeftStageImage: + return SkinUtils.As(getManiaImage(existing, "StageLeft")); + + case LegacyManiaSkinConfigurationLookups.RightStageImage: + return SkinUtils.As(getManiaImage(existing, "StageRight")); } return null; From 83db6cebb655d9ea25c5c6d1dfde8f3057d57630 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 8 Apr 2020 16:20:26 +0900 Subject: [PATCH 0911/2376] Implement bottom stage image --- .../Resources/metrics-skin/mania-key1@2x.png | Bin 0 -> 12914 bytes .../metrics-skin/mania-stage-bottom@2x.png | Bin 0 -> 1965 bytes .../Skinning/TestSceneStageForeground.cs | 33 +++++++++++ osu.Game.Rulesets.Mania/ManiaSkinComponent.cs | 3 +- .../Skinning/LegacyStageForeground.cs | 56 ++++++++++++++++++ .../Skinning/ManiaLegacySkinTransformer.cs | 3 + osu.Game.Rulesets.Mania/UI/Stage.cs | 4 ++ .../LegacyManiaSkinConfigurationLookup.cs | 1 + 8 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Rulesets.Mania.Tests/Resources/metrics-skin/mania-key1@2x.png create mode 100644 osu.Game.Rulesets.Mania.Tests/Resources/metrics-skin/mania-stage-bottom@2x.png create mode 100644 osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs create mode 100644 osu.Game.Rulesets.Mania/Skinning/LegacyStageForeground.cs diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/metrics-skin/mania-key1@2x.png b/osu.Game.Rulesets.Mania.Tests/Resources/metrics-skin/mania-key1@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..aa681f6f223ea44319b3a383935237d1a2e4b345 GIT binary patch literal 12914 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 literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/metrics-skin/mania-stage-bottom@2x.png b/osu.Game.Rulesets.Mania.Tests/Resources/metrics-skin/mania-stage-bottom@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..ca590eaf08e65dc1ae40bce9f9f74e69c3a0f8a4 GIT binary patch literal 1965 zcmV;e2U7TnP)3L?K*_rKGQL~x> z08mZsB?AB;00{&D5QGE*00=??0RRLcfdBx4kU#(cK}a9~fFL9g06-8D2ml}m2?PKT zgaiTr2ton@00ep2fXKN$&0jO!YxaDt*~2%@9=&BYwcLXAPMQr~G21k5w&6kJxjb8A z_D|IAZ~bOB28!G08;c&-tz|}co88}PcJ#Q}%BR*6wH~u$-xc>$EBeCeshNF!$ZXkE z{4@Pc`rGCr-6ySR6WyVQ-FPAY_E=?Chs?hHsuAqU`YZ}YpPY$;++^|Vo<)i3z{N(e z%4_H7h_!?Q{lQCS*LIoxvfXU+xV41Z-S{|4x@)q3->SZFltBJiXSV8T{Ikhd%-($* zp6p+HV)pyGz&!{aR!1JHReDt%hW5g z1K;zRuk~Pe(Cp;lMj{afL)Gwe%S|Ve0rQ)YM(Ha}HGe(sZ;8)r6ciOiZ}ulkWz);! zV4G#u@vkXPAZ6QC-xPgLGxO0G&FJ%#jMQEgnrBKU55PwW3zcakeCk+SS65)``!)Nu zs-X{Fj-OxMlN~U98rf^a?Aq5z_{3rBc&+M-vwN)+|NrxT#S0Gfo_*)y=MO6;A1nFH7I=uowik4k0!)k7a$v5vLSeW3((Z%h21DwFB;V?Aq$W35s$!_q+l zK`=jQWYyQ}lkPL4SOv73A6O*{ewqUi1+`6gtYa-kU+8n(nTy`@@i{9+$LiMDsEw{LB_v=Mv^2@D zpOsnbgQbVdcrlBv6w-umnnt|}B>sf?4^fHa%hTEItb7)KKOGP-B|T4|1QxogdNKKZ zQT5W_y!R=IG;6D#1dhe?A3AfqFr43q>B;kZTMWoW0>LZ_Hk(VlE^iIPd|K9_31l(p z3`BkA7K7^ln_eg?S(E2VP(3U)Mh;3?2=@6%W92jDjrOCU?>0azJDHxIJRjX*W-2aC<`i!vK1W|>Q0q$ktUI|pP`0cSxKJ0lB-ksec|RJs+vn*xXE-!7fSfYz~nhXkU$VjH#_pBS30K`7rs$(9fwB6 z{(Gu!2G~y0>pnvIV&z2qJftsl4DPH^$-}K?&zn34njwK8SUSioOO;p@m}!+!y2V_aS|uvyf{;@iFMEQPPv={(K2ggam?M=`zf8#+-+LJJy{Q_p(w+ zrP*rtPBPcN@V7zC8pJa@9nXoW_&wPpoYO-EPqieNo>|zCSrBU*!f@fI|YoEM>|n zPSIwHzK3s{ZT~a5YF%eAdh8g>e$U-)X1jk5--xkYwL@Ph;U763PoC?HgiCi(6(uZG zq9CJSb1EuXz5b$H@^Ccz-uNWB6-t)7Tg?V;H%98+mOLE4FN&^unyfTq&~sl z1tmf(yq*;`W3Npt3XQlm0(WyM3ZzOb-I_Ft&==@HH4+E_AViH4762d!2?PKTgaiTr z2ton@00bd{004rJKmY(iScwh*)v^2`>Vy9Q*Vy#8=R%=B00000NkvXXu0mjflsBGC literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs new file mode 100644 index 0000000000..d436445b59 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs @@ -0,0 +1,33 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Rulesets.Mania.Skinning; +using osu.Game.Skinning; + +namespace osu.Game.Rulesets.Mania.Tests.Skinning +{ + public class TestSceneStageForeground : ManiaSkinnableTestScene + { + public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] + { + typeof(LegacyStageForeground), + }).ToList(); + + [BackgroundDependencyLoader] + private void load() + { + SetContents(() => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageForeground), _ => null) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Width = 0.5f, + }); + } + } +} diff --git a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs index a7252a348a..c0c8505f44 100644 --- a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs +++ b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs @@ -40,6 +40,7 @@ namespace osu.Game.Rulesets.Mania HoldNoteTail, HoldNoteBody, HitExplosion, - StageBackground + StageBackground, + StageForeground, } } diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyStageForeground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyStageForeground.cs new file mode 100644 index 0000000000..9719005d54 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyStageForeground.cs @@ -0,0 +1,56 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Skinning; +using osuTK; + +namespace osu.Game.Rulesets.Mania.Skinning +{ + public class LegacyStageForeground : LegacyManiaElement + { + private readonly IBindable direction = new Bindable(); + + private Drawable sprite; + + public LegacyStageForeground() + { + RelativeSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load(ISkinSource skin, IScrollingInfo scrollingInfo) + { + string bottomImage = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.BottomStageImage)?.Value + ?? "mania-stage-bottom"; + + sprite = skin.GetAnimation(bottomImage, true, true)?.With(d => + { + if (d == null) + return; + + d.Scale = new Vector2(1.6f); + }); + + if (sprite != null) + InternalChild = sprite; + + direction.BindTo(scrollingInfo.Direction); + direction.BindValueChanged(onDirectionChanged, true); + } + + private void onDirectionChanged(ValueChangedEvent direction) + { + if (sprite == null) + return; + + if (direction.NewValue == ScrollingDirection.Up) + sprite.Anchor = sprite.Origin = Anchor.TopCentre; + else + sprite.Anchor = sprite.Origin = Anchor.BottomCentre; + } + } +} diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs index 27df534ddd..e64178083a 100644 --- a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs @@ -84,6 +84,9 @@ namespace osu.Game.Rulesets.Mania.Skinning case ManiaSkinComponents.StageBackground: return new LegacyStageBackground(); + + case ManiaSkinComponents.StageForeground: + return new LegacyStageForeground(); } break; diff --git a/osu.Game.Rulesets.Mania/UI/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs index 91839bd043..faa04dea97 100644 --- a/osu.Game.Rulesets.Mania/UI/Stage.cs +++ b/osu.Game.Rulesets.Mania/UI/Stage.cs @@ -100,6 +100,10 @@ namespace osu.Game.Rulesets.Mania.UI RelativeSizeAxes = Axes.Y, } }, + new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageForeground), _ => null) + { + RelativeSizeAxes = Axes.Both + }, judgements = new JudgementContainer { Anchor = Anchor.TopCentre, diff --git a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs index 59847017ec..c76d5c8784 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs @@ -43,5 +43,6 @@ namespace osu.Game.Skinning MinimumColumnWidth, LeftStageImage, RightStageImage, + BottomStageImage } } From 2ddea018cfe443bd82a2f72cd0320dd1bef001af Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 8 Apr 2020 17:15:59 +0900 Subject: [PATCH 0912/2376] Fix hidden notes due to 0 minimum width --- .../Resources/mania-skin-zero-minwidth.ini | 4 ++++ .../Skins/LegacyManiaSkinDecoderTest.cs | 15 +++++++++++++++ osu.Game/Skinning/LegacyManiaSkinConfiguration.cs | 2 +- 3 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Tests/Resources/mania-skin-zero-minwidth.ini diff --git a/osu.Game.Tests/Resources/mania-skin-zero-minwidth.ini b/osu.Game.Tests/Resources/mania-skin-zero-minwidth.ini new file mode 100644 index 0000000000..fd22e2e299 --- /dev/null +++ b/osu.Game.Tests/Resources/mania-skin-zero-minwidth.ini @@ -0,0 +1,4 @@ +[Mania] +Keys: 4 +ColumnWidth: 10,10,10,10 +WidthForNoteHeightScale: 0 \ No newline at end of file diff --git a/osu.Game.Tests/Skins/LegacyManiaSkinDecoderTest.cs b/osu.Game.Tests/Skins/LegacyManiaSkinDecoderTest.cs index 83fd4878aa..e811979aed 100644 --- a/osu.Game.Tests/Skins/LegacyManiaSkinDecoderTest.cs +++ b/osu.Game.Tests/Skins/LegacyManiaSkinDecoderTest.cs @@ -99,5 +99,20 @@ namespace osu.Game.Tests.Skins Assert.That(configs[0].CustomColours, Contains.Key("ColourBarline").And.ContainValue(new Color4(50, 50, 50, 50))); } } + + [Test] + public void TestMinimumColumnWidthFallsBackWhenZeroIsProvided() + { + var decoder = new LegacyManiaSkinDecoder(); + + using (var resStream = TestResources.OpenResource("mania-skin-zero-minwidth.ini")) + using (var stream = new LineBufferedReader(resStream)) + { + var configs = decoder.Decode(stream); + + Assert.That(configs.Count, Is.EqualTo(1)); + Assert.That(configs[0].MinimumColumnWidth, Is.EqualTo(16)); + } + } } } diff --git a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs index af7d6007f3..fb591969fb 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs @@ -54,7 +54,7 @@ namespace osu.Game.Skinning public float MinimumColumnWidth { get => minimumColumnWidth ?? ColumnWidth.Min(); - set => minimumColumnWidth = value; + set => minimumColumnWidth = value > 0 ? (float?)value : null; } } } From 65823fb2e1101f4b73bc7446063520ca95bbf846 Mon Sep 17 00:00:00 2001 From: Alchyr Date: Wed, 8 Apr 2020 01:42:35 -0700 Subject: [PATCH 0913/2376] Use redundancy test --- .../NonVisual/ControlPointInfoTest.cs | 23 ++++++++++++------- .../Beatmaps/ControlPoints/ControlPoint.cs | 8 +++++++ .../ControlPoints/ControlPointInfo.cs | 2 +- .../ControlPoints/DifficultyControlPoint.cs | 1 + .../ControlPoints/EffectControlPoint.cs | 1 + .../ControlPoints/SampleControlPoint.cs | 1 + .../ControlPoints/TimingControlPoint.cs | 6 ++++- 7 files changed, 32 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs index 2782e902fe..158954106d 100644 --- a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs +++ b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs @@ -29,11 +29,17 @@ namespace osu.Game.Tests.NonVisual var cpi = new ControlPointInfo(); cpi.Add(0, new TimingControlPoint()); // is *not* redundant, special exception for first timing point. - cpi.Add(1000, new TimingControlPoint()); // is redundant + cpi.Add(1000, new TimingControlPoint()); // is also not redundant, due to change of offset - Assert.That(cpi.Groups.Count, Is.EqualTo(1)); - Assert.That(cpi.TimingPoints.Count, Is.EqualTo(1)); - Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(1)); + Assert.That(cpi.Groups.Count, Is.EqualTo(2)); + Assert.That(cpi.TimingPoints.Count, Is.EqualTo(2)); + Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(2)); + + cpi.Add(1000, new TimingControlPoint()); //is redundant + + Assert.That(cpi.Groups.Count, Is.EqualTo(2)); + Assert.That(cpi.TimingPoints.Count, Is.EqualTo(2)); + Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(2)); } [Test] @@ -86,11 +92,12 @@ namespace osu.Game.Tests.NonVisual Assert.That(cpi.EffectPoints.Count, Is.EqualTo(0)); Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(0)); - cpi.Add(1000, new EffectControlPoint { KiaiMode = true }); // is not redundant + cpi.Add(1000, new EffectControlPoint { KiaiMode = true, OmitFirstBarLine = true }); // is not redundant + cpi.Add(1400, new EffectControlPoint { KiaiMode = true, OmitFirstBarLine = true }); // same settings, but is not redundant - Assert.That(cpi.Groups.Count, Is.EqualTo(1)); - Assert.That(cpi.EffectPoints.Count, Is.EqualTo(1)); - Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(1)); + Assert.That(cpi.Groups.Count, Is.EqualTo(2)); + Assert.That(cpi.EffectPoints.Count, Is.EqualTo(2)); + Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(2)); } [Test] diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs index 39a0e6f6d4..411a4441de 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs @@ -25,6 +25,14 @@ namespace osu.Game.Beatmaps.ControlPoints /// Whether equivalent. public abstract bool EquivalentTo(ControlPoint other); + /// + /// Whether this control point results in a meaningful change when placed after another. + /// + /// Another control point to compare with. + /// The time this timing point will be placed at. + /// Whether redundant. + public abstract bool IsRedundant(ControlPoint other, double time); + public bool Equals(ControlPoint other) => Time == other?.Time && EquivalentTo(other); } } diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs index df68d8acd2..37a3dbf592 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs @@ -247,7 +247,7 @@ namespace osu.Game.Beatmaps.ControlPoints break; } - return existing?.EquivalentTo(newPoint) == true; + return newPoint.IsRedundant(existing, time); } private void groupItemAdded(ControlPoint controlPoint) diff --git a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs index 8b21098a51..44522dc927 100644 --- a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs @@ -29,5 +29,6 @@ namespace osu.Game.Beatmaps.ControlPoints public override bool EquivalentTo(ControlPoint other) => other is DifficultyControlPoint otherTyped && otherTyped.SpeedMultiplier.Equals(SpeedMultiplier); + public override bool IsRedundant(ControlPoint other, double time) => EquivalentTo(other); } } diff --git a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs index 369b93ff3d..8066c6b577 100644 --- a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs @@ -38,5 +38,6 @@ namespace osu.Game.Beatmaps.ControlPoints public override bool EquivalentTo(ControlPoint other) => other is EffectControlPoint otherTyped && KiaiMode == otherTyped.KiaiMode && OmitFirstBarLine == otherTyped.OmitFirstBarLine; + public override bool IsRedundant(ControlPoint other, double time) => !OmitFirstBarLine && EquivalentTo(other); } } diff --git a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs index 393bcfdb3c..cf7c842b24 100644 --- a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs @@ -71,5 +71,6 @@ namespace osu.Game.Beatmaps.ControlPoints public override bool EquivalentTo(ControlPoint other) => other is SampleControlPoint otherTyped && SampleBank == otherTyped.SampleBank && SampleVolume == otherTyped.SampleVolume; + public override bool IsRedundant(ControlPoint other, double time) => EquivalentTo(other); } } diff --git a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs index 158788964b..d14ac1221b 100644 --- a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs @@ -50,6 +50,10 @@ namespace osu.Game.Beatmaps.ControlPoints public override bool EquivalentTo(ControlPoint other) => other is TimingControlPoint otherTyped - && Time == otherTyped.Time && TimeSignature == otherTyped.TimeSignature && BeatLength.Equals(otherTyped.BeatLength); + && TimeSignature == otherTyped.TimeSignature && BeatLength.Equals(otherTyped.BeatLength); + + public override bool IsRedundant(ControlPoint other, double time) => + EquivalentTo(other) + && other.Time == time; } } From e6b87656ba1164e4e8e81ac532be9916a6ae0426 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 8 Apr 2020 18:04:53 +0900 Subject: [PATCH 0914/2376] Fix TestSceneColumn columns not getting a width --- osu.Game.Rulesets.Mania.Tests/TestSceneColumn.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneColumn.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneColumn.cs index 9aad08c433..8b35a57380 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneColumn.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneColumn.cs @@ -28,7 +28,9 @@ namespace osu.Game.Rulesets.Mania.Tests { typeof(Column), typeof(ColumnBackground), - typeof(ColumnHitObjectArea) + typeof(ColumnHitObjectArea), + typeof(DefaultKeyArea), + typeof(DefaultHitTarget) }; [Cached(typeof(IReadOnlyList))] @@ -94,6 +96,7 @@ namespace osu.Game.Rulesets.Mania.Tests { Anchor = Anchor.Centre, Origin = Anchor.Centre, + Width = 50, Height = 0.85f, AccentColour = Color4.OrangeRed, Action = { Value = action }, From 7d787dde8926331a03a96140ed0ff1827520ee95 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 8 Apr 2020 18:17:45 +0900 Subject: [PATCH 0915/2376] Move comparison to decoder --- osu.Game/Skinning/LegacyManiaSkinConfiguration.cs | 2 +- osu.Game/Skinning/LegacyManiaSkinDecoder.cs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs index fb591969fb..af7d6007f3 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs @@ -54,7 +54,7 @@ namespace osu.Game.Skinning public float MinimumColumnWidth { get => minimumColumnWidth ?? ColumnWidth.Min(); - set => minimumColumnWidth = value > 0 ? (float?)value : null; + set => minimumColumnWidth = value; } } } diff --git a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs index 8b76749e3e..2db902c182 100644 --- a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs +++ b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs @@ -102,7 +102,9 @@ namespace osu.Game.Skinning break; case "WidthForNoteHeightScale": - currentConfig.MinimumColumnWidth = float.Parse(pair.Value, CultureInfo.InvariantCulture) * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR; + float minWidth = float.Parse(pair.Value, CultureInfo.InvariantCulture) * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR; + if (minWidth > 0) + currentConfig.MinimumColumnWidth = minWidth; break; case string _ when pair.Key.StartsWith("Colour"): From d13231eff744ae95590ef7a06987a4529593c77a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 8 Apr 2020 18:23:24 +0900 Subject: [PATCH 0916/2376] Use ctor for default width --- osu.Game.Rulesets.Mania.Tests/TestSceneColumn.cs | 1 - osu.Game.Rulesets.Mania/UI/Column.cs | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneColumn.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneColumn.cs index 8b35a57380..5e06002f41 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneColumn.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneColumn.cs @@ -96,7 +96,6 @@ namespace osu.Game.Rulesets.Mania.Tests { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Width = 50, Height = 0.85f, AccentColour = Color4.OrangeRed, Action = { Value = action }, diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index d1da102be5..506a07f26b 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -42,6 +42,7 @@ namespace osu.Game.Rulesets.Mania.UI Index = index; RelativeSizeAxes = Axes.Y; + Width = COLUMN_WIDTH; Drawable background = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground, Index), _ => new DefaultColumnBackground()) { From f3e909539df1566e43b86ff0af2520369e04f995 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 8 Apr 2020 18:39:18 +0900 Subject: [PATCH 0917/2376] Fix slider ball and follow circle blending for legacy skins --- osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs index 5a6dd49c44..395c76a233 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs @@ -40,7 +40,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces this.drawableSlider = drawableSlider; this.slider = slider; - Blending = BlendingParameters.Additive; Origin = Anchor.Centre; Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); @@ -241,6 +240,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces Scale = new Vector2(radius / OsuHitObject.OBJECT_RADIUS), Anchor = Anchor.Centre, Origin = Anchor.Centre, + Blending = BlendingParameters.Additive, BorderThickness = 10, BorderColour = Color4.White, Alpha = 1, From 067ec2785919118b183bbda158aa40ce1dff83b2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 8 Apr 2020 18:58:09 +0900 Subject: [PATCH 0918/2376] Also fix slider repeat circles --- .../Objects/Drawables/DrawableSliderRepeat.cs | 1 - .../Objects/Drawables/Pieces/ReverseArrowPiece.cs | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs index b04d484195..720ffcd51c 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs @@ -31,7 +31,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); - Blending = BlendingParameters.Additive; Origin = Anchor.Centre; InternalChild = scaleContainer = new ReverseArrowPiece(); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ReverseArrowPiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ReverseArrowPiece.cs index 35a27bb0a6..c0ee874545 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ReverseArrowPiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ReverseArrowPiece.cs @@ -21,13 +21,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces Anchor = Anchor.Centre; Origin = Anchor.Centre; - Blending = BlendingParameters.Additive; - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); Child = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.ReverseArrow), _ => new SpriteIcon { RelativeSizeAxes = Axes.Both, + Blending = BlendingParameters.Additive, Icon = FontAwesome.Solid.ChevronRight, Size = new Vector2(0.35f) }) From 40267cb1fe9e1bbc4878fceef1d24c57adc02fc7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Apr 2020 20:13:25 +0900 Subject: [PATCH 0919/2376] Add test sprites and make alignment initially better --- .../Resources/old-skin/skin.ini | 5 +++++ .../Resources/old-skin/taiko-bar-left.png | Bin 0 -> 17758 bytes .../Resources/old-skin/taiko-drum-inner.png | Bin 0 -> 4661 bytes .../Resources/old-skin/taiko-drum-outer.png | Bin 0 -> 5585 bytes .../Skinning/LegacyInputDrum.cs | 8 ++++---- 5 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/skin.ini create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-bar-left.png create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-drum-inner.png create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-drum-outer.png diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/skin.ini b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/skin.ini new file mode 100644 index 0000000000..462c2c278e --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/skin.ini @@ -0,0 +1,5 @@ +[General] +Name: an old skin +Author: an old guy + +// no version specified means v1 \ No newline at end of file diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-bar-left.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-bar-left.png new file mode 100644 index 0000000000000000000000000000000000000000..ad55fd5a96f702c4a6cda17692d78723b2e8c487 GIT binary patch literal 17758 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 literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-drum-inner.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-drum-inner.png new file mode 100644 index 0000000000000000000000000000000000000000..f5c02509fb63c4f5d68a81f626b9ae07fd22e800 GIT binary patch literal 4661 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 literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-drum-outer.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-drum-outer.png new file mode 100644 index 0000000000000000000000000000000000000000..53905792cb2e40cc5e799a146c1ccfbcffcfa369 GIT binary patch literal 5585 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 literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs index 8fe7c5e566..de01999e7f 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs @@ -82,15 +82,15 @@ namespace osu.Game.Rulesets.Taiko.Skinning { rimHit = new Sprite { - Anchor = flipped ? Anchor.CentreRight : Anchor.CentreLeft, - Origin = flipped ? Anchor.CentreLeft : Anchor.CentreRight, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreLeft, // opposite due to scale inversion. Scale = new Vector2(-1, 1), Alpha = 0, }, centreHit = new Sprite { - Anchor = flipped ? Anchor.CentreRight : Anchor.CentreLeft, - Origin = flipped ? Anchor.CentreRight : Anchor.CentreLeft, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, Alpha = 0, } }; From 4b16b2e720eced66af563293aeded37b1a6b59d9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 8 Apr 2020 19:19:14 +0900 Subject: [PATCH 0920/2376] Bump legacy skin version --- osu.Game/Skinning/DefaultLegacySkin.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/DefaultLegacySkin.cs b/osu.Game/Skinning/DefaultLegacySkin.cs index 1929a7e5d2..78d3a37f7c 100644 --- a/osu.Game/Skinning/DefaultLegacySkin.cs +++ b/osu.Game/Skinning/DefaultLegacySkin.cs @@ -20,7 +20,7 @@ namespace osu.Game.Skinning new Color4(242, 24, 57, 255) ); - Configuration.LegacyVersion = 2.0m; + Configuration.LegacyVersion = 2.7m; } public static SkinInfo Info { get; } = new SkinInfo From d786a2c5b33a0ab6be7ff0924094124f3ee82f30 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 7 Apr 2020 19:14:31 +0900 Subject: [PATCH 0921/2376] Add alignment support for skin versions older than 2.1 --- .../Skinning/LegacyInputDrum.cs | 59 +++++++++++++------ 1 file changed, 41 insertions(+), 18 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs index de01999e7f..c61e35692b 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs @@ -18,9 +18,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning /// internal class LegacyInputDrum : Container { + private LegacyHalfDrum left; + private LegacyHalfDrum right; + public LegacyInputDrum() { - AutoSizeAxes = Axes.Both; + Size = new Vector2(180, 200); } [BackgroundDependencyLoader] @@ -32,25 +35,47 @@ namespace osu.Game.Rulesets.Taiko.Skinning { Texture = skin.GetTexture("taiko-bar-left") }, - new LegacyHalfDrum(false) + left = new LegacyHalfDrum(false) { Name = "Left Half", RelativeSizeAxes = Axes.Both, - Width = 0.5f, RimAction = TaikoAction.LeftRim, CentreAction = TaikoAction.LeftCentre }, - new LegacyHalfDrum(true) + right = new LegacyHalfDrum(true) { Name = "Right Half", - Anchor = Anchor.TopRight, RelativeSizeAxes = Axes.Both, - Width = 0.5f, + Origin = Anchor.TopRight, Scale = new Vector2(-1, 1), RimAction = TaikoAction.RightRim, CentreAction = TaikoAction.RightCentre } }; + + // this will be used in the future for stable skin alignment. keeping here for reference. + const float taiko_bar_y = 0; + + // stable things + const float ratio = 1.6f; + + // because the right half is flipped, we need to position using width - position to get the true "topleft" origin position + float negativeScaleAdjust = Width / ratio; + + if (skin.GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value >= 2.1m) + { + left.Centre.Position = new Vector2(0, taiko_bar_y) * ratio; + right.Centre.Position = new Vector2(negativeScaleAdjust - 56, taiko_bar_y) * ratio; + left.Rim.Position = new Vector2(0, taiko_bar_y) * ratio; + right.Rim.Position = new Vector2(negativeScaleAdjust - 56, taiko_bar_y) * ratio; + } + else + { + left.Centre.Position = new Vector2(18, taiko_bar_y + 31) * ratio; + right.Centre.Position = new Vector2(negativeScaleAdjust - 54, taiko_bar_y + 31) * ratio; + left.Rim.Position = new Vector2(8, taiko_bar_y + 23) * ratio; + right.Rim.Position = new Vector2(negativeScaleAdjust - 53, taiko_bar_y + 23) * ratio; + } } /// @@ -68,8 +93,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning /// public TaikoAction CentreAction; - private readonly Sprite rimHit; - private readonly Sprite centreHit; + public readonly Sprite Rim; + public readonly Sprite Centre; [Resolved] private DrumSampleMapping sampleMappings { get; set; } @@ -80,18 +105,16 @@ namespace osu.Game.Rulesets.Taiko.Skinning Children = new Drawable[] { - rimHit = new Sprite + Rim = new Sprite { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreLeft, // opposite due to scale inversion. Scale = new Vector2(-1, 1), + Origin = flipped ? Anchor.TopLeft : Anchor.TopRight, Alpha = 0, }, - centreHit = new Sprite + Centre = new Sprite { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, Alpha = 0, + Origin = flipped ? Anchor.TopRight : Anchor.TopLeft, } }; } @@ -99,8 +122,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning [BackgroundDependencyLoader] private void load(ISkinSource skin) { - rimHit.Texture = skin.GetTexture(@"taiko-drum-outer"); - centreHit.Texture = skin.GetTexture(@"taiko-drum-inner"); + Rim.Texture = skin.GetTexture(@"taiko-drum-outer"); + Centre.Texture = skin.GetTexture(@"taiko-drum-inner"); } public bool OnPressed(TaikoAction action) @@ -110,12 +133,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning if (action == CentreAction) { - target = centreHit; + target = Centre; drumSample.Centre?.Play(); } else if (action == RimAction) { - target = rimHit; + target = Rim; drumSample.Rim?.Play(); } From 61d8cfd2241453074c3d5067b18b7bda13afccbd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 8 Apr 2020 19:51:55 +0900 Subject: [PATCH 0922/2376] Fix triangle intro video being out of time --- osu.Game/Screens/Menu/IntroTriangles.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/IntroTriangles.cs b/osu.Game/Screens/Menu/IntroTriangles.cs index b44b6ea993..188a49c147 100644 --- a/osu.Game/Screens/Menu/IntroTriangles.cs +++ b/osu.Game/Screens/Menu/IntroTriangles.cs @@ -270,7 +270,7 @@ namespace osu.Game.Screens.Menu [BackgroundDependencyLoader] private void load() { - InternalChild = new Video(videoStream, false) + InternalChild = new Video(videoStream) { RelativeSizeAxes = Axes.Both, }; From d27d8671ab08e6a334f9859e46a295c68b3d5f01 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 8 Apr 2020 14:23:29 +0300 Subject: [PATCH 0923/2376] Convert all static getter-only properties to static readonly fields --- .../TestSceneHyperDashColouring.cs | 18 +++++++++--------- .../Objects/Drawables/FruitPiece.cs | 4 ++-- .../Skinning/CatchSkinExtensions.cs | 2 +- osu.Game.Rulesets.Catch/UI/Catcher.cs | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs index c8d28dbaeb..846b17f324 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs @@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Catch.Tests }, false, false, false); }); - AddAssert("hyper-dash fruit has default colour", () => checkLegacyFruitHyperDashColour(drawableFruit, Catcher.DefaultHyperDashColour)); + AddAssert("hyper-dash fruit has default colour", () => checkLegacyFruitHyperDashColour(drawableFruit, Catcher.DEFAULT_HYPER_DASH_COLOUR)); } [TestCase(true)] @@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Catch.Tests }, customCatcherHyperDashColour, false, true); }); - AddAssert("hyper-dash fruit use fruit colour from skin", () => checkLegacyFruitHyperDashColour(drawableFruit, TestSkin.CustomHyperDashFruitColour)); + AddAssert("hyper-dash fruit use fruit colour from skin", () => checkLegacyFruitHyperDashColour(drawableFruit, TestSkin.CUSTOM_HYPER_DASH_FRUIT_COLOUR)); } [Test] @@ -88,7 +88,7 @@ namespace osu.Game.Rulesets.Catch.Tests }, true, false, false); }); - AddAssert("hyper-dash fruit colour falls back to catcher colour from skin", () => checkLegacyFruitHyperDashColour(drawableFruit, TestSkin.CustomHyperDashColour)); + AddAssert("hyper-dash fruit colour falls back to catcher colour from skin", () => checkLegacyFruitHyperDashColour(drawableFruit, TestSkin.CUSTOM_HYPER_DASH_COLOUR)); } private Drawable setupSkinHierarchy(Drawable child, bool customCatcherColour, bool customAfterColour, bool customFruitColour) @@ -108,21 +108,21 @@ namespace osu.Game.Rulesets.Catch.Tests private class TestSkin : LegacySkin { - public static Color4 CustomHyperDashColour { get; } = Color4.Goldenrod; - public static Color4 CustomHyperDashFruitColour { get; } = Color4.Cyan; - public static Color4 CustomHyperDashAfterColour { get; } = Color4.Lime; + public static readonly Color4 CUSTOM_HYPER_DASH_COLOUR = Color4.Goldenrod; + public static readonly Color4 CUSTOM_HYPER_DASH_AFTER_COLOUR = Color4.Lime; + public static readonly Color4 CUSTOM_HYPER_DASH_FRUIT_COLOUR = Color4.Cyan; public TestSkin(bool customCatcherColour, bool customAfterColour, bool customFruitColour) : base(new SkinInfo(), null, null, string.Empty) { if (customCatcherColour) - Configuration.CustomColours[CatchSkinColour.HyperDash.ToString()] = CustomHyperDashColour; + Configuration.CustomColours[CatchSkinColour.HyperDash.ToString()] = CUSTOM_HYPER_DASH_COLOUR; if (customAfterColour) - Configuration.CustomColours[CatchSkinColour.HyperDashAfterImage.ToString()] = CustomHyperDashAfterColour; + Configuration.CustomColours[CatchSkinColour.HyperDashAfterImage.ToString()] = CUSTOM_HYPER_DASH_AFTER_COLOUR; if (customFruitColour) - Configuration.CustomColours[CatchSkinColour.HyperDashFruit.ToString()] = CustomHyperDashFruitColour; + Configuration.CustomColours[CatchSkinColour.HyperDashFruit.ToString()] = CUSTOM_HYPER_DASH_FRUIT_COLOUR; } } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs index 359329885c..7ac9f11ad6 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs @@ -68,7 +68,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, - BorderColour = Catcher.DefaultHyperDashColour, + BorderColour = Catcher.DEFAULT_HYPER_DASH_COLOUR, BorderThickness = 12f * RADIUS_ADJUST, Children = new Drawable[] { @@ -78,7 +78,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables Alpha = 0.3f, Blending = BlendingParameters.Additive, RelativeSizeAxes = Axes.Both, - Colour = Catcher.DefaultHyperDashColour, + Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR, } } }); diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchSkinExtensions.cs b/osu.Game.Rulesets.Catch/Skinning/CatchSkinExtensions.cs index 623f87bf11..718b22a0fb 100644 --- a/osu.Game.Rulesets.Catch/Skinning/CatchSkinExtensions.cs +++ b/osu.Game.Rulesets.Catch/Skinning/CatchSkinExtensions.cs @@ -15,6 +15,6 @@ namespace osu.Game.Rulesets.Catch.Skinning public static IBindable GetHyperDashFruitColour(this ISkin skin) => skin.GetConfig(CatchSkinColour.HyperDashFruit) ?? skin.GetConfig(CatchSkinColour.HyperDash) ?? - new Bindable(Catcher.DefaultHyperDashColour); + new Bindable(Catcher.DEFAULT_HYPER_DASH_COLOUR); } } diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 9bfff209d5..920d804e72 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Catch.UI { public class Catcher : Container, IKeyBindingHandler { - public static Color4 DefaultHyperDashColour { get; } = Color4.Red; + public static readonly Color4 DEFAULT_HYPER_DASH_COLOUR = Color4.Red; /// /// Whether we are hyper-dashing or not. From 4976f80b7107ae0e7554b02423ef0515c1b023f6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 9 Apr 2020 14:31:25 +0900 Subject: [PATCH 0924/2376] Move implementation to HUD --- osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs | 8 -------- osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs | 3 --- osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs | 8 -------- osu.Game/Screens/Play/HUD/FailingLayer.cs | 12 ++++++++++++ osu.Game/Screens/Play/HUD/HealthDisplay.cs | 2 +- osu.Game/Screens/Play/HUDOverlay.cs | 7 ++++++- 6 files changed, 19 insertions(+), 21 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs index 4df2bc0f52..ebe45aa3ab 100644 --- a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using osu.Framework.Allocation; using osu.Framework.Input; using osu.Game.Beatmaps; using osu.Game.Configuration; @@ -15,7 +14,6 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; -using osu.Game.Screens.Play.HUD; namespace osu.Game.Rulesets.Catch.UI { @@ -32,12 +30,6 @@ namespace osu.Game.Rulesets.Catch.UI TimeRange.Value = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450); } - [BackgroundDependencyLoader] - private void load() - { - Overlays.Add(new FailingLayer()); - } - protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new CatchFramedReplayInputHandler(replay); protected override ReplayRecorder CreateReplayRecorder(Replay replay) => new CatchReplayRecorder(replay, (CatchPlayfield)Playfield); diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index 18bdfa5b5d..14cad39b04 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -21,7 +21,6 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; -using osu.Game.Screens.Play.HUD; using osuTK; namespace osu.Game.Rulesets.Mania.UI @@ -78,8 +77,6 @@ namespace osu.Game.Rulesets.Mania.UI configDirection.BindValueChanged(direction => Direction.Value = (ScrollingDirection)direction.NewValue, true); Config.BindWith(ManiaRulesetSetting.ScrollTime, TimeRange); - - Overlays.Add(new FailingLayer()); } protected override void AdjustScrollSpeed(int amount) diff --git a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs index b04e3cef3b..b4d51d11c9 100644 --- a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using osu.Framework.Allocation; using osu.Framework.Input; using osu.Game.Beatmaps; using osu.Game.Input.Handlers; @@ -17,7 +16,6 @@ using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.UI; using osu.Game.Screens.Play; -using osu.Game.Screens.Play.HUD; using osuTK; namespace osu.Game.Rulesets.Osu.UI @@ -31,12 +29,6 @@ namespace osu.Game.Rulesets.Osu.UI { } - [BackgroundDependencyLoader] - private void load() - { - Overlays.Add(new FailingLayer()); - } - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // always show the gameplay cursor protected override Playfield CreatePlayfield() => new OsuPlayfield(); diff --git a/osu.Game/Screens/Play/HUD/FailingLayer.cs b/osu.Game/Screens/Play/HUD/FailingLayer.cs index 761178b93d..f026d09c39 100644 --- a/osu.Game/Screens/Play/HUD/FailingLayer.cs +++ b/osu.Game/Screens/Play/HUD/FailingLayer.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Graphics; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Screens.Play.HUD { @@ -48,6 +49,17 @@ namespace osu.Game.Screens.Play.HUD enabled.BindValueChanged(e => this.FadeTo(e.NewValue ? 1 : 0, fade_time, Easing.OutQuint), true); } + public override void BindHealthProcessor(HealthProcessor processor) + { + base.BindHealthProcessor(processor); + + if (!(processor is DrainingHealthProcessor)) + { + enabled.UnbindBindings(); + enabled.Value = false; + } + } + protected override void Update() { box.Alpha = (float)Interpolation.ValueAt(Math.Clamp(Clock.ElapsedFrameTime, 0, fade_time), box.Alpha, diff --git a/osu.Game/Screens/Play/HUD/HealthDisplay.cs b/osu.Game/Screens/Play/HUD/HealthDisplay.cs index 01cb64a88c..08cb07d7ee 100644 --- a/osu.Game/Screens/Play/HUD/HealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/HealthDisplay.cs @@ -23,7 +23,7 @@ namespace osu.Game.Screens.Play.HUD /// /// Bind the tracked fields of to this health display. /// - public void BindHealthProcessor(HealthProcessor processor) + public virtual void BindHealthProcessor(HealthProcessor processor) { Current.BindTo(processor.Health); } diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index e06f6d19c2..5114efd9a9 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -37,6 +37,7 @@ namespace osu.Game.Screens.Play public readonly HitErrorDisplay HitErrorDisplay; public readonly HoldForMenuButton HoldToQuit; public readonly PlayerSettingsOverlay PlayerSettingsOverlay; + public readonly FailingLayer FailingLayer; public Bindable ShowHealthbar = new Bindable(true); @@ -75,6 +76,7 @@ namespace osu.Game.Screens.Play Children = new Drawable[] { + FailingLayer = CreateFailingLayer(), visibilityContainer = new Container { RelativeSizeAxes = Axes.Both, @@ -260,6 +262,8 @@ namespace osu.Game.Screens.Play Margin = new MarginPadding { Top = 20 } }; + protected virtual FailingLayer CreateFailingLayer() => new FailingLayer(); + protected virtual KeyCounterDisplay CreateKeyCounter() => new KeyCounterDisplay { Anchor = Anchor.BottomRight, @@ -304,7 +308,8 @@ namespace osu.Game.Screens.Play protected virtual void BindHealthProcessor(HealthProcessor processor) { - HealthDisplay?.Current.BindTo(processor.Health); + HealthDisplay?.BindHealthProcessor(processor); + FailingLayer?.BindHealthProcessor(processor); } } } From 947745d87eff2d73b2f6f3c7da091897245217e7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 9 Apr 2020 14:33:11 +0900 Subject: [PATCH 0925/2376] Change fail effect to be less distracting --- .../Visual/Gameplay/TestSceneFailingLayer.cs | 5 +-- osu.Game/Screens/Play/HUD/FailingLayer.cs | 40 +++++++++++++++---- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs index 97fe0ac769..42a211cb3d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs @@ -1,11 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Testing; using osu.Game.Configuration; using osu.Game.Screens.Play.HUD; @@ -40,7 +37,7 @@ namespace osu.Game.Tests.Visual.Gameplay public void TestLayerFading() { AddSliderStep("current health", 0.0, 1.0, 1.0, val => layer.Current.Value = val); - var box = layer.ChildrenOfType().First(); + var box = layer.Child; AddStep("set health to 0.10", () => layer.Current.Value = 0.10); AddWaitStep("wait for fade to finish", 5); diff --git a/osu.Game/Screens/Play/HUD/FailingLayer.cs b/osu.Game/Screens/Play/HUD/FailingLayer.cs index f026d09c39..79f6855804 100644 --- a/osu.Game/Screens/Play/HUD/FailingLayer.cs +++ b/osu.Game/Screens/Play/HUD/FailingLayer.cs @@ -4,12 +4,16 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Rulesets.Scoring; +using osuTK.Graphics; namespace osu.Game.Screens.Play.HUD { @@ -22,8 +26,6 @@ namespace osu.Game.Screens.Play.HUD private const int fade_time = 400; - private readonly Box box; - private Bindable enabled; /// @@ -31,20 +33,43 @@ namespace osu.Game.Screens.Play.HUD /// public double LowHealthThreshold = 0.20f; + private readonly Container boxes; + public FailingLayer() { RelativeSizeAxes = Axes.Both; - Child = box = new Box + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Alpha = 0 + boxes = new Container + { + Blending = BlendingParameters.Additive, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientVertical(Color4.White, Color4.White.Opacity(0)), + Height = 0.2f, + }, + new Box + { + RelativeSizeAxes = Axes.Both, + Height = 0.2f, + Colour = ColourInfo.GradientVertical(Color4.White.Opacity(0), Color4.White), + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + }, + } + }, }; } [BackgroundDependencyLoader] private void load(OsuColour color, OsuConfigManager config) { - box.Colour = color.Red; + boxes.Colour = color.Red; + enabled = config.GetBindable(OsuSetting.FadePlayfieldWhenHealthLow); enabled.BindValueChanged(e => this.FadeTo(e.NewValue ? 1 : 0, fade_time, Easing.OutQuint), true); } @@ -53,6 +78,7 @@ namespace osu.Game.Screens.Play.HUD { base.BindHealthProcessor(processor); + // don't display ever if the ruleset is not using a draining health display. if (!(processor is DrainingHealthProcessor)) { enabled.UnbindBindings(); @@ -62,7 +88,7 @@ namespace osu.Game.Screens.Play.HUD protected override void Update() { - box.Alpha = (float)Interpolation.ValueAt(Math.Clamp(Clock.ElapsedFrameTime, 0, fade_time), box.Alpha, + boxes.Alpha = (float)Interpolation.ValueAt(Math.Clamp(Clock.ElapsedFrameTime, 0, fade_time), boxes.Alpha, Math.Clamp(max_alpha * (1 - Current.Value / LowHealthThreshold), 0, max_alpha), 0, fade_time, Easing.Out); base.Update(); From 52c976265146e754738a5c8f94222687537cd769 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 9 Apr 2020 14:36:04 +0900 Subject: [PATCH 0926/2376] Remove pointless keywords --- osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs index 4b75910454..0e854e8e9f 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs @@ -56,7 +56,6 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay { LabelText = "Fade playfield to red when health is low", Bindable = config.GetBindable(OsuSetting.FadePlayfieldWhenHealthLow), - Keywords = new[] { "hp", "low", "playfield", "red" } }, new SettingsCheckbox { From 6db22366e2bcd50bb60aaa5b327d42e6fa639ad0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 9 Apr 2020 14:47:48 +0900 Subject: [PATCH 0927/2376] Add new tests and tidy up existing tests --- .../Visual/Gameplay/TestSceneFailingLayer.cs | 51 ++++++++++++------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs index 42a211cb3d..0b5f023007 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs @@ -3,48 +3,63 @@ using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Testing; using osu.Game.Configuration; +using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play.HUD; namespace osu.Game.Tests.Visual.Gameplay { public class TestSceneFailingLayer : OsuTestScene { - private readonly FailingLayer layer; + private FailingLayer layer; [Resolved] private OsuConfigManager config { get; set; } - public TestSceneFailingLayer() + [SetUpSteps] + public void SetUpSteps() { - Child = layer = new FailingLayer(); + AddStep("create layer", () => Child = layer = new FailingLayer()); + AddStep("enable layer", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, true)); + AddUntilStep("layer is visible", () => layer.IsPresent); } [Test] - public void TestLayerConfig() + public void TestLayerDisabledViaConfig() { - AddStep("enable layer", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, true)); - AddWaitStep("wait for transition to finish", 5); - AddAssert("layer is enabled", () => layer.IsPresent); - AddStep("disable layer", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, false)); - AddWaitStep("wait for transition to finish", 5); - AddAssert("layer is disabled", () => !layer.IsPresent); - AddStep("restore layer enabling", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, true)); + AddUntilStep("layer is not visible", () => !layer.IsPresent); + } + + [Test] + public void TestLayerVisibilityWithAccumulatingProcessor() + { + AddStep("bind accumulating processor", () => layer.BindHealthProcessor(new AccumulatingHealthProcessor(1))); + AddUntilStep("layer is not visible", () => !layer.IsPresent); + } + + [Test] + public void TestLayerVisibilityWithDrainingProcessor() + { + AddStep("bind accumulating processor", () => layer.BindHealthProcessor(new DrainingHealthProcessor(1))); + AddWaitStep("wait for potential fade", 10); + AddAssert("layer is still visible", () => layer.IsPresent); } [Test] public void TestLayerFading() { - AddSliderStep("current health", 0.0, 1.0, 1.0, val => layer.Current.Value = val); - var box = layer.Child; + AddSliderStep("current health", 0.0, 1.0, 1.0, val => + { + if (layer != null) + layer.Current.Value = val; + }); - AddStep("set health to 0.10", () => layer.Current.Value = 0.10); - AddWaitStep("wait for fade to finish", 5); - AddAssert("layer fade is visible", () => box.IsPresent); + AddStep("set health to 0.10", () => layer.Current.Value = 0.1); + AddUntilStep("layer fade is visible", () => layer.Child.Alpha > 0.1f); AddStep("set health to 1", () => layer.Current.Value = 1f); - AddWaitStep("wait for fade to finish", 10); - AddAssert("layer fade is invisible", () => !box.IsPresent); + AddUntilStep("layer fade is invisible", () => !layer.Child.IsPresent); } } } From c44957db3f8261da13f277c1b225e0bedbdebf3b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 9 Apr 2020 14:49:09 +0900 Subject: [PATCH 0928/2376] Change initial health to 1 to avoid false fail display --- osu.Game/Screens/Play/HUD/FailingLayer.cs | 1 + osu.Game/Screens/Play/HUD/HealthDisplay.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/FailingLayer.cs b/osu.Game/Screens/Play/HUD/FailingLayer.cs index 79f6855804..335516a767 100644 --- a/osu.Game/Screens/Play/HUD/FailingLayer.cs +++ b/osu.Game/Screens/Play/HUD/FailingLayer.cs @@ -42,6 +42,7 @@ namespace osu.Game.Screens.Play.HUD { boxes = new Container { + Alpha = 0, Blending = BlendingParameters.Additive, RelativeSizeAxes = Axes.Both, Children = new Drawable[] diff --git a/osu.Game/Screens/Play/HUD/HealthDisplay.cs b/osu.Game/Screens/Play/HUD/HealthDisplay.cs index 08cb07d7ee..edc9dedf24 100644 --- a/osu.Game/Screens/Play/HUD/HealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/HealthDisplay.cs @@ -14,7 +14,7 @@ namespace osu.Game.Screens.Play.HUD /// public abstract class HealthDisplay : Container { - public readonly BindableDouble Current = new BindableDouble + public readonly BindableDouble Current = new BindableDouble(1) { MinValue = 0, MaxValue = 1 From 5a78e74470740ab31141c71b623d3d0c4bfa2987 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 9 Apr 2020 14:51:50 +0900 Subject: [PATCH 0929/2376] Use Lerp instead of ValueAt --- osu.Game/Screens/Play/HUD/FailingLayer.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/FailingLayer.cs b/osu.Game/Screens/Play/HUD/FailingLayer.cs index 335516a767..2a98e277b3 100644 --- a/osu.Game/Screens/Play/HUD/FailingLayer.cs +++ b/osu.Game/Screens/Play/HUD/FailingLayer.cs @@ -89,8 +89,9 @@ namespace osu.Game.Screens.Play.HUD protected override void Update() { - boxes.Alpha = (float)Interpolation.ValueAt(Math.Clamp(Clock.ElapsedFrameTime, 0, fade_time), boxes.Alpha, - Math.Clamp(max_alpha * (1 - Current.Value / LowHealthThreshold), 0, max_alpha), 0, fade_time, Easing.Out); + double target = Math.Clamp(max_alpha * (1 - Current.Value / LowHealthThreshold), 0, max_alpha); + + boxes.Alpha = (float)Interpolation.Lerp(boxes.Alpha, target, Clock.ElapsedFrameTime * 0.01f); base.Update(); } From 1c72afe8c49bfbe8df6f7670bb21c8f45cbcd271 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 9 Apr 2020 14:52:40 +0900 Subject: [PATCH 0930/2376] Move fading test to top for convenience --- .../Visual/Gameplay/TestSceneFailingLayer.cs | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs index 0b5f023007..d831ea1835 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs @@ -25,6 +25,21 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("layer is visible", () => layer.IsPresent); } + [Test] + public void TestLayerFading() + { + AddSliderStep("current health", 0.0, 1.0, 1.0, val => + { + if (layer != null) + layer.Current.Value = val; + }); + + AddStep("set health to 0.10", () => layer.Current.Value = 0.1); + AddUntilStep("layer fade is visible", () => layer.Child.Alpha > 0.1f); + AddStep("set health to 1", () => layer.Current.Value = 1f); + AddUntilStep("layer fade is invisible", () => !layer.Child.IsPresent); + } + [Test] public void TestLayerDisabledViaConfig() { @@ -46,20 +61,5 @@ namespace osu.Game.Tests.Visual.Gameplay AddWaitStep("wait for potential fade", 10); AddAssert("layer is still visible", () => layer.IsPresent); } - - [Test] - public void TestLayerFading() - { - AddSliderStep("current health", 0.0, 1.0, 1.0, val => - { - if (layer != null) - layer.Current.Value = val; - }); - - AddStep("set health to 0.10", () => layer.Current.Value = 0.1); - AddUntilStep("layer fade is visible", () => layer.Child.Alpha > 0.1f); - AddStep("set health to 1", () => layer.Current.Value = 1f); - AddUntilStep("layer fade is invisible", () => !layer.Child.IsPresent); - } } } From c5005eb378c657fff32971aebcb336ea4ff20ad1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 9 Apr 2020 14:55:02 +0900 Subject: [PATCH 0931/2376] Adjust gradient size slightly and make const --- osu.Game/Screens/Play/HUD/FailingLayer.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/FailingLayer.cs b/osu.Game/Screens/Play/HUD/FailingLayer.cs index 2a98e277b3..a1188343ac 100644 --- a/osu.Game/Screens/Play/HUD/FailingLayer.cs +++ b/osu.Game/Screens/Play/HUD/FailingLayer.cs @@ -33,6 +33,8 @@ namespace osu.Game.Screens.Play.HUD /// public double LowHealthThreshold = 0.20f; + private const float gradient_size = 0.3f; + private readonly Container boxes; public FailingLayer() @@ -51,12 +53,12 @@ namespace osu.Game.Screens.Play.HUD { RelativeSizeAxes = Axes.Both, Colour = ColourInfo.GradientVertical(Color4.White, Color4.White.Opacity(0)), - Height = 0.2f, + Height = gradient_size, }, new Box { RelativeSizeAxes = Axes.Both, - Height = 0.2f, + Height = gradient_size, Colour = ColourInfo.GradientVertical(Color4.White.Opacity(0), Color4.White), Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, From 134feefa141b27b81ca9674a8332257216ee4755 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 9 Apr 2020 13:10:09 +0300 Subject: [PATCH 0932/2376] Remove bindable --- .../TestSceneOverlayScrollContainer.cs | 6 ++-- osu.Game/Overlays/OverlayScrollContainer.cs | 36 +++++++++---------- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs index 0eccc907a1..4205d65100 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs @@ -55,13 +55,13 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void TestButtonVisibility() { - AddAssert("button is hidden", () => scroll.Button.Current.Value == Visibility.Hidden); + AddAssert("button is hidden", () => scroll.Button.State == Visibility.Hidden); AddStep("scroll to end", () => scroll.ScrollToEnd(false)); - AddAssert("button is visible", () => scroll.Button.Current.Value == Visibility.Visible); + AddAssert("button is visible", () => scroll.Button.State == Visibility.Visible); AddStep("scroll to start", () => scroll.ScrollToStart(false)); - AddAssert("button is hidden", () => scroll.Button.Current.Value == Visibility.Hidden); + AddAssert("button is hidden", () => scroll.Button.State == Visibility.Hidden); } [Test] diff --git a/osu.Game/Overlays/OverlayScrollContainer.cs b/osu.Game/Overlays/OverlayScrollContainer.cs index f96d9e3a31..a6c687f28f 100644 --- a/osu.Game/Overlays/OverlayScrollContainer.cs +++ b/osu.Game/Overlays/OverlayScrollContainer.cs @@ -3,14 +3,12 @@ using System.Collections.Generic; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Game.Graphics.Containers; using osuTK; @@ -43,7 +41,7 @@ namespace osu.Game.Overlays { ScrollToStart(); currentTarget = Target; - Button.Current.Value = Visibility.Hidden; + Button.State = Visibility.Hidden; } }); } @@ -54,7 +52,7 @@ namespace osu.Game.Overlays if (ScrollContent.DrawHeight + button_scroll_position < DrawHeight) { - Button.Current.Value = Visibility.Hidden; + Button.State = Visibility.Hidden; return; } @@ -62,19 +60,27 @@ namespace osu.Game.Overlays return; currentTarget = Target; - Button.Current.Value = Current > button_scroll_position ? Visibility.Visible : Visibility.Hidden; + Button.State = Current > button_scroll_position ? Visibility.Visible : Visibility.Hidden; } - public class ScrollToTopButton : OsuHoverContainer, IHasCurrentValue + public class ScrollToTopButton : OsuHoverContainer { private const int fade_duration = 500; - private readonly BindableWithCurrent current = new BindableWithCurrent(); + private Visibility state; - public Bindable Current + public Visibility State { - get => current.Current; - set => current.Current = value; + get => state; + set + { + if (value == state) + return; + + state = value; + Enabled.Value = state == Visibility.Visible; + this.FadeTo(state == Visibility.Visible ? 1 : 0, fade_duration, Easing.OutQuint); + } } protected override IEnumerable EffectTargets => new[] { background }; @@ -128,16 +134,6 @@ namespace osu.Game.Overlays flashColour = colourProvider.Light1; } - protected override void LoadComplete() - { - base.LoadComplete(); - Current.BindValueChanged(visibility => - { - Enabled.Value = visibility.NewValue == Visibility.Visible; - this.FadeTo(visibility.NewValue == Visibility.Visible ? 1 : 0, fade_duration, Easing.OutQuint); - }, true); - } - protected override bool OnClick(ClickEvent e) { background.FlashColour(flashColour, 800, Easing.OutQuint); From ee6ea08cf85a5c4cdb6de99ed8a445c84248d9ea Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 9 Apr 2020 19:54:58 +0900 Subject: [PATCH 0933/2376] Cleanup handling of hitobject updates --- .../Sliders/SliderSelectionBlueprint.cs | 6 ++++- osu.Game.Tests/Beatmaps/EditorBeatmapTest.cs | 6 ++--- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 27 ------------------- osu.Game/Rulesets/Edit/SelectionBlueprint.cs | 5 ---- osu.Game/Screens/Edit/EditorBeatmap.cs | 18 ++++++++++--- 5 files changed, 22 insertions(+), 40 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index c18b3b0ff3..001100d3ce 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -15,6 +15,7 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Compose; using osuTK; using osuTK.Input; @@ -34,6 +35,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders [Resolved(CanBeNull = true)] private IPlacementHandler placementHandler { get; set; } + [Resolved(CanBeNull = true)] + private EditorBeatmap editorBeatmap { get; set; } + public SliderSelectionBlueprint(DrawableSlider slider) : base(slider) { @@ -162,7 +166,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private void updatePath() { HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance; - UpdateHitObject(); + editorBeatmap?.UpdateHitObject(HitObject); } public override MenuItem[] ContextMenuItems => new MenuItem[] diff --git a/osu.Game.Tests/Beatmaps/EditorBeatmapTest.cs b/osu.Game.Tests/Beatmaps/EditorBeatmapTest.cs index 12d729d09f..f2b13e3a85 100644 --- a/osu.Game.Tests/Beatmaps/EditorBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/EditorBeatmapTest.cs @@ -58,7 +58,7 @@ namespace osu.Game.Tests.Beatmaps var editorBeatmap = new EditorBeatmap(new OsuBeatmap { HitObjects = { hitCircle } }); HitObject changedObject = null; - editorBeatmap.StartTimeChanged += h => changedObject = h; + editorBeatmap.HitObjectUpdated += h => changedObject = h; hitCircle.StartTime = 1000; Assert.That(changedObject, Is.EqualTo(hitCircle)); @@ -74,7 +74,7 @@ namespace osu.Game.Tests.Beatmaps var editorBeatmap = new EditorBeatmap(new OsuBeatmap()); HitObject changedObject = null; - editorBeatmap.StartTimeChanged += h => changedObject = h; + editorBeatmap.HitObjectUpdated += h => changedObject = h; var hitCircle = new HitCircle(); @@ -95,7 +95,7 @@ namespace osu.Game.Tests.Beatmaps var editorBeatmap = new EditorBeatmap(new OsuBeatmap { HitObjects = { hitCircle } }); HitObject changedObject = null; - editorBeatmap.StartTimeChanged += h => changedObject = h; + editorBeatmap.HitObjectUpdated += h => changedObject = h; editorBeatmap.Remove(hitCircle); Assert.That(changedObject, Is.Null); diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index fb4e945701..883288d6d7 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -69,10 +69,6 @@ namespace osu.Game.Rulesets.Edit [BackgroundDependencyLoader] private void load(IFrameBasedClock framedClock) { - EditorBeatmap.HitObjectAdded += addHitObject; - EditorBeatmap.HitObjectRemoved += removeHitObject; - EditorBeatmap.StartTimeChanged += UpdateHitObject; - Config = Dependencies.Get().GetConfigFor(Ruleset); try @@ -236,10 +232,6 @@ namespace osu.Game.Rulesets.Edit lastGridUpdateTime = EditorClock.CurrentTime; } - private void addHitObject(HitObject hitObject) => UpdateHitObject(hitObject); - - private void removeHitObject(HitObject hitObject) => UpdateHitObject(null); - public override IEnumerable HitObjects => drawableRulesetWrapper.Playfield.AllHitObjects; public override bool CursorInPlacementArea => drawableRulesetWrapper.Playfield.ReceivePositionalInputAt(inputManager.CurrentState.Mouse.Position); @@ -302,19 +294,6 @@ namespace osu.Game.Rulesets.Edit return DurationToDistance(referenceTime, snappedEndTime - referenceTime); } - - public override void UpdateHitObject(HitObject hitObject) => EditorBeatmap.UpdateHitObject(hitObject); - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - if (EditorBeatmap != null) - { - EditorBeatmap.HitObjectAdded -= addHitObject; - EditorBeatmap.HitObjectRemoved -= removeHitObject; - } - } } [Cached(typeof(HitObjectComposer))] @@ -344,12 +323,6 @@ namespace osu.Game.Rulesets.Edit [CanBeNull] protected virtual DistanceSnapGrid CreateDistanceSnapGrid([NotNull] IEnumerable selectedHitObjects) => null; - /// - /// Updates a , invoking and re-processing the beatmap. - /// - /// The to update. - public abstract void UpdateHitObject([CanBeNull] HitObject hitObject); - public abstract (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time); public abstract float GetBeatSnapDistanceAt(double referenceTime); diff --git a/osu.Game/Rulesets/Edit/SelectionBlueprint.cs b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs index a972d28480..e6a63eae4f 100644 --- a/osu.Game/Rulesets/Edit/SelectionBlueprint.cs +++ b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs @@ -108,11 +108,6 @@ namespace osu.Game.Rulesets.Edit public bool IsSelected => State == SelectionState.Selected; - /// - /// Updates the , invoking and re-processing the beatmap. - /// - protected void UpdateHitObject() => composer?.UpdateHitObject(HitObject); - /// /// The s to be displayed in the context menu for this . /// diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 5216e85903..7f04a7a58d 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -4,6 +4,7 @@ using System; using System.Collections; using System.Collections.Generic; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -29,9 +30,9 @@ namespace osu.Game.Screens.Edit public event Action HitObjectRemoved; /// - /// Invoked when the start time of a in this was changed. + /// Invoked when a is updated. /// - public event Action StartTimeChanged; + public event Action HitObjectUpdated; /// /// All currently selected s. @@ -68,7 +69,9 @@ namespace osu.Game.Screens.Edit /// Updates a , invoking and re-processing the beatmap. /// /// The to update. - public void UpdateHitObject(HitObject hitObject) + public void UpdateHitObject([NotNull] HitObject hitObject) => updateHitObject(hitObject, false); + + private void updateHitObject([CanBeNull] HitObject hitObject, bool silent) { scheduledUpdate?.Cancel(); scheduledUpdate = Scheduler.AddDelayed(() => @@ -76,6 +79,9 @@ namespace osu.Game.Screens.Edit beatmapProcessor?.PreProcess(); hitObject?.ApplyDefaults(ControlPointInfo, BeatmapInfo.BaseDifficulty); beatmapProcessor?.PostProcess(); + + if (!silent) + HitObjectUpdated?.Invoke(hitObject); }, 0); } @@ -114,6 +120,8 @@ namespace osu.Game.Screens.Edit mutableHitObjects.Insert(insertionIndex + 1, hitObject); HitObjectAdded?.Invoke(hitObject); + + updateHitObject(hitObject, true); } /// @@ -132,6 +140,8 @@ namespace osu.Game.Screens.Edit startTimeBindables.Remove(hitObject); HitObjectRemoved?.Invoke(hitObject); + + updateHitObject(null, true); } private void trackStartTime(HitObject hitObject) @@ -145,7 +155,7 @@ namespace osu.Game.Screens.Edit var insertionIndex = findInsertionIndex(PlayableBeatmap.HitObjects, hitObject.StartTime); mutableHitObjects.Insert(insertionIndex + 1, hitObject); - StartTimeChanged?.Invoke(hitObject); + UpdateHitObject(hitObject); }; } From b900f229e778c4c9ca5b65daccfea8837e6cc6eb Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 9 Apr 2020 20:21:42 +0900 Subject: [PATCH 0934/2376] Fix possible legacy beatmap encoder nullref --- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index ec2ca30535..12f2c58e35 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -111,7 +111,7 @@ namespace osu.Game.Beatmaps.Formats writer.WriteLine(FormattableString.Invariant($"Source: {beatmap.Metadata.Source}")); writer.WriteLine(FormattableString.Invariant($"Tags: {beatmap.Metadata.Tags}")); writer.WriteLine(FormattableString.Invariant($"BeatmapID: {beatmap.BeatmapInfo.OnlineBeatmapID ?? 0}")); - writer.WriteLine(FormattableString.Invariant($"BeatmapSetID: {beatmap.BeatmapInfo.BeatmapSet.OnlineBeatmapSetID ?? -1}")); + writer.WriteLine(FormattableString.Invariant($"BeatmapSetID: {beatmap.BeatmapInfo.BeatmapSet?.OnlineBeatmapSetID ?? -1}")); } private void handleDifficulty(TextWriter writer) From 683302a77d63a223ca902ac8f19b558908b941c7 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 9 Apr 2020 20:25:26 +0900 Subject: [PATCH 0935/2376] Fix crash when trying to edit long beatmaps --- osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index ddca5e42c2..1cb4f737c1 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -60,8 +60,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline waveform.Waveform = b.NewValue.Waveform; track = b.NewValue.Track; - MinZoom = getZoomLevelForVisibleMilliseconds(10000); MaxZoom = getZoomLevelForVisibleMilliseconds(500); + MinZoom = getZoomLevelForVisibleMilliseconds(10000); Zoom = getZoomLevelForVisibleMilliseconds(2000); }, true); } From e58bf8a0d08154d87043f1a90170e8f46f6eea8c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 9 Apr 2020 20:38:38 +0900 Subject: [PATCH 0936/2376] Add DiffPlex package --- osu.Game/osu.Game.csproj | 1 + osu.iOS.props | 1 + 2 files changed, 2 insertions(+) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 3e2c2b1599..3dd84caea9 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -18,6 +18,7 @@ + diff --git a/osu.iOS.props b/osu.iOS.props index 7903d964ce..7e6f6b5246 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -75,6 +75,7 @@ + From 86243d463f423367c2f140be93d00986c044894b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 9 Apr 2020 20:48:59 +0900 Subject: [PATCH 0937/2376] Add legacy beatmap diffing --- .../Editor/LegacyEditorBeatmapDifferTest.cs | 342 ++++++++++++++++++ osu.Game/Screens/Edit/EditorBeatmap.cs | 40 +- .../Screens/Edit/LegacyEditorBeatmapDiffer.cs | 110 ++++++ 3 files changed, 488 insertions(+), 4 deletions(-) create mode 100644 osu.Game.Tests/Editor/LegacyEditorBeatmapDifferTest.cs create mode 100644 osu.Game/Screens/Edit/LegacyEditorBeatmapDiffer.cs diff --git a/osu.Game.Tests/Editor/LegacyEditorBeatmapDifferTest.cs b/osu.Game.Tests/Editor/LegacyEditorBeatmapDifferTest.cs new file mode 100644 index 0000000000..d70a112b7f --- /dev/null +++ b/osu.Game.Tests/Editor/LegacyEditorBeatmapDifferTest.cs @@ -0,0 +1,342 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.IO; +using System.Text; +using NUnit.Framework; +using osu.Game.Audio; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Formats; +using osu.Game.IO; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Beatmaps; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Screens.Edit; +using osuTK; +using Decoder = osu.Game.Beatmaps.Formats.Decoder; + +namespace osu.Game.Tests.Editor +{ + [TestFixture] + public class LegacyEditorBeatmapDifferTest + { + private LegacyEditorBeatmapDiffer differ; + private EditorBeatmap current; + + [SetUp] + public void Setup() + { + differ = new LegacyEditorBeatmapDiffer(current = new EditorBeatmap(new OsuBeatmap + { + BeatmapInfo = + { + Ruleset = new OsuRuleset().RulesetInfo + } + })); + } + + [Test] + public void TestAddHitObject() + { + var patch = new OsuBeatmap + { + HitObjects = + { + new HitCircle { StartTime = 1000 } + } + }; + + runTest(patch); + } + + [Test] + public void TestInsertHitObject() + { + current.AddRange(new[] + { + new HitCircle { StartTime = 1000 }, + new HitCircle { StartTime = 3000 }, + }); + + var patch = new OsuBeatmap + { + HitObjects = + { + (OsuHitObject)current.HitObjects[0], + new HitCircle { StartTime = 2000 }, + (OsuHitObject)current.HitObjects[1], + } + }; + + runTest(patch); + } + + [Test] + public void TestDeleteHitObject() + { + current.AddRange(new[] + { + new HitCircle { StartTime = 1000 }, + new HitCircle { StartTime = 2000 }, + new HitCircle { StartTime = 3000 }, + }); + + var patch = new OsuBeatmap + { + HitObjects = + { + (OsuHitObject)current.HitObjects[0], + (OsuHitObject)current.HitObjects[2], + } + }; + + runTest(patch); + } + + [Test] + public void TestChangeStartTime() + { + current.AddRange(new[] + { + new HitCircle { StartTime = 1000 }, + new HitCircle { StartTime = 2000 }, + new HitCircle { StartTime = 3000 }, + }); + + var patch = new OsuBeatmap + { + HitObjects = + { + new HitCircle { StartTime = 500 }, + (OsuHitObject)current.HitObjects[1], + (OsuHitObject)current.HitObjects[2], + } + }; + + runTest(patch); + } + + [Test] + public void TestChangeSample() + { + current.AddRange(new[] + { + new HitCircle { StartTime = 1000 }, + new HitCircle { StartTime = 2000 }, + new HitCircle { StartTime = 3000 }, + }); + + var patch = new OsuBeatmap + { + HitObjects = + { + (OsuHitObject)current.HitObjects[0], + new HitCircle { StartTime = 2000, Samples = { new HitSampleInfo { Name = HitSampleInfo.HIT_FINISH } } }, + (OsuHitObject)current.HitObjects[2], + } + }; + + runTest(patch); + } + + [Test] + public void TestChangeSliderPath() + { + current.AddRange(new OsuHitObject[] + { + new HitCircle { StartTime = 1000 }, + new Slider + { + StartTime = 2000, + Path = new SliderPath(new[] + { + new PathControlPoint(Vector2.Zero), + new PathControlPoint(Vector2.One), + new PathControlPoint(new Vector2(2), PathType.Bezier), + new PathControlPoint(new Vector2(3)), + }, 50) + }, + new HitCircle { StartTime = 3000 }, + }); + + var patch = new OsuBeatmap + { + HitObjects = + { + (OsuHitObject)current.HitObjects[0], + new Slider + { + StartTime = 2000, + Path = new SliderPath(new[] + { + new PathControlPoint(Vector2.Zero, PathType.Bezier), + new PathControlPoint(new Vector2(4)), + new PathControlPoint(new Vector2(5)), + }, 100) + }, + (OsuHitObject)current.HitObjects[2], + } + }; + + runTest(patch); + } + + [Test] + public void TestAddMultipleHitObjects() + { + current.AddRange(new[] + { + new HitCircle { StartTime = 1000 }, + new HitCircle { StartTime = 2000 }, + new HitCircle { StartTime = 3000 }, + }); + + var patch = new OsuBeatmap + { + HitObjects = + { + new HitCircle { StartTime = 500 }, + (OsuHitObject)current.HitObjects[0], + new HitCircle { StartTime = 1500 }, + (OsuHitObject)current.HitObjects[1], + new HitCircle { StartTime = 2250 }, + new HitCircle { StartTime = 2500 }, + (OsuHitObject)current.HitObjects[2], + new HitCircle { StartTime = 3500 }, + } + }; + + runTest(patch); + } + + [Test] + public void TestDeleteMultipleHitObjects() + { + current.AddRange(new[] + { + new HitCircle { StartTime = 500 }, + new HitCircle { StartTime = 1000 }, + new HitCircle { StartTime = 1500 }, + new HitCircle { StartTime = 2000 }, + new HitCircle { StartTime = 2250 }, + new HitCircle { StartTime = 2500 }, + new HitCircle { StartTime = 3000 }, + new HitCircle { StartTime = 3500 }, + }); + + var patch = new OsuBeatmap + { + HitObjects = + { + (OsuHitObject)current.HitObjects[1], + (OsuHitObject)current.HitObjects[3], + (OsuHitObject)current.HitObjects[6], + } + }; + + runTest(patch); + } + + [Test] + public void TestChangeSamplesOfMultipleHitObjects() + { + current.AddRange(new[] + { + new HitCircle { StartTime = 500 }, + new HitCircle { StartTime = 1000 }, + new HitCircle { StartTime = 1500 }, + new HitCircle { StartTime = 2000 }, + new HitCircle { StartTime = 2250 }, + new HitCircle { StartTime = 2500 }, + new HitCircle { StartTime = 3000 }, + new HitCircle { StartTime = 3500 }, + }); + + var patch = new OsuBeatmap + { + HitObjects = + { + (OsuHitObject)current.HitObjects[0], + new HitCircle { StartTime = 1000, Samples = { new HitSampleInfo { Name = HitSampleInfo.HIT_FINISH } } }, + (OsuHitObject)current.HitObjects[2], + (OsuHitObject)current.HitObjects[3], + new HitCircle { StartTime = 2250, Samples = { new HitSampleInfo { Name = HitSampleInfo.HIT_WHISTLE } } }, + (OsuHitObject)current.HitObjects[5], + new HitCircle { StartTime = 3000, Samples = { new HitSampleInfo { Name = HitSampleInfo.HIT_CLAP } } }, + (OsuHitObject)current.HitObjects[7], + } + }; + + runTest(patch); + } + + [Test] + public void TestAddAndDeleteHitObjects() + { + current.AddRange(new[] + { + new HitCircle { StartTime = 500 }, + new HitCircle { StartTime = 1000 }, + new HitCircle { StartTime = 1500 }, + new HitCircle { StartTime = 2000 }, + new HitCircle { StartTime = 2250 }, + new HitCircle { StartTime = 2500 }, + new HitCircle { StartTime = 3000 }, + new HitCircle { StartTime = 3500 }, + }); + + var patch = new OsuBeatmap + { + HitObjects = + { + new HitCircle { StartTime = 750 }, + (OsuHitObject)current.HitObjects[1], + (OsuHitObject)current.HitObjects[4], + (OsuHitObject)current.HitObjects[5], + new HitCircle { StartTime = 2650 }, + new HitCircle { StartTime = 2750 }, + new HitCircle { StartTime = 4000 }, + } + }; + + runTest(patch); + } + + private void runTest(IBeatmap patch) + { + // Due to the method of testing, "patch" comes in without having been decoded via a beatmap decoder. + // This causes issues because the decoder adds various default properties (e.g. new combo on first object, default samples). + // To resolve "patch" into a sane state it is encoded and then re-decoded. + patch = decode(encode(patch)); + + // Apply the patch. + differ.Patch(encode(current), encode(patch)); + + // Convert beatmaps to strings for assertion purposes. + string currentStr = Encoding.ASCII.GetString(encode(current).ToArray()); + string patchStr = Encoding.ASCII.GetString(encode(patch).ToArray()); + + Assert.That(currentStr, Is.EqualTo(patchStr)); + } + + private MemoryStream encode(IBeatmap beatmap) + { + var encoded = new MemoryStream(); + + using (var sw = new StreamWriter(encoded, leaveOpen: true)) + new LegacyBeatmapEncoder(beatmap).Encode(sw); + + return encoded; + } + + private IBeatmap decode(Stream stream) + { + stream.Seek(0, SeekOrigin.Begin); + + using (var reader = new LineBufferedReader(stream, true)) + return Decoder.GetDecoder(reader).Decode(reader); + } + } +} diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 7f04a7a58d..22e0061b61 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -107,6 +107,16 @@ namespace osu.Game.Screens.Edit private IList mutableHitObjects => (IList)PlayableBeatmap.HitObjects; + /// + /// Adds a collection of s to this . + /// + /// The s to add. + public void AddRange(IEnumerable hitObjects) + { + foreach (var h in hitObjects) + Add(h); + } + /// /// Adds a to this . /// @@ -128,12 +138,34 @@ namespace osu.Game.Screens.Edit /// Removes a from this . /// /// The to add. - public void Remove(HitObject hitObject) + /// True if the has been removed, false otherwise. + public bool Remove(HitObject hitObject) { - if (!mutableHitObjects.Contains(hitObject)) - return; + int index = FindIndex(hitObject); - mutableHitObjects.Remove(hitObject); + if (index == -1) + return false; + + RemoveAt(index); + return true; + } + + /// + /// Finds the index of a in this . + /// + /// The to search for. + /// The index of . + public int FindIndex(HitObject hitObject) => mutableHitObjects.IndexOf(hitObject); + + /// + /// Removes a at an index in this . + /// + /// The index of the to remove. + public void RemoveAt(int index) + { + var hitObject = (HitObject)mutableHitObjects[index]; + + mutableHitObjects.RemoveAt(index); var bindable = startTimeBindables[hitObject]; bindable.UnbindAll(); diff --git a/osu.Game/Screens/Edit/LegacyEditorBeatmapDiffer.cs b/osu.Game/Screens/Edit/LegacyEditorBeatmapDiffer.cs new file mode 100644 index 0000000000..8d2f577a1d --- /dev/null +++ b/osu.Game/Screens/Edit/LegacyEditorBeatmapDiffer.cs @@ -0,0 +1,110 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.IO; +using DiffPlex; +using osu.Framework.Audio.Track; +using osu.Framework.Graphics.Textures; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Formats; +using osu.Game.IO; + +namespace osu.Game.Screens.Edit +{ + public class LegacyEditorBeatmapDiffer + { + private readonly EditorBeatmap editorBeatmap; + + public LegacyEditorBeatmapDiffer(EditorBeatmap editorBeatmap) + { + this.editorBeatmap = editorBeatmap; + } + + public void Patch(Stream currentState, Stream newState) + { + // Diff the beatmaps + var result = new Differ().CreateLineDiffs(readString(currentState), readString(newState), true, false); + + // Find the index of [HitObject] sections. Lines changed prior to this index are ignored. + int oldHitObjectsIndex = Array.IndexOf(result.PiecesOld, "[HitObjects]"); + int newHitObjectsIndex = Array.IndexOf(result.PiecesNew, "[HitObjects]"); + + var toRemove = new List(); + var toAdd = new List(); + + foreach (var block in result.DiffBlocks) + { + // Removed hitobject + for (int i = 0; i < block.DeleteCountA; i++) + { + int hoIndex = block.DeleteStartA + i - oldHitObjectsIndex - 1; + + if (hoIndex < 0) + continue; + + toRemove.Add(hoIndex); + } + + // Added hitobject + for (int i = 0; i < block.InsertCountB; i++) + { + int hoIndex = block.InsertStartB + i - newHitObjectsIndex - 1; + + if (hoIndex < 0) + continue; + + toAdd.Add(hoIndex); + } + } + + // Make the removal indices are sorted so that iteration order doesn't get messed up post-removal. + toRemove.Sort(); + + // Apply the changes. + for (int i = toRemove.Count - 1; i >= 0; i--) + editorBeatmap.RemoveAt(toRemove[i]); + + if (toAdd.Count > 0) + { + IBeatmap newBeatmap = readBeatmap(newState); + foreach (var i in toAdd) + editorBeatmap.Add(newBeatmap.HitObjects[i]); + } + } + + private string readString(Stream stream) + { + stream.Seek(0, SeekOrigin.Begin); + + using (var sr = new StreamReader(stream, System.Text.Encoding.UTF8, true, 1024, true)) + return sr.ReadToEnd(); + } + + private IBeatmap readBeatmap(Stream stream) + { + stream.Seek(0, SeekOrigin.Begin); + + using (var reader = new LineBufferedReader(stream, true)) + return new PassThroughWorkingBeatmap(Decoder.GetDecoder(reader).Decode(reader)).GetPlayableBeatmap(editorBeatmap.BeatmapInfo.Ruleset); + } + + private class PassThroughWorkingBeatmap : WorkingBeatmap + { + private readonly IBeatmap beatmap; + + public PassThroughWorkingBeatmap(IBeatmap beatmap) + : base(beatmap.BeatmapInfo, null) + { + this.beatmap = beatmap; + } + + protected override IBeatmap GetBeatmap() => beatmap; + + protected override Texture GetBackground() => throw new NotImplementedException(); + + protected override Track GetTrack() => throw new NotImplementedException(); + } + } +} From ecd7ce4b98648f786d9861f1e4c4bf5bd8f5f358 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 9 Apr 2020 21:00:23 +0900 Subject: [PATCH 0938/2376] Fix test scene --- ...atmapTest.cs => TestSceneEditorBeatmap.cs} | 38 ++++++++++++------- 1 file changed, 25 insertions(+), 13 deletions(-) rename osu.Game.Tests/Beatmaps/{EditorBeatmapTest.cs => TestSceneEditorBeatmap.cs} (80%) diff --git a/osu.Game.Tests/Beatmaps/EditorBeatmapTest.cs b/osu.Game.Tests/Beatmaps/TestSceneEditorBeatmap.cs similarity index 80% rename from osu.Game.Tests/Beatmaps/EditorBeatmapTest.cs rename to osu.Game.Tests/Beatmaps/TestSceneEditorBeatmap.cs index f2b13e3a85..d367d9f88b 100644 --- a/osu.Game.Tests/Beatmaps/EditorBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/TestSceneEditorBeatmap.cs @@ -4,15 +4,17 @@ using System.Linq; using Microsoft.EntityFrameworkCore.Internal; using NUnit.Framework; +using osu.Framework.Testing; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit; +using osu.Game.Tests.Visual; namespace osu.Game.Tests.Beatmaps { - [TestFixture] - public class EditorBeatmapTest + [HeadlessTest] + public class TestSceneEditorBeatmap : EditorClockTestScene { /// /// Tests that the addition event is correctly invoked after a hitobject is added. @@ -55,13 +57,19 @@ namespace osu.Game.Tests.Beatmaps public void TestInitialHitObjectStartTimeChangeEvent() { var hitCircle = new HitCircle(); - var editorBeatmap = new EditorBeatmap(new OsuBeatmap { HitObjects = { hitCircle } }); HitObject changedObject = null; - editorBeatmap.HitObjectUpdated += h => changedObject = h; - hitCircle.StartTime = 1000; - Assert.That(changedObject, Is.EqualTo(hitCircle)); + AddStep("add beatmap", () => + { + EditorBeatmap editorBeatmap; + + Child = editorBeatmap = new EditorBeatmap(new OsuBeatmap { HitObjects = { hitCircle } }); + editorBeatmap.HitObjectUpdated += h => changedObject = h; + }); + + AddStep("change start time", () => hitCircle.StartTime = 1000); + AddAssert("received change event", () => changedObject == hitCircle); } /// @@ -71,18 +79,22 @@ namespace osu.Game.Tests.Beatmaps [Test] public void TestAddedHitObjectStartTimeChangeEvent() { - var editorBeatmap = new EditorBeatmap(new OsuBeatmap()); - + EditorBeatmap editorBeatmap = null; HitObject changedObject = null; - editorBeatmap.HitObjectUpdated += h => changedObject = h; + + AddStep("add beatmap", () => + { + Child = editorBeatmap = new EditorBeatmap(new OsuBeatmap()); + editorBeatmap.HitObjectUpdated += h => changedObject = h; + }); var hitCircle = new HitCircle(); - editorBeatmap.Add(hitCircle); - Assert.That(changedObject, Is.Null); + AddStep("add object", () => editorBeatmap.Add(hitCircle)); + AddAssert("event not received", () => changedObject == null); - hitCircle.StartTime = 1000; - Assert.That(changedObject, Is.EqualTo(hitCircle)); + AddStep("change start time", () => hitCircle.StartTime = 1000); + AddAssert("event received", () => changedObject == hitCircle); } /// From 14eca3655b752222b609b19f5fe268d1f467e6e1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 9 Apr 2020 21:22:07 +0900 Subject: [PATCH 0939/2376] Add change state handling to the editor --- osu.Game/Screens/Edit/Editor.cs | 30 ++++- osu.Game/Screens/Edit/EditorChangeHandler.cs | 108 ++++++++++++++++++ osu.Game/Screens/Edit/IEditorChangeHandler.cs | 33 ++++++ 3 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Screens/Edit/EditorChangeHandler.cs create mode 100644 osu.Game/Screens/Edit/IEditorChangeHandler.cs diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index f1cbed57f1..14a227eb07 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -62,6 +62,7 @@ namespace osu.Game.Screens.Edit private IBeatmap playableBeatmap; private EditorBeatmap editorBeatmap; + private EditorChangeHandler changeHandler; private DependencyContainer dependencies; @@ -100,9 +101,11 @@ namespace osu.Game.Screens.Edit } AddInternal(editorBeatmap = new EditorBeatmap(playableBeatmap)); - dependencies.CacheAs(editorBeatmap); + changeHandler = new EditorChangeHandler(editorBeatmap); + dependencies.CacheAs(changeHandler); + EditorMenuBar menuBar; var fileMenuItems = new List @@ -147,6 +150,14 @@ namespace osu.Game.Screens.Edit new MenuItem("File") { Items = fileMenuItems + }, + new MenuItem("Edit") + { + Items = new[] + { + new EditorMenuItem("Undo", MenuItemType.Standard, undo), + new EditorMenuItem("Redo", MenuItemType.Standard, redo) + } } } } @@ -233,6 +244,19 @@ namespace osu.Game.Screens.Edit return true; } + break; + + case Key.Z: + if (e.ControlPressed) + { + if (e.ShiftPressed) + redo(); + else + undo(); + + return true; + } + break; } @@ -297,6 +321,10 @@ namespace osu.Game.Screens.Edit return base.OnExiting(next); } + private void undo() => changeHandler.RestoreState(-1); + + private void redo() => changeHandler.RestoreState(1); + private void resetTrack(bool seekToStart = false) { Beatmap.Value.Track?.Stop(); diff --git a/osu.Game/Screens/Edit/EditorChangeHandler.cs b/osu.Game/Screens/Edit/EditorChangeHandler.cs new file mode 100644 index 0000000000..7e372926ba --- /dev/null +++ b/osu.Game/Screens/Edit/EditorChangeHandler.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.Text; +using osu.Game.Beatmaps.Formats; +using osu.Game.Rulesets.Objects; + +namespace osu.Game.Screens.Edit +{ + /// + /// Tracks changes to the . + /// + public class EditorChangeHandler : IEditorChangeHandler + { + private readonly LegacyEditorBeatmapDiffer differ; + private readonly List savedStates = new List(); + private int currentState = -1; + + private readonly EditorBeatmap editorBeatmap; + private int bulkChangesStarted; + private bool isRestoring; + + /// + /// Creates a new . + /// + /// The to track the s of. + public EditorChangeHandler(EditorBeatmap editorBeatmap) + { + this.editorBeatmap = editorBeatmap; + + editorBeatmap.HitObjectAdded += hitObjectAdded; + editorBeatmap.HitObjectRemoved += hitObjectRemoved; + editorBeatmap.HitObjectUpdated += hitObjectUpdated; + + differ = new LegacyEditorBeatmapDiffer(editorBeatmap); + + // Initial state. + SaveState(); + } + + private void hitObjectAdded(HitObject obj) => SaveState(); + + private void hitObjectRemoved(HitObject obj) => SaveState(); + + private void hitObjectUpdated(HitObject obj) => SaveState(); + + public void BeginChange() => bulkChangesStarted++; + + public void EndChange() + { + if (bulkChangesStarted == 0) + throw new InvalidOperationException($"Cannot call {nameof(EndChange)} without a previous call to {nameof(BeginChange)}."); + + if (--bulkChangesStarted == 0) + SaveState(); + } + + /// + /// Saves the current state. + /// + public void SaveState() + { + if (bulkChangesStarted > 0) + return; + + if (isRestoring) + return; + + var stream = new MemoryStream(); + + using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) + new LegacyBeatmapEncoder(editorBeatmap).Encode(sw); + + if (currentState < savedStates.Count - 1) + savedStates.RemoveRange(currentState + 1, savedStates.Count - currentState - 1); + + savedStates.Add(stream); + currentState = savedStates.Count - 1; + } + + /// + /// Restores an older or newer state. + /// + /// The direction to restore in. If less than 0, an older state will be used. If greater than 0, a newer state will be used. + public void RestoreState(int direction) + { + if (bulkChangesStarted > 0) + return; + + if (savedStates.Count == 0) + return; + + int newState = Math.Clamp(currentState + direction, 0, savedStates.Count - 1); + if (currentState == newState) + return; + + isRestoring = true; + + differ.Patch(savedStates[currentState], savedStates[newState]); + currentState = newState; + + isRestoring = false; + } + } +} diff --git a/osu.Game/Screens/Edit/IEditorChangeHandler.cs b/osu.Game/Screens/Edit/IEditorChangeHandler.cs new file mode 100644 index 0000000000..c1328252d4 --- /dev/null +++ b/osu.Game/Screens/Edit/IEditorChangeHandler.cs @@ -0,0 +1,33 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Objects; + +namespace osu.Game.Screens.Edit +{ + /// + /// Interface for a component that manages changes in the . + /// + public interface IEditorChangeHandler + { + /// + /// Begins a bulk state change event. should be invoked soon after. + /// + /// + /// This should be invoked when multiple changes to the should be bundled together into one state change event. + /// When nested invocations are involved, a state change will not occur until an equal number of invocations of are received. + /// + /// + /// When a group of s are deleted, a single undo and redo state change should update the state of all . + /// + void BeginChange(); + + /// + /// Ends a bulk state change event. + /// + /// + /// This should be invoked as soon as possible after to cause a state change. + /// + void EndChange(); + } +} From ed4ce54ac3e7149b3a6f3d4cc9406ef74d2f6e38 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 9 Apr 2020 21:56:36 +0900 Subject: [PATCH 0940/2376] Add tests --- .../Editor/TestSceneEditorChangeStates.cs | 193 ++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 osu.Game.Tests/Visual/Editor/TestSceneEditorChangeStates.cs diff --git a/osu.Game.Tests/Visual/Editor/TestSceneEditorChangeStates.cs b/osu.Game.Tests/Visual/Editor/TestSceneEditorChangeStates.cs new file mode 100644 index 0000000000..abaa373cac --- /dev/null +++ b/osu.Game.Tests/Visual/Editor/TestSceneEditorChangeStates.cs @@ -0,0 +1,193 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Screens.Edit; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.Editor +{ + public class TestSceneEditorChangeStates : ScreenTestScene + { + private EditorBeatmap editorBeatmap; + + public override void SetUpSteps() + { + base.SetUpSteps(); + + Screens.Edit.Editor editor = null; + + AddStep("load editor", () => + { + Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); + LoadScreen(editor = new Screens.Edit.Editor()); + }); + + AddUntilStep("wait for editor to load", () => editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); + AddStep("get beatmap", () => editorBeatmap = editor.ChildrenOfType().Single()); + } + + [Test] + public void TestUndoFromInitialState() + { + int hitObjectCount = 0; + + AddStep("get initial state", () => hitObjectCount = editorBeatmap.HitObjects.Count); + + addUndoSteps(); + + AddAssert("no change occurred", () => hitObjectCount == editorBeatmap.HitObjects.Count); + } + + [Test] + public void TestRedoFromInitialState() + { + int hitObjectCount = 0; + + AddStep("get initial state", () => hitObjectCount = editorBeatmap.HitObjects.Count); + + addRedoSteps(); + + AddAssert("no change occurred", () => hitObjectCount == editorBeatmap.HitObjects.Count); + } + + [Test] + public void TestAddObjectAndUndo() + { + HitObject addedObject = null; + HitObject removedObject = null; + HitObject expectedObject = null; + + AddStep("bind removal", () => + { + editorBeatmap.HitObjectAdded += h => addedObject = h; + editorBeatmap.HitObjectRemoved += h => removedObject = h; + }); + + AddStep("add hitobject", () => editorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 })); + AddAssert("hitobject added", () => addedObject == expectedObject); + + addUndoSteps(); + AddAssert("hitobject removed", () => removedObject == expectedObject); + } + + [Test] + public void TestAddObjectThenUndoThenRedo() + { + HitObject addedObject = null; + HitObject removedObject = null; + HitObject expectedObject = null; + + AddStep("bind removal", () => + { + editorBeatmap.HitObjectAdded += h => addedObject = h; + editorBeatmap.HitObjectRemoved += h => removedObject = h; + }); + + AddStep("add hitobject", () => editorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 })); + addUndoSteps(); + + AddStep("reset variables", () => + { + addedObject = null; + removedObject = null; + }); + + addRedoSteps(); + AddAssert("hitobject added", () => addedObject.StartTime == expectedObject.StartTime); // Can't compare via equality (new hitobject instance) + AddAssert("no hitobject removed", () => removedObject == null); + } + + [Test] + public void TestRemoveObjectThenUndo() + { + HitObject addedObject = null; + HitObject removedObject = null; + HitObject expectedObject = null; + + AddStep("bind removal", () => + { + editorBeatmap.HitObjectAdded += h => addedObject = h; + editorBeatmap.HitObjectRemoved += h => removedObject = h; + }); + + AddStep("add hitobject", () => editorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 })); + AddStep("remove object", () => editorBeatmap.Remove(expectedObject)); + AddStep("reset variables", () => + { + addedObject = null; + removedObject = null; + }); + + addUndoSteps(); + AddAssert("hitobject added", () => addedObject.StartTime == expectedObject.StartTime); // Can't compare via equality (new hitobject instance) + AddAssert("no hitobject removed", () => removedObject == null); + } + + [Test] + public void TestRemoveObjectThenUndoThenRedo() + { + HitObject addedObject = null; + HitObject removedObject = null; + HitObject expectedObject = null; + + AddStep("bind removal", () => + { + editorBeatmap.HitObjectAdded += h => addedObject = h; + editorBeatmap.HitObjectRemoved += h => removedObject = h; + }); + + AddStep("add hitobject", () => editorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 })); + AddStep("remove object", () => editorBeatmap.Remove(expectedObject)); + addUndoSteps(); + + AddStep("reset variables", () => + { + addedObject = null; + removedObject = null; + }); + + addRedoSteps(); + AddAssert("hitobject removed", () => removedObject.StartTime == expectedObject.StartTime); // Can't compare via equality (new hitobject instance after undo) + AddAssert("no hitobject added", () => addedObject == null); + } + + private void addUndoSteps() + { + AddStep("press undo", () => + { + InputManager.PressKey(Key.LControl); + InputManager.PressKey(Key.Z); + }); + + AddStep("release keys", () => + { + InputManager.ReleaseKey(Key.LControl); + InputManager.ReleaseKey(Key.Z); + }); + } + + private void addRedoSteps() + { + AddStep("press redo", () => + { + InputManager.PressKey(Key.LControl); + InputManager.PressKey(Key.LShift); + InputManager.PressKey(Key.Z); + }); + + AddStep("release keys", () => + { + InputManager.ReleaseKey(Key.LControl); + InputManager.ReleaseKey(Key.LShift); + InputManager.ReleaseKey(Key.Z); + }); + } + } +} From 575b061dd76a59fd39079904f53193bb524ea261 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 9 Apr 2020 22:00:56 +0900 Subject: [PATCH 0941/2376] Add change state support to more editor components --- .../Components/PathControlPointPiece.cs | 17 +++++++++++++++- .../Sliders/SliderSelectionBlueprint.cs | 20 +++++++++++++++++-- .../Compose/Components/BlueprintContainer.cs | 14 +++++++++++++ .../Compose/Components/SelectionHandler.cs | 9 ++++++++- .../Timeline/TimelineHitObjectBlueprint.cs | 12 +++++++++-- 5 files changed, 66 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs index af4da5e853..092a13cca5 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs @@ -12,6 +12,7 @@ using osu.Game.Graphics; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Screens.Edit; using osuTK; using osuTK.Graphics; using osuTK.Input; @@ -33,6 +34,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components private readonly Container marker; private readonly Drawable markerRing; + [Resolved(CanBeNull = true)] + private IEditorChangeHandler changeHandler { get; set; } + [Resolved(CanBeNull = true)] private IDistanceSnapProvider snapProvider { get; set; } @@ -137,7 +141,16 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components protected override bool OnClick(ClickEvent e) => RequestSelection != null; - protected override bool OnDragStart(DragStartEvent e) => e.Button == MouseButton.Left; + protected override bool OnDragStart(DragStartEvent e) + { + if (e.Button == MouseButton.Left) + { + changeHandler?.BeginChange(); + return true; + } + + return false; + } protected override void OnDrag(DragEvent e) { @@ -158,6 +171,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components ControlPoint.Position.Value += e.Delta; } + protected override void OnDragEnd(DragEndEvent e) => changeHandler?.EndChange(); + /// /// Updates the state of the circular control point marker. /// diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 001100d3ce..b7074b7ee5 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -38,6 +38,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders [Resolved(CanBeNull = true)] private EditorBeatmap editorBeatmap { get; set; } + [Resolved(CanBeNull = true)] + private IEditorChangeHandler changeHandler { get; set; } + public SliderSelectionBlueprint(DrawableSlider slider) : base(slider) { @@ -92,7 +95,16 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private int? placementControlPointIndex; - protected override bool OnDragStart(DragStartEvent e) => placementControlPointIndex != null; + protected override bool OnDragStart(DragStartEvent e) + { + if (placementControlPointIndex != null) + { + changeHandler?.BeginChange(); + return true; + } + + return false; + } protected override void OnDrag(DragEvent e) { @@ -103,7 +115,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders protected override void OnDragEnd(DragEndEvent e) { - placementControlPointIndex = null; + if (placementControlPointIndex != null) + { + placementControlPointIndex = null; + changeHandler?.EndChange(); + } } private BindableList controlPoints => HitObject.Path.ControlPoints; diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index c81c6059cc..ad16e22e5e 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -37,6 +37,9 @@ namespace osu.Game.Screens.Edit.Compose.Components private SelectionHandler selectionHandler; + [Resolved(CanBeNull = true)] + private IEditorChangeHandler changeHandler { get; set; } + [Resolved] private IAdjustableClock adjustableClock { get; set; } @@ -164,7 +167,11 @@ namespace osu.Game.Screens.Edit.Compose.Components return false; if (movementBlueprint != null) + { + isDraggingBlueprint = true; + changeHandler?.BeginChange(); return true; + } if (DragBox.HandleDrag(e)) { @@ -191,6 +198,12 @@ namespace osu.Game.Screens.Edit.Compose.Components if (e.Button == MouseButton.Right) return; + if (isDraggingBlueprint) + { + changeHandler?.EndChange(); + isDraggingBlueprint = false; + } + if (DragBox.State == Visibility.Visible) { DragBox.Hide(); @@ -354,6 +367,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private Vector2? movementBlueprintOriginalPosition; private SelectionBlueprint movementBlueprint; + private bool isDraggingBlueprint; /// /// Attempts to begin the movement of any selected blueprints. diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index fc46bf3fed..e212979433 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -40,6 +40,9 @@ namespace osu.Game.Screens.Edit.Compose.Components [Resolved(CanBeNull = true)] private EditorBeatmap editorBeatmap { get; set; } + [Resolved(CanBeNull = true)] + private IEditorChangeHandler changeHandler { get; set; } + public SelectionHandler() { selectedBlueprints = new List(); @@ -152,8 +155,12 @@ namespace osu.Game.Screens.Edit.Compose.Components private void deleteSelected() { + changeHandler?.BeginChange(); + foreach (var h in selectedBlueprints.ToList()) - editorBeatmap.Remove(h.HitObject); + editorBeatmap?.Remove(h.HitObject); + + changeHandler?.EndChange(); } #endregion diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index 8f12c2f0ed..16ba3ba89a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -254,14 +254,21 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Colour = IsHovered || hasMouseDown ? Color4.OrangeRed : Color4.White; } - protected override bool OnDragStart(DragStartEvent e) => true; - [Resolved] private EditorBeatmap beatmap { get; set; } [Resolved] private IBeatSnapProvider beatSnapProvider { get; set; } + [Resolved(CanBeNull = true)] + private IEditorChangeHandler changeHandler { get; set; } + + protected override bool OnDragStart(DragStartEvent e) + { + changeHandler?.BeginChange(); + return true; + } + protected override void OnDrag(DragEvent e) { base.OnDrag(e); @@ -301,6 +308,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline base.OnDragEnd(e); OnDragHandled?.Invoke(null); + changeHandler?.EndChange(); } } } From e208251fc6f1729eac62cfa2e7e756b651eedbc4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 9 Apr 2020 23:18:43 +0900 Subject: [PATCH 0942/2376] Wait for timeline to also load --- osu.Game.Tests/Visual/Editor/TestSceneEditorChangeStates.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Editor/TestSceneEditorChangeStates.cs b/osu.Game.Tests/Visual/Editor/TestSceneEditorChangeStates.cs index abaa373cac..dd1b6cf6aa 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneEditorChangeStates.cs +++ b/osu.Game.Tests/Visual/Editor/TestSceneEditorChangeStates.cs @@ -9,6 +9,7 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit; +using osu.Game.Screens.Edit.Compose.Components.Timeline; using osuTK.Input; namespace osu.Game.Tests.Visual.Editor @@ -29,7 +30,8 @@ namespace osu.Game.Tests.Visual.Editor LoadScreen(editor = new Screens.Edit.Editor()); }); - AddUntilStep("wait for editor to load", () => editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); + AddUntilStep("wait for editor to load", () => editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true + && editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); AddStep("get beatmap", () => editorBeatmap = editor.ChildrenOfType().Single()); } From f40bdcd34e835731855d48b1ddf9af4574331320 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Thu, 9 Apr 2020 18:47:28 +0300 Subject: [PATCH 0943/2376] Initial rewrite, moving API logic to SongSelect --- osu.Game/Screens/Select/BeatmapCarousel.cs | 35 +++------- .../Select/Carousel/CarouselBeatmapSet.cs | 16 ++--- osu.Game/Screens/Select/SongSelect.cs | 65 +++++++++++++++++++ 3 files changed, 79 insertions(+), 37 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index c7221699de..5a62184ad8 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -34,8 +34,6 @@ namespace osu.Game.Screens.Select private const float bleed_top = FilterControl.HEIGHT; private const float bleed_bottom = Footer.HEIGHT; - protected readonly Bindable RecommendedStarDifficulty = new Bindable(); - /// /// Triggered when the loaded change and are completely loaded. /// @@ -122,6 +120,7 @@ namespace osu.Game.Screens.Select protected List Items = new List(); private CarouselRoot root; + public SongSelect.DifficultyRecommender DifficultyRecommender; public BeatmapCarousel() { @@ -145,10 +144,10 @@ namespace osu.Game.Screens.Select private BeatmapManager beatmaps { get; set; } [Resolved] - private IAPIProvider api { get; set; } + private Bindable decoupledRuleset { get; set; } [BackgroundDependencyLoader(permitNulls: true)] - private void load(OsuConfigManager config, Bindable decoupledRuleset) + private void load(OsuConfigManager config) { config.BindWith(OsuSetting.RandomSelectAlgorithm, RandomAlgorithm); config.BindWith(OsuSetting.SongSelectRightMouseScroll, RightClickScrollingEnabled); @@ -162,27 +161,6 @@ namespace osu.Game.Screens.Select beatmaps.BeatmapRestored += beatmapRestored; loadBeatmapSets(GetLoadableBeatmaps()); - - decoupledRuleset.BindValueChanged(UpdateRecommendedStarDifficulty, true); - } - - protected void UpdateRecommendedStarDifficulty(ValueChangedEvent ruleset) - { - if (api.LocalUser.Value is GuestUser) - { - RecommendedStarDifficulty.Value = 0; - return; - } - - var req = new GetUserRequest(api.LocalUser.Value.Id, ruleset.NewValue); - - req.Success += result => - { - // algorithm taken from https://github.com/ppy/osu-web/blob/e6e2825516449e3d0f3f5e1852c6bdd3428c3437/app/Models/User.php#L1505 - RecommendedStarDifficulty.Value = Math.Pow((double)(result.Statistics.PP ?? 0), 0.4) * 0.195; - }; - - api.PerformAsync(req); } protected virtual IEnumerable GetLoadableBeatmaps() => beatmaps.GetAllUsableBeatmapSetsEnumerable(); @@ -608,7 +586,12 @@ namespace osu.Game.Screens.Select b.Metadata = beatmapSet.Metadata; } - var set = new CarouselBeatmapSet(beatmapSet, RecommendedStarDifficulty); + BeatmapInfo recommender(IEnumerable beatmaps) + { + return DifficultyRecommender?.GetRecommendedBeatmap(beatmaps, decoupledRuleset.Value); + } + + var set = new CarouselBeatmapSet(beatmapSet, recommender); foreach (var c in set.Beatmaps) { diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs index 064840d99a..1b715cad02 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs @@ -13,13 +13,13 @@ namespace osu.Game.Screens.Select.Carousel { public class CarouselBeatmapSet : CarouselGroupEagerSelect { - private readonly Bindable recommendedStarDifficulty = new Bindable(); + private Func, BeatmapInfo> getRecommendedBeatmap; public IEnumerable Beatmaps => InternalChildren.OfType(); public BeatmapSetInfo BeatmapSet; - public CarouselBeatmapSet(BeatmapSetInfo beatmapSet, Bindable recommendedStarDifficulty) + public CarouselBeatmapSet(BeatmapSetInfo beatmapSet, Func, BeatmapInfo> getRecommendedBeatmap) { BeatmapSet = beatmapSet ?? throw new ArgumentNullException(nameof(beatmapSet)); @@ -28,7 +28,7 @@ namespace osu.Game.Screens.Select.Carousel .Select(b => new CarouselBeatmap(b)) .ForEach(AddChild); - this.recommendedStarDifficulty.BindTo(recommendedStarDifficulty); + this.getRecommendedBeatmap = getRecommendedBeatmap; } protected override DrawableCarouselItem CreateDrawableRepresentation() => new DrawableCarouselBeatmapSet(this); @@ -37,14 +37,8 @@ namespace osu.Game.Screens.Select.Carousel { if (LastSelected == null) { - return Children.OfType() - .Where(b => !b.Filtered.Value) - .OrderBy(b => - { - var difference = b.Beatmap.StarDifficulty - recommendedStarDifficulty.Value; - return difference >= 0 ? difference * 2 : difference * -1; // prefer easier over harder - }) - .FirstOrDefault(); + var recommendedBeatmapInfo = getRecommendedBeatmap(Children.OfType().Where(b => !b.Filtered.Value).Select(b => b.Beatmap)); + return Children.OfType().Where(b => b.Beatmap == recommendedBeatmapInfo).First(); } return base.GetNextToSelect(); diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 895a8ad0c9..fda5872f3b 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -36,6 +36,9 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; using osu.Game.Overlays.Notifications; using osu.Game.Scoring; +using osu.Game.Online.API; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Game.Online.API.Requests; namespace osu.Game.Screens.Select { @@ -80,6 +83,7 @@ namespace osu.Game.Screens.Select protected override BackgroundScreen CreateBackground() => new BackgroundScreenBeatmap(Beatmap.Value); protected BeatmapCarousel Carousel { get; private set; } + private DifficultyRecommender difficultyRecommender; private BeatmapInfoWedge beatmapInfoWedge; private DialogOverlay dialogOverlay; @@ -107,6 +111,8 @@ namespace osu.Game.Screens.Select // initial value transfer is required for FilterControl (it uses our re-cached bindables in its async load for the initial filter). transferRulesetValue(); + AddInternal(difficultyRecommender = new DifficultyRecommender()); + AddRangeInternal(new Drawable[] { new ResetScrollContainer(() => Carousel.ScrollToSelected()) @@ -156,6 +162,7 @@ namespace osu.Game.Screens.Select RelativeSizeAxes = Axes.Both, SelectionChanged = updateSelectedBeatmap, BeatmapSetsChanged = carouselBeatmapsLoaded, + DifficultyRecommender = difficultyRecommender, }, } }, @@ -780,6 +787,64 @@ namespace osu.Game.Screens.Select return base.OnKeyDown(e); } + public class DifficultyRecommender : Component + { + [Resolved] + private IAPIProvider api { get; set; } + + [Resolved] + private RulesetStore rulesets { get; set; } + + private Dictionary recommendedStarDifficulty = new Dictionary(); + + private int pendingAPIRequests = 0; + + [BackgroundDependencyLoader] + private void load() + { + updateRecommended(); + } + + private void updateRecommended() + { + if (pendingAPIRequests > 0) + return; + if (api.LocalUser.Value is GuestUser) + return; + rulesets.AvailableRulesets.ForEach(rulesetInfo => + { + var req = new GetUserRequest(api.LocalUser.Value.Id, rulesetInfo); + + req.Success += result => + { + // algorithm taken from https://github.com/ppy/osu-web/blob/e6e2825516449e3d0f3f5e1852c6bdd3428c3437/app/Models/User.php#L1505 + recommendedStarDifficulty[rulesetInfo] = Math.Pow((double)(result.Statistics.PP ?? 0), 0.4) * 0.195; + pendingAPIRequests--; + }; + + req.Failure += _ => pendingAPIRequests--; + + pendingAPIRequests++; + api.Queue(req); + }); + } + + public BeatmapInfo GetRecommendedBeatmap(IEnumerable beatmaps, RulesetInfo currentRuleset) + { + if (!recommendedStarDifficulty.ContainsKey(currentRuleset)) + { + updateRecommended(); + return null; + } + + return beatmaps.OrderBy(b => + { + var difference = b.StarDifficulty - recommendedStarDifficulty[currentRuleset]; + return difference >= 0 ? difference * 2 : difference * -1; // prefer easier over harder + }).FirstOrDefault(); + } + } + private class VerticalMaskingContainer : Container { private const float panel_overflow = 1.2f; From caa404f8fa6024b1fadff3d61ad46339bc50c34f Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Thu, 9 Apr 2020 18:48:13 +0300 Subject: [PATCH 0944/2376] Remove test for now --- .../SongSelect/TestSceneBeatmapCarousel.cs | 30 ------------------- 1 file changed, 30 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 5a199885c0..8d207be216 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -656,34 +656,6 @@ namespace osu.Game.Tests.Visual.SongSelect checkVisibleItemCount(true, 15); } - [Test] - public void TestSelectRecommendedDifficulty() - { - void setRecommendedAndExpect(double recommended, int expectedSet, int expectedDiff) - { - AddStep($"Recommend SR {recommended}", () => carousel.RecommendedStarDifficulty.Value = recommended); - advanceSelection(direction: 1, diff: false); - waitForSelection(expectedSet, expectedDiff); - } - - createCarousel(); - AddStep("Add beatmaps", () => - { - for (int i = 1; i <= 7; i++) - { - var set = createTestBeatmapSet(i); - carousel.UpdateBeatmapSet(set); - } - }); - waitForSelection(1, 1); - setRecommendedAndExpect(1, 2, 1); - setRecommendedAndExpect(3.9, 3, 1); - setRecommendedAndExpect(4.1, 4, 2); - setRecommendedAndExpect(5.6, 5, 2); - setRecommendedAndExpect(5.7, 6, 3); - setRecommendedAndExpect(10, 7, 3); - } - private void loadBeatmaps(List beatmapSets = null, Func initialCriteria = null) { createCarousel(); @@ -886,8 +858,6 @@ namespace osu.Game.Tests.Visual.SongSelect { public new List Items => base.Items; - public new Bindable RecommendedStarDifficulty => base.RecommendedStarDifficulty; - public bool PendingFilterTask => PendingFilter != null; protected override IEnumerable GetLoadableBeatmaps() => Enumerable.Empty(); From 35f97dfc7521a7952dad16896babc6cc649353c0 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Thu, 9 Apr 2020 18:59:18 +0300 Subject: [PATCH 0945/2376] Style changes --- osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs | 1 - osu.Game/Screens/Select/BeatmapCarousel.cs | 2 -- osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs | 5 ++--- osu.Game/Screens/Select/SongSelect.cs | 5 +++-- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 8d207be216..76a8ee9914 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -8,7 +8,6 @@ using System.Linq; using System.Text; using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 5a62184ad8..3e619a1f80 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -23,9 +23,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; using osu.Game.Input.Bindings; using osu.Game.Screens.Select.Carousel; -using osu.Game.Online.API; using osu.Game.Rulesets; -using osu.Game.Online.API.Requests; namespace osu.Game.Screens.Select { diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs index 1b715cad02..e7a18e15c7 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Game.Beatmaps; using osu.Game.Screens.Select.Filter; @@ -13,7 +12,7 @@ namespace osu.Game.Screens.Select.Carousel { public class CarouselBeatmapSet : CarouselGroupEagerSelect { - private Func, BeatmapInfo> getRecommendedBeatmap; + private readonly Func, BeatmapInfo> getRecommendedBeatmap; public IEnumerable Beatmaps => InternalChildren.OfType(); @@ -38,7 +37,7 @@ namespace osu.Game.Screens.Select.Carousel if (LastSelected == null) { var recommendedBeatmapInfo = getRecommendedBeatmap(Children.OfType().Where(b => !b.Filtered.Value).Select(b => b.Beatmap)); - return Children.OfType().Where(b => b.Beatmap == recommendedBeatmapInfo).First(); + return Children.OfType().First(b => b.Beatmap == recommendedBeatmapInfo); } return base.GetNextToSelect(); diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index fda5872f3b..d6bc20df39 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -795,9 +795,9 @@ namespace osu.Game.Screens.Select [Resolved] private RulesetStore rulesets { get; set; } - private Dictionary recommendedStarDifficulty = new Dictionary(); + private readonly Dictionary recommendedStarDifficulty = new Dictionary(); - private int pendingAPIRequests = 0; + private int pendingAPIRequests; [BackgroundDependencyLoader] private void load() @@ -811,6 +811,7 @@ namespace osu.Game.Screens.Select return; if (api.LocalUser.Value is GuestUser) return; + rulesets.AvailableRulesets.ForEach(rulesetInfo => { var req = new GetUserRequest(api.LocalUser.Value.Id, rulesetInfo); From 2201e9b4ae00154d14a4b0ccb45b881550c7bdfb Mon Sep 17 00:00:00 2001 From: Fire937 Date: Thu, 9 Apr 2020 18:12:15 +0200 Subject: [PATCH 0946/2376] Add stereo shifted hitsound playback support There is now a setting in the general settings called "Positional hitsounds". If the setting is enabled, the hitsounds playback will be shifted according to their position on the beatmap. --- osu.Game/Configuration/OsuConfigManager.cs | 2 ++ .../Sections/Gameplay/GeneralSettings.cs | 5 ++++ .../Objects/Drawables/DrawableHitObject.cs | 26 ++++++++++++++++++- 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 41f6747b74..ab5a652a94 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -88,6 +88,7 @@ namespace osu.Game.Configuration Set(OsuSetting.ShowProgressGraph, true); Set(OsuSetting.ShowHealthDisplayWhenCantFail, true); Set(OsuSetting.KeyOverlay, false); + Set(OsuSetting.PositionalHitSounds, true); Set(OsuSetting.ScoreMeter, ScoreMeterType.HitErrorBoth); Set(OsuSetting.FloatingComments, false); @@ -176,6 +177,7 @@ namespace osu.Game.Configuration LightenDuringBreaks, ShowStoryboard, KeyOverlay, + PositionalHitSounds, ScoreMeter, FloatingComments, ShowInterface, diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs index 2d2cd42213..ef03c0622a 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs @@ -57,6 +57,11 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay LabelText = "Always show key overlay", Bindable = config.GetBindable(OsuSetting.KeyOverlay) }, + new SettingsCheckbox + { + LabelText = "Positional hitsounds", + Bindable = config.GetBindable(OsuSetting.PositionalHitSounds) + }, new SettingsEnumDropdown { LabelText = "Score meter type", diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 0011faefbb..ed9efba89f 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -12,11 +12,13 @@ using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; using osu.Framework.Threading; +using osu.Framework.Audio; using osu.Game.Audio; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; +using osu.Game.Configuration; using osuTK.Graphics; namespace osu.Game.Rulesets.Objects.Drawables @@ -31,6 +33,11 @@ namespace osu.Game.Rulesets.Objects.Drawables /// public readonly Bindable AccentColour = new Bindable(Color4.Gray); + /// + /// The stereo balance of the samples if the Positional hitsounds setting is set. + /// + private readonly BindableDouble positionalSoundAdjustment = new BindableDouble(); + protected SkinnableSound Samples { get; private set; } protected virtual IEnumerable GetSamples() => HitObject.Samples; @@ -84,8 +91,14 @@ namespace osu.Game.Rulesets.Objects.Drawables /// public JudgementResult Result { get; private set; } + /// + /// The stereo balance of the samples played if Positional hitsounds is set. + /// + protected virtual float PositionalSound => (HitObject is IHasXPosition position) ? (position.X / 512f - 0.5f) * 0.8f : 0; + private BindableList samplesBindable; private Bindable startTimeBindable; + private Bindable userPositionalHitSounds; private Bindable comboIndexBindable; public override bool RemoveWhenNotAlive => false; @@ -101,10 +114,11 @@ namespace osu.Game.Rulesets.Objects.Drawables protected DrawableHitObject([NotNull] HitObject hitObject) { HitObject = hitObject ?? throw new ArgumentNullException(nameof(hitObject)); + positionalSoundAdjustment.Value = PositionalSound; } [BackgroundDependencyLoader] - private void load() + private void load(OsuConfigManager config) { var judgement = HitObject.CreateJudgement(); @@ -113,6 +127,16 @@ namespace osu.Game.Rulesets.Objects.Drawables throw new InvalidOperationException($"{GetType().ReadableName()} must provide a {nameof(JudgementResult)} through {nameof(CreateResult)}."); loadSamples(); + + userPositionalHitSounds = config.GetBindable(OsuSetting.PositionalHitSounds); + userPositionalHitSounds.BindValueChanged(positional => + { + if (positional.NewValue) + Samples?.AddAdjustment(AdjustableProperty.Balance, positionalSoundAdjustment); + else + Samples?.RemoveAdjustment(AdjustableProperty.Balance, positionalSoundAdjustment); + }); + userPositionalHitSounds.TriggerChange(); } protected override void LoadComplete() From 116b952dfe973218621de51532c8620c0f65e015 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 10 Apr 2020 01:20:43 +0900 Subject: [PATCH 0947/2376] Change param to hitobject rather than result --- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index db8a47e4a2..9c066c367b 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -113,7 +113,7 @@ namespace osu.Game.Rulesets.Osu.UI private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) { - missAllEarlier(result); + missAllEarlier(result.HitObject); if (!judgedObject.DisplayResult || !DisplayJudgements.Value) return; @@ -131,14 +131,14 @@ namespace osu.Game.Rulesets.Osu.UI /// /// Misses all s occurring earlier than the start time of a judged . /// - /// The of the judged . - private void missAllEarlier(JudgementResult result) + /// The marker , which all s earlier than will get missed. + private void missAllEarlier(HitObject hitObject) { - if (!causesNoteLockMisses(result.HitObject)) + if (!causesNoteLockMisses(hitObject)) return; // The minimum start time required for hitobjects so that they aren't missed. - double minimumTime = result.HitObject.StartTime; + double minimumTime = hitObject.StartTime; foreach (var obj in HitObjectContainer.AliveObjects) { From b8d7b78b55a3022e8556110a44bc4d40c977c86e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 10 Apr 2020 01:21:37 +0900 Subject: [PATCH 0948/2376] Remove unnecessary null set --- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 9c066c367b..9011f21fd5 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -78,12 +78,7 @@ namespace osu.Game.Rulesets.Osu.UI bool result = base.Remove(h); if (result) - { - DrawableOsuHitObject osuHitObject = (DrawableOsuHitObject)h; - osuHitObject.CheckHittable = null; - - followPoints.RemoveFollowPoints(osuHitObject); - } + followPoints.RemoveFollowPoints((DrawableOsuHitObject)h); return result; } From deaf24f1419f25f26297271e819caf73445bac4b Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Thu, 9 Apr 2020 19:30:40 +0300 Subject: [PATCH 0949/2376] Fix oversight on null --- osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs index e7a18e15c7..99ded4c58e 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs @@ -37,7 +37,8 @@ namespace osu.Game.Screens.Select.Carousel if (LastSelected == null) { var recommendedBeatmapInfo = getRecommendedBeatmap(Children.OfType().Where(b => !b.Filtered.Value).Select(b => b.Beatmap)); - return Children.OfType().First(b => b.Beatmap == recommendedBeatmapInfo); + if (recommendedBeatmapInfo != null) + return Children.OfType().First(b => b.Beatmap == recommendedBeatmapInfo); } return base.GetNextToSelect(); From f115fecb23c13b7a2fbfc99e38b1eef458866199 Mon Sep 17 00:00:00 2001 From: Alchyr Date: Thu, 9 Apr 2020 09:34:40 -0700 Subject: [PATCH 0950/2376] Fix formatting --- osu.Game/Beatmaps/ControlPoints/ControlPoint.cs | 6 +++--- osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs | 3 ++- osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs | 3 ++- osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs | 3 ++- osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs | 6 +++--- 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs index 411a4441de..9599ad184b 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs @@ -28,10 +28,10 @@ namespace osu.Game.Beatmaps.ControlPoints /// /// Whether this control point results in a meaningful change when placed after another. /// - /// Another control point to compare with. - /// The time this timing point will be placed at. + /// An existing control point to compare with. + /// The time this control point will be placed at if it is added. /// Whether redundant. - public abstract bool IsRedundant(ControlPoint other, double time); + public abstract bool IsRedundant(ControlPoint existing, double time); public bool Equals(ControlPoint other) => Time == other?.Time && EquivalentTo(other); } diff --git a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs index 44522dc927..dc856b0a0a 100644 --- a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs @@ -29,6 +29,7 @@ namespace osu.Game.Beatmaps.ControlPoints public override bool EquivalentTo(ControlPoint other) => other is DifficultyControlPoint otherTyped && otherTyped.SpeedMultiplier.Equals(SpeedMultiplier); - public override bool IsRedundant(ControlPoint other, double time) => EquivalentTo(other); + + public override bool IsRedundant(ControlPoint existing, double time) => EquivalentTo(existing); } } diff --git a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs index 8066c6b577..d050f44ba4 100644 --- a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs @@ -38,6 +38,7 @@ namespace osu.Game.Beatmaps.ControlPoints public override bool EquivalentTo(ControlPoint other) => other is EffectControlPoint otherTyped && KiaiMode == otherTyped.KiaiMode && OmitFirstBarLine == otherTyped.OmitFirstBarLine; - public override bool IsRedundant(ControlPoint other, double time) => !OmitFirstBarLine && EquivalentTo(other); + + public override bool IsRedundant(ControlPoint existing, double time) => !OmitFirstBarLine && EquivalentTo(existing); } } diff --git a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs index cf7c842b24..38edbe70da 100644 --- a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs @@ -71,6 +71,7 @@ namespace osu.Game.Beatmaps.ControlPoints public override bool EquivalentTo(ControlPoint other) => other is SampleControlPoint otherTyped && SampleBank == otherTyped.SampleBank && SampleVolume == otherTyped.SampleVolume; - public override bool IsRedundant(ControlPoint other, double time) => EquivalentTo(other); + + public override bool IsRedundant(ControlPoint existing, double time) => EquivalentTo(existing); } } diff --git a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs index d14ac1221b..316c603ece 100644 --- a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs @@ -52,8 +52,8 @@ namespace osu.Game.Beatmaps.ControlPoints other is TimingControlPoint otherTyped && TimeSignature == otherTyped.TimeSignature && BeatLength.Equals(otherTyped.BeatLength); - public override bool IsRedundant(ControlPoint other, double time) => - EquivalentTo(other) - && other.Time == time; + public override bool IsRedundant(ControlPoint existing, double time) => + EquivalentTo(existing) + && existing.Time == time; } } From ea1bec85ae7ef875f133add73c5051a6fa9b5a4c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 10 Apr 2020 01:40:20 +0900 Subject: [PATCH 0951/2376] Simplify code/language --- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 65 +++++++++--------------- 1 file changed, 24 insertions(+), 41 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 9011f21fd5..f4009a281c 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.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.Extensions.IEnumerableExtensions; using osuTK; using osu.Framework.Graphics; @@ -87,10 +86,10 @@ namespace osu.Game.Rulesets.Osu.UI { DrawableHitObject lastObject = osuHitObject; - // Get the last hitobject that contributes to note lock + // Get the last hitobject that can block future hits while ((lastObject = HitObjectContainer.AliveObjects.GetPrevious(lastObject)) != null) { - if (contributesToNoteLock(lastObject.HitObject)) + if (canBlockFutureHits(lastObject.HitObject)) break; } @@ -108,7 +107,9 @@ namespace osu.Game.Rulesets.Osu.UI private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) { - missAllEarlier(result.HitObject); + // Hitobjects that block future hits should miss previous hitobjects if they're hit out-of-order. + if (canBlockFutureHits(result.HitObject)) + missAllEarlierObjects(result.HitObject); if (!judgedObject.DisplayResult || !DisplayJudgements.Value) return; @@ -127,12 +128,8 @@ namespace osu.Game.Rulesets.Osu.UI /// Misses all s occurring earlier than the start time of a judged . /// /// The marker , which all s earlier than will get missed. - private void missAllEarlier(HitObject hitObject) + private void missAllEarlierObjects(HitObject hitObject) { - if (!causesNoteLockMisses(hitObject)) - return; - - // The minimum start time required for hitobjects so that they aren't missed. double minimumTime = hitObject.StartTime; foreach (var obj in HitObjectContainer.AliveObjects) @@ -140,50 +137,36 @@ namespace osu.Game.Rulesets.Osu.UI if (obj.HitObject.StartTime >= minimumTime) break; - performMiss(obj); - - foreach (var n in obj.NestedHitObjects) + switch (obj) { - if (n.HitObject.StartTime >= minimumTime) + case DrawableHitCircle circle: + miss(circle); break; - performMiss(n); + case DrawableSlider slider: + miss(slider.HeadCircle); + break; } } + + static void miss(DrawableOsuHitObject obj) + { + // Hitobjects that have already been judged cannot be missed. + if (obj.Judged) + return; + + obj.MissForcefully(); + } } - private void performMiss(DrawableHitObject obj) - { - if (!(obj is DrawableOsuHitObject osuObject)) - throw new InvalidOperationException($"{obj.GetType()} is not a {nameof(DrawableOsuHitObject)}."); - - // Hitobjects that have already been judged cannot be missed. - if (osuObject.Judged) - return; - - if (!causesNoteLockMisses(obj.HitObject)) - return; - - osuObject.MissForcefully(); - } - /// - /// Whether a is contributes to note lock. - /// Future contributing s will not be hittable until the start time of the last contributing is reached. + /// Whether a can block hits on future s until its start time is reached. /// /// The to test. - /// Whether causes note lock. - private bool contributesToNoteLock(HitObject hitObject) + /// Whether can block hits on future s. + private bool canBlockFutureHits(HitObject hitObject) => hitObject is HitCircle || hitObject is Slider; - /// - /// Whether a can be missed and causes other s to be missed when hit out-of-order during note lock. - /// - /// The to test. - /// Whether contributes to note lock misses. - private bool causesNoteLockMisses(HitObject hitObject) - => hitObject is HitCircle && !(hitObject is SliderTailCircle); - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => HitObjectContainer.ReceivePositionalInputAt(screenSpacePos); private class ApproachCircleProxyContainer : LifetimeManagementContainer From 518acf03e9b8319a0e533652f7cacadc7a2afa96 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 9 Apr 2020 19:41:35 +0300 Subject: [PATCH 0952/2376] Remove BeatmapSearchSmallFilterRow component --- .../TestSceneBeatmapSearchFilter.cs | 5 ++- .../BeatmapListingSearchSection.cs | 8 ++--- .../BeatmapListing/BeatmapSearchFilterRow.cs | 5 +-- .../BeatmapSearchSmallFilterRow.cs | 32 ------------------- 4 files changed, 9 insertions(+), 41 deletions(-) delete mode 100644 osu.Game/Overlays/BeatmapListing/BeatmapSearchSmallFilterRow.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapSearchFilter.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapSearchFilter.cs index 7b4424e568..fac58a6754 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapSearchFilter.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapSearchFilter.cs @@ -20,8 +20,7 @@ namespace osu.Game.Tests.Visual.UserInterface public override IReadOnlyList RequiredTypes => new[] { typeof(BeatmapSearchFilterRow<>), - typeof(BeatmapSearchRulesetFilterRow), - typeof(BeatmapSearchSmallFilterRow<>), + typeof(BeatmapSearchRulesetFilterRow) }; [Cached] @@ -43,7 +42,7 @@ namespace osu.Game.Tests.Visual.UserInterface { new BeatmapSearchRulesetFilterRow(), new BeatmapSearchFilterRow("Categories"), - new BeatmapSearchSmallFilterRow("Header Name") + new BeatmapSearchFilterRow("Header Name") } }); } diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchSection.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchSection.cs index 501abbf2c8..3f9cc211df 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchSection.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchSection.cs @@ -47,8 +47,8 @@ namespace osu.Game.Overlays.BeatmapListing private readonly BeatmapSearchTextBox textBox; private readonly BeatmapSearchRulesetFilterRow modeFilter; private readonly BeatmapSearchFilterRow categoryFilter; - private readonly BeatmapSearchSmallFilterRow genreFilter; - private readonly BeatmapSearchSmallFilterRow languageFilter; + private readonly BeatmapSearchFilterRow genreFilter; + private readonly BeatmapSearchFilterRow languageFilter; private readonly Box background; private readonly UpdateableBeatmapSetCover beatmapCover; @@ -104,8 +104,8 @@ namespace osu.Game.Overlays.BeatmapListing { modeFilter = new BeatmapSearchRulesetFilterRow(), categoryFilter = new BeatmapSearchFilterRow(@"Categories"), - genreFilter = new BeatmapSearchSmallFilterRow(@"Genre"), - languageFilter = new BeatmapSearchSmallFilterRow(@"Language"), + genreFilter = new BeatmapSearchFilterRow(@"Genre"), + languageFilter = new BeatmapSearchFilterRow(@"Language"), } } } diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs index 467399dd20..bc0a011e31 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs @@ -17,6 +17,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Online.API.Requests; using osuTK; using osuTK.Graphics; +using Humanizer; namespace osu.Game.Overlays.BeatmapListing { @@ -55,8 +56,8 @@ namespace osu.Game.Overlays.BeatmapListing { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - Font = OsuFont.GetFont(size: 10), - Text = headerName.ToUpper() + Font = OsuFont.GetFont(size: 13), + Text = headerName.Titleize() }, CreateFilter().With(f => { diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchSmallFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchSmallFilterRow.cs deleted file mode 100644 index 6daa7cb0e0..0000000000 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchSmallFilterRow.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Graphics.UserInterface; - -namespace osu.Game.Overlays.BeatmapListing -{ - public class BeatmapSearchSmallFilterRow : BeatmapSearchFilterRow - { - public BeatmapSearchSmallFilterRow(string headerName) - : base(headerName) - { - } - - protected override BeatmapSearchFilter CreateFilter() => new SmallBeatmapSearchFilter(); - - private class SmallBeatmapSearchFilter : BeatmapSearchFilter - { - protected override TabItem CreateTabItem(T value) => new SmallTabItem(value); - - private class SmallTabItem : FilterTabItem - { - public SmallTabItem(T value) - : base(value) - { - } - - protected override float TextSize => 10; - } - } - } -} From 10e849d19616d3fa1314e8fa81ea10e12111e1da Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 10 Apr 2020 02:02:09 +0900 Subject: [PATCH 0953/2376] Separate into separate class --- .../Objects/Drawables/DrawableHitCircle.cs | 2 +- .../Objects/Drawables/DrawableOsuHitObject.cs | 2 +- osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs | 104 ++++++++++++++++++ osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 76 +------------ 4 files changed, 111 insertions(+), 73 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index 5776c64c86..d73ad888f4 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -120,7 +120,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables var result = HitObject.HitWindows.ResultFor(timeOffset); - if (result == HitResult.None || CheckHittable?.Invoke(this) == false) + if (result == HitResult.None || CheckHittable?.Invoke(this, Time.Current) == false) { Shake(Math.Abs(timeOffset) - HitObject.HitWindows.WindowFor(HitResult.Miss)); return; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index 13829dc2f7..fe23e3729d 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables /// Whether this can be hit. /// If non-null, judgements will be ignored (resulting in a shake) whilst the function returns false. /// - public Func CheckHittable; + public Func CheckHittable; protected DrawableOsuHitObject(OsuHitObject hitObject) : base(hitObject) diff --git a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs new file mode 100644 index 0000000000..ddaf714e5b --- /dev/null +++ b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs @@ -0,0 +1,104 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Rulesets.Osu.UI +{ + /// + /// Ensures that s are hit in-order. + /// If a is hit out of order: + /// + /// The hit is blocked if it occurred earlier than the previous 's start time. + /// The hit causes all previous s to missed otherwise. + /// + /// + public class OrderedHitPolicy + { + private readonly HitObjectContainer hitObjectContainer; + + public OrderedHitPolicy(HitObjectContainer hitObjectContainer) + { + this.hitObjectContainer = hitObjectContainer; + } + + /// + /// Determines whether a can be hit at a point in time. + /// + /// The to check. + /// The time to check. + /// Whether can be hit at the given . + public bool IsHittable(DrawableHitObject hitObject, double time) + { + DrawableHitObject lastObject = hitObject; + + // Get the last hitobject that can block future hits + while ((lastObject = hitObjectContainer.AliveObjects.GetPrevious(lastObject)) != null) + { + if (canBlockFutureHits(lastObject.HitObject)) + break; + } + + // If there is no previous object alive, allow the hit. + if (lastObject == null) + return true; + + // Ensure that either the last object has received a judgement or the hit time occurs at or after the last object's start time. + // Simultaneous hitobjects are allowed to be hit at the same time value to account for edge-cases such as Centipede. + if (lastObject.Judged || time >= lastObject.HitObject.StartTime) + return true; + + return false; + } + + /// + /// Handles a being hit to potentially miss all earlier s. + /// + /// The that was hit. + public void HandleHit(HitObject hitObject) + { + if (!canBlockFutureHits(hitObject)) + return; + + double minimumTime = hitObject.StartTime; + + foreach (var obj in hitObjectContainer.AliveObjects) + { + if (obj.HitObject.StartTime >= minimumTime) + break; + + switch (obj) + { + case DrawableHitCircle circle: + miss(circle); + break; + + case DrawableSlider slider: + miss(slider.HeadCircle); + break; + } + } + + static void miss(DrawableOsuHitObject obj) + { + // Hitobjects that have already been judged cannot be missed. + if (obj.Judged) + return; + + obj.MissForcefully(); + } + } + + /// + /// Whether a blocks hits on future s until its start time is reached. + /// + /// The to test. + private bool canBlockFutureHits(HitObject hitObject) + => hitObject is HitCircle || hitObject is Slider; + } +} diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index f4009a281c..2f222f59b4 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Extensions.IEnumerableExtensions; using osuTK; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -11,7 +10,6 @@ using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables.Connections; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Skinning; @@ -22,6 +20,7 @@ namespace osu.Game.Rulesets.Osu.UI private readonly ApproachCircleProxyContainer approachCircles; private readonly JudgementContainer judgementLayer; private readonly FollowPointRenderer followPoints; + private readonly OrderedHitPolicy hitPolicy; public static readonly Vector2 BASE_SIZE = new Vector2(512, 384); @@ -53,6 +52,8 @@ namespace osu.Game.Rulesets.Osu.UI Depth = -1, }, }; + + hitPolicy = new OrderedHitPolicy(HitObjectContainer); } public override void Add(DrawableHitObject h) @@ -67,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.UI base.Add(h); DrawableOsuHitObject osuHitObject = (DrawableOsuHitObject)h; - osuHitObject.CheckHittable = checkHittable; + osuHitObject.CheckHittable = hitPolicy.IsHittable; followPoints.AddFollowPoints(osuHitObject); } @@ -82,34 +83,10 @@ namespace osu.Game.Rulesets.Osu.UI return result; } - private bool checkHittable(DrawableOsuHitObject osuHitObject) - { - DrawableHitObject lastObject = osuHitObject; - - // Get the last hitobject that can block future hits - while ((lastObject = HitObjectContainer.AliveObjects.GetPrevious(lastObject)) != null) - { - if (canBlockFutureHits(lastObject.HitObject)) - break; - } - - // If there is no previous object alive, allow the hit. - if (lastObject == null) - return true; - - // Ensure that either the last object has received a judgement or the hit time occurs at or after the last object's start time. - // Simultaneous hitobjects are allowed to be hit at the same time value to account for edge-cases such as Centipede. - if (lastObject.Judged || Time.Current >= lastObject.HitObject.StartTime) - return true; - - return false; - } - private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) { // Hitobjects that block future hits should miss previous hitobjects if they're hit out-of-order. - if (canBlockFutureHits(result.HitObject)) - missAllEarlierObjects(result.HitObject); + hitPolicy.HandleHit(result.HitObject); if (!judgedObject.DisplayResult || !DisplayJudgements.Value) return; @@ -124,49 +101,6 @@ namespace osu.Game.Rulesets.Osu.UI judgementLayer.Add(explosion); } - /// - /// Misses all s occurring earlier than the start time of a judged . - /// - /// The marker , which all s earlier than will get missed. - private void missAllEarlierObjects(HitObject hitObject) - { - double minimumTime = hitObject.StartTime; - - foreach (var obj in HitObjectContainer.AliveObjects) - { - if (obj.HitObject.StartTime >= minimumTime) - break; - - switch (obj) - { - case DrawableHitCircle circle: - miss(circle); - break; - - case DrawableSlider slider: - miss(slider.HeadCircle); - break; - } - } - - static void miss(DrawableOsuHitObject obj) - { - // Hitobjects that have already been judged cannot be missed. - if (obj.Judged) - return; - - obj.MissForcefully(); - } - } - - /// - /// Whether a can block hits on future s until its start time is reached. - /// - /// The to test. - /// Whether can block hits on future s. - private bool canBlockFutureHits(HitObject hitObject) - => hitObject is HitCircle || hitObject is Slider; - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => HitObjectContainer.ReceivePositionalInputAt(screenSpacePos); private class ApproachCircleProxyContainer : LifetimeManagementContainer From b54bbc5f6a217352a03ed77eb05eb20e50a948fa Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 10 Apr 2020 02:41:37 +0900 Subject: [PATCH 0954/2376] Improve commenting + refactor --- osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs | 79 +++++++++++++------- 1 file changed, 54 insertions(+), 25 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs index ddaf714e5b..0a09b5be7c 100644 --- a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Extensions.IEnumerableExtensions; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; @@ -35,22 +34,27 @@ namespace osu.Game.Rulesets.Osu.UI /// Whether can be hit at the given . public bool IsHittable(DrawableHitObject hitObject, double time) { - DrawableHitObject lastObject = hitObject; + DrawableHitObject blockingObject = null; - // Get the last hitobject that can block future hits - while ((lastObject = hitObjectContainer.AliveObjects.GetPrevious(lastObject)) != null) + // Find the last hitobject which blocks future hits. + foreach (var obj in hitObjectContainer.AliveObjects) { - if (canBlockFutureHits(lastObject.HitObject)) + if (obj == hitObject) break; + + if (canBlockFutureHits(obj)) + blockingObject = obj; } - // If there is no previous object alive, allow the hit. - if (lastObject == null) + // If there is no previous hitobject, allow the hit. + if (blockingObject == null) return true; - // Ensure that either the last object has received a judgement or the hit time occurs at or after the last object's start time. - // Simultaneous hitobjects are allowed to be hit at the same time value to account for edge-cases such as Centipede. - if (lastObject.Judged || time >= lastObject.HitObject.StartTime) + // A hit is allowed if: + // 1. The last blocking hitobject has been judged. + // 2. The current time is after the last hitobject's start time. + // Hits at exactly the same time as the blocking hitobject are allowed for maps that contain simultaneous hitobjects (e.g. /b/372245). + if (blockingObject.Judged || time >= blockingObject.HitObject.StartTime) return true; return false; @@ -62,6 +66,7 @@ namespace osu.Game.Rulesets.Osu.UI /// The that was hit. public void HandleHit(HitObject hitObject) { + // Hitobjects which themselves don't block future hitobjects don't cause misses (e.g. slider ticks) if (!canBlockFutureHits(hitObject)) return; @@ -72,33 +77,57 @@ namespace osu.Game.Rulesets.Osu.UI if (obj.HitObject.StartTime >= minimumTime) break; - switch (obj) + // If the parent hitobject cannot cause a miss, neither can any nested hitobject. + if (!canBlockFutureHits(obj)) + continue; + + applyMiss(obj); + + foreach (var nested in obj.NestedHitObjects) { - case DrawableHitCircle circle: - miss(circle); + if (nested.HitObject.StartTime >= minimumTime) break; - case DrawableSlider slider: - miss(slider.HeadCircle); - break; + if (canBlockFutureHits(nested)) + applyMiss(nested); } } - static void miss(DrawableOsuHitObject obj) - { - // Hitobjects that have already been judged cannot be missed. - if (obj.Judged) - return; + static void applyMiss(DrawableHitObject obj) => ((DrawableOsuHitObject)obj).MissForcefully(); + } - obj.MissForcefully(); - } + /// + /// Whether a blocks hits on future s until its start time is reached. + /// + /// + /// Must only be used when iterating through top-most drawable hitobjects. + /// + /// The to test. + private static bool canBlockFutureHits(DrawableHitObject hitObject) + { + // Judged hitobjects can never block hits. + if (hitObject.Judged) + return false; + + // Special considerations for slider tails aren't required since only top-most drawable hitobjects are being iterated over. + return hitObject is DrawableHitCircle || hitObject is DrawableSlider; } /// /// Whether a blocks hits on future s until its start time is reached. /// + /// + /// Must only be used when iterating through nested hitobjects. + /// /// The to test. - private bool canBlockFutureHits(HitObject hitObject) - => hitObject is HitCircle || hitObject is Slider; + private static bool canBlockFutureHits(HitObject hitObject) + { + // Unlike the above we will receive slider tails, but they do not block future hits. + if (hitObject is SliderTailCircle) + return false; + + // All other hitcircles continue to block future hits. + return hitObject is HitCircle; + } } } From 42b3ff805b60c740fbd85948a8db366f8e91952b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 10 Apr 2020 02:57:31 +0900 Subject: [PATCH 0955/2376] Rename methods + fix incorrect method usage --- osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs | 33 ++++++++------------ 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs index 0a09b5be7c..cfb850b785 100644 --- a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Osu.UI if (obj == hitObject) break; - if (canBlockFutureHits(obj)) + if (drawableCanBlockFutureHits(obj)) blockingObject = obj; } @@ -66,29 +66,26 @@ namespace osu.Game.Rulesets.Osu.UI /// The that was hit. public void HandleHit(HitObject hitObject) { - // Hitobjects which themselves don't block future hitobjects don't cause misses (e.g. slider ticks) - if (!canBlockFutureHits(hitObject)) + // Hitobjects which themselves don't block future hitobjects don't cause misses (e.g. slider ticks, spinners) + if (!hitObjectCanBlockFutureHits(hitObject)) return; double minimumTime = hitObject.StartTime; foreach (var obj in hitObjectContainer.AliveObjects) { - if (obj.HitObject.StartTime >= minimumTime) - break; - - // If the parent hitobject cannot cause a miss, neither can any nested hitobject. - if (!canBlockFutureHits(obj)) + if (obj.Judged || obj.HitObject.StartTime >= minimumTime) continue; - applyMiss(obj); + if (hitObjectCanBlockFutureHits(obj.HitObject)) + applyMiss(obj); foreach (var nested in obj.NestedHitObjects) { - if (nested.HitObject.StartTime >= minimumTime) - break; + if (nested.Judged || nested.HitObject.StartTime >= minimumTime) + continue; - if (canBlockFutureHits(nested)) + if (hitObjectCanBlockFutureHits(nested.HitObject)) applyMiss(nested); } } @@ -100,15 +97,11 @@ namespace osu.Game.Rulesets.Osu.UI /// Whether a blocks hits on future s until its start time is reached. /// /// - /// Must only be used when iterating through top-most drawable hitobjects. + /// This will ONLY match on top-most s. /// /// The to test. - private static bool canBlockFutureHits(DrawableHitObject hitObject) + private static bool drawableCanBlockFutureHits(DrawableHitObject hitObject) { - // Judged hitobjects can never block hits. - if (hitObject.Judged) - return false; - // Special considerations for slider tails aren't required since only top-most drawable hitobjects are being iterated over. return hitObject is DrawableHitCircle || hitObject is DrawableSlider; } @@ -117,10 +110,10 @@ namespace osu.Game.Rulesets.Osu.UI /// Whether a blocks hits on future s until its start time is reached. /// /// - /// Must only be used when iterating through nested hitobjects. + /// This is more rigorous and may not match on top-most s as does. /// /// The to test. - private static bool canBlockFutureHits(HitObject hitObject) + private static bool hitObjectCanBlockFutureHits(HitObject hitObject) { // Unlike the above we will receive slider tails, but they do not block future hits. if (hitObject is SliderTailCircle) From 15a92d1451c9fe0fb916f4c4e392c4da2411dcf3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 10 Apr 2020 02:57:35 +0900 Subject: [PATCH 0956/2376] Rename test scene --- .../{TestSceneNoteLock.cs => TestSceneOutOfOrderHits.cs} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename osu.Game.Rulesets.Osu.Tests/{TestSceneNoteLock.cs => TestSceneOutOfOrderHits.cs} (99%) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs similarity index 99% rename from osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs rename to osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs index 2c69540951..d6858f831e 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneNoteLock.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs @@ -25,7 +25,7 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Tests { - public class TestSceneNoteLock : RateAdjustedBeatmapTestScene + public class TestSceneOutOfOrderHits : RateAdjustedBeatmapTestScene { private const double early_miss_window = 1000; // time after -1000 to -500 is considered a miss private const double late_miss_window = 500; // time after +500 is considered a miss From 6988df30bd9cdfe7be77a21f6de63b11ca462a45 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 10 Apr 2020 03:12:13 +0900 Subject: [PATCH 0957/2376] Rename variable, add comment --- osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs index cfb850b785..dfca2aff7b 100644 --- a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs @@ -66,15 +66,16 @@ namespace osu.Game.Rulesets.Osu.UI /// The that was hit. public void HandleHit(HitObject hitObject) { - // Hitobjects which themselves don't block future hitobjects don't cause misses (e.g. slider ticks, spinners) + // Hitobjects which themselves don't block future hitobjects don't cause misses (e.g. slider ticks, spinners). if (!hitObjectCanBlockFutureHits(hitObject)) return; - double minimumTime = hitObject.StartTime; + double maximumTime = hitObject.StartTime; + // Iterate through and apply miss results to all top-level and nested hitobjects which block future hits. foreach (var obj in hitObjectContainer.AliveObjects) { - if (obj.Judged || obj.HitObject.StartTime >= minimumTime) + if (obj.Judged || obj.HitObject.StartTime >= maximumTime) continue; if (hitObjectCanBlockFutureHits(obj.HitObject)) @@ -82,7 +83,7 @@ namespace osu.Game.Rulesets.Osu.UI foreach (var nested in obj.NestedHitObjects) { - if (nested.Judged || nested.HitObject.StartTime >= minimumTime) + if (nested.Judged || nested.HitObject.StartTime >= maximumTime) continue; if (hitObjectCanBlockFutureHits(nested.HitObject)) From 91b3aa2914427cfa0af3cd1dbc58ea4b25ad9919 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 9 Apr 2020 22:56:10 +0300 Subject: [PATCH 0958/2376] Implement interval list --- osu.Game/Lists/IntervalList.cs | 128 +++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 osu.Game/Lists/IntervalList.cs diff --git a/osu.Game/Lists/IntervalList.cs b/osu.Game/Lists/IntervalList.cs new file mode 100644 index 0000000000..493b6b6e72 --- /dev/null +++ b/osu.Game/Lists/IntervalList.cs @@ -0,0 +1,128 @@ +// 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; +using System.Collections.Generic; +using osu.Framework.Lists; + +namespace osu.Game.Lists +{ + /// + /// Represents a list of intervals that can be used for whether a specific value falls into one of them. + /// + /// The type of interval values. + public class IntervalList : ICollection>, IReadOnlyList> + { + private static readonly IComparer type_comparer = Comparer.Default; + + private readonly SortedList> intervals = new SortedList>((x, y) => type_comparer.Compare(x.Start, y.Start)); + + /// + /// The index of the nearest interval from last call. + /// + protected int NearestIntervalIndex; + + /// + /// Whether the provided value is in any interval added to this list. + /// + /// The value to check for. + public bool IsInAnyInterval(T value) + { + if (intervals.Count == 0) + return false; + + // Clamp the nearest index in case there were intervals + // removed from the list causing the index to go out of range. + NearestIntervalIndex = Math.Clamp(NearestIntervalIndex, 0, Count - 1); + + if (type_comparer.Compare(value, this[NearestIntervalIndex].End) > 0) + { + while (type_comparer.Compare(value, this[NearestIntervalIndex].End) > 0 && NearestIntervalIndex < Count - 1) + NearestIntervalIndex++; + } + else + { + while (type_comparer.Compare(value, this[NearestIntervalIndex].Start) < 0 && NearestIntervalIndex > 0) + NearestIntervalIndex--; + } + + var nearestInterval = this[NearestIntervalIndex]; + + return type_comparer.Compare(value, nearestInterval.Start) >= 0 && + type_comparer.Compare(value, nearestInterval.End) <= 0; + } + + /// + /// Adds a new interval to the list. + /// + /// The start value of the interval. + /// The end value of the interval. + public void Add(T start, T end) => Add(new Interval(start, end)); + + #region ICollection> + + public int Count => intervals.Count; + + bool ICollection>.IsReadOnly => false; + + /// + /// Adds a new interval to the list + /// + /// The interval to add. + public void Add(Interval interval) => intervals.Add(interval); + + /// + /// Removes an existing interval from the list. + /// + /// The interval to remove. + /// Whether the provided interval exists in the list and has been removed. + public bool Remove(Interval interval) => intervals.Remove(interval); + + /// + /// Removes all intervals from the list. + /// + public void Clear() => intervals.Clear(); + + public void CopyTo(Interval[] array, int arrayIndex) => intervals.CopyTo(array, arrayIndex); + + public bool Contains(Interval item) => intervals.Contains(item); + + public IEnumerator> GetEnumerator() => intervals.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + #endregion + + #region IReadOnlyList> + + public Interval this[int index] + { + get => intervals[index]; + set => intervals[index] = value; + } + + #endregion + } + + public readonly struct Interval + { + /// + /// The start value of this interval. + /// + public readonly T Start; + + /// + /// The end value of this interval. + /// + public readonly T End; + + public Interval(T start, T end) + { + bool startLessThanEnd = Comparer.Default.Compare(start, end) < 0; + + Start = startLessThanEnd ? start : end; + End = startLessThanEnd ? end : start; + } + } +} From 38ee5f310302e3d05b240c4d55e1bb4e1bd5f8e7 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 9 Apr 2020 22:57:54 +0300 Subject: [PATCH 0959/2376] Add tests covering most cases of interval list checking --- osu.Game.Tests/Lists/IntervalListTest.cs | 141 +++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 osu.Game.Tests/Lists/IntervalListTest.cs diff --git a/osu.Game.Tests/Lists/IntervalListTest.cs b/osu.Game.Tests/Lists/IntervalListTest.cs new file mode 100644 index 0000000000..1bc1483e15 --- /dev/null +++ b/osu.Game.Tests/Lists/IntervalListTest.cs @@ -0,0 +1,141 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Lists; + +namespace osu.Game.Tests.Lists +{ + [TestFixture] + public class IntervalListTest + { + // this is intended to be unordered to test adding intervals in unordered way. + private static readonly (double, double)[] test_intervals = + { + (-9.1d, -8.3d), + (-3.4d, 2.1d), + (50.0d, 9.0d), // intentionally reversing interval. + (5.25d, 10.50d), + }; + + [Test] + public void TestCheckValueInsideSingleInterval() + { + var list = new IntervalList { { 1.0d, 2.0d } }; + + Assert.IsTrue(list.IsInAnyInterval(1.0d)); + Assert.IsTrue(list.IsInAnyInterval(1.5d)); + Assert.IsTrue(list.IsInAnyInterval(2.0d)); + } + + [Test] + public void TestCheckValuesInsideIntervals() + { + var list = new IntervalList(); + + foreach (var (start, end) in test_intervals) + list.Add(start, end); + + Assert.IsTrue(list.IsInAnyInterval(-8.75d)); + Assert.IsTrue(list.IsInAnyInterval(1.0d)); + Assert.IsTrue(list.IsInAnyInterval(7.89d)); + Assert.IsTrue(list.IsInAnyInterval(9.8d)); + Assert.IsTrue(list.IsInAnyInterval(15.83d)); + } + + [Test] + public void TestCheckValuesInRandomOrder() + { + var list = new IntervalList(); + + foreach (var (start, end) in test_intervals) + list.Add(start, end); + + Assert.IsTrue(list.IsInAnyInterval(9.8d)); + Assert.IsTrue(list.IsInAnyInterval(7.89d)); + Assert.IsTrue(list.IsInAnyInterval(1.0d)); + Assert.IsTrue(list.IsInAnyInterval(15.83d)); + Assert.IsTrue(list.IsInAnyInterval(-8.75d)); + } + + [Test] + public void TestCheckValuesOutOfIntervals() + { + var list = new IntervalList(); + + foreach (var (start, end) in test_intervals) + list.Add(start, end); + + Assert.IsFalse(list.IsInAnyInterval(-9.2d)); + Assert.IsFalse(list.IsInAnyInterval(2.2d)); + Assert.IsFalse(list.IsInAnyInterval(5.15d)); + Assert.IsFalse(list.IsInAnyInterval(51.2d)); + } + + [Test] + public void TestCheckValueAfterRemovedInterval() + { + var list = new IntervalList { { 50, 100 }, { 150, 200 }, { 250, 300 } }; + + Assert.IsTrue(list.IsInAnyInterval(75)); + Assert.IsTrue(list.IsInAnyInterval(175)); + Assert.IsTrue(list.IsInAnyInterval(275)); + + list.Remove(list[1]); + + Assert.IsFalse(list.IsInAnyInterval(175)); + Assert.IsTrue(list.IsInAnyInterval(75)); + Assert.IsTrue(list.IsInAnyInterval(275)); + } + + [Test] + public void TestCheckValueAfterAddedInterval() + { + var list = new IntervalList { { 50, 100 }, { 250, 300 } }; + + Assert.IsFalse(list.IsInAnyInterval(175)); + Assert.IsTrue(list.IsInAnyInterval(75)); + Assert.IsTrue(list.IsInAnyInterval(275)); + + list.Add(150, 200); + + Assert.IsTrue(list.IsInAnyInterval(175)); + } + + [Test] + public void TestCheckIntervalIndexOnChecks() + { + var list = new TestIntervalList { { 1.0d, 2.0d }, { 3.0d, 4.0d }, { 5.0d, 6.0d }, { 7.0d, 8.0d } }; + + Assert.IsTrue(list.IsInAnyInterval(1.5d)); + Assert.IsTrue(list.NearestIntervalIndex == 0); + + Assert.IsTrue(list.IsInAnyInterval(5.5d)); + Assert.IsTrue(list.NearestIntervalIndex == 2); + + Assert.IsTrue(list.IsInAnyInterval(7.5d)); + Assert.IsTrue(list.NearestIntervalIndex == 3); + } + + [Test] + public void TestCheckIntervalIndexOnOutOfIntervalsChecks() + { + var list = new TestIntervalList { { 1.0d, 2.0d }, { 3.0d, 4.0d }, { 5.0d, 6.0d }, { 7.0d, 8.0d } }; + + Assert.IsFalse(list.IsInAnyInterval(4.5d)); + Assert.IsTrue(list.NearestIntervalIndex == 1 || + list.NearestIntervalIndex == 2); // 4.5 in between 3.0-4.0 and 5.0-6.0 + + Assert.IsFalse(list.IsInAnyInterval(9.0d)); + Assert.IsTrue(list.NearestIntervalIndex == 3); // 9.0 goes above 7.0-8.0 + + Assert.IsFalse(list.IsInAnyInterval(0.0d)); + Assert.IsTrue(list.NearestIntervalIndex == 0); // 0.0 goes below 1.0-2.0 + } + + private class TestIntervalList : IntervalList + { + public new int NearestIntervalIndex => base.NearestIntervalIndex; + } + } +} From 9a29797a5bee661a767d43584af541d6619d5ea5 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 9 Apr 2020 23:00:16 +0300 Subject: [PATCH 0960/2376] Use IntervalList for tracking break periods --- .../Visual/Gameplay/TestSceneAutoplay.cs | 2 +- .../Visual/Gameplay/TestSceneBreakTracker.cs | 4 -- osu.Game/Screens/Play/BreakTracker.cs | 50 ++++++------------- 3 files changed, 16 insertions(+), 40 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs index 4b1c2ec256..0be949650e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs @@ -52,7 +52,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep($"seek to break {breakIndex}", () => Player.GameplayClockContainer.Seek(destBreak().StartTime)); AddUntilStep("wait for seek to complete", () => Player.HUDOverlay.Progress.ReferenceClock.CurrentTime >= destBreak().StartTime); - BreakPeriod destBreak() => Player.ChildrenOfType().First().Breaks.ElementAt(breakIndex); + BreakPeriod destBreak() => Beatmap.Value.Beatmap.Breaks.ElementAt(breakIndex); } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs index 91d6f2f143..a6f996c30d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs @@ -97,8 +97,6 @@ namespace osu.Game.Tests.Visual.Gameplay loadBreaksStep("multiple breaks", testBreaks); seekAndAssertBreak("seek to break start", testBreaks[1].StartTime, true); - AddAssert("is skipped to break #2", () => breakTracker.CurrentBreakIndex == 1); - seekAndAssertBreak("seek to break middle", testBreaks[1].StartTime + testBreaks[1].Duration / 2, true); seekAndAssertBreak("seek to break end", testBreaks[1].EndTime, false); seekAndAssertBreak("seek to break after end", testBreaks[1].EndTime + 500, false); @@ -174,8 +172,6 @@ namespace osu.Game.Tests.Visual.Gameplay private readonly ManualClock manualClock; private IFrameBasedClock originalClock; - public new int CurrentBreakIndex => base.CurrentBreakIndex; - public double ManualClockTime { get => manualClock.CurrentTime; diff --git a/osu.Game/Screens/Play/BreakTracker.cs b/osu.Game/Screens/Play/BreakTracker.cs index 64262d52b5..c2eb069ee6 100644 --- a/osu.Game/Screens/Play/BreakTracker.cs +++ b/osu.Game/Screens/Play/BreakTracker.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Beatmaps.Timing; +using osu.Game.Lists; using osu.Game.Rulesets.Scoring; namespace osu.Game.Screens.Play @@ -20,22 +21,24 @@ namespace osu.Game.Screens.Play /// public IBindable IsBreakTime => isBreakTime; - protected int CurrentBreakIndex; - private readonly BindableBool isBreakTime = new BindableBool(); - private IReadOnlyList breaks; + private readonly IntervalList breakIntervals = new IntervalList(); public IReadOnlyList Breaks { - get => breaks; set { - breaks = value; - - // reset index in case the new breaks list is smaller than last one isBreakTime.Value = false; - CurrentBreakIndex = 0; + breakIntervals.Clear(); + + foreach (var b in value) + { + if (!b.HasEffect) + continue; + + breakIntervals.Add(b.StartTime, b.EndTime - BreakOverlay.BREAK_FADE_DURATION); + } } } @@ -49,34 +52,11 @@ namespace osu.Game.Screens.Play { base.Update(); - isBreakTime.Value = getCurrentBreak()?.HasEffect == true - || Clock.CurrentTime < gameplayStartTime + var time = Clock.CurrentTime; + + isBreakTime.Value = breakIntervals.IsInAnyInterval(time) + || time < gameplayStartTime || scoreProcessor?.HasCompleted == true; } - - private BreakPeriod getCurrentBreak() - { - if (breaks?.Count > 0) - { - var time = Clock.CurrentTime; - - if (time > breaks[CurrentBreakIndex].EndTime) - { - while (time > breaks[CurrentBreakIndex].EndTime && CurrentBreakIndex < breaks.Count - 1) - CurrentBreakIndex++; - } - else - { - while (time < breaks[CurrentBreakIndex].StartTime && CurrentBreakIndex > 0) - CurrentBreakIndex--; - } - - var closest = breaks[CurrentBreakIndex]; - - return closest.Contains(time) ? closest : null; - } - - return null; - } } } From c17e47026623afde64aaf11b8334b5ebcd221696 Mon Sep 17 00:00:00 2001 From: Fire937 Date: Fri, 10 Apr 2020 00:01:35 +0200 Subject: [PATCH 0961/2376] Fix PositionalSound calculation implementation The position used to calculate the stereo balance is now the position of the drawable (as opposed to the position specified in the beatmap file previously). --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index ed9efba89f..30a9106ddc 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Objects.Drawables /// /// The stereo balance of the samples played if Positional hitsounds is set. /// - protected virtual float PositionalSound => (HitObject is IHasXPosition position) ? (position.X / 512f - 0.5f) * 0.8f : 0; + protected virtual float PositionalSound => (Position.X / 512f - 0.5f) * 0.8f; private BindableList samplesBindable; private Bindable startTimeBindable; @@ -114,7 +114,6 @@ namespace osu.Game.Rulesets.Objects.Drawables protected DrawableHitObject([NotNull] HitObject hitObject) { HitObject = hitObject ?? throw new ArgumentNullException(nameof(hitObject)); - positionalSoundAdjustment.Value = PositionalSound; } [BackgroundDependencyLoader] @@ -377,7 +376,11 @@ namespace osu.Game.Rulesets.Objects.Drawables /// Plays all the hit sounds for this . /// This is invoked automatically when this is hit. ///
- public virtual void PlaySamples() => Samples?.Play(); + public virtual void PlaySamples() + { + positionalSoundAdjustment.Value = PositionalSound; + Samples?.Play(); + } protected override void Update() { From ee7e2b0854a8096dd55c1e3472b8964311df2897 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 10 Apr 2020 13:29:46 +0900 Subject: [PATCH 0962/2376] Fix editor beatmap potentially not updating hitobjects --- osu.Game/Screens/Edit/EditorBeatmap.cs | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 7f04a7a58d..efffde54b3 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -63,6 +63,7 @@ namespace osu.Game.Screens.Edit trackStartTime(obj); } + private readonly HashSet pendingUpdates = new HashSet(); private ScheduledDelegate scheduledUpdate; /// @@ -74,15 +75,27 @@ namespace osu.Game.Screens.Edit private void updateHitObject([CanBeNull] HitObject hitObject, bool silent) { scheduledUpdate?.Cancel(); - scheduledUpdate = Scheduler.AddDelayed(() => + + if (hitObject != null) + pendingUpdates.Add(hitObject); + + scheduledUpdate = Schedule(() => { beatmapProcessor?.PreProcess(); - hitObject?.ApplyDefaults(ControlPointInfo, BeatmapInfo.BaseDifficulty); + + foreach (var obj in pendingUpdates) + obj.ApplyDefaults(ControlPointInfo, BeatmapInfo.BaseDifficulty); + beatmapProcessor?.PostProcess(); if (!silent) - HitObjectUpdated?.Invoke(hitObject); - }, 0); + { + foreach (var obj in pendingUpdates) + HitObjectUpdated?.Invoke(obj); + } + + pendingUpdates.Clear(); + }); } public BeatmapInfo BeatmapInfo From 41caa378565d807853dd53ec6b0727d60139bd33 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 10 Apr 2020 13:29:49 +0900 Subject: [PATCH 0963/2376] Add tests --- .../Beatmaps/TestSceneEditorBeatmap.cs | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/osu.Game.Tests/Beatmaps/TestSceneEditorBeatmap.cs b/osu.Game.Tests/Beatmaps/TestSceneEditorBeatmap.cs index d367d9f88b..2d4587341d 100644 --- a/osu.Game.Tests/Beatmaps/TestSceneEditorBeatmap.cs +++ b/osu.Game.Tests/Beatmaps/TestSceneEditorBeatmap.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 Microsoft.EntityFrameworkCore.Internal; using NUnit.Framework; @@ -162,5 +163,69 @@ namespace osu.Game.Tests.Beatmaps Assert.That(editorBeatmap.HitObjects.Count(h => h == hitCircle), Is.EqualTo(1)); Assert.That(editorBeatmap.HitObjects.IndexOf(hitCircle), Is.EqualTo(1)); } + + /// + /// Tests that multiple hitobjects are updated simultaneously. + /// + [Test] + public void TestMultipleHitObjectUpdate() + { + var updatedObjects = new List(); + var allHitObjects = new List(); + EditorBeatmap editorBeatmap = null; + + AddStep("add beatmap", () => + { + updatedObjects.Clear(); + + Child = editorBeatmap = new EditorBeatmap(new OsuBeatmap()); + + for (int i = 0; i < 10; i++) + { + var h = new HitCircle(); + editorBeatmap.Add(h); + allHitObjects.Add(h); + } + }); + + AddStep("change all start times", () => + { + editorBeatmap.HitObjectUpdated += h => updatedObjects.Add(h); + + for (int i = 0; i < 10; i++) + allHitObjects[i].StartTime += 10; + }); + + // Distinct ensures that all hitobjects have been updated once, debounce is tested below. + AddAssert("all hitobjects updated", () => updatedObjects.Distinct().Count() == 10); + } + + /// + /// Tests that hitobject updates are debounced when they happen too soon. + /// + [Test] + public void TestDebouncedUpdate() + { + var updatedObjects = new List(); + EditorBeatmap editorBeatmap = null; + + AddStep("add beatmap", () => + { + updatedObjects.Clear(); + + Child = editorBeatmap = new EditorBeatmap(new OsuBeatmap()); + editorBeatmap.Add(new HitCircle()); + }); + + AddStep("change start time twice", () => + { + editorBeatmap.HitObjectUpdated += h => updatedObjects.Add(h); + + editorBeatmap.HitObjects[0].StartTime = 10; + editorBeatmap.HitObjects[0].StartTime = 20; + }); + + AddAssert("only updated once", () => updatedObjects.Count == 1); + } } } From 4a87ac784061318026fe650a48e42ca455e7e7f9 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 10 Apr 2020 13:53:09 +0900 Subject: [PATCH 0964/2376] Add support for sample changes --- .../Screens/Edit/Compose/Components/SelectionHandler.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index e212979433..764eae1056 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -212,6 +212,8 @@ namespace osu.Game.Screens.Edit.Compose.Components /// The name of the hit sample. public void AddHitSample(string sampleName) { + changeHandler?.BeginChange(); + foreach (var h in SelectedHitObjects) { // Make sure there isn't already an existing sample @@ -220,6 +222,8 @@ namespace osu.Game.Screens.Edit.Compose.Components h.Samples.Add(new HitSampleInfo { Name = sampleName }); } + + changeHandler?.EndChange(); } /// @@ -228,8 +232,12 @@ namespace osu.Game.Screens.Edit.Compose.Components /// The name of the hit sample. public void RemoveHitSample(string sampleName) { + changeHandler?.BeginChange(); + foreach (var h in SelectedHitObjects) h.SamplesBindable.RemoveAll(s => s.Name == sampleName); + + changeHandler?.EndChange(); } #endregion From 1001fcfb94d28787d62fa9e185499188620b4522 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 10 Apr 2020 15:48:00 +0300 Subject: [PATCH 0965/2376] Revert nearest index accessibility back to private Implementation details should not be tested. --- osu.Game/Lists/IntervalList.cs | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/osu.Game/Lists/IntervalList.cs b/osu.Game/Lists/IntervalList.cs index 493b6b6e72..6a71d94ea8 100644 --- a/osu.Game/Lists/IntervalList.cs +++ b/osu.Game/Lists/IntervalList.cs @@ -17,11 +17,8 @@ namespace osu.Game.Lists private static readonly IComparer type_comparer = Comparer.Default; private readonly SortedList> intervals = new SortedList>((x, y) => type_comparer.Compare(x.Start, y.Start)); + private int nearestIndex; - /// - /// The index of the nearest interval from last call. - /// - protected int NearestIntervalIndex; /// /// Whether the provided value is in any interval added to this list. @@ -34,20 +31,20 @@ namespace osu.Game.Lists // Clamp the nearest index in case there were intervals // removed from the list causing the index to go out of range. - NearestIntervalIndex = Math.Clamp(NearestIntervalIndex, 0, Count - 1); + nearestIndex = Math.Clamp(nearestIndex, 0, intervals.Count - 1); - if (type_comparer.Compare(value, this[NearestIntervalIndex].End) > 0) + if (type_comparer.Compare(value, this[nearestIndex].End) > 0) { - while (type_comparer.Compare(value, this[NearestIntervalIndex].End) > 0 && NearestIntervalIndex < Count - 1) - NearestIntervalIndex++; + while (type_comparer.Compare(value, this[nearestIndex].End) > 0 && nearestIndex < intervals.Count - 1) + nearestIndex++; } else { - while (type_comparer.Compare(value, this[NearestIntervalIndex].Start) < 0 && NearestIntervalIndex > 0) - NearestIntervalIndex--; + while (type_comparer.Compare(value, this[nearestIndex].Start) < 0 && nearestIndex > 0) + nearestIndex--; } - var nearestInterval = this[NearestIntervalIndex]; + var nearestInterval = this[nearestIndex]; return type_comparer.Compare(value, nearestInterval.Start) >= 0 && type_comparer.Compare(value, nearestInterval.End) <= 0; From b7ed17dfbd16a78561da25516757bc19170e9af2 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 10 Apr 2020 15:49:06 +0300 Subject: [PATCH 0966/2376] Throw if interval with reversed values added May reduce in subtle bugs. --- osu.Game.Tests/Lists/IntervalListTest.cs | 38 ++++-------------------- osu.Game/Lists/IntervalList.cs | 7 +++-- 2 files changed, 10 insertions(+), 35 deletions(-) diff --git a/osu.Game.Tests/Lists/IntervalListTest.cs b/osu.Game.Tests/Lists/IntervalListTest.cs index 1bc1483e15..0958f0fa7c 100644 --- a/osu.Game.Tests/Lists/IntervalListTest.cs +++ b/osu.Game.Tests/Lists/IntervalListTest.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 NUnit.Framework; using osu.Game.Lists; @@ -14,7 +15,7 @@ namespace osu.Game.Tests.Lists { (-9.1d, -8.3d), (-3.4d, 2.1d), - (50.0d, 9.0d), // intentionally reversing interval. + (9.0d, 50.0d), (5.25d, 10.50d), }; @@ -103,39 +104,12 @@ namespace osu.Game.Tests.Lists } [Test] - public void TestCheckIntervalIndexOnChecks() + public void TestReversedIntervalThrows() { - var list = new TestIntervalList { { 1.0d, 2.0d }, { 3.0d, 4.0d }, { 5.0d, 6.0d }, { 7.0d, 8.0d } }; + var list = new IntervalList(); - Assert.IsTrue(list.IsInAnyInterval(1.5d)); - Assert.IsTrue(list.NearestIntervalIndex == 0); - - Assert.IsTrue(list.IsInAnyInterval(5.5d)); - Assert.IsTrue(list.NearestIntervalIndex == 2); - - Assert.IsTrue(list.IsInAnyInterval(7.5d)); - Assert.IsTrue(list.NearestIntervalIndex == 3); - } - - [Test] - public void TestCheckIntervalIndexOnOutOfIntervalsChecks() - { - var list = new TestIntervalList { { 1.0d, 2.0d }, { 3.0d, 4.0d }, { 5.0d, 6.0d }, { 7.0d, 8.0d } }; - - Assert.IsFalse(list.IsInAnyInterval(4.5d)); - Assert.IsTrue(list.NearestIntervalIndex == 1 || - list.NearestIntervalIndex == 2); // 4.5 in between 3.0-4.0 and 5.0-6.0 - - Assert.IsFalse(list.IsInAnyInterval(9.0d)); - Assert.IsTrue(list.NearestIntervalIndex == 3); // 9.0 goes above 7.0-8.0 - - Assert.IsFalse(list.IsInAnyInterval(0.0d)); - Assert.IsTrue(list.NearestIntervalIndex == 0); // 0.0 goes below 1.0-2.0 - } - - private class TestIntervalList : IntervalList - { - public new int NearestIntervalIndex => base.NearestIntervalIndex; + Assert.Throws(() => list.Add(50, 25)); + Assert.Throws(() => list.Add(new Interval(50, 25))); } } } diff --git a/osu.Game/Lists/IntervalList.cs b/osu.Game/Lists/IntervalList.cs index 6a71d94ea8..15d9a349dc 100644 --- a/osu.Game/Lists/IntervalList.cs +++ b/osu.Game/Lists/IntervalList.cs @@ -116,10 +116,11 @@ namespace osu.Game.Lists public Interval(T start, T end) { - bool startLessThanEnd = Comparer.Default.Compare(start, end) < 0; + if (Comparer.Default.Compare(start, end) >= 0) + throw new ArgumentException($"Invalid interval, {nameof(start)} must be less than {nameof(end)}", nameof(start)); - Start = startLessThanEnd ? start : end; - End = startLessThanEnd ? end : start; + Start = start; + End = end; } } } From 5966c9af9ccdd05d84a92921eae77b3cff787712 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 10 Apr 2020 15:50:07 +0300 Subject: [PATCH 0967/2376] Limit generic type to IConvertible structures (common number types) --- osu.Game/Lists/IntervalList.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Lists/IntervalList.cs b/osu.Game/Lists/IntervalList.cs index 15d9a349dc..5a5866ca91 100644 --- a/osu.Game/Lists/IntervalList.cs +++ b/osu.Game/Lists/IntervalList.cs @@ -13,6 +13,7 @@ namespace osu.Game.Lists /// /// The type of interval values. public class IntervalList : ICollection>, IReadOnlyList> + where T : struct, IConvertible { private static readonly IComparer type_comparer = Comparer.Default; @@ -103,6 +104,7 @@ namespace osu.Game.Lists } public readonly struct Interval + where T : struct, IConvertible { /// /// The start value of this interval. From bf124a5cc6d9d99566f099e405f2c03bcdc6ac3f Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 10 Apr 2020 15:50:40 +0300 Subject: [PATCH 0968/2376] Simplify IntervalList implementation enough to work for its usages --- osu.Game/Lists/IntervalList.cs | 29 ++++++----------------------- 1 file changed, 6 insertions(+), 23 deletions(-) diff --git a/osu.Game/Lists/IntervalList.cs b/osu.Game/Lists/IntervalList.cs index 5a5866ca91..580015bb96 100644 --- a/osu.Game/Lists/IntervalList.cs +++ b/osu.Game/Lists/IntervalList.cs @@ -12,7 +12,7 @@ namespace osu.Game.Lists /// Represents a list of intervals that can be used for whether a specific value falls into one of them. /// /// The type of interval values. - public class IntervalList : ICollection>, IReadOnlyList> + public class IntervalList : IEnumerable> where T : struct, IConvertible { private static readonly IComparer type_comparer = Comparer.Default; @@ -20,6 +20,11 @@ namespace osu.Game.Lists private readonly SortedList> intervals = new SortedList>((x, y) => type_comparer.Compare(x.Start, y.Start)); private int nearestIndex; + public Interval this[int i] + { + get => intervals[i]; + set => intervals[i] = value; + } /// /// Whether the provided value is in any interval added to this list. @@ -58,12 +63,6 @@ namespace osu.Game.Lists /// The end value of the interval. public void Add(T start, T end) => Add(new Interval(start, end)); - #region ICollection> - - public int Count => intervals.Count; - - bool ICollection>.IsReadOnly => false; - /// /// Adds a new interval to the list /// @@ -82,25 +81,9 @@ namespace osu.Game.Lists /// public void Clear() => intervals.Clear(); - public void CopyTo(Interval[] array, int arrayIndex) => intervals.CopyTo(array, arrayIndex); - - public bool Contains(Interval item) => intervals.Contains(item); - public IEnumerator> GetEnumerator() => intervals.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - #endregion - - #region IReadOnlyList> - - public Interval this[int index] - { - get => intervals[index]; - set => intervals[index] = value; - } - - #endregion } public readonly struct Interval From 235d3046c65d3e65fe924067ad57096537c86a92 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 11 Apr 2020 04:22:23 +0300 Subject: [PATCH 0969/2376] Move ruleset dependencies caching to its own container --- osu.Game/Rulesets/UI/DrawableRuleset.cs | 93 ++--------- .../UI/DrawableRulesetDependencies.cs | 148 ++++++++++++++++++ 2 files changed, 157 insertions(+), 84 deletions(-) create mode 100644 osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 5062c92afe..0a46f5207e 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -11,20 +11,15 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Threading; -using System.Threading.Tasks; using JetBrains.Annotations; -using osu.Framework.Audio; -using osu.Framework.Audio.Sample; using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Textures; using osu.Framework.Input; using osu.Framework.Input.Events; -using osu.Framework.IO.Stores; using osu.Game.Configuration; using osu.Game.Graphics.Cursor; using osu.Game.Input.Handlers; @@ -113,6 +108,8 @@ namespace osu.Game.Rulesets.UI private OnScreenDisplay onScreenDisplay; + private DrawableRulesetDependencies dependencies; + /// /// Creates a ruleset visualisation for the provided ruleset and beatmap. /// @@ -147,30 +144,15 @@ namespace osu.Game.Rulesets.UI protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { - var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + dependencies = new DrawableRulesetDependencies(Ruleset, base.CreateChildDependencies(parent)); - var resources = Ruleset.CreateResourceStore(); - - if (resources != null) - { - textureStore = new TextureStore(new TextureLoaderStore(new NamespacedResourceStore(resources, "Textures"))); - textureStore.AddStore(dependencies.Get()); - dependencies.Cache(textureStore); - - localSampleStore = dependencies.Get().GetSampleStore(new NamespacedResourceStore(resources, "Samples")); - localSampleStore.PlaybackConcurrency = OsuGameBase.SAMPLE_CONCURRENCY; - dependencies.CacheAs(new FallbackSampleStore(localSampleStore, dependencies.Get())); - } + textureStore = dependencies.TextureStore; + localSampleStore = dependencies.SampleStore; + Config = dependencies.RulesetConfigManager; onScreenDisplay = dependencies.Get(); - - Config = dependencies.Get().GetConfigFor(Ruleset); - if (Config != null) - { - dependencies.Cache(Config); onScreenDisplay?.BeginTracking(this, Config); - } return dependencies; } @@ -362,13 +344,14 @@ namespace osu.Game.Rulesets.UI { base.Dispose(isDisposing); - localSampleStore?.Dispose(); - if (Config != null) { onScreenDisplay?.StopTracking(this, Config); Config = null; } + + // Dispose the components created by this dependency container. + dependencies.Dispose(); } } @@ -519,62 +502,4 @@ namespace osu.Game.Rulesets.UI { } } - - /// - /// A sample store which adds a fallback source. - /// - /// - /// This is a temporary implementation to workaround ISampleStore limitations. - /// - public class FallbackSampleStore : ISampleStore - { - private readonly ISampleStore primary; - private readonly ISampleStore secondary; - - public FallbackSampleStore(ISampleStore primary, ISampleStore secondary) - { - this.primary = primary; - this.secondary = secondary; - } - - public SampleChannel Get(string name) => primary.Get(name) ?? secondary.Get(name); - - public Task GetAsync(string name) => primary.GetAsync(name) ?? secondary.GetAsync(name); - - public Stream GetStream(string name) => primary.GetStream(name) ?? secondary.GetStream(name); - - public IEnumerable GetAvailableResources() => throw new NotSupportedException(); - - public void AddAdjustment(AdjustableProperty type, BindableNumber adjustBindable) => throw new NotSupportedException(); - - public void RemoveAdjustment(AdjustableProperty type, BindableNumber adjustBindable) => throw new NotSupportedException(); - - public BindableNumber Volume => throw new NotSupportedException(); - - public BindableNumber Balance => throw new NotSupportedException(); - - public BindableNumber Frequency => throw new NotSupportedException(); - - public BindableNumber Tempo => throw new NotSupportedException(); - - public IBindable GetAggregate(AdjustableProperty type) => throw new NotSupportedException(); - - public IBindable AggregateVolume => throw new NotSupportedException(); - - public IBindable AggregateBalance => throw new NotSupportedException(); - - public IBindable AggregateFrequency => throw new NotSupportedException(); - - public IBindable AggregateTempo => throw new NotSupportedException(); - - public int PlaybackConcurrency - { - get => throw new NotSupportedException(); - set => throw new NotSupportedException(); - } - - public void Dispose() - { - } - } } diff --git a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs new file mode 100644 index 0000000000..33b340a974 --- /dev/null +++ b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs @@ -0,0 +1,148 @@ +// 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.Tasks; +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.Textures; +using osu.Framework.IO.Stores; +using osu.Game.Rulesets.Configuration; + +namespace osu.Game.Rulesets.UI +{ + public class DrawableRulesetDependencies : DependencyContainer, IDisposable + { + /// + /// The texture store to be used for the ruleset. + /// + public TextureStore TextureStore { get; private set; } + + /// + /// The sample store to be used for the ruleset. + /// + /// + /// This is the local sample store pointing to the ruleset sample resources, + /// the cached sample store () retrieves from + /// this store and falls back to the parent store if this store doesn't have the requested sample. + /// + public ISampleStore SampleStore { get; private set; } + + /// + /// The ruleset config manager. + /// + public IRulesetConfigManager RulesetConfigManager { get; private set; } + + public DrawableRulesetDependencies(Ruleset ruleset, IReadOnlyDependencyContainer parent) + : base(parent) + { + var resources = ruleset.CreateResourceStore(); + + if (resources != null) + { + TextureStore = new TextureStore(new TextureLoaderStore(new NamespacedResourceStore(resources, @"Textures"))); + TextureStore.AddStore(parent.Get()); + Cache(TextureStore); + + SampleStore = parent.Get().GetSampleStore(new NamespacedResourceStore(resources, @"Samples")); + SampleStore.PlaybackConcurrency = OsuGameBase.SAMPLE_CONCURRENCY; + CacheAs(new FallbackSampleStore(SampleStore, parent.Get())); + } + + RulesetConfigManager = parent.Get().GetConfigFor(ruleset); + if (RulesetConfigManager != null) + Cache(RulesetConfigManager); + } + + #region Disposal + + ~DrawableRulesetDependencies() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private bool isDisposed; + + protected void Dispose(bool disposing) + { + if (isDisposed) + return; + + isDisposed = true; + + SampleStore?.Dispose(); + RulesetConfigManager = null; + } + + #endregion + } + + /// + /// A sample store which adds a fallback source. + /// + /// + /// This is a temporary implementation to workaround ISampleStore limitations. + /// + public class FallbackSampleStore : ISampleStore + { + private readonly ISampleStore primary; + private readonly ISampleStore secondary; + + public FallbackSampleStore(ISampleStore primary, ISampleStore secondary) + { + this.primary = primary; + this.secondary = secondary; + } + + public SampleChannel Get(string name) => primary.Get(name) ?? secondary.Get(name); + + public Task GetAsync(string name) => primary.GetAsync(name) ?? secondary.GetAsync(name); + + public Stream GetStream(string name) => primary.GetStream(name) ?? secondary.GetStream(name); + + public IEnumerable GetAvailableResources() => throw new NotSupportedException(); + + public void AddAdjustment(AdjustableProperty type, BindableNumber adjustBindable) => throw new NotSupportedException(); + + public void RemoveAdjustment(AdjustableProperty type, BindableNumber adjustBindable) => throw new NotSupportedException(); + + public BindableNumber Volume => throw new NotSupportedException(); + + public BindableNumber Balance => throw new NotSupportedException(); + + public BindableNumber Frequency => throw new NotSupportedException(); + + public BindableNumber Tempo => throw new NotSupportedException(); + + public IBindable GetAggregate(AdjustableProperty type) => throw new NotSupportedException(); + + public IBindable AggregateVolume => throw new NotSupportedException(); + + public IBindable AggregateBalance => throw new NotSupportedException(); + + public IBindable AggregateFrequency => throw new NotSupportedException(); + + public IBindable AggregateTempo => throw new NotSupportedException(); + + public int PlaybackConcurrency + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + public void Dispose() + { + } + } +} From 2b4208bebfa4e81894d7a9a107701f474478325b Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 11 Apr 2020 04:23:31 +0300 Subject: [PATCH 0970/2376] Cache ruleset dependencies if the scene tests ruleset-specific components --- .../Rulesets/Testing/IRulesetTestScene.cs | 20 +++++++++++++++++++ osu.Game/Tests/Visual/OsuTestScene.cs | 13 +++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Rulesets/Testing/IRulesetTestScene.cs diff --git a/osu.Game/Rulesets/Testing/IRulesetTestScene.cs b/osu.Game/Rulesets/Testing/IRulesetTestScene.cs new file mode 100644 index 0000000000..e8b8a79eb5 --- /dev/null +++ b/osu.Game/Rulesets/Testing/IRulesetTestScene.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. + +namespace osu.Game.Rulesets.Testing +{ + /// + /// An interface that can be assigned to test scenes to indicate + /// that the test scene is testing ruleset-specific components. + /// This is to cache required ruleset dependencies for the components. + /// + public interface IRulesetTestScene + { + /// + /// Retrieves the ruleset that is going + /// to be tested by this test scene. + /// + /// The . + Ruleset CreateRuleset(); + } +} diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index d1d8059cb1..eb1905cbe1 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -20,6 +20,8 @@ using osu.Game.Database; using osu.Game.Online.API; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Testing; +using osu.Game.Rulesets.UI; using osu.Game.Screens; using osu.Game.Storyboards; using osu.Game.Tests.Beatmaps; @@ -36,6 +38,8 @@ namespace osu.Game.Tests.Visual protected new OsuScreenDependencies Dependencies { get; private set; } + private DrawableRulesetDependencies rulesetDependencies; + private Lazy localStorage; protected Storage LocalStorage => localStorage.Value; @@ -64,7 +68,12 @@ namespace osu.Game.Tests.Visual protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { - Dependencies = new OsuScreenDependencies(false, base.CreateChildDependencies(parent)); + var baseDependencies = base.CreateChildDependencies(parent); + + if (this is IRulesetTestScene rts) + baseDependencies = rulesetDependencies = new DrawableRulesetDependencies(rts.CreateRuleset(), baseDependencies); + + Dependencies = new OsuScreenDependencies(false, baseDependencies); Beatmap = Dependencies.Beatmap; Beatmap.SetDefault(); @@ -142,6 +151,8 @@ namespace osu.Game.Tests.Visual { base.Dispose(isDisposing); + rulesetDependencies?.Dispose(); + if (Beatmap?.Value.TrackLoaded == true) Beatmap.Value.Track.Stop(); From e10c973aa69b8b59df985c35debac260647b3845 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 11 Apr 2020 04:24:34 +0300 Subject: [PATCH 0971/2376] Add test cases for behaviour of ruleset dependencies caching on tests --- .../Gameplay/TestSceneStoryboardSamples.cs | 6 +- .../Resources/{ => Samples}/test-sample.mp3 | Bin .../Resources/Textures/test-image.png | Bin 0 -> 4852 bytes .../Testing/TestSceneRulesetTestScene.cs | 80 ++++++++++++++++++ 4 files changed, 83 insertions(+), 3 deletions(-) rename osu.Game.Tests/Resources/{ => Samples}/test-sample.mp3 (100%) create mode 100644 osu.Game.Tests/Resources/Textures/test-image.png create mode 100644 osu.Game.Tests/Testing/TestSceneRulesetTestScene.cs diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs index 84506739ab..8adf6064f5 100644 --- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs @@ -60,11 +60,11 @@ namespace osu.Game.Tests.Gameplay this.resourceName = resourceName; } - public byte[] Get(string name) => name == resourceName ? TestResources.GetStore().Get("Resources/test-sample.mp3") : null; + public byte[] Get(string name) => name == resourceName ? TestResources.GetStore().Get("Resources/Samples/test-sample.mp3") : null; - public Task GetAsync(string name) => name == resourceName ? TestResources.GetStore().GetAsync("Resources/test-sample.mp3") : null; + public Task GetAsync(string name) => name == resourceName ? TestResources.GetStore().GetAsync("Resources/Samples/test-sample.mp3") : null; - public Stream GetStream(string name) => name == resourceName ? TestResources.GetStore().GetStream("Resources/test-sample.mp3") : null; + public Stream GetStream(string name) => name == resourceName ? TestResources.GetStore().GetStream("Resources/Samples/test-sample.mp3") : null; public IEnumerable GetAvailableResources() => new[] { resourceName }; diff --git a/osu.Game.Tests/Resources/test-sample.mp3 b/osu.Game.Tests/Resources/Samples/test-sample.mp3 similarity index 100% rename from osu.Game.Tests/Resources/test-sample.mp3 rename to osu.Game.Tests/Resources/Samples/test-sample.mp3 diff --git a/osu.Game.Tests/Resources/Textures/test-image.png b/osu.Game.Tests/Resources/Textures/test-image.png new file mode 100644 index 0000000000000000000000000000000000000000..5d0092edc89e6cb91a46c1dacba724642e3d880d GIT binary patch literal 4852 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 mkLc+&yII5jkHf{w{y_mZ|9^*. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Audio.Track; +using osu.Framework.Configuration.Tracking; +using osu.Framework.Graphics.Textures; +using osu.Framework.IO.Stores; +using osu.Game.Beatmaps; +using osu.Game.Configuration; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Configuration; +using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Testing; +using osu.Game.Rulesets.UI; +using osu.Game.Tests.Resources; +using osu.Game.Tests.Visual; + +namespace osu.Game.Tests.Testing +{ + public class TestSceneRulesetTestScene : OsuTestScene, IRulesetTestScene + { + [Test] + public void TestRetrieveTexture() + { + AddAssert("ruleset texture retrieved", () => + Dependencies.Get().Get(@"test-image") != null); + } + + [Test] + public void TestRetrieveSample() + { + AddAssert("ruleset sample retrieved", () => + Dependencies.Get().Get(@"test-sample") != null); + } + + [Test] + public void TestResolveConfigManager() + { + AddAssert("ruleset config resolved", () => + Dependencies.Get() != null); + } + + public Ruleset CreateRuleset() => new TestRuleset(); + + private class TestRuleset : Ruleset + { + public override string Description => string.Empty; + public override string ShortName => string.Empty; + + public TestRuleset() + { + // temporary ID to let RulesetConfigCache pass our + // config manager to the ruleset dependencies. + RulesetInfo.ID = -1; + } + + public override IResourceStore CreateResourceStore() => new NamespacedResourceStore(TestResources.GetStore(), @"Resources"); + public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new TestRulesetConfigManager(); + + public override IEnumerable GetModsFor(ModType type) => throw new NotImplementedException(); + public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => throw new NotImplementedException(); + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => throw new NotImplementedException(); + public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => throw new NotImplementedException(); + } + + private class TestRulesetConfigManager : IRulesetConfigManager + { + public void Load() => throw new NotImplementedException(); + public bool Save() => throw new NotImplementedException(); + public TrackedSettings CreateTrackedSettings() => throw new NotImplementedException(); + public void LoadInto(TrackedSettings settings) => throw new NotImplementedException(); + public void Dispose() => throw new NotImplementedException(); + } + } +} From a314a6119a9461155fdb083976f45121c704d796 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 11 Apr 2020 05:13:04 +0300 Subject: [PATCH 0972/2376] Fix CI issues --- osu.Game/Rulesets/UI/DrawableRuleset.cs | 8 -------- osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs | 4 ++-- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 0a46f5207e..265c6a7319 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -14,10 +14,8 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using JetBrains.Annotations; -using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics.Cursor; -using osu.Framework.Graphics.Textures; using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Game.Configuration; @@ -58,10 +56,6 @@ namespace osu.Game.Rulesets.UI private readonly Lazy playfield; - private TextureStore textureStore; - - private ISampleStore localSampleStore; - /// /// The playfield. /// @@ -146,8 +140,6 @@ namespace osu.Game.Rulesets.UI { dependencies = new DrawableRulesetDependencies(Ruleset, base.CreateChildDependencies(parent)); - textureStore = dependencies.TextureStore; - localSampleStore = dependencies.SampleStore; Config = dependencies.RulesetConfigManager; onScreenDisplay = dependencies.Get(); diff --git a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs index 33b340a974..168e937256 100644 --- a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs +++ b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.UI /// /// The texture store to be used for the ruleset. /// - public TextureStore TextureStore { get; private set; } + public TextureStore TextureStore { get; } /// /// The sample store to be used for the ruleset. @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.UI /// the cached sample store () retrieves from /// this store and falls back to the parent store if this store doesn't have the requested sample. /// - public ISampleStore SampleStore { get; private set; } + public ISampleStore SampleStore { get; } /// /// The ruleset config manager. From 7fba29113466d5fe6aa43eef0cd284323f1c969a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 11 Apr 2020 13:14:34 +0900 Subject: [PATCH 0973/2376] Change inheritance of taiko hit pieces to better allow for skinning --- .../TestSceneDrawableHit.cs | 54 ++++++++++++++++++ .../Objects/Drawables/DrawableCentreHit.cs | 10 +--- .../Objects/Drawables/DrawableDrumRoll.cs | 10 ++-- .../Objects/Drawables/DrawableDrumRollTick.cs | 3 +- .../Objects/Drawables/DrawableRimHit.cs | 10 +--- .../Objects/Drawables/DrawableSwell.cs | 16 +++--- .../Objects/Drawables/DrawableSwellTick.cs | 4 ++ .../Drawables/DrawableTaikoHitObject.cs | 14 ++--- .../Drawables/Pieces/CentreHitSymbolPiece.cs | 50 +++++++++++------ .../Drawables/Pieces/RimHitCirclePiece.cs | 55 +++++++++++++++++++ .../Drawables/Pieces/RimHitSymbolPiece.cs | 39 ------------- .../Drawables/Pieces/SwellSymbolPiece.cs | 50 +++++++++++------ .../Objects/Drawables/Pieces/TickPiece.cs | 6 +- 13 files changed, 209 insertions(+), 112 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko.Tests/TestSceneDrawableHit.cs create mode 100644 osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/RimHitCirclePiece.cs delete mode 100644 osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/RimHitSymbolPiece.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrawableHit.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrawableHit.cs new file mode 100644 index 0000000000..b927f0294b --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrawableHit.cs @@ -0,0 +1,54 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Objects.Drawables; + +namespace osu.Game.Rulesets.Taiko.Tests +{ + [TestFixture] + public class TestSceneDrawableHit : TaikoSkinnableTestScene + { + public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] + { + typeof(DrawableHit), + typeof(DrawableCentreHit), + typeof(DrawableRimHit), + }).ToList(); + + [BackgroundDependencyLoader] + private void load() + { + AddStep("Centre hit", () => SetContents(() => new DrawableCentreHit(createHitAtCurrentTime()) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + })); + AddStep("Rim hit", () => SetContents(() => new DrawableRimHit(createHitAtCurrentTime()) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + })); + } + + private Hit createHitAtCurrentTime() + { + var hit = new Hit + { + StartTime = Time.Current + 3000, + }; + + hit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + + return hit; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs index 4979135f50..22d62442cf 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.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 osu.Framework.Allocation; -using osu.Game.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; namespace osu.Game.Rulesets.Taiko.Objects.Drawables @@ -14,13 +13,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables public DrawableCentreHit(Hit hit) : base(hit) { - MainPiece.Add(new CentreHitSymbolPiece()); } - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - MainPiece.AccentColour = colours.PinkDarker; - } + protected override CompositeDrawable CreateMainPiece() => new CentreHitCirclePiece(); } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index 5806c90115..0627eb95fd 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -34,17 +34,19 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private Color4 colourIdle; private Color4 colourEngaged; + private ElongatedCirclePiece elongatedPiece; + public DrawableDrumRoll(DrumRoll drumRoll) : base(drumRoll) { RelativeSizeAxes = Axes.Y; - MainPiece.Add(tickContainer = new Container { RelativeSizeAxes = Axes.Both }); + elongatedPiece.Add(tickContainer = new Container { RelativeSizeAxes = Axes.Both }); } [BackgroundDependencyLoader] private void load(OsuColour colours) { - MainPiece.AccentColour = colourIdle = colours.YellowDark; + elongatedPiece.AccentColour = colourIdle = colours.YellowDark; colourEngaged = colours.YellowDarker; } @@ -84,7 +86,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables return base.CreateNestedHitObject(hitObject); } - protected override TaikoPiece CreateMainPiece() => new ElongatedCirclePiece(); + protected override CompositeDrawable CreateMainPiece() => elongatedPiece = new ElongatedCirclePiece(); public override bool OnPressed(TaikoAction action) => false; @@ -101,7 +103,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables rollingHits = Math.Clamp(rollingHits, 0, rolling_hits_for_engaged_colour); Color4 newColour = Interpolation.ValueAt((float)rollingHits / rolling_hits_for_engaged_colour, colourIdle, colourEngaged, 0, 1); - MainPiece.FadeAccent(newColour, 100); + (MainPiece as IHasAccentColour)?.FadeAccent(newColour, 100); } protected override void CheckForResult(bool userTriggered, double timeOffset) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs index 25b6141a0e..fea3eea6a9 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; @@ -19,7 +20,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables public override bool DisplayResult => false; - protected override TaikoPiece CreateMainPiece() => new TickPiece + protected override CompositeDrawable CreateMainPiece() => new TickPiece { Filled = HitObject.FirstTick }; diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs index 5a12d71cea..6dad7af907 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.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 osu.Framework.Allocation; -using osu.Game.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; namespace osu.Game.Rulesets.Taiko.Objects.Drawables @@ -14,13 +13,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables public DrawableRimHit(Hit hit) : base(hit) { - MainPiece.Add(new RimHitSymbolPiece()); } - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - MainPiece.AccentColour = colours.BlueDarker; - } + protected override CompositeDrawable CreateMainPiece() => new RimHitCirclePiece(); } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index fa39819199..3a2e44038f 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -9,11 +9,11 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; using osuTK.Graphics; using osu.Framework.Graphics.Shapes; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { @@ -34,8 +34,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private readonly CircularContainer targetRing; private readonly CircularContainer expandingRing; - private readonly SwellSymbolPiece symbol; - public DrawableSwell(Swell swell) : base(swell) { @@ -107,18 +105,22 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables }); AddInternal(ticks = new Container { RelativeSizeAxes = Axes.Both }); - - MainPiece.Add(symbol = new SwellSymbolPiece()); } [BackgroundDependencyLoader] private void load(OsuColour colours) { - MainPiece.AccentColour = colours.YellowDark; expandingRing.Colour = colours.YellowLight; targetRing.BorderColour = colours.YellowDark.Opacity(0.25f); } + protected override CompositeDrawable CreateMainPiece() => new SwellCirclePiece + { + // to allow for rotation transform + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + protected override void LoadComplete() { base.LoadComplete(); @@ -182,7 +184,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables .Then() .FadeTo(completion / 8, 2000, Easing.OutQuint); - symbol.RotateTo((float)(completion * HitObject.Duration / 8), 4000, Easing.OutQuint); + MainPiece.RotateTo((float)(completion * HitObject.Duration / 8), 4000, Easing.OutQuint); expandingRing.ScaleTo(1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 260, Easing.OutQuint); diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs index ce875ebba8..5a954addfb 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs @@ -2,7 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { @@ -28,5 +30,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables } public override bool OnPressed(TaikoAction action) => false; + + protected override CompositeDrawable CreateMainPiece() => new TickPiece(); } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs index 5f892dd2fa..397888bb11 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs @@ -4,7 +4,6 @@ using osu.Framework.Graphics; using osu.Framework.Input.Bindings; using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; using osuTK; using System.Linq; using osu.Game.Audio; @@ -108,19 +107,19 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables } } - public abstract class DrawableTaikoHitObject : DrawableTaikoHitObject - where TTaikoHit : TaikoHitObject + public abstract class DrawableTaikoHitObject : DrawableTaikoHitObject + where TObject : TaikoHitObject { public override Vector2 OriginPosition => new Vector2(DrawHeight / 2); - public new TTaikoHit HitObject; + public new TObject HitObject; protected readonly Vector2 BaseSize; - protected readonly TaikoPiece MainPiece; + protected readonly CompositeDrawable MainPiece; private readonly Container strongHitContainer; - protected DrawableTaikoHitObject(TTaikoHit hitObject) + protected DrawableTaikoHitObject(TObject hitObject) : base(hitObject) { HitObject = hitObject; @@ -132,7 +131,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables Size = BaseSize = new Vector2(HitObject.IsStrong ? TaikoHitObject.DEFAULT_STRONG_SIZE : TaikoHitObject.DEFAULT_SIZE); Content.Add(MainPiece = CreateMainPiece()); - MainPiece.KiaiMode = HitObject.Kiai; AddInternal(strongHitContainer = new Container()); } @@ -169,7 +167,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables // Normal and clap samples are handled by the drum protected override IEnumerable GetSamples() => HitObject.Samples.Where(s => s.Name != HitSampleInfo.HIT_NORMAL && s.Name != HitSampleInfo.HIT_CLAP); - protected virtual TaikoPiece CreateMainPiece() => new CirclePiece(); + protected abstract CompositeDrawable CreateMainPiece(); /// /// Creates the handler for this 's . diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CentreHitSymbolPiece.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CentreHitSymbolPiece.cs index 7ed61ede96..0509841ba8 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CentreHitSymbolPiece.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CentreHitSymbolPiece.cs @@ -1,36 +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.Graphics.Containers; using osuTK; using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces { - /// - /// The symbol used for centre hit pieces. - /// - public class CentreHitSymbolPiece : Container + public class CentreHitCirclePiece : CirclePiece { - public CentreHitSymbolPiece() + public CentreHitCirclePiece() { - Anchor = Anchor.Centre; - Origin = Anchor.Centre; + Add(new CentreHitSymbolPiece()); + } - RelativeSizeAxes = Axes.Both; - Size = new Vector2(CirclePiece.SYMBOL_SIZE); - Padding = new MarginPadding(CirclePiece.SYMBOL_BORDER); + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + AccentColour = colours.PinkDarker; + } - Children = new[] + /// + /// The symbol used for centre hit pieces. + /// + public class CentreHitSymbolPiece : Container + { + public CentreHitSymbolPiece() { - new CircularContainer + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + RelativeSizeAxes = Axes.Both; + Size = new Vector2(SYMBOL_SIZE); + Padding = new MarginPadding(SYMBOL_BORDER); + + Children = new[] { - RelativeSizeAxes = Axes.Both, - Masking = true, - Children = new[] { new Box { RelativeSizeAxes = Axes.Both } } - } - }; + new CircularContainer + { + RelativeSizeAxes = Axes.Both, + Masking = true, + Children = new[] { new Box { RelativeSizeAxes = Axes.Both } } + } + }; + } } } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/RimHitCirclePiece.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/RimHitCirclePiece.cs new file mode 100644 index 0000000000..3273ab7fa7 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/RimHitCirclePiece.cs @@ -0,0 +1,55 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces +{ + public class RimHitCirclePiece : CirclePiece + { + public RimHitCirclePiece() + { + Add(new RimHitSymbolPiece()); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + AccentColour = colours.BlueDarker; + } + + /// + /// The symbol used for rim hit pieces. + /// + public class RimHitSymbolPiece : CircularContainer + { + public RimHitSymbolPiece() + { + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + RelativeSizeAxes = Axes.Both; + Size = new Vector2(SYMBOL_SIZE); + + BorderThickness = SYMBOL_BORDER; + BorderColour = Color4.White; + Masking = true; + Children = new[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true + } + }; + } + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/RimHitSymbolPiece.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/RimHitSymbolPiece.cs deleted file mode 100644 index e4c964a884..0000000000 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/RimHitSymbolPiece.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osuTK; -using osuTK.Graphics; -using osu.Framework.Graphics.Shapes; - -namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces -{ - /// - /// The symbol used for rim hit pieces. - /// - public class RimHitSymbolPiece : CircularContainer - { - public RimHitSymbolPiece() - { - Anchor = Anchor.Centre; - Origin = Anchor.Centre; - - RelativeSizeAxes = Axes.Both; - Size = new Vector2(CirclePiece.SYMBOL_SIZE); - - BorderThickness = CirclePiece.SYMBOL_BORDER; - BorderColour = Color4.White; - Masking = true; - Children = new[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - AlwaysPresent = true - } - }; - } - } -} diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/SwellSymbolPiece.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/SwellSymbolPiece.cs index 0ed9923924..a8f9f0b94d 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/SwellSymbolPiece.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/SwellSymbolPiece.cs @@ -1,36 +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 osuTK; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces { - /// - /// The symbol used for swell pieces. - /// - public class SwellSymbolPiece : Container + public class SwellCirclePiece : CirclePiece { - public SwellSymbolPiece() + public SwellCirclePiece() { - Anchor = Anchor.Centre; - Origin = Anchor.Centre; + Add(new SwellSymbolPiece()); + } - RelativeSizeAxes = Axes.Both; - Size = new Vector2(CirclePiece.SYMBOL_SIZE); - Padding = new MarginPadding(CirclePiece.SYMBOL_BORDER); + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + AccentColour = colours.YellowDark; + } - Children = new[] + /// + /// The symbol used for swell pieces. + /// + public class SwellSymbolPiece : Container + { + public SwellSymbolPiece() { - new SpriteIcon + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + RelativeSizeAxes = Axes.Both; + Size = new Vector2(SYMBOL_SIZE); + Padding = new MarginPadding(SYMBOL_BORDER); + + Children = new[] { - RelativeSizeAxes = Axes.Both, - Icon = FontAwesome.Solid.Asterisk, - Shadow = false - } - }; + new SpriteIcon + { + RelativeSizeAxes = Axes.Both, + Icon = FontAwesome.Solid.Asterisk, + Shadow = false + } + }; + } } } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/TickPiece.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/TickPiece.cs index 83cf7a64ec..0648bcebcd 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/TickPiece.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/TickPiece.cs @@ -9,7 +9,7 @@ using osu.Framework.Graphics.Shapes; namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces { - public class TickPiece : TaikoPiece + public class TickPiece : CompositeDrawable { /// /// Any tick that is not the first for a drumroll is not filled, but is instead displayed @@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces FillMode = FillMode.Fit; Size = new Vector2(tick_size); - Add(new CircularContainer + InternalChild = new CircularContainer { RelativeSizeAxes = Axes.Both, Masking = true, @@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces AlwaysPresent = true } } - }); + }; } } } From ca2df77c7684899cc107d72f646e90461244a8e5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 11 Apr 2020 13:14:41 +0900 Subject: [PATCH 0974/2376] Add default skin test resources --- .../metrics-skin/taikohitcircle@2x.png | Bin 0 -> 13140 bytes .../metrics-skin/taikohitcircleoverlay@2x.png | Bin 0 -> 38217 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taikohitcircle@2x.png create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taikohitcircleoverlay@2x.png diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taikohitcircle@2x.png b/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taikohitcircle@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..043bfbfae1a77317aec68051b2d917a42d143fb1 GIT binary patch literal 13140 zcmaKTWn2{B+xP6Uq;#n?OSm*F-QC?N(k@~2*AU^oSynrIbu!^ z{glo83_TtF0&RR80C{^)TL&g}cN-@M0|y)XVDBLZDFDEVb1^dWGt<_Ru=8~1wfTpJ zH^|)!!wmpZvO!)pcCHS7OtubAE*{d*gZ3^clZ(AH)KplTU)xL3!P!ME#K*xfM90W3 z#MMsR9x5xtBo!oq5#a9NXTuca?&jeu5hM-$ms|-<`=4$;DAT`4{9L7>|2E1@TaQW6 z)5n2Hm{)|yPC!VQNmQIyKtxbTT%4N;&MzRw$1lbwAjl&iA|b>t0f#gF=Lf}T^Rai7 zFi=wYk1ouWG}PJ8&r5=jFEB8WH&Bq*)5nQVKwMm$j~~tlhx1@4czlCB{A_}FJbYRH zD?!P@*Urbq%g@ErgXtfMHnyJre$r5krT=!p-Ah~he*}B@{wGkFknsiCc<~AF^7Fa7 z|Kr!csD1qm9R9Bv|0}hxQLvW-pMis~r@xOKW;`5O{>zMs-Tz-Oo#oXbwcd?hSW<@d(+9i|~jE*ok2d;{5yqA`W)8 z0)qdQ^S|+9!X>06uA(e2uA(F$pe(MSETpK&uP81kA}kIUf&UM$x`(fyjfb7Xe|5WH zbpMA}>Ho_sq3Gjaf%f=Z5ulmkAFHj{u!u$ z`>o{Q;}YOtuj1qB&h)QHOSt?mRyYa@+K39-iSpP9V+LHv*3pi~#!gU>2QJ`fBPd`C z7v&dsfc}Ty{{M;KzmbaR!Fb~JUxOrUghg!Z9fZVr_(d@=bbt#9^4Qu7^7DuZ2?>fh z+6dd*i#kI2FedW-1D^i}O#kh|AoQQ+|D*=y;(wCM!2^>~KA6;5Wg|=k06u$lC3&Nu zU%M8#pN;#D3VmHBm67!PP|m6ZHki~say_!$-w(8>XZic0*s5kHoL?fa?TmyXrmKn( zMrEbCNHaYo6q!jRo`7^-%S89j{?)*^32y@JYMj%yG%B z+RGveYKj<@YT%rYU^hDOM9+IU4W@}u-psj>$kU~v(T<#X-31dhv|Yu;Y@|7h z$619Zdsgq5t+r5hCw@!Z!cKy615bCzrKOq;tmS^+YdshU#tG6mQ}q~lA>MT-V%egS z@hmH6IlWDLy;(}Ns2$Xjv_MR8zx-8-@t*r<28eLls5<2X;T`cv;aUu9=cIW#B;}qT zr6Ef0-C(sj`yb9~L|;haB+?On!zfQRQa<=3pS||0QirkzFxUNfVV@=afn8+69g~1I zRW9yR9`2I;2iNrEMIpYS@}9}$a_nC47SKY#d;3323RmQBd6WIg5+Q$%_wC^qDb>+J zt4}6vC!ZP7xSyaSKl;4#vt)f}(}6O->cq3vDQ&!L2|j$)@Wm&T?P8hM!_+%eS=j`i ziC#=6?6y9Inc_gf!_c>Qz4D|>HEoCucqoid876LA78`aJ?dgNP@{&6>yu#DyVQ|cQ zr5`O>Z>@AoA0+h5WEK4l?qBwR#)%%^z7A0%J%4LMx(pF#`h6)V@Kse%+_7Di;~U`&y$hng zmb__^w1bt;dSdEvbAQ}+NbPdY!URaGH@b#n4wbTG$kdM7G?Wh8THU9N$}@qdh+vZ3 zph2?}1%(*@ouP*&L3oEnt^Pztu2^|HIi^mHX%XCa&6Bb9u}=3rzDcb;WNB+{82y#g zcZ8lFYKg}33R^&ZLtb=?l(d@Rx^6g`kpv`8=#uZ5jdq#C&|SjT=KAL2>o+A!@innD zJ^id^TG6?rmZOJ)yWM-aq`@p^Vrg3JrTYd(;92%-?{hxjO*uk$d z{L@!W0f+UUx>D7*Mi9;;t1S+P!L}~vOC_yDX~-1+;ArG0a=8ceY|hh0;r&nJt5B~V z8icq(uDM#i22=k|V!!Hd4$^g;=Ra3GdFh=v_rd$MjG_ddwYeGRPXfA%hn;ii`#9BU zR-x=xjnVg=j$iX+^>jQ8=^X59&EOQV=%EqM_1msr$1nbL`LgmwTXLQNa$fzR!L3b9 zF*F0kWAajTh3)3U980&>+?b%sE~rpoUNBONT+X4p;4-FblX72{dW8KJLcDq~mwUs4 z1xM6{OGb2R?GpEf24r^;v{>i#;)-yBG8p)=q_`{v_^r4j4<@TmXGpUuL=1cjw>uD) z>^J=nK5nh|P%l565Y#_+W6Fc*JzN@*&gs2tFL_-qyw2c9a!qx6qIoNhPWjW-*))2~ zv*FYW8LK3>5aCt^SMh>M>^3%qZTHc_-;l``;g7zH8l_u0Ff*>v4nV6U`mxA<;Pvl?U>%QmkzFYb_OdALJbOTQBtK5&_U@v!9k^lcx8!9 zMjcOYFnWlh?c=wy_S9quDGr?1TD*h0k)ZzAn|*13cQM&bSbA)U*+TxwAjkygnY!C@ z$p`Zo4L}@=dW=2Rj6r$k8v1@4s*sQ)DPtjF4(*C5!YoySm9*Jds|gr zQf-?Cl{#UUe~Q_e>K5W*gZk6(#{FIsh#!^cOV$8l@0E`x;a>&wrM_*>BkqgHjy>_B z8AGDo)Q(8bwPM!)y`%s`4VMQ;<}<(E2b2W zm|G#9Tp&sY5B4Tk!q=?|3_GntFcjV|rAOcKOm3ZSk=k8mGb&^UjbL%lCQ|%vFc&Uf zgAN%~&Z9S$LG40%hwcC)3m}Nd)k3kN@B)GtOCWj=5K3i8kIj^!mQ}&MM!hq@=h<+kv5+fa_s)Dk<*>!#R1m`{TAhrVZ5R$ zUl^HJs);gMTdKI+7kq2$OJ?x3LBRC|xf}}AenHy%mW%s2MN6om5vpN#Gq9}EtllzZ zoqqjy5wxd6K}CQJfgKfFH@L56AG-Xo;NY9xi%Y?XB{GiBCua-$)PKp%i@ksL2(ck< zd+_byZHKcBYaNw2ZbO_XZBS&`)=b5XQZWEIN2QhZ(z5}g){z(T;f^f5Gzgxcg81cK zJ=9ob#o?G(`jK$1b+J&w@3W*>FBu zA0&3420(2F57P)RR)D93`M+(+DCk`}4%BT{8??07dG_UvfLcG*0)ruVkUk`S^cMRV z@$=E!%RzBQxq}=3EuF3}Ba*q;n@{R4Da&W^yH~@#?*T@_pB59RRu&6x@6v0Q@VF>k zEWau4_dLhE{&2G#t_Ot~)ZSA%-kHvI$qFRwf1QcA8h7XxWGES zjk&vg&NG3@d)zxj33*U9yg!!TIKTf^yo>Bq?_6^#!N>a%&uao6q|+<7fVZgx?}(^# zqHAtH-hBTW?jNG>7_%1C-!&w7Pr4kUH{EU8W!~(J<#g!VHJYbou^RvADrr!3&a@Uc z*4_x8=~WQFy=_J5t=+cjj!H`}mx)i^(de&mj*<-gZrcYG-B=3XLFkpa8%F_|XBrMU zBJl}nJN~_m?F;qvf8H?$7LC3U3L|fw***Hfl?8Jp!JpEAiEIm=C}>}an{9} zSA*;?su!yvDGb)ZV;;SiQrH{l!Z(-hTgR<)R0tTl+w9XwB#eM42$h00h_$}Da~W{j zEStBnvCgW#?Az99VQ-4hgk4*LL zt>J{>2>z>6V%fo8r)gG4h-Bjpl4AL8cRTKp5qhZLqekS@zrVFr`)`!OvUe1~lE_`? zWJ+K=-pSGSn&5NpVb4HIWn&aeZr@TxIP*q)QRQ%*S5E^uBUtZf+Y;@0sPSN5;`HsbmT7J>Uqu5M~CyvACPgbhk_$g1hHJilx>l> za!LEgpt{0sjGK0b6H94kP&cl|y%*J@`rs;H?Ah0!FE=t4O!E0$Qmh`4 z9nw<17rW9fQNw+Ld#ky^dKE*xs__Ppu!=?@3A!iE=#omO5DQy&Kr{6is6NxRG zG}r!mo6iYP2!J~EOm7TqMW9%-zJ`!3+C&XXyx0xoFa{%3g`w*gMYQ@?1Cvh+|K4a| zGunIk|L_l2rv7dW$6*ePxN!aM_vFD5J=5P4rE@DX3z#@qF4j2zbJSWk@vExa=TSTz zGkw}fQU*0oV+_}XIXm6pQq9rzY0>Ne+0qJA>Rm|bJvL{^j_tkRjzd?2|)I}Vqr%Qes()-_x^2{ zRUT^O4Z7}f6E$ix|DIbV?hmLOH(}d~YCwW==ia5?tC_?D_l)Jj8mq9`0G#8cu8D_h{l3dqy9T;v5tRd+WLpB9sedtZhQm zo<>^9id|93{SJR+&T&kZG9vrJi}*}BWIh7=WlOn~mi5zQc@EyPGMvhQ^iEbF>iFUf z)wFok82D~L{JFdF7-jHF5ol$~4DRid)1_H+GL<5jgn@h<1|Mvk ztF!Hov+v1N%q?8BAM$b)x`2bULh^IqykA<)x73#omrI^qa}mr^A#N3tiq3Rj^83&k z=fh9UO2~l#sXge-e(=~vgQ^SYW9j|gQ*<WoFGA#mmegs=q1WY@s}AV z%Q}~w$odOZv7R?Ue?@yPqBs?iV4U>}$oSxi?{wUw)-JK6TP?oa7U$#crV2=p7OULdxd2>D-)S;nU?j2CeCrx z=ut{A4M;n{Lw|az)eStoyACc@d=U1$gtXhn-nJMK%E`)k`|{B4l*+1Q^B^>G426V# zi7ib1dlhz9%D6GTycA~(<{^j#xZ79Whp$ULqxY}qf!|yh1?y8XS3e~=bN%i*R!Jxy zk4n}GSQft00j#UA*00~-HKFr*9=gdMo-r4Shsl?~MC59jmr_n_P~v1NziLjpDuqnD z)kHy3oG1bj5vfh7XFT+ou)ojKhF_E>cpH2ns8acAH?CR_R?!ezX$)fnGOl;nXQH8_ zD2gqI!n=s|zgB{Qen01|^m`FuietYReqYR z`oNcW?!i`_@f8&t9@mAPOFUj#VJa#rj2Wob{>fxGf9o+Y*Y${}9OpDL2kpcKf@zR$ zCM-nshVb*B7p_$`EFAtaDNYGvLddE!It*A@(J;q{Fbs&6WDQ4^Li7#S8BdlWvcp+c zw9c}!GZ4g?THe)#?xud;mK=@yRJi}SX$F7wZP!YukjT#g?+k4g8EIpBolF1)J7~zC zkD&X81_@0hW5>$OYceU7z>GgoNOz|#2cabf^rw0WW?z&RHKdw=>s`@J(3j%rDnrJP zo}&nq6-d|(qVdWWQ&H1T$-BcR+l%5prM;fC7eFVQo>-NrZS@%aOx(0nq*EpOsH0Si zS9~vb3muoDYmj+`Cd_QAizbT?{bO$&TRIJr-r9Po9WpOF=RJM1(7D|FsHMY49xzoy z8^;Lz-X?03P$+6wNbM%W|5#fPL+cgBi`N~#PL|Y!miZ7xgX>7__3|~EKp8v`;{%TC zwG5zh7$=wForfSs+464Ch_xK^*;`RZokuknm3nS*pGxn4Pxe{zS7Btin^<_}ln%J* zgSjok{tkN!>DLaQ26IuGcklEOIyqSOxPOMB_2*cDcE;e>Ckd~nxouE^Mi}R2!6tk% z_RMM$Ah| z!i*NRnW z@He~5*$8zL5A=S}kTlN{#u8WDK*@%0S7RRr|g zAqFY1r#5Hn_cp~47Oz~hUBJ$yufGn6-=L?s=7h)CTa*L(g5^uH>5%W1AuV90jfN?g+EG;K4kj{UUw1~_mV~`g>GpJcRS@g( zM}gAk?G(>lx>95eW4{ythOIh>}komubf2WDE{v}phY%Jv^LND;09OZPKX z-V^}OG_V%E>88^Dytwz>zygT<^3cSp^TNX4uk$pNRJ@Ks2@16Lx4u{~MC7rF+&g$T zZdBsc&pXKn?VWsD4U|J7Skg~>cq6WQ z-jx-8p+$-y;^p>bII_?aGIENDg;H@@*G4$YkjiOpXNz@wl4`zx!}ljd^$bmg9IOv% z%J?Ow+lC|Q6a~0h9@`Gg9FX0u!6=cC>c!g(v+z2CCDX|tAIK5k*37pquc_txx)0T2 z704{sOmI~ZZPwoG_{E*4iF8(7<|)H?yT#)MB?*M@Mc?NuZlwzI5`d;_*PV>C0ojos z=hxi-R`@HBrxa~y^y)ps%Gxwi4Un89zz6@FVzOQ=k$Q8%v_!T=^!NLdP54Sc>{#%G zG?LONSq(UgdKYQ4DPN-3S|$(0(VOz_J+7j;9}Dcdno4wb65sG<5l~*mHMY7SZZl?6MoU}=7zrm6;Bo(vnP9Uv88%{&Ir3tT7GF10VGqE*VZD6^0Ma;zGAnPzfc$$$OXE18kc(SJac zEW5``pR~vkuCG^yA&l$AVSrjE4fM$n{OO))XGp20#BB{bR7G>@XGT z`n^?arQ6r?#YojX>rL;OV+L8)rwHR~!XMartnQ#`!X%VW7gN*6PcN2c=?au{nGnjEC^h4p zJlL~VqoN80vWJ=!)ZHyfECIu;pwaBk7gO&?R!6Qf`(Iq?{Oh)f=dG`S|= zOQVnr@}h52=_&PU(FjbknRPtYJdjMPe*}JBfDgXZyOtPZ3CK2#>HOjJ7857wNV`(B zybNjgz$D9jRpeB{^#`W<`_{O(<>Q7WUO1~LS1`STNs*>4$s{6>#gN*~h8@pZ+IX5y z@{9NALhtJyh39VwVg;*zEX<0(ud6scfhoT2rhGj>;v04+be+kH^=(QFp|J9@PRWWU zv=-E1G8sX(F|Ndhs4Mq8xV)Zm9*^O{Z16l4P`y?W`s&@=IT?|q4!%?iX<0*#tQHyi zJ|-~2Jhv8X&MIWI{pcoiu9F4T0Z)yt^pgZ>sq}s^(|?{B1%Q&(yC$l=xV!SALUr&1 z9_e@BHXdA&vhnB)>uK!oFEJ2&#!dBnu25tm5k(hICfrJ|<~V|?%w+}Kc0bNXwTmU? zs<^X}iW$}bv85Vj(k}b?lkM53MTWh{l0zA>4f;cN^j5O@#3}D}PvUp zKgBF>Y((&Yk&x!$B~SVR!cs+6QZeTOp)_%hQ0ZrwyeFDV% zk;KPZ#w)u2di&tNXZ*N@KR~9cfewI2@ID6D58>|>sP2ycQCBQUQohcoLh{tBot81U zm(JXkB$XwNVS}Wg(#Y^OWBghb-O0K>qMT|IghB)Mn*_@b<#S3po1oQ{!diO~;;u|> zuw2_0#`S9nRZ(lw`FnpfwWpnG^xQ!2?+bt4q*JBiyMt&PL&l88;zLxg8OB&7f~KO& z`vUKfiX@Z-vEtZO3-J}ZQ>rKyjNbHczs3j=Dsa%t>Yi03{kFV>(E>vB2az6(JV^1h zM7f^i`^S_{o>4*e`w;#@+A;30`Qe>qwEQ{5_eGcKpZ_7~qdxbOrr8x+C4~w8LQ zEg&X#a4=abJu6&=K9JyfauVUpi*HSuyFpM6DO%gY`K;|g-VkP5)S;02J^IS8X$^1p zj*lh3yP%Z1LIW7wZ=zDG^=9yYd7+ddf0)YHfJjx)x4$$yEw<$6Nb;leuRY~UHSd(` z#{+`YuAk?t`n^KG_%uwtLWWVR@xTNX_{IiaA~82HNuqu>UKtG(LYrvxvgftO%94&9 z$rM0#VJ*_S1j-FI(Sx$5)=5oQ$qG`lck#zd>Bo@(kQX2J;v`{Y?M6QoA4jIr1W^hR zP+AYKWIDUh9MJDd5BHLgQTT*coP5fIzb#WMW->+`ri(A9A64&zroZd+Rp~C~qnhj2R{M};3wk{qFmoLTK1>6zj1^c+pg!rcqgSAzy{By&5^ODQ)#HYWJMMuKIX{X3&q6symx-aBc;5*yFyWdN30 z;Z(oX_g$?;eltj7i~eB&0oh3LXt`zau`->rYurlQ5~eTa&#+<02PvL|!3EY>hjIh@ zdwsX@eTU8xvO1seflAzYU#$HA27kxTqV#+X2vcJfO68ZPN$GcTlZG>34%JRurCs77pa$|Ax2eyM8@`dk|9_OzlUD+WIG z+{CH@N>uprH$H#=FvuS!m+%&b*GlE7T_+(Ewf0%q2d3nDluS%5hSAStXJIpbBT$O4zd{z0Wj ziQFa$uF)QGA+sR0VI_!WGHI5>Lm*>;Wh`l?nc1IPmcm;~`-mwCQ~e`bF``tRwlJ@SHHCJgdr_CZnzg47p>f_3ZJyQ+10$ z(mGW0YZiz6xz&%?@jvI+B-4!?^GH}+OLIIiqGbP}({m1`Qwd1^1V&0h)_qj?vm?}H zA36Ho&zE~M`v@Fs>UhH9lm~bvweiIHx%W13y{)`}xU2qwRoG}?SitLKK937Tb;qi`woSV%cqb_+w*M*49aU%<`NLiCzp#^H%9n zX+F<^_%{A!%lc{_2+ciK`PiDNO4t7SNzed}KcrzXBrgpC#ckF8dE+`k{)A$BH_>O1 z-UPqiklMT9#^;R6M;&N=JdirKKa2S?W_iVMknCi-%==F9&Gh7ROc=OVWjHKMqb+{< z`w`|6?bdE7vnw-*gViX186hEn*2+(-+Ekud;mNJ+I~l(WQQk}HuOG9Dp_+(%B9i9I zYq#?vs);Z(RZFSINSB6xvP>E314ru5Y;l;h=|EK ze9*{HUw(&~pF;`sPOgzk>@)<+DjZW5(ON#=Q|$w;EmHp0rSs`_vE=@Uj9YM2VUV6! zO2*{kTuhLduaOSb!|wcV8C{IZbra}v7+UL{*p?zbLJN7wK~CbFONk%0ELsuJ2n%wN z!a6ggVj~)ya=@%IM>2@2gyE)O4~Nz<5uD>yn}BEO1|yp%^psTVUfmKiP7!~QT{4Tg zU-#0L2&5cx_;E0OXfO0N%-}U2f~lnD0JkY3pfl`am@kk@!7pEXun}O?d*uCE=asW( zz@u;zO}W%Jx#&qH%%|8pUiOQrFc9Q!_v%5Q7j2fV6(1vuob^cT@tlgcE1CCg$e)=v z)34QmftR=Pc3>py5q3+WS@@{8Fki}$^{YnvmX4JEF?(d_`L~w3oMZ@~`kp@b*^b#p zQCe?kb@}*P=Dm^UDfUC2;@TUJ$v`RD_e2G6#><>v$NRz!wi}Q%u@F$ zQnMa0{M|NWj}$qzJ2d1A&#@EMm0JM@)$5eHNv`f9inoR&&;vy6;I0xWf@>-Q5MVe& zsT%WhpBLw`pQ|mkup>GDO1Gqtot{266JsE&J>=2T3k};paeZFAxr6e7%pe1B0Yir){h8rr+`T=WdR`(gFHe>eG;{MQ)651CZ|xEF(SP-`{@TuNyqI+n-){HZvb_&z$*s^9zK)T-M#~Gf%RgE_5|AK zAC)c~oU-6JKFVPp*oOk9>=&BW>P&N`)PQB(#f%|Y(FRLpiB!03v2dZ0Z-d-UFgcu&jS8S;bG@H-i;^yxz6Do!Ea zWQZzmk4*Kk-E-9Vf(Yx82b!Ityx)u!U)tEYg~h5F>k$@Rv2)o6-?on$D;*&Qh9R-x zqzY1VET4XIc__pB_PWIyHmP#YTpQ0?Wz33nvyO|{DfVz*Shs})F)yYmgkOt^9pSX~ zldqWC6Vt2Xu$W!ZHfYxKdxObL@H?561%UCR>zL!UqKNuq#+>qFMW@0tWJPCT?7Q1o z9RLs^w9uh)=&O&nu47$t6>o>MA4^*3o?Tj1ljqaVoZuze{2cJ@J*igL9LnzqBvO7q z@7aKr%W~i@&i8rdQlV2ib{FjC@g=XE*$far`M16=Ak768l>|t!( ztK*gH0k%bTkH6-<&N(Zh?n869l{e8LPu!#&lxG?8?%}FyGNN z)rFkCKTnggY2a3gxEw=Dm~_5*;^^d))y+=Bn3 zd%4)nHG85E)vxfE^A$csTql9H4r!Qu^e4jTwSW(|FAHL3)7n`3F7|ZVzmI(2S7U#} z6ypa?mlM^+XPS}ATr|D&A|QFExssBkYZx^1%ic$4`C!fQH*ZZva^$p~v z!@>CS`@u#Y1)D5sU!9I@<7MgCB9n%+)V$p7o$tC+`Kj%TV1EHFRSr@_pX`v4Z>*y& z(F6bAH*dB(^02Ap>37CfZG{-;HHbGyrlixVBFW|8-RU8zRN-l!f9%RGH6Od_%hUV_ znIud3n#Mr9qa?iJPEWOwQUA0Cm-p-I@X2b+Y85d+FTuaMIT$9yoVSeP)F{{(kJ4IU@);zhh7 zZLRj&62j!S!mDiw5re<1tNpkF_;fmPggaayH&DzotCLq>&utWryd&2>=#kh`R`3x- zNaTK%CWabzm(^XO*gih`K#8d#F!G3Q`;lI>iK%aC7eW_|EqEc1Z169Dt-)-Eg}@`N z*0)CZ0M0Es%vbJKNmgr0(@dx+<)K7@d~1l2w+0E~-PCNqWCvwtF=ki&30Bp7cw;Y) z$S}9B0S<~?u&<8EJ09?!DCwUjOJm$So^b}wY) z7r@KDM-M9unhgfDMV&$L&sk z{hK>iyYV9h;RtdQV*de)Uj6;m=Eo0bgk^E|tGM5!e6z?Cz(2v%Qhzmzy(O6mlw>}+ z#{wprln`V`mKMSz*7N0Y1zeYJwRu+b$4RrXNe7OhsqpJB2a#LS0TVXSKRMYHT}!QPJ5B0 zb($J11O7R;8fA{x5ECWgcVCfDxb)_<<&&aXezXQ^xgj2mU&)N4y$et7n3%4kVZy(_ z0=JunJCfXneas8B(Dct(m#a%l8s;htnCep(`A|4MYb6eW1HVCaOlnAIV-$n!p9{R) zYqP$#_<7Zk&T}ezEV+){&!`e!P&xWcUqVZrb)U?9OX=7hXz^fU*f?Kfc%V_Arv|Aa>CD-Qt^X zh|*doy6(qv}iE}!yZqQ3AU!6=5Vx_U}{_grz*aYql07|9(@aNtzyd}T%}W!`(^bj z(mZ&Nv%zh+qG$4P4W^!1u7T}cB6;QzVF+ThYF^^@)ch!~>kaayw2=J)i(N!+E2Z9- zv(liI9L3)$*po2(M!Odj?S%DB)|Gnm7OM~2J=RX#C=)j*8mxz7KO4%rN$mMJ z$H$|svlOkk4z@6?=u9C0h2*(8u5R8)G73iO;b-jC-I(1CB_diX{GA3mo5`Etq(66Wj-E~1& R|M{Pbx-w?>O~E?q{{RqUO*sGn literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taikohitcircleoverlay@2x.png b/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taikohitcircleoverlay@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..4233d9bb6e6aa88c7da5c1998439789cc5f9c7b2 GIT binary patch literal 38217 zcmbSycQ{;M+wPt*dX1VOOc0$x^xk_-v_y1*=)E&KA$pCT5YZ(>i8eYxNDwVyqKh_q z8#Bl6eZTiR=a26?*SXI8F?;R3*R!5`J!S2+?!9(`zOEW65j_zA0Hhk~%8vm6bXx=g z1bDZH=UyLP+#c@vsF?aZaev|CZ|!9dDA>8%*h4j3tsU$i+gsZOdXCsj0{~8dlcA}P zsgAa!t-C9~^*?p^16)0Br2#-%F2KXu*4f?%YGd!<Sl@5@+ZNSyu#~K>o>f+`t86d;*uXZJG%l}-4u|WS- z#m8BO<=>!8b@ZW1?q2p#5q?oVTR~wFsF(!5py(rE2?<`Pkbs~#Oh6na_=rzXR8m+# zQb-8;Uq6;xZC-XSBp)lQ{+F)XGZ_{~A0H1%7|h?_pWpuxzq^+MOi)5X0wy2?6B6RP zt-tD6KeIDEYZy5h$ZEwRs4|~{SdvAANFWXyxykPy0@U89s z?~4AZc#B3-&&%o7Db_B^?zX?CajM8t$dMa20;#f5G8gl!~5 z`NRcn#c%&41Ox;{?QLxYAN{AD|4Ck1TuensP*p)eOhH^wP(?!Vk%X|SfXE|7p+~|> ziYg-ik=1bX_OW)ewf~Q9r(50sB`fiNm6cTTvbXke_cC;Mcll2g=sUXmxO+Rgdq9filVw)b-Kv$s?Aa(9LP%hHlg{{xEuHw*k*-tPa`g0Ndq zuz$kk{|KM|T)K_Pe~SN|47Vr$olf>{w>ji>n=Gw-mOTKF=AfahU>LBl8;W1RIFJ?Q zt2b9TZot4+`-7W9p5P%Fg(7@J)GA7_nJ_L?HqN`>;oAK=WUk8_aaQ<|R1|Nw4-`d; zr%j;%c_?24G!p)mIA-m!yBKntN~~O@+V>Tn$-&x^td{MAzl$4|WcXxJ#|hqRJqxW!{T_ zZH>uQHk)UL1_^DRgOZrZ;O0LhoZa(f9CAp;fV#3)nH7nIxVydA$r-)kxA!Z1>hQfj zj<8Q?;H?QnLO3b)K9K)3gM?SnJ_=0koDkj2 zXD`Q7@$Lku;75rVPDc&sQ{nsb^B_%yYW^{sa6ZC*5_o;5>t`pdg&@Jh!4azhKv!at z0rb1vd(r-=wdR<9Z|AeJbs1h#M;BATyO&c-$xcjSL8024eq+j9Nf|xjpn~a=S)7hh zNJmGT?|!~W&PFrSQ?@jq^T@x-@`RWjIU6bSR`jshAn_%Jzsl(aq*RSy)lQ%YN1RKc zm}rf~L*}n*%47$g!^gIm#*3wu&BSFL=@-OE$_Nhd6G*73cp{&j2bnWQHAOvXSRsd` zzoSR|UJ?)dJEY5kw;s215kN6EFir;k-II>mp!rs#!L;Hk-rgKAxG-Wrlc=R7?fdu+ zR|;)(A~@!IhrY!y)k*C0Fn9U0cE!i~$?Dcz%3#XQ!jt?CO{UPkkb&9&>L96X=3|vk z1IuBCZ^UhpOC=7_@Km4n*j|oI%-LS&4gT!?9f_-#THTZ{pmb*X;yWT3Mh=2BeHKYO z+xKMYiZPxncRAqCgM(%C+}RgDTimz$HY;4bG4`gOMGjk|_pQE8uG=Qkp;^efp`*Qn zqrS7D@At}UTY+=@b*9HOm84m?)%e8iqwRgc<;_$upU<6V)rEVQI(c}PvP;ExMN0&zw;bn$wenLT16eA#Jlzeg%^N@7j+9p3_?pM?*^lqP zTjK2N8GgrYto711VL=`6(CO|e7?L$mGweq& zq#RHwEfxrsj~#|?b3{F6-p&bf+^BGRDP#&E{;(w}=N>K&gV!`F$?{9k6X#^L?#^_)=B*B82v-Vu+SY&{oZl`PhN{f#}tc1pv% zCL+wV=&aqLT(7B_^8H=c?38rdqX%AdpK};}_SoW{w~vI)O0i-@ExDji6dUn0^Erh5 z>&cly4;kDU_oQU!)Qah+h12e~DiY*zLS&367&sz5!NcsR*OY#5Yv?+I)0zuC7Yn{f z6j!-37R(5)t7DcZUA1MUg6N-8Kh@n($w^fux`=gTy@96iw~hnFT=#P43GAN#LHTt zVv^qW{n1uFbsl)0|Bh+sYnosIaM%3JgIDXn>%>MxXW3N=>H{sbgSsT2F*`XHde^Lj zu=|_cZR?J_lsgsVi8^eMZuZI#MtG`ik^c(XCiub6!xIbAEn-|m4yyUeOpAg2_hadE z$D4hwy}Av^)`LHP{+OunvuLqaSK6VrBd53g-YX+B>D5njzwbePH|pCgp$xw#(p~p; zc%$#KM}1N6OK4Mjv%j}|vl8ABmMJ^UA*@RfMK3fNOSo_wz667Wh&xE{gb4*pHlT~z zb)%fr*Wo5$qjzI4^fh0dEfXg1n8iVVvK~qoEr;lk#u6=fb0f_RaNgYk=l-5;=PN2D zD>_DJrS-U-=O@hmRON`=^~h$ifOxf(41O^sNJ?-jq3L;pK`xmiTpXwz)dTUEt$xK z>_(bjKa=va5`(DYhV2E$UHcw&Y`P0SpHn~k((-Z0{;J;7(V*ohVC0NLuFG1Fjbg8q z{M?Zsf`byLKY&ev4J;cyEFpi71Ad-8OP3LBW22WpuqjkLdM)%hOHcM)-4z<2uGrJ@ zrQ{B^8yl&y)C2Z3$x0m0A%@JmD{Dho%3LGlQW`(=!Hv-KvFRKRskh!k3 z#pa*4tQk#At+}4#o#pCr8Pa~Pm%RE$r|S~9+H;?P{0l^Ai^dE0g>s1z zzuyk2KVWBVytH{-ZwQ2~2U?m*)}HxId@6icuY-({;OV1Ft5y_CGdke~i$6hCtll(c z0orsIx^QNP!!x{zM{5QLp?0h?RFp93XOk^8otNE8`Lp2%r~XDPNV~~u4f>aA( zj-V?7Ll_fPR{R0t6B?-R{wnLb?MHT_eTrrbPIm- z{>Yzd>YrJeVl?G_-<8VIxx|&yb;_yQgN@LU4!tn+tbBDXqJue2?RZ~{jfC!m-~BhX zyvU>(^I}#oi$MHd{404grw{i9{{>J55bfy7o!N|$D*v^JgPxiw7IvCfdNMI@&=$y& zb7g`jr|U+Dk*a+BQX5o2gwE^75;>L9UW&?eD@{mk2a3rs>$^$~-RQJyvAk{?FTXEc zTtxTd*y-eXz8@1J89cePpZaX1GjN$BW&O7tBfSs5T-SP3oTcoE;u9*1w-3M;re*g< z0wz90Jc$KJgwR2KK?|NC`-dR(OG_0@w6mq8|NQ*t&2WMo%Cc=$wDXr)UW zOlf+Am4MvjteH?*|F7?7&3{3!hqAe=7Zaa*^&36^VX`%Tyh%q*MomL~K}`G#T^=q4dW&=3XnD^mKB`-@y$|qalWax#5G}IZyqQ76N}?pWt8PwLI>9N zoH&!k9#hx29Jp0*T7TINcGadGNPuq=H2Fea^u<QO>dlkFAY7MXeGCm^iw=T_env}C7v1REg*Nh^L(aEQ+D z-vc@_=JqC!qF3dpd3s_<^&(9=EgdiRN~pi{1T zuh9NG6_z`B#qYj<&@T2a{eE3zWLU&mEPiPks$;%W_O)s$VLdA<bixu30vY1NkfE)UZP9O(aE}-g~ zrZ_Q{cf&;!`6>z?k-s$8`+k-Le!zq2+OO;FE4*KhpTb-{-|l{1AX7U&wQziN)Z3&o z*HJRnD{)LdRwUl$dv^O=nBuOEqfe(%6zNo_`;mAe8`KrO z1e+j01tIAUx_1N9r3?JY=l_P~$%y$oYD_GpoXNJYrs1d=vTW)1MKTpOK+6npG<Ox1$tF@16LOABp#HTTm{(Gh4gx?y6I_z7HfOG5ELZTp^u ztZKvrT(^f%Zvs)5^?~tZrN?rJoGc_P-VS;u#-#L)4aW#-fEYb_XV<-aKm-Bv2l-7+ zcuh6NEuBv`nxva0>Y05C#Uwo)qMeyO6UjCq(V>=2uxOn*d26nv#BPci;H`LDV2A)SR?xYl%kq-ELLO(+}cgjxe zEA8u*)V^5Xq@DP1p>WZwv}BGOqJA)nk(%$l7lddg3b94SRn8!$mhs*7q+~Jy6KC)3 z54=v7=?+|u4&#U_E%H9!VOTAmEy-QfLhY6)*n0>PC^^S4~ZD&NB6 zswLq-MNZ1II3q$Gf)@Q&KrK%o9z`*^<(#lHIy;$}(l)z^pWhL8{!q9@N|`X~RKOm>vk)dv*)zG!sC_vE_u zGPNr4$uxYamWGD?Yceuw;z(=L z>Cqu&k5xn-P619n!-H-;kOcUIdhQEdVOL7oL!7Hijedke%5?!${2p>F3&#sDAc?9M zfdGi9w(i*>w|90%@$U`So&9Ju>HgxSx|;Biii11@6w7G7rW#}*rx0#vyKuC;B{I>D zRShe~Qhl2ET*&9z954|m#q=DFt~MlX`k;*HiBx12iKm4b9cG8FD(v8w_^`5SRKM6Lj*I|u{F)aW8zCeU-Ai0dyb-ff`|tiJ@1`Rl%`Xom=ig zn}?##val|$P#c`Ghc2O}#{Rq(SSGqt61sez*YDUfhJ1^`nI)C&I{qGYSIFhN0Zv$9 zT4aVK%;N_Ioa|zpO|G~Br#?kt`wO{D+u4c#^7%p)sM6#OWub=b{nn`Qfmxj3DMARIf zyxRJG=}g%x3DYBN5Rt59&V-jRv87S8(o<51XJ~w`v*C;9b`-cEs}k-|$~9a| zsVL`rBzF>8B)xgV6O1|8_tr(*9xm$nGw(>fPnVav#}5@ECe-DoygLQ4O5gR~vAax+ z_6$|WT$PLQ=Ng~*r>Q!elQy=Yfa=*U6Qg)K3N?mhKupHReqUcl-GT* z^(Xc{Ise0 zs)(D)TnITEE%)spu?&#~*CZlTR9C@!PE%rlYV5bZWlCiq;xN0a&{<-p-{g7g)_kbk zE2I^B#Z61!I^wW>a8`bDQJD#R3qM>Mp9X$97EtEV?6%HdU+i=*M?n4%K43%k#Vi;^ z4v%CrGB!f2Ko%(?I&X<0J;fLOfNrNav*KZSZ%wQ`)bj> z^2StTG&|d5GG&1en*ci@0)k$!W3J!gG3g8|+|Ax`|JCR>{`20>MoFEU&eqMEGp4xl z@=_Jrq69HG5+g!~R|sM(&a52NMwV`Z>5GFySL1$~7#flo=oJI9=Vu)~1f2Sco@pUV^WSA^3nPx;^0zNr|2B> zT8Cfq7`Y(Ro!_{BY0BoOI8NGBDT1@o&(gSt%w>xZR0HskbPlKl~(7X$cY`}t=p?{n%cO^l309Pw_a z$XxLOZ(%QJP9))aU@81Bv}*y<=17J+AP&%;ZBQAMGV-n{N=XavIVipg&U3pSDPc?1 z(_&iZ#5%6rPZHDR#!^%i6@rP1$(%JO$QMlMkIaH{uV4I@?_9)qx*zczE z+Hv_#7UY?TQjCq4eu2L53k@snJ8%`$aX`27>vIt1D$luMtl%{I;H!UUaEQGo&E*+V zX02xH@?PN0#c`PC=JJC(*~mn7rKqKE{^YuWn0XFa3;{f)MmFo(g_9hicAr1z8jwuc zlen097Gge`_@gL_Nrw&KYtVu3pTuOH#4BhKNop!kj>zj1vSvn95mB-sym1RC^uv;Z z>?lC0q>kdqfe)Z~ICoU~XnokpE8N6qn93551`@?Pmmu;>_Pt$n(lo-kj&8Vx*qv48 zF@7Fp(Vm=#6Y}HDPV%maQlzm6Y?5OQSC^jh?xTNF%jQ!`$^q+AallUwy0OEXj$<$0 z_SXXvU5^4_d1cxYb2~%BiEJN!X-0&{%Y66o>5c7{L#HiY969(rMqP7lrTrrIOi76f z4k>6GgYoie2ZS(Y3R9k|=uI3XmdpkP;2_M};x*sH^-k$o6)8sG#VYm)8UVL(EJ1!8 zXINSqsSJNaR!1NYF(h3xphP|3Md2r#gkG$sgf)2@FXkPRyOa%ji;0}f+^dXO{n5T= zhTD>AzAvU>)FeuYb~&Xo3p@PgcI7uT(*e}eO|s(;2@@uXfE&PJg7aeAz0CZUlh*?> z2TGsZ124cwC@=7C2tj@QIm$OkP?3?KF_#?Z}r95@Ae(6lG`Hz5^oD z#nZ>l4TnEaCb|EZQa-FC;0&FUT@Z}f=V71r(-{~l>6lXrQx5*XyXR37XM>g_memjq z{P}UsYCC09`gAu(>;UdQ$%g+V9U%^y!XF}4Yp|eDQdBKm_IF~19rASQA!H<$d-5T;h_gSB3it*EdBEa<%ZTP?QD!fk zljm0-7nwTjH+Fy8ACgy9;GF^@S0Y?J$x&UK@k8%4Geg>u#@UQxCLv{iy4EM;f<_bF zE7>M%eiA$K;1BTNKVgz#`VUP!MCtE1_O+67A&Rw7kmcP9v<%>vmzs;WyTAlCX)luWm^7 z{$-G|hgwg?@RKFEPcmei7~j+C@Gl#AeB6WulO$9xopImVCO}lP0AZT2pz_WY^vz2e zbcP)IL}k?f`AM&)i(;S?3Qu$_hX}Qi9!__9HOup=Nrq{MxjOi=KK$aBxv#J9P3kS? z8J42>yQ{V!GHYJB_b*Oud*?biPxXwE4sR*h+N%)p1OcBY*bo^p(h?u|ZB|yQfR^px zFV|OB0xnz5uC6ikoYz~m-QDE|MmVNl#lGDjG z=5?J4WM;K`q`kYrh}>|eY|b_6DmQTg1Pcn8SsW4qx~M#F95(?* zR$AMztM=G_FG z;evNr8z0oPB z3!bQ5LRWmAa8*|TFW8hCutpS6!39p<@g2-0A15N+Th9)hjrY2w_b;b#NUoe!jjURhN-3-@@L~9ygdWe?~(= zT^mK5=PLEZHsbD#=*m9)(MwQc0|0?XVQRGs%@9=4ZWdybOG_36l)2KUGXo|T$Ad$! zUR`rHEJx%>D=2&%7PxeXfixh7Gx4*#FV0iC(O+UAK~GZQUYb<5>k)J1a7oz2nb`9S zOCCDkko8(E!Ra_$=`PxfHrGk=q3a2Kl%>wy!WvqOd!){m3ZX2sfXEZ6`kHmeVK{SCmASEthe0X!q(E~V9YZ&48wH93tw zC8trD`E8Pa^lh2eadB5(+kQxX=V=?~x;otA+5%HC4`#i~QiJK)){{M+dA*2BxdpwN zN_h891uVe|6ee~188hRDKTNrtxY~?4UT;saoZ;{tSq&DWUI>wreT}%k#z?wK*GD`{gM$ziPHg?buB+6yMmuDa7jy z`LHA?m+hwM`0}#9DE25e{a*E%-WN=&e~!D+Em!}$2S48A>m%GR81Jo_iSHISmS?!l zZW1>wi!Do6Yqvv%{tTVI$&wD+HS$IR^*Nmxg6K(U6j0#ETAQHF&3Xc?-6OyMHwMCH zA)l!awy^*lD1aj{6ci0H4S)oKG=YJk|f#)k_Xc7hL?E^Xv5GQC2S6|p?Z8+`K;QUUG`T6(P zEq7itiBY9^iHQL7w1g*VZb$QPtA}N0MTNStHOXq+OBdd}lv3I&G*KbkA6xHee9MUR zN-pni{xP8zyLr-R-y!?SVH_6-SE+dugt@+Vv6phF=BT%XK%^AbVh-nMmfd8pdd5HO zJ{h_YjDmr}c*pWoP*QvMmUrST!a3Hhqro-(BK;!kvvjl1%p>w<0uZ!TRKjc|2nFpt zq4FJ653%U7|M}lyE(NRs;(D7dOQN#oEIW$*gJ%33GFFvO6Cl-|S7eb5o}?_%~Hv zn+^2bTNm7bI|p&W5e1v&Ne6nF1dq14W3T9(!zmNi;&Np)nC;K(AD;IYN0Ihrdv*wh z{&tybl?U5bfj8KY27)ZR(1C3bBEqtmY z|E?$7daA*7PTZxizoOBkl1=}EjiMo_?)jXyF#*D=WBFaeP2kV&=8jT3uXPws4v7X1 zW%JgRCbZMOBNes!30pZNi~8Nu^|`%rjd=}elCzfls)#ujKu+CFWr(+7yVgO}msul! zh;wb7seRFyypvdoa*j939R2#0@|{HM&q#XpXq;!1@ICDU@sI5-!Smmi(MKJ{Jc9lt zX6-6v6Z6eFbHXkRq85i**oV^_9Jc^VZAuYv$eK$m*ee^MM-qFZygjjs`J%NXySHMy z9~GF$GCDFFx35Wsw$!q_E)GKr-?3QMkm>g+28(sg&DD6@b7S$POk06Ji_6pU2`vR<+QKDQQOB80oCaBp!#&R zF3}V3m9&m#DN)5Rjm7hMd^zmN0(O79)yB-+{5Mo|rG&4hZXqM}rWLsmU};*(k!=(2 z&I^CROWALFU8gad9EaBt8)fM)Ln$9Mj_|WmYABD5jotrA#nW&wc;6)soJW5-Rzt~LE?pvIok{i^j2+#&Nw=z;zI1_c zd~sPWI6Zj62_h%;C@h6syde!y<;V>JbbXy23NvN8j?rOUR~-lryTZc%bkOz zm(&UzS*{f!(6vbV9t6Ac_I!ae7)!_ca z>~0;oZLy+L9AA4(ttIsp^iV76NB{CLT`;lqb6is*+(EY6iRo3l$d6*!j*<7xVl{T# z{9ylW-uIZb>IkfcElsg85hl?lT^K6HTpzYN(!GT7{yKTXdDMd&6|E%A5m_7@&UY~~ z{3VP>qr#9W5Pg?{6~d}6uYK}T1HMS0M}2&Yil|woFCyaV>T3V`0E6sQBcDAmR)-PZ zq5q~;+q`?ee1q+4elNH7+Xz)EHX$`+XNagJ#Vs%ddC?+Z{JUKIds*t}+N)ltN6iyq zfwGvhj0pFSc~8!WL+cC#%3Eqb+iWgle69=cwh!Pc{u<;#M@KKmdxm`nDCl0!F8f3G znI}$CmIxyL^wfWBkhP^GjBoDk12Hym&C$A$G^?7;!os}9f4lNgk@J)#M36{hbYII@}C}oH67}iibszZ5j6?NkP)qH>2s-yQ`&5>1nmoThK?qc}UE!9sf zUJCU0u=ViH5Jr}FEn<@fDKWgBMkrvg__LuO2@Xj^VTl|Xn_ z_Pa6-A~D8FE7IKB2MW(C%Y;ZCbZ-`1-zd#=3LX(9sJASBvYnF^0C)i^1fGUD15}FC zl!Ge#85n7c2^rldoOP3RSidu2vD`6y@0Lx&ip;s?=hpl==b(HbXJ0>mafEx`x?Zkh z+Di2#sOLvWrCaA;*|&MqtZr2jc#m#YjZ^m8#o4V`W9G=C?KC=B@c(p~v?Zq)1< z2j6nJ1+t8|F5{3{D-V<5MBq^q>M-)sOP`TRnd}xueT={p!~u=+r`^M6q~K;mh^c`Q zphs{e%nfz=H2Hx79#h#xcwlL`S-sc#Qf!66%NJ!#KS%xa2#xkjlGaer8Ktw3Fk@ITM7iQpTs&mkdX~K9@U0 z<%GrT^4P!>uG9v82VHm+i|!AV&gZ)e+nyfUx}2HxiuQ*9Fv=B8d2<ZPpJ zhpMWo^CinLr2l=_Cn;QId9h&2=I89ZHg*$Z7x{Z#ncMnh6G+XlA z6zZ}_6d;1@aYCD}too;ITsaY&WF9di=SlLb8VVxRcT1Efk0+ToH7~kUEi|aM}E@O2o@I5Xft^Y67YjR5Fz2DF@!UL%F4lOB$87ud9aO} zikpV5=UrJQw|gdpe=_HtV#%*olpZ^%&3A>PPkH=p6WxY+eS{8tJ|ors{3w>KFZ@$s z2fZFzLH%?jA1SQqi)`*DZn^Ku2{5cacuzspKmGNIs4#(9p@6mGa%Xo4_8_>u;xp$O zK#JRC`}79PgCJ?ZA=&-_m##2`^ZgSLf#Y(=7@n>UkE{I^v8tw}~4F2DeAQ z&d$uV5_vGDxfgo$F_wes9oVjT9ZhEfLQK9|q|>D{l#d_;%^8Ed-W2jMuwgG)ifK-l zznYEAp7U_bG*gCKAF24{`JhdIzke6UA70(mB>YiFESj3J$gi z*%-ucaA&s5Y6!%4RxA{Sof#-^vjgGMqjIvEsRohyO%s_t3-jAQ> z?ZGB!3CcQ>-a6a0r%KneOPDfAGYV7i`(f1Mm6#XYP##E0<5&!>Qc8U z*Q1-Xk-|F~lDzN7W?mtKwS33xP~27`;5~$FI-_fuy2O&u3q@q zD4H35nf*ir#CiZv9F`hNbRPm;KDqluy~#X46jwLXYqVPA+YpSNbB-EU=I!n^r7D}x z*`0EJszf}?)(Dvp$1|HFXqDmF!1YGVB_VDF%i}q7R`s?SDKYI*9P-Ktcyr=#3wEa0Lj;4q;BK|qthz45W z`KNzCHt-Mmd%kSl+g*x5YNzZ;X6ZXo#UHnC4&IBs5!`JF-+9kawD}qH_Iz`McK`A+ z=DY5(4DKiN53XN>+Sn?yq(x1lm+wibYKbUZjUED`Ch+En&7x*dT9)R?iRnww>P?D!(@Z{bKjD zGz2wR*MS{(7ExXUJf1YD(3jVkOl|9$shx`9rtl7X;P3mx^hdw*NdI!BT|LX`_ZrOXiV>w8n`Sj`2-fM5aazP{Q#?Aa& z)mgmvCMsE4^w#O&E|x9*ju{`o0hG@SE8h|!o<%m1b{(x$y$+RmU28fm%Kba*F)qr_ zXzRt)pnA2r->YeFfO2dfEd_n+A%Ya8$lT61Zhi_4qU3*%Sr z#1_`DlJA=}B@b%eJtN);40rvNg?k`pJkw(e?3r>T7lhS4<(JpPo%FyV?0N8a?#d#} zvv>5i=TrT?4=N1-T2fFm{G(QQ`^#o}JjYG^IbFQ3qLM#99HrcWhYTQCIs)u36t#X7 zGaDS?kDXH;AV+Ud zgGLcCy!KUdwVQ0>XPiiV5QC-c&gY1(yuw_*yAR3fZEPWglLGLFvm=ZO7?i6+^-UW@ zK-ufqqsPXg`NHCdetSLAb(r!9gBs^rA4A!GNVx*-AXbN7xI8WoSI^x{X`3BJJLh=N zuIGrVj-KXr)97XDcoY*QbQ&JW7{}tN&D}KtHK-Zn^}ZOm4f=mAu`Mi?uxI(}^^a(v zH@O~}YWq>GIA?BDtE?{tHcQbOIrQgn!HNUU~*XHqVQ+ZMVC| z@{z0wJR`CSOquar9vt%Ssc$a@4;eQX*S#Qg{pe5p*ca#Ac68Nb+U1#^ixt$L{`gwC zUZyy}s0pBZ-+}p+n3)&Y0i$rul zb}t5$t%kEkvL?q0pTT-=WYW$GlK&L7Wa>nCNxw@~a;cSLtncvny=uD1>`9q}l@5EX z6_$`|1)6&Q^yJYSyVUBC9#ai)5wB>xI2*%?kIgpM-&rJfDzoFQJrel`$;5MkzlR;K_(5-I$kv* zpG_>k+6EXMlGTQnZp;M8?uPYgC@vgd_Z|u$9ryO}tn_^caqZS*Bf0tji5Rq4{*_tN zMzB`J$?*NUm^6Dk-IJzF%YxgSf`pc+{N3f0@$oo#xX8s|lQzLwjC_}arlR7LU_9p_ zyVsQwE@n*A6a1yPUjV=+jL5&8cL18n@?re^XWf?z=t3o&#^4pDNIlbD3cqQK+s_k% z)^umz9KZ@$DByoI6;!R|xpARE;ht0EA*Kf7am4zm^16vN>Q7Htx#66F@Zavvb<3(! zEb{gS5ZiLe5jl$&PVCq$!Sua}{&+oOkyPS(x$FczK_Z-JJF7^7=o^T@lhhOflKo|~ z=EQYy89uMAqo*<{PMyQuX-asa>2uD0O#-vTs56Us;{2&@{qb@!LuV2u#%;rah0l;+(d>X$fPQlXgU(R*BF*eBve(WNd zg{`Pyyx!d849r|2(%5sQ368|%-Jf&2HzLP>qcYE?9{%%V$L+R0;w)B#7FkCaFL#7X zFh<)_jvG1jAQtqD1-NK?eJlZP!Ubcl&M-^3dnfgkL;<|^1g{qnj1BlC{B2H4kO;tK z9keFA60Qmy%Zjz7*|RzE+R&gqvxDOc^@Mc-GX~`qe6h`?#Gc%ZfC!yIC{jq7@{)L`1fTuotvTmMFRauxa)3;_Zweq-itSdDA_hc zux-z^)St`3{ZW^N#J6Jf2-n;=P{*MiR6yPgIWolVo=k=CCgCvdh0~;c-jx<^_pZ_e zaP3-|GlqNoD37 z?ytVJi8ai`{?#WEti1OBkuT*MvfZWY1^lv|zt*z-P~TWiPbo^wK-&i#!uMDqs{ByK zs{QSQr*p~kA77mWXyU?c1KBJB{D)f_YkX`B5@La0^dx5N0P8{V2jKZlXE~_?mwdLW zafV{~+~WCm))d~-U;lP=*0K8Zf?FF4QIMujK?;Xk`xFjaw*xQGD4h0Y2a%J zcVD8dHX@qzBS6}YrVm&OFb9}S{O^+e*jVna_Y)gRpwYHoA{8LuEjs`G@QDurw4A7c z3cThC<}}}XF(6l=V*Cv6cJiq*^Dymv|9O{@`R*fjc1(1PERD|N7=mhSvTCeUPQ9lh zZqj@3aP0ZSnR9`3`g?-JltgY5PVzy+a{>$W z6$ny^ar90@?k&DGYc`MJS`D2nco9nLaj$OR^x200j6U+$z{i$C*>W zUA|P@po}lO!S~B6g;yhz$$B;&PQ&p7itE&e-QxIgE_nJvZ|}RM&=}t9yApk|Xv?8* z=J$>88zhNV!tE4CP>(eUU#xw6Le^7U>1iKDT}A@P($c^)l^UcJXp=WUtq|y64`??j z$%nQE3PX8kty95dI-a>VbP|QX@z^7Dtl1DcCviBg9H0Rr`Q=wI-YW78il-D$HJQg6 zGl8VhH+1uD+m5Sb^ygL?6PyPmObiLn5{9s}q(Y07;k;?Rm1Tez;h6#9oqlN`Tzd=` z@hbv18gzMi*|XYrU@4kjMq*w_;iU5HUOzh{0*;AgzPzI z>>B2GY$%t=z;Uf2ndSTPizEzhojqx z55u9Hp9)cGQQgWr&6<~h@8sXN#TvHQkS*Bw#d4)xT81XVDGNC*U@dAS|Ah!@7P`~Q zO#lIB$1Bz|!8buD=mgF`gc<>qmhT<`?gj}M_Uh#Q!z54M*b0*AHx%$df)WkjmQIC% zik6?wjs3(pC-F6CX9#{0WWxFzF}*#KtKq*Gk&F%wFObms>jV8L4z_B_E|^+cO4$4n zxI{GUEu*+%_(_j`eaCu0uwjw<1D6_@#6_IUP~q@M;BGD;>L^aJyZb@n&d@lX79CC_ zj;9ZG0Vfp!LhEESB7-u*I0LQ?qBf?xc=KEBaIoybf};dzn6Tt?;E%9=$&Ra(#uD)} zcbr{aPH;uhD9r;qdipg*T+`6dW>7JyQR+x33Y~57jW}07ZcqvTEQ@gkVufp)Vf$!q zkM>*;CZKt%gzq8flp3f9pA-e|ba3w);0<~^=#BY+B!H5{1PWeFkY{F~6 zQWDYf()4j`c*b5o(yE$i<7aL;!l-uEKK_!v;;1Xxy3 z#W;4Uq{z)zoCQquO-j^xT{#&<@?Zaa_9`Ig*EIe<_&GtI5rOB*d++Xv9wWtJ#$RNv z&-g22XKJ`s)OMN2U@{m*mhIaJL1xbJ_B}{QV+9VGZ$F6f0(&NZdG7Ti_k+bz^z7pl z1Edz2j9U&x8Hs#hZ5u?F6G$(|+;?Ts_xbs8@^h#NV{+wP!t+yOzXm!jDQ&^QluE&7 zK8}0b2er^Et>_9v;DrRB3djWR)qp*0T)yn*Or~I0A(OwK&OrQ>zT@20BTY!BMGFJn zLU<7e)EeS$))l~Ny>)tj$BWjNr<)MkhG>4|Tj71Glxk-T zPnvpk{E?*^;oSIs3LdKPOmbeGXyav6n#;qR_`X?d?P$COJ7@!-4KN@G)RCwM%ZK7& zetDA5lKU1o%?eYR+Vf?Wz$v2m(0gg{r9se_{0Py4CuWpsy=9WdVs9aH+HW}5tk8*{ zb)7e~{uf7A;n(E*cAt&W-Klgpf}q3(DBU3--6<^{Ly#2dlK#@trAUtMPDKG}q&qg? z-S7PicJK4tSKQ}1=UkzV-3ju>fM}$2NR8ab`b>(k_cMaVD^TNCOeHk9N}Hgj&k3B6 zFw#3io7Jb2F%ls}C4Dr3jIqQ245fK-^E&NxXqD@YxCYyEJQ_ zHj?ER1<8^1XTPJ90R9{OV@6$JGDm0rV`h~wFp6Xb3U&(^jG(EU;Y^vV;b!Sg3A^wF zxKrT2D8r=n<5cR2X)Cm$eIum0Kb;+fy71*0hCJ7!{yTNzlF?PE0>=c8~|Yx8?CmtD(`01?wl5amXkdP~Y4pqilQ8A&$oo+bDvQijiK`W=&5{=#M+g za!l7WIGE$Mq$wJ{Eb-kx#9giY+}cX7Q1L@p12ZdlN|MJ~?9{zLgNzcsMHzM>;9w<} zsQ1*G71*kGQvDfv8h3pi_zQIrJriy(b?F3%0HMz}9}GFdT?n!5j4?lnP1#HG38g*; zGy@1MH1K(GaX~;pL~&)v4p=m>n3c+>HYhO*=ixf=LX^tkO4?I{IIxQ6YQSd(ki%zk zSpMA4n&J6GS`+Ug8i9D=E$t+quj7NKf zunVDtIW;m7+@WJ{Zl2EwjeS6n+!w0&`@hhGe+my@burYw*HHOlwi^3;a^5*womigl zgRrr3*IdR=lp6*9q%yae+f}A+?ewqAKZAVc*h{TnFyf}YY#bc87XJL_ zp+(5{c~wZPsW$Wbr6qeKBie<#jCuu@`fsNG90_F@>n_Ri&xe?d`wEtB2>LmgX*$e? zNlA-U3GD8zGH=kM>AMal{X1cvOI*5Olm7}bxD*m;2G6H!1WE)S44$N~WLrNwzzukP z@Kzg8Amb|pbG-H8rkHDNLx&&CL~g6!43XZ};`GB(U&{Ce$$@IUeSDJ9 zH1HUiqtd056E-F)+`WGUS1}SIp6w=t?k-rS@@DbZ;smoKe;MS034P~dar?ync> zn0F?DNS8`bM@UQsVo<_7LIp`f)Ym#ULFt4P>4zKl6~2_tB!#JZ6^lPOWE7&Pi4Ug3 zSecOHBcUlrZR!Gc$u2mvp?r>ox|j|w+#Z;-vxnAr@X5zA=oboVYR-_?&d!x`C7Q(z z4P`dA&@@oUMN2>|i~f?tr}wDfH<#orsr2{8~^#|V8>qY2J>NwDO$*E7;r5^|dP z>LM;wjS_AYX+Webdo}2<(n;7zANvSRWn6k|IZQ}` z;#-Q{iICwGae}0?YSD`ZOblComDTj%=*?zJ#nWR(f|jH?A(I0PRuIi}&MPDX6+9k)omfdFk@k3_O((A5L>_ zrGSq=Z;E^F!V|xJ>w)!d&OLh|Vvw-dMjKL$$R(5P6mY1yFGCtr`V!+4ZTQ$QW|Qvs zI38zW#>d;=zjQ+Ec1Y~L_Gg^WvCi`LwzRZdZ@iZe*Di|4H^L!_!zUV!9sjY{*6=-b zy)tHQ7-mlpNdkjj9Rj|5M?h!151Yr zA)S5>Zmlg8ex!FA3oT7UI)Y{X_^sk*<>A(iukdofrK>>gR2O#ltmE0@2@EnpOBese zu=kjnK7C*RNmkXN*RGHcmx~beJzuK}k@k;zxokyH_gInpiqH!v@+nC17Kk+J`Pk^G zBo7uyZm$iCqFH>U%E?V?vwU7w1UaBU7N_iv_7xeke=HxEReTR;e74KjU$IYiGWr!9 zZFBgcwW2g}?$kHouDZU1q#P0+i`Vgp8IUKfX`AXz#A96SAj-3az+nZ`wulyfP%R?q z>n9Vz46KP}I@E#(T$QY!R~*^D0A=#8Eo)=PD(EYyllhT`o1~;YEFNm=J?zbiPJ6h-uU$1WEJ?v6|MWMsaM@bABnphF_q=8d=8d1~n5DR@BBC`Lu0FEyu+`0b=wB8PLwN)zTOxtn|DLH>G>`D4 z)el6(X(*k&!mWWcF^BVFd=z?%hiDl2Px#UK>kgE6{Cd>p_bNB0#bR$A7y(${B zJ(NmrdYPmh_&w-W%nh=;k8$y}2R9*#2=0InV<~i$(nc zhj-3NS7=Wjb{U$fU^d<{hP7%bGcEAg0+Jp!e|EN?YX>Y9;$~y7NNuEPm82gV7}y_k zM(+0gjb9Ij4@P*Y?Y!v!AkFQ#LUo*NmVlX#@* zbN;yJ?^Li{pVZc}gRT^A>p$R#`7Xle?3aS0ANyYdF%*p%>8V(;t&%LQP8d+S-0oq- z`fFyQm4!gHu`{P@mDJ&(iu49tZkuqN84WBuzfTHhnyNF}PvF^q-Vn&>PJ^n}nQ)8S ziO?!E7j~wxXky6v2pt4|E2IU?lbgI|P*iyFosCcVl)*e?jCvl_^>l)e68_(zx7!?b z&nq~nLi}{+Eh0gRXX$Zk>tWrOyHdL}1>`-Hi!MMqT3WEVpYY<+(#VtMX+B`}>)+)h z54%kgv?QCBTj^z=#VeX*b(WzB0+Eq(lAwoJ9v0v>qF!1#(5oVCgZIrSKJw zYZ}at%MxSnjmZ9`a#3CD{TBrXrZ6%+oD1aED9X~m44 zNGRyG<3HTj<{0;?DZARCyVf_|`zZU}36pIY1-jSb#rmo(EF{&jGX}4vDUZ;Dt7C6! zREDSC6H@-3ndxbVGO%8v5Fd-J`t`N{^2B#(rxh+zAA7loRB`YYXKK@|X{6on^l~1B zNkKamzt6c&BULZr7TqzmG(uD39<*rrq8Qb(67P4{*IPzp`CgqPg{jSl(VmcBXMg#^#`geP zK0ZFS@ApuO)@7(;lQ;p;WsZWgCRs-V5aW)6(?sgIv?9+@Z2m3JO2AAU)bYb6CB{$L zboZi$4yihzof7BU#pPTkpBW<rZoHAoI#{ft_krTrB zFVi}a0Ov!oGO)KvLlK3G_!meQzh?}3QIG4#+0nIChj$7+GaY(wi7O;_U`qqP#~^0{ zuYn8;oe-ubWS`05$pPFU77ePdaiN9@fm0Tn=;_fA$BaOQAKC&;rny$$Rr&UHWZ*j| zlYgm^l|ohBlzYQHmaR0+g{^IBLF3+tJ-)mlGSAC@}>upx6t$j!T3PtSBx`=5!co*72j0sPq z`5~nLWE6q-uBPX|Ffvj_ZmMziT+g7>si~MHfY#`uI2)3x8v*>D>UpvpXcSqfFO#Pe z>wsHO$EEW+G$;GH5j*&5gA_?kNon~`bU}MJXM5W&olm)OBiArZTsK@=l z8Xd2q6QaD?xW?bhsO4C?cK`M18_(t7sUK&>UpIFJPG$n81nwcNVe;z9jvj^7h#p+6ZjzzB{2!&8dCOOK{!EEWdPvI4O$Cszra5jJf> zAAq7LbBXC%+-ZdffZ^N@kIB)jiHxK0pM!MvJ9iTq7e_;u6$}r`DT&I)yax$!O)R;q z6Di=B;6WS*W9H&!55Dr*LxT3HL?Muq$)2g4ndtZ~v=tBLdDHqr7E575`NMx7)mi(t zd?pmT;GiRjHkYD)Skz19$t?RV@xE1?NcQGnIhm1(`RP`YIvx`Ebe+ox1Dpn22hsJf z`@*k2!^YUafTd&uk`z}v%*6w*#lVgZZxeQuNxhRvu!?{CO5+zW#-AWmlC@N3k&+$s zb^4SR@1s3MZ8}qI5|b1E-V2X>>=10i18CsSARJsbrf2}gVu4|C?uLG%Iih)khhsFH zJkmS4Nx`-|@;x9d4d&Vv*f0Isi7I!71{xQghMsov@!PBxl=_fB03WYY#`x8!Wr-=} z=y*Chx}_k8UzQ5;!a7vNK`--Im*t~0I3%7)=Vb0$i;mfQOU^P&S5A_qT(eQhOg62q z4M|KT-Cz4j;o3*-+`70*3AU5D5j4O4`60U$*48|D- z@6NJgan8ZM)-nCjLO4=dh%wYv0#(~!HtgDx4S&$lJ;>eW5v?d=Qv=I^6BB^wQ>c)f zv6+XXy)8MalW<=V>d#B{K$MJn;46D1=0maKsglSJy&gqjuZyJf0u-Xn!g!U>O! zJ$ZF!-N6I|XpCSKJacEv?WHBnTgwqVCj zbUhQySj=2vs=qlgaprA9XNoV>5|R?q!Vn?|xFMF%&CD!s^6*8f;QAnebqk?d!<4_N zeKrTA4h^w*0&1XkpUKZe)bVszZN5|3y;NSneA7Icn^~3nKvg)HcXZEkX!( zAe!8oIPevfHscqsJCHH43;)wspnsf?KEN!bfD+R}QQDU5+Y4LHib#O`y3oaPp9_SJ zR};_)TJQD{4hXb6%;-6U=u1Px@VpP#Uj?yo7v5e$)s;b0_^!}JR<VPt4LIw`Umh z#U&(mx1S(-fe^-ANJWG{diw0dS!#?*nZSJiOKV0SBZ~`eLV)qI1Q4M;8^x?&*7jAt z?$4?Y`X#9e#1fuD?H;vH0=a27HQg98xz{-;aSt+9W1@bBH**1aICxB7%YeRCMqWI) zV8$2@BDgq_lHu5+EB8j^m*1;Ea}M4O0)0u*ln<+_q2*BxraCl;a{wh{fIfp=GV}Mw zfr8DtLR)8tDGjbfN0F&mJonaJ&L?Dv46QX;n}9+hEdwo)gqXnNyp{pg7BrY>WmT2& zJT3i6Hev;4WEsmF68aspS9nk?alBy3*+D;cajS0dO5Si3mjUFk=;y$Em?5&qj@j3s?wkL@w% z>`G%Pe7?9TqCQk+tn9r{<~1>AIR_v;_hMV{?QbDj&v)-J1QGF+pFnXEzt)#TS%A?O zfHY+y6MWp))onnaSn_u*Cl!vEb=l3&%4C3f$L!+qFHBD-ojklP8Qp@S zQCh9z{KqiocKg1tAE{P@DP-mdCiG2p{cmZG8IV7AM8hU@Gw{DRbmgbMc9*17+?E4LH2;XoWk$Gg z!Cki9a`eJmyPkZG3k+rZX1Qnx7~~!cdhKBUKe3q|d$p&(!{zs6$vQh_XA3Hy!G!_x zeE7tUgVb0VmiwyU4Jcwx{5qp~?t|ly@eyIG#Uc+NO6||sW)9^RqR-O!Mf>#MA-N@> z2P~0(|A7bF*h=|gDa&gK;}|Ns4X(FDFNh^PD&dm@L02b)$yss0d- z&ct5BS0%WtPGE=+y}>fAbMA7GB5g^%dc)2Grnj4T@sbvoSeRgKB|imT4xVC^<4V*} zOD8-|9GwPLoHEeIq(PF0}#p zZffojb3b4G;=$ld_AF%|yh}&w`*isR(`0UD=S4*KcaYe`t?e|U`^tsT;Qb^i_WbrE zqx_}BDRhc`GY<+6w_j-&5m_Bf8W^={Rh`}uA5i#IUie4if0MG0%ZE-yFXGZrVk`}k|LpL)|aZ` z1mTGFyV`fsa*@6-$wuMts&R@lu%jJu3$AZqtKN@6pI#)7AMJQ|m9J!MTf#P~R%Z7S zuBIm}OS+{XTAaAU6el9bLK6kMdX}1E!(b+bUZi9aHVtoY*bkbAQPBrRULGb7gvQ@G zt=qwO8435SlkCYLmGwO@KBQ(oZ>C7K?Q@J0&!mdQo$um57VDR1QRe}0<>dDHEHhf6 zdU+r~J`8jzj};uqlOO~>kt(YMnxITiu}nr3ARL;ff?VA1DvBYAgHaf_4sZ#R51%(m zx`UMZU99^Fr2B^%@NBFlgp7Mbt<~?!oEcjq@J_5NC6%S;#=7{%Zc=`9^j?L5BzpDj zHQn6(%}*?$>PA2MM2~j9fA`C?-}%s?A2oc^83jsVM$AE&*l-o9@LolJUw-=g{=5{! zZ~FOlX(}E(pIt{oAMw@)Im7FlA1(ywAeJ)?dqvy$nzl4BZ^EzC9|L7>a`Df^oOmgTPk~-vqV+-YX#duR*7r||j z5I=C=zE_|Egjkti6qw$QHh`xz@a-UVmz}27V$;e=8#TVePZy2TJKd!STGIdIi{hN#m zh;mbFiS`qe*lgKw9`!jK-{{8-S-Qlw9sv;!gk*sYQ1_MBcnv6cftNb>`9o=bkM;nQ zcB7Vnwn{l(OXi<;)P3864rOA3k(MDrSa@C~u;Oom2{$5)L>zEnc+v6{XY&q;QqVZ# z!B~Fs?mvm&f3fbNS9|ueO2Y4e_*QV>vF<&^;}i)(5w~EsNIXjLQ#ff0S2M>W_qKaE z4TF|2Z4>z;JsJF?6u=6n?z|dEYB03*ZVP<&bLW@qU4Q?e$>zp^R8jpS!7p6Qrf@_A zYjRjS2TV$o4d!_utWm0E(L{kTOn&&O;~l9Im2v;AsmUT*bB5z_?Y_d)`han`3mhBM z5R;O*sPtK%dee6w2gk*0P|bALV^Cl5<3fi8|TiIP1dCJ)_RrPRXIYWSD)&1?PZ4M)%7t~w>yy{Z}IgR>96tUL{cr2uNQ zkJ)5!CfbWP*z1?T{eD-+rINb0r{8y^dPDzxyX(59AcK2~v61;ijr@6_9mR%woI4 z^5ObmUJ10IcJP22F3<-_F#x#HV?6zfPkni4 zGv8UOT%$p4A~gd{8`g&mg$l;pmZMS|1Toj#!onK~KC0HQffhrq-?P~tES7U~bN%P0 zDQ6bbgZ3)r4F+ZXWR_urDmMh5>JJ{nej@iO4EhUv1Oe5vRkY^Z>+rE-X}K7&V#pjN z39|tn^gNtOy+C|}-OrVn=g4&($9IsGKSN_wLyvA9ahQq@sEf?%-qVe| znOAqQTeka=Ciu2T4Mt~0gR#K1T}MQd@OY2*(+gEODPsfhEz!|+ugUPvk-?;I0WWAS z&|GDWQzbmDxrurw30Ev!*R|dDygxe50lU73>8@Rsk6?xe&;SqNk_4uOWzng9Ba6^r zgH&J1w>|?sQwAq9TxM^QFc)7uFrdnHNq4H<`*}DX=UPLlkh6;!K^xE@1D+Wy+$%wD z>bE_ktLU0C9hv9E?vgkX zqW^q}NZ%+k(3Jg8UT6I#=45tD( z)A=-gm0((sl>g{JM_T=_UE@4Ln<7auTmoX;kO)HDg?Zv}zg0@(iNYs(IHeS$;mr9k zz~W6R&iCeKS&e!PgGxxhvV0Z?M&q8ch&INXE5*8(bQT$Gvsrh%@t&c!aU^7jozKxx zB@z~8bp}WGg*KL>bLWAU%c3^?_o1tKe!@xSKHW#QTddr|J&5Cwbig>R{tH(f2MIi% zO%O=dd(0OG?NVUAj*xnQ>^`gl$N^+0}v@-9qoK za_eRQ+KtCL5dzFh|K|_vcFFxw?dw6x_Y#|Lf0?^OOQcQYIW*{U-fxLz8-#NUtCIz@ zMS}i)U{G_rj_gC@Lz!no+kfGD}(M&QW@)jPULV<)94uiMN`3CkN@o z36W77kE*So6?CEAc+hH^E5$EY&j{HGyv$IpxqQ0Rrh02&D&-)I!3WYsEZouixL_{m zjX736^4UKglEKra;Vaf|&wcqE%)8U^ktxU2+@VWC+!R`5H#}`!-bTxqPo6wsZ@O*4 zgDJA)t>elH(+(*E)R@$RR&t|XwKsdpJuARE>_9aY$aWNIFXFk;=efddPAthQAYfG8yBV>T4g$ReX`V;H+Y_3tqh4`%} z1co}*x^fvUiKIQ+KA!#=;f-lu!Ibpn&KJ__W08Ru+IJAw2S54*S5UIU|L`nep=teA zAT8dr)f%vn3RYyF&{{mUuju5yaV#O;@ck`R4Hsp(eLkIk^WG)Z;pL2( z5O>B)46|oJ5(qFBQxOs z`dGB`%-k4dEqQC74G<)UBt=9X6FEH%A*NKm=?#n(V0S_%v4@lg=py|E) zouL}m_f6!&+C%sX`enMUP4eL_G?@W0_`Ols%6IHN*2;v2gxFOPki7{^rsvSf;jdlO zEgG$pUbV!M8*4i&`3CnM$X82vXr+@5=bLXbzQTR&!z0L%l~kq|w{9zEEsFPQ!Z-fq%MHGTMv#G6;|lT?*tWeYaH zX}Ueo9QIj_{8uFr?bAg6gfQ#lB?b&E^+1*e)7B>KvUt;O_*^<}HtubrXtx_Dse^bQ z({p9#D!*W3_AaXdb7GW9Azn-u*y%gFC_y<2W$?>QZ%F$aSItT!V}Ri<$06=(m$E_Z3UM7B$ z>iTj)4+;^uPr3;=mAhj-U7VQ#@0mZ`3XU75nBN$3AN+iR46`LbwXOvooQE-6D6_*? z^j20sqcjabBvzeP?p-|kFNs5KPuR2W&bxM`VV_5^`Gn0;11{?v`S#+19cN4PdYGxr z-@Z{nh9~eb{evIW=I{;zyMVtNApgOv_ht+L40;61gWT;(vT+f8SqdM-(D_ zRJH;oO=eO1coBsb&Q*|An|u8Y`SzRofcCVDl7JzFSY!;4L?q%pxOOAyN#TdR&q2jn zASv1*J;XwrFQe(wj}PxSALv)Fbw(a-C{Nz~vV5jw8oYWT7<|8{siVMO_+(TLM1cPR zf@p1j4ksq`BwM2<_zUkX0%w7t#+Ip}iGx5FIlIkyOUkgrcKQhZFyXoc6_ZyZzXdN@2YV-#C)-oJ(VlmP5k03k`G#hZm38TY>*))Ix{bAQ>BHcxuqCAT`)NFu0^@cW+6)xV^K zdPE*6o}ssSIP2gRpl20GN}nF2{a0X256I{Ev{bTD?*IXV|J^AofA<|N=em-+=^r#BObMHh9Q95l$BLoGy6+z=;u?0`=$O7yo z|BZy0WOE2KGXKR*#l2-PR4{hFT)KyY7y4N8f|?@Z-dfJ&4hnnMqPAG`W88}XASd2g z?L9J(es0g&uU5%%Mf~#cK35BPOGz?MOA3THKVE*hJRYNcSyW~uBNuXjOdi)-L-my- znio5ee9J*Orm#V<$%={TK2z zFA58}5ax4pb9x$wmD@n-)`0QJb+Rx4k_FaP4O(9R70Dl#AiI3@WpqsItRvIK~h zc*da~szlDeiK*IdPxdfTW` z$MXHZIsx31B+w~Kb~$|d%%W&ZxI`D_t8BNDFSt2e(q*_C{*v4;L}rk2h0*vY+J|qe za4sXV$o9hIzOjvCW%E+ni~xDT^{99_@BH&y7-srSp#3e8#jA~b47%g|H^XiVXcL^T zbv}=9GcgK>K)-%o`qBuy+xi4VC}q%ssKAVjV(O1nNXU3z*V=y@i(@&B{=@awgYKXk zfnHoPt!K>wdH5vra3w5mJ-Dbs7>n&+>0;rt4$t|}$gNJRG{*eDgWbf6;lIf}wdT%y zLHfZ*_~(lY8V~xcD@#Scs`XH{EK;ZEswSh~NB=0y8JoqI zOtxav-p5U}t#MObw+-!N2ESFl>o0k1B)~w(9UI_3Km%7nt8bF3p$7BGzXN?^$#rs9 zaM(F;!w@r!VVKWP9=sdEFsZ(sP*J6Sv)K#1I|Abif_en_J0v1RlM|05`XxLVmOXyA z=^snldn?kKn=2bM97x2ZuA+|S(^yn}w~-UR6FDy$$?BP3KkUbxH8HZn6Q$+k0G>sh zI{4bKdhLfQ6X^ zsVlmvkPR6JoR|K(z}zvy+&A3?r23u#EzJ7t7LZ*rpH$>S`@!IV$bB(|cj^Nv+UqM0 zt0)D?PZE1fN?v|#*#+Ow-U;YFUb3yGRJb?Z=G^n8d++}CZ>fO~rPo4NR^!9j{e#>( zGRg!rqy*Cdf;>q?Oy(ay8#P)ciq+nhUorU)quk-Az!UfFovFYqwr6c2Q;hj$!i+E3 z|L#r~WqzOy1JSX84kWgTxVS+jw!JfmU2Y@ZD{v-8Z~{%wt1zy^OZSnA@(j!6D!IJg0`ppxyD-c=O{ zqv=C})9$Q*{P_$MP!|Y#V6A{6BqXFW1D&KZ5@LO(=RlI891PuE!U|)e&gZUWnjU^x zyrqiS&^*2)3h{_Fubia&{GQ=l;!4%+yI^DUPfewTuDqPDm-hOxcTsnxA5#Go1agbl zBhjy|e=|Qhh~%g53Tp+ILX)iJ>UnOxd7u`M8_j&JW9QwI)|hAZ)MGsS{PEZlci_f*l!)SW0`3@YRL4_?^c^<&j! zw%hLfcMnxv=t|Gwxnd?py^yE9*>}W*MPHMxqnu<@B{^lftj24m-&4O?g7EsUpq#C! z-+guC$lJ*K7qC8&-+6b(+QlH_G`iW)&~S5h=BWZ+3RA;8Q2``#1o1@4l87E`>bAY?IFO;MHd@Pn9&qmal?AIpQ(B!U$fqwdDI|Tfm#z^dk&ZYU1c8cC zle!xB-cy}b-GcPPU=L!|ezj#)H`3=<(usdU_m_$k6LiQd zV&sAb8%#isZNl4Feqd12ThzeNl!x($ z;?VNy&;)KEi?=|Oe`%Pq9J=7${vb0e(n!NyBO<_oKbBCQfazz4l^3p&rTF-OQuKvp zeyO)+vvXj^RpRay)r9N2$@&75{=RTcx^;RlTrgAk3RdS^)#`!dXiy)hPeu;nF31!NGLWN4uzVfba8O+JK+;DN1Rmo}|5USYW{7_hL6y4j7kZ?VC zHpHxGR8Wkr?CS5JFO(v??p^U*Sc_Vtt{VIYh6G?}k=PGGZ`s_&8u}Bq(r{-*Vu7#l zfc+1voSnTs11`ul91bHGxh+&XReXPRaCe9xohjHTN(EHW=z~lin_akoUqw(5Num;0t zoaIr$PXWzetqxXla&qoIefqS3?ih$thBt-i;x&LK-OBmai+~Q*ve_R5SQxgy%FDT^ ztxfziId4l~U?(e#j*h^(FKdRHT!g|nUFPa0h($5*8v9f}3qu=9Mge^Dh2n7M`|;W4 zRgonkAVf?|Otg!UK+y!98P7Vd6S9(^`|+c%gy!CQvcef>gT!R2yQFq~LoZx(Dl1qv zRPCOk{MNpfoHT!ylhn{CL7gRq8f$7GFM0l#%vo7MugJFlDk!rheqbw@cDyq+5Ozl>JehK@AudS`^yu7orl2V<5g2FF! z83HkDfnh9&Y{J@URw5o3p=haBOrePVSD1h%=LrToSWKlzJO&{MIw8)36no(XxVRk4 z{2tFZd@+1=Y`um(s)>P?euUgi<2G`dMl>+;KQaK1QQkG?{5RQs0cTos&6@?T;g6xkchN1tF4ULVtC~IgO!N`D0O=SQVn1}aU z6|N1k`T$?S&zBMjB-YNUloCm0%TwXM7C$hDS){>fAf(<=By-@dS+{-}*{KaLK$@3u zuy)%zJ)i0V_~|AUg7RaVUo zZaMOs8=YQf-Gd>y#_f}h()*iFP~N>S$C?vo?g~~8jse%_ z*-vmnCfG@`=wK|cy2j5Ih#g#5i7E--RCx14SuZ9I#epnFK892hxiFYvG$3|lW$Sd8AxPuGpFZp!W+hf z8Bs<|8H`X<(_GN#s3n59n|N_%C7^37Y-njK=BT0*N>aT~+Yiqd6uF7J4Qcq@5}n!4 zrTq+w%&vB$I!faU#oT-Q*T8JOZ4t3r30Y%*T9Af`NsG%SLY8^ibL&5!aM=EzyaT6! zp&NUbTc9)(nZ;smPoR6o&NR|QCr9T7rTc6J3tW!D;-I3e>=+jp*J$ZZVhYv*=PCh` z;g~q5Lv5HdKWbZGT>4>jlvCLPFrfnCxnIc3OWGU%VL{A0NQFrbq6#WN?dIw_pkvnJ zkNp8kN1Y-IwQIk7b14<>sLUp3F4!pxSv39n9}0E8Gk!iyw!vvQpTQ6^P4DZq zR{Iw*iG3enJ96Aa4$m)Of<7Y|O+m~vnu`7%dY66vwE+uqQ;`I3}09pM3D15r`K$Sj-yQ?O4Tk(DA=$P3()HzSHJOoksx z_;4X|SXLx9Bq~#tSC?7M_SX1lBg{XZl?jrn%s11kXx>QOT0n@&ym*M!%*icr21;eB zdd^zy7n1;(YeFY2w8tD-c`NQMPn=uMdH*0*vX=d0|NAgvb&Yj>b$<<-3t=M03t$an zQvff9=0Y#m^!LkJ*32+f!55m&S3Ra($!idGXH~fk=*r=>$ME0NHd#W^H3C^HE0%bC zCR`;C&})`8$}mAPEm|%DOxlZx%j3^@I2_4>cR&#Jk4a$i^$n)orI9)m>!GxiWBZuC zdwnCelST`Oxn66zwB@kK* zxz~^c`QhRfKjhmvneG|49pG zJDo*%W*Zd&DFl3RL~%{$w@(e&}$EuvDXb6`)7Tq1jxKO zjnPzz9Szv-KWif>bz}LH^l+I4R`5+Qt4apaP*Kd_cO*2to1xcWe2N}qp`6{s*VO$Ekm`vwSv>+3V0t=Sv_$$%GrB97l8oG{?XWSV^!ix%Q^DZd3SXNSxe7=XJNZrbM* zohK2Kd@{r#zw0onJN4R#ov#MFjdiHn%>wQ94rbWCZ!%)8J?hoN%jh2S)h|I?DTR;2 zt983~lu$C?NJhq!6yp%dy|A7mWW;s;uS=8O*k>>yV~yf3`#0xd z-RfiV>H@-890(Ix)QRGhZS9nYv3{2R)wQpeOl<|72aO(#oP(&44j`*z#w}o3Q-*zc zNs+~4Q3HCAq2>0&gu!h{st%}IaUWpH2*F+de8yvt+u@5fa=TmF-K+y__>6we?*4{? zu*0RZlatoj#v4R6;1E<{8x_? zki(2%m7&3{gE6xLPjRHUAuy~z^=E&^AqSu3cIEI0mfHL6Axz&2{#n|IM@uZad6Rau!zRvBjDLp`y(4Lh zm+?=#0r(LYZTToe_|T~Yykf_#`y|U2_9^B%p2jJl=!$y&m&&2mpyAn0I^(Yd?*SrdBob8W={q14Ne&XK$MLdueUvYXq05S4RO zFcm8JGk~^a|KE`^;;yuJY>jyTTv84UFA5yWR!<^aZBw#ep_Xt-}*kALi=37_oQ1hryHkqVJn#8+TEJviHWSjQCU z!SRQdgMJ*`_Zf)hV@raK6bQg=0{}Ye;R#gv)yvrUn%}qOpo~>A3n8DdDAQ48Wy?Ek zCGSS4y5DYSbrae&)j&aOEFT)~mg;Qo7YB&h0dIgt`4oc%G*6AoHQi3>>;q&A$sGxe$Z1HM<#ofgdj70*E?%;gaj&xM#y?P?y_L?d9PMru$ zDn;Ph?RnQ!QD}a6_dbcf3pu%T90vJ9PDZ9h*ksLNX=0M|#DGeK3?>J=hWp~vaC3#N z<~tW)WlGE9dDcBkMx%dbn;kn0vhk})pgghmS-_hZw@yW7Y+W_*EDQ0=c#VeeI6ib! zxYxG#ELJhD2PF91AIW;&w2Jc2W|t8K=wW)n^XNuifG_ zUz%nAwMyN+YLAN`#vL*s{bD;CK|`(7^)S0yTix2=UeN-$|yqwLyRUYk#kwE8Ya>A_Uowy}>fPGclD{bufE*d3lIoAbt~9%hT%< zH&*-447wr{hdUcES}adSxZuPYzB_K>ku<0BP6t!je;YSE>P4u_t*{w|Xa z`7i5|%-P${tRt0ocTzVrrXz@|zV-_C*9{_KHqZx=G z65bOx@1dytG2@FT2-*UjdvIhBo%jc$5$^<`X}f?tkoKkV1M;@%DO$Qa+n#H!y~ng< z+duan<|Kcumq`B^2mU@!018utf)^5yFDGU$L26ufbmr`xqi64oUyr#1WQOf}8W)Hp z(TX7V6&L7wy#sptJs^S?Fmph14RUL@Y%*{C{L>NN1Nd=;A;t%A#=wd>1Xy=d_qRPo&UefT!ul&+ZzE3O zZ5RPBL(iX}ueG%fZY1Q7@?nb3RuC(4te#aFUQ4^#0c#nPCKn>XYZ0{B}&4NygZWPmdVj10KIVJ9-+Wu%-8)Pg?{ zQcwcB(sd;QU~U`p=h->ie-F=bF9rlXrtBi<;b;H?hyd`L;An>TMgQvd=n0cAD4!Hz zSrUC}UUL{o{ojDpzr~ZqCkMDcz{_8M!91UC(Txw>sIzpged*56_Lq=Tn|9PGNPb_z zzB2~=XB+`2kO04k$eT+LEx81kLY$(H(5L=#=Oq{J8d5Oa4g`*shi~in4av7(-3>)wFQzaGeg^U)OvfVcAe`i4b zyeK{MSbw7VLCavTAu|B@^I@=FPHXc80(@`R3-n);e?`1P4(ay5pfS)bx)+Wv_`%*m zH|ne+r%HA!P4UV1?Gw*L@Sl+c;3qif%_Uv~qHW$MGD2gW{cz4VBo2wx|c+HNF| zwk;rLF*0Zcp!6CMO29q%2aJ-BJn6;m*@I!#dv{0e`(k0@7Kc5#0Z5<&$pA!a z5s-jGN;jq}5fR8(tdyw(9`O9YQv!UTZh;cmrO)X^3_SR;VJR-Q@dhUBV7YCS`nWR_ z2K4~&MgY4JK@YE+h7V6nziv+J`TYizOd7hyU+^0 z{v%|i;dA=l{lLCpV6_R56p#!sQHXaL<0 z03{1A?WRiskM;IgdM*MV0S_QIz&Xl%Bb*!IedehbCiKsr-Z&<^UwMD6VJ)}QiV-^T zQt<8X0GU6Vc42pbFTb|P`2We55g(9BO(}^G&El|K7wlO$dCg|0&B=gYqhR&`MSydv43H3j0Gw<_ zfo*JpGXWZnhAIU{0NC&v0-Sdve1RM@`54GuE|=$s4FS(K)5Og+(Vs`~qrAu6nBg3! z@F?)%T0~AxT>Q+-$cPbnHACVO%92E3E4L+`9y)6#0RBd3zo1ewf~W1yK=i}{gvP2K4~DLunb1lG8k3MaOqK}--d)xb26#& zV3DO{{W|@jveL*J06o?$#M}ZrV(PI2z)hDZv+P55MBw8&9$ACyINj;#cnRS2$ev?E zDKEXIVX3%y#QpSe1s>Z^4+uaHN7~?bMNF6!n=yHcb#Qk7+I}&yWr>1zyS7t*!7bNN z&|eAdGTH^+fE@_<<g_#1gEN=kg14*X%h@}5ofAQpf!+scnZULJu`0w_|NwSy`Fz|hWzf-J$ zK6e2bY?H{K0NS=rt^yt4t^`+GYj$&eZA?u?g|YI$eqDLtUR`-@jini^J|zOFq&YoG zOO@#nRRR3HF0%*BkB6~<pH3>;MYpPXJxwE6e9eNvrY1!wFKaW1lTMAzo3X$1M(mtBZ9<%)3Y)sewK65 z-h|B=F53U@#YK0v|DME&NdbY~9|?FrT7eLDz{uMSZ3DC|o;(BjLlk3Oc3YCw+7aE} z+G1&LZHZ`YYBV%AHtL%j>V=lNII_=ff+79-A!G!?6&}w4iY{tk) zogp@^!yFZ@F~!D;=Ga(QWL&%}Dk|C*6&Y!bGFe(8jmCxut?qzvZ{Kd&h}E=heJ_j=QdEP;HwkgY7_iNL}ULA*o+3FGb@mNjTW?8Wm);q`@ z9gmR&S4y{lua*q>b0h$f>;Pie2j8X$&I_RxLu=^$`wafNK)^jB!(ku|fg3#n&c{&# z;Kx;chWnSpU=e`;SSWua)F}Bk8<=hXJ2o!N*VU zxz0x5pVb5qpc-Iqfl(G8h~*SP{N(w0Iaz;+xBz!87NvRpS=JlL4c2?eQ0JJS{C_pC z$7{%nB?qgdJml0-o* zh`8!H8_qMK^#epC$W(~v^|@Nm2NDM;((WW*8s8(!jh{&!E#NM^HOHUm_+j(wd&)TA z*OOC=clbGdx!gY|to}Jc04mjh(rkf|ix2Rw0jh%dX@3~sKjm@VdT}1T_XJF0{G7v0`N0az*imeMFcEZ z812!%au=ZToMxNAFL7xRuT9c7iukkcU8cT zy0x_Y_A0|-&*vq$Qk?w{jV>ys$l=IY|IABH;HZ z^fhhB#|T)kFp^URR2k?Zw6U{4I)8}fil)3`%icH^Fg(vOii~nhA=B;Gk>QT#Sb-O)*z1-H3^{yq+dzQ}KbimQ!Y~URUpqJU39; z42C+!r~>`j76b4G`Xb#bIA0~j%5COmHkPjM0@r0275O15j)kxhn`6!b^52R!e?vfEjeLywk!(7eCk z&)bemKyT#aJs;HLSLua;A0~hvB?2Y)fS)~JK||h6piQIGL`{EsQg-TG)6ionMOi=w zomSpVwC9tNj!BdhhC0U*lV}MU_=8mgzQ#tNB!H{%inKcb>|4lQXj}CKo!$?icVt%$ zEj#g1+krKI+FI&zxSBbzH*=O=K8Ne!MMv!c0F5QGOkjQ^;#{k$h+_d$WwwM%a z_R)?;NOv}lfPa7)aI zl*;;b*0Oa^Y%gs*-qOT@oCkXw>6)UeWa+(G`!Mjs1kj^IAa@V=X+)i$m;rMYOq@#K zn<*+G(lYgt(J9HZEm^jH&1rR(6S0-LV+JQtUellCN+T(*ENB@d-jz&Sf|x?0#5fY^ zio(*?)TJ;1-{?d=F$oqLb6^i78>{GE8qh4bQ;Jctw+M}-Mbiw|>Pf9oMd~ysX{*wd zLn|ZIno7m*qlKRig5DP2n%ZD2jjpU((o(kZZ@a3RYg?FnZzs$R)#a;etG2&aRNY$B*1=hD*4kx9-ojhGSa>B% zF9*FRb1w}1Fah)hRlq9)txO&Ga~XK0Aa4flNSnc|H|9>yj?KI9`~R+3fG~--|Zr`Fo@<{jLm53}8I?cu?}eH+kd1 ztpsvDV8>f_C4b(Vr4L*CP^q}^E$CKI%rg)rG7u$jxnyME@SzU0UV@7L+zF*7|G)g- zk`n;`IKTl)lmU(hqnrr*HMwl}26A6kJ`DP>0yyiGf!tl-f$EDOlwSAtet8pQ;Fptt zC?f$;21b_;z$yXQAHbaopDl(#A0~ivg$$Hp2Z9&y`f5u5Ursp05BR;208s#>OGXaf zh{9JBefaWW(1!`&tS19CgrF2pP;>OwSp4002ovPDHLkV1mc~ZUq1U literal 0 HcmV?d00001 From 78db83fd0e88ad1217f96ff83cb077c800ad9494 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 11 Apr 2020 13:33:19 +0900 Subject: [PATCH 0975/2376] Remove TaikoPiece class and localise kiai for now --- ...SymbolPiece.cs => CentreHitCirclePiece.cs} | 0 .../Objects/Drawables/Pieces/CirclePiece.cs | 19 ++++++++----- .../Objects/Drawables/Pieces/TaikoPiece.cs | 28 ------------------- 3 files changed, 12 insertions(+), 35 deletions(-) rename osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/{CentreHitSymbolPiece.cs => CentreHitCirclePiece.cs} (100%) delete mode 100644 osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/TaikoPiece.cs diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CentreHitSymbolPiece.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CentreHitCirclePiece.cs similarity index 100% rename from osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CentreHitSymbolPiece.cs rename to osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CentreHitCirclePiece.cs diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs index d9c0664ecd..ce2882656a 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs @@ -10,6 +10,7 @@ using osuTK.Graphics; using osu.Game.Beatmaps.ControlPoints; using osu.Framework.Audio.Track; using osu.Framework.Graphics.Effects; +using osu.Game.Graphics.Containers; namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces { @@ -20,21 +21,23 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces /// for a usage example. /// /// - public class CirclePiece : TaikoPiece + public class CirclePiece : BeatSyncedContainer { public const float SYMBOL_SIZE = 0.45f; public const float SYMBOL_BORDER = 8; private const double pre_beat_transition_time = 80; + private Color4 accentColour; + /// /// The colour of the inner circle and outer glows. /// - public override Color4 AccentColour + public Color4 AccentColour { - get => base.AccentColour; + get => accentColour; set { - base.AccentColour = value; + accentColour = value; background.Colour = AccentColour; @@ -42,15 +45,17 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces } } + private bool kiaiMode; + /// /// Whether Kiai mode effects are enabled for this circle piece. /// - public override bool KiaiMode + public bool KiaiMode { - get => base.KiaiMode; + get => kiaiMode; set { - base.KiaiMode = value; + kiaiMode = value; resetEdgeEffects(); } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/TaikoPiece.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/TaikoPiece.cs deleted file mode 100644 index 8067054f8f..0000000000 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/TaikoPiece.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Graphics; -using osuTK.Graphics; -using osu.Game.Graphics.Containers; -using osu.Framework.Graphics; - -namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces -{ - public class TaikoPiece : BeatSyncedContainer, IHasAccentColour - { - /// - /// The colour of the inner circle and outer glows. - /// - public virtual Color4 AccentColour { get; set; } - - /// - /// Whether Kiai mode effects are enabled for this circle piece. - /// - public virtual bool KiaiMode { get; set; } - - public TaikoPiece() - { - RelativeSizeAxes = Axes.Both; - } - } -} From 009b1383648dd267e76ee13f72daac2ee1bb1458 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 11 Apr 2020 13:41:14 +0900 Subject: [PATCH 0976/2376] Prepare for skinnable versions --- .../Objects/Drawables/DrawableCentreHit.cs | 4 +++- osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs | 4 +++- .../Objects/Drawables/Pieces/CirclePiece.cs | 2 ++ osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs | 2 ++ 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs index 22d62442cf..f3f4c59a62 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs @@ -3,6 +3,7 @@ using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { @@ -15,6 +16,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { } - protected override CompositeDrawable CreateMainPiece() => new CentreHitCirclePiece(); + protected override CompositeDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.CentreHit), + _ => new CentreHitCirclePiece(), confineMode: ConfineMode.ScaleToFit); } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs index 6dad7af907..463a8b746c 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs @@ -3,6 +3,7 @@ using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { @@ -15,6 +16,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { } - protected override CompositeDrawable CreateMainPiece() => new RimHitCirclePiece(); + protected override CompositeDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.RimHit), + _ => new RimHitCirclePiece(), confineMode: ConfineMode.ScaleToFit); } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs index ce2882656a..70fe4b7bb2 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs @@ -71,6 +71,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces public CirclePiece() { + RelativeSizeAxes = Axes.Both; + EarlyActivationMilliseconds = pre_beat_transition_time; AddRangeInternal(new Drawable[] diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs index 6d4581db80..babf21b6a9 100644 --- a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs +++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs @@ -6,5 +6,7 @@ namespace osu.Game.Rulesets.Taiko public enum TaikoSkinComponents { InputDrum, + CentreHit, + RimHit } } From dc56be0a1d946d08acede69fc6619dcc47298e3c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 11 Apr 2020 14:20:09 +0900 Subject: [PATCH 0977/2376] Add support for skinned hits --- .../TestSceneDrawableHit.cs | 2 + osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs | 62 +++++++++++++++++++ .../Skinning/TaikoLegacySkinTransformer.cs | 8 +++ 3 files changed, 72 insertions(+) create mode 100644 osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrawableHit.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrawableHit.cs index b927f0294b..f2198031db 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrawableHit.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrawableHit.cs @@ -11,6 +11,7 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects.Drawables; +using osu.Game.Rulesets.Taiko.Skinning; namespace osu.Game.Rulesets.Taiko.Tests { @@ -22,6 +23,7 @@ namespace osu.Game.Rulesets.Taiko.Tests typeof(DrawableHit), typeof(DrawableCentreHit), typeof(DrawableRimHit), + typeof(LegacyHit), }).ToList(); [BackgroundDependencyLoader] diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs new file mode 100644 index 0000000000..bb76eac865 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.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 osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Animations; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Skinning; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Taiko.Skinning +{ + public class LegacyHit : CompositeDrawable, IHasAccentColour + { + private readonly TaikoSkinComponents component; + + private Drawable backgroundLayer; + + public LegacyHit(TaikoSkinComponents component) + { + this.component = component; + } + + [BackgroundDependencyLoader] + private void load(ISkinSource skin) + { + InternalChildren = new Drawable[] + { + backgroundLayer = skin.GetAnimation("taikohitcircle", true, false), + skin.GetAnimation("taikohitcircleoverlay", true, false), + }; + + // animations in taiko skins are used in a custom way (>150 combo and animating in time with beat). + // for now just stop at first frame for sanity. + foreach (var c in InternalChildren) + { + (c as IFramedAnimation)?.Stop(); + c.Anchor = Anchor.Centre; + c.Origin = Anchor.Centre; + } + + AccentColour = component == TaikoSkinComponents.CentreHit + ? new Color4(235, 69, 44, 255) + : new Color4(67, 142, 172, 255); + } + + private Color4 accentColour; + + public Color4 AccentColour + { + get => accentColour; + set + { + if (value == accentColour) + return; + + backgroundLayer.Colour = accentColour = value; + } + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs index 78eec94590..9cd625c35f 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs @@ -32,6 +32,14 @@ namespace osu.Game.Rulesets.Taiko.Skinning return new LegacyInputDrum(); return null; + + case TaikoSkinComponents.CentreHit: + case TaikoSkinComponents.RimHit: + + if (GetTexture("taikohitcircle") != null) + return new LegacyHit(taikoComponent.Component); + + return null; } return source.GetDrawableComponent(component); From 96bf86099c89444e5328adaa3c169a60d47854c6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 11 Apr 2020 14:43:57 +0900 Subject: [PATCH 0978/2376] Fix scaling of strong hits --- .../TestSceneDrawableHit.cs | 16 +++++++++++++++- osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs | 16 +++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrawableHit.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrawableHit.cs index f2198031db..301295253d 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrawableHit.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrawableHit.cs @@ -34,17 +34,31 @@ namespace osu.Game.Rulesets.Taiko.Tests Anchor = Anchor.Centre, Origin = Anchor.Centre, })); + + AddStep("Centre hit (strong)", () => SetContents(() => new DrawableCentreHit(createHitAtCurrentTime(true)) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + })); + AddStep("Rim hit", () => SetContents(() => new DrawableRimHit(createHitAtCurrentTime()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, })); + + AddStep("Rim hit (strong)", () => SetContents(() => new DrawableRimHit(createHitAtCurrentTime(true)) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + })); } - private Hit createHitAtCurrentTime() + private Hit createHitAtCurrentTime(bool strong = false) { var hit = new Hit { + IsStrong = strong, StartTime = Time.Current + 3000, }; diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs index bb76eac865..af10944ee9 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Game.Skinning; +using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Taiko.Skinning @@ -20,12 +21,14 @@ namespace osu.Game.Rulesets.Taiko.Skinning public LegacyHit(TaikoSkinComponents component) { this.component = component; + + RelativeSizeAxes = Axes.Both; } [BackgroundDependencyLoader] private void load(ISkinSource skin) { - InternalChildren = new Drawable[] + InternalChildren = new[] { backgroundLayer = skin.GetAnimation("taikohitcircle", true, false), skin.GetAnimation("taikohitcircleoverlay", true, false), @@ -36,6 +39,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning foreach (var c in InternalChildren) { (c as IFramedAnimation)?.Stop(); + c.Anchor = Anchor.Centre; c.Origin = Anchor.Centre; } @@ -45,6 +49,16 @@ namespace osu.Game.Rulesets.Taiko.Skinning : new Color4(67, 142, 172, 255); } + protected override void Update() + { + base.Update(); + + // not all skins (including the default osu-stable) have similar sizes for hitcircle and hitcircleoverlay. + // this ensures they are scaled relative to each other but also match the expected DrawableHit size. + foreach (var c in InternalChildren) + c.Scale = new Vector2(DrawWidth / 128); + } + private Color4 accentColour; public Color4 AccentColour From bf938a37e3d374075e0b9f8e2231e41dc2297949 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 11 Apr 2020 14:51:27 +0900 Subject: [PATCH 0979/2376] Add old skin test resources (with "animation") --- .../Resources/old-skin/taikobigcircle.png | Bin 0 -> 3079 bytes .../old-skin/taikobigcircleoverlay-0.png | Bin 0 -> 17018 bytes .../old-skin/taikobigcircleoverlay-1.png | Bin 0 -> 18837 bytes .../Resources/old-skin/taikohitcircle.png | Bin 0 -> 6028 bytes .../old-skin/taikohitcircleoverlay-0.png | Bin 0 -> 20284 bytes .../old-skin/taikohitcircleoverlay-1.png | Bin 0 -> 20333 bytes 6 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taikobigcircle.png create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taikobigcircleoverlay-0.png create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taikobigcircleoverlay-1.png create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taikohitcircle.png create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taikohitcircleoverlay-0.png create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taikohitcircleoverlay-1.png diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taikobigcircle.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taikobigcircle.png new file mode 100644 index 0000000000000000000000000000000000000000..63504dd52db36e9da6b3b40a08a9d3ec6e1e34f9 GIT binary patch literal 3079 zcmZXWX*d*$7sh8F#x}MrSw<*Zj9r#76GG8dcEU)uByRS?jG3Y&L>Z(@xi?F;Yl&ja z$R#3%8f(VL&8`_ClNkT{_J5xH;XUv3p65L0!#Q7mcU;ceiy-6?002P5(ZTloPbdD8 z0PJV&#sm)hG)O4U83zE=_nAU7mw#r#Xb0~Y06?_+mw<1gDlh$P%Ej7w#kxj?#3lq@ z3kJ9ZhF^`9SLN^_4ac+tJ>yMLag6+gJaNoA|QMXqG8nw!R z(&_@2bKlWXm3>E0r=%Vb8#V4`9yxhQuowB^pN6Avm1R*_Tkj(fC>SPy>I(j-q9L4_ zS&2>!SbB6pDT-yaL&fZDov1MX{#oQ>%#UwX)pd1s)ph`RpjRCRdp|3pv=&*(w^=0n*?J=65!qZVDI8yKzyA&}eeYPQ_ce4qO3l>9?S36G=;X;;(ak zKdEWCZ0iDsI(O2B$-ZfZKM3C5ezi@Z`3+U-od{7-6iinJ5jG*ZDGj+(czap+hK!l$ z1t=fjHvN}TOZPlGBw8X`^SUvkP#7oci9}7|dk1)aL@mA&H&#=jG+1GnTXg2W5AKEv zGixxfKSv_v}BmTe>*osjH6v}N78ht9`hW$(3Xvdycd2{OEd={8Jv zqF?m~fr_-p#P$s7=VvpAtSKQ>!}%8MT~_3{wFRBGTwivmU2!Rj(OQAoTt;UTd z%k9Zoj^h@@XrmJR`Q)<5d5;&RQ*;@4J>&;@6({o{#eFhvde<--G9gg&A<5l(kJO2t zfGlP2ySrD%abMaeR^+Go4}Q@TtV_ZkyQ5cgi^F=!aUT9`N>u96WH{4s7dvtCGp&n& z0L!UOsc1(YkkMV`$Ed~UpBEW}wu8V%U+$Mv3qe;?W(1c3tLaF!1#Kn=ZVmHm{ko_< zK`R57XPakFQEeOs$2T@`c!rvfF=7d46x!u?-oh9N9%uW)+v;Y#*QxVWDu_Mva8kT3 zT$sgc6Yn@ou8ny{w@H4FZW7d6-;g)(+WFqQP7^7<%^WWJ1WnyImgQJKceytDkiLsm zvUyUcfD<*-#2R6irt2hGeI-YQZ1l8Z?rDZziREU{9=CP7s0873?RZQmTOMk(JJ`u2 z-YKAfaz%CN7+cVQ3xE-w~g=z4qSPzI-_^(@e7BdZ*X~u36X}+CziB+Q`D-}dvoA(1A;js~w5Rjx< z%dYAxPwRO&4gbfSC;cR&1lN}n!CPJzvOf5?(3+=CnYP0-Pc>tU_jjwD*95wwzJkhd ziAw0A_^&>wcl?CS@mj~`5Dl(x>X_?bJ`)uFZ8eu+!K}D66f)7V7GV7MLInLd@84ew zEZ$57+_WsTV2d~RuL+EOp9BA%oY1_(m+*`DJ-cuHG(2zQ%9009{46XPv^oKk2J#|7LBS=Et6D#1f?{3h^OurDaOPW;l+Fb>>7O1~_PJ9$V6L8dSSa1Klc2-j4kCVm z)ad0#9m9sgh!|m=L512Yh(dI=ee}C4N;vZvA>Qx!ms?@Mu5`cRq~eIrQh<856ENFd z&&fgMtRcw#WPKo0kf(DS8EpY!XMF@Um?)-8Rw@oAJSeCht_D6^Vrbx-<6YJtm5{6aeZl9Imy4kfu}sllQO*K-MSmc4E8$@tcpwVXS@bcKEBr ze0vKopOgnJW+W$=m6T2uCOffF1+j$q_QAu$v!M%h*Xl+WuDMaOtlhtLf8~TbN%Z%|1uLzy8RXn(M~VsMU1opIP=B~aB5rK5)QD&Syo>Ao zo7->rVnwgGK2W^hcm+vJQ(2-~|N7YHi{C)se-mhb28aNAJA{L~9WXZb zv6=-cJL6%0a%59J{naG%AQ!NNY&2o-2alGGmw#n^!JuZ7h|77liv_oKFzrpao9P32 zrc9b$`|7y^8y|Wf`w6?hkwqIXAK5j%5Ky|SECE=BP({2NEq&yq5(@*@U|)R?40XU? zU^>!41<`7^jr0JzJuD37<i zj}AMmkqfl(D1|^5MfH{~Je4!M%5wgWQP6SO%Ol_Y)?$?T{CYx{^2q^ zKW^9Up%N^llPIU@A13=dDfuM3@w#U8a6rLIX`m;*gl?loxn-5>3`#xw8w-}lY4c=y zTTvDjG|-kw_9hZrt>r!8Go~Qlbf)!b4I>w#Z#NlRa;&7CdtG*Xnm+hiD`uv&#~LsH z^3<9RuM>$;c9im;~)@lDc)S6M!L>zDY=QU-da z9uJeIJB6w>fYK&~#&P-L&t$$bwinh(e~2 z!D)hj%^9RVK^6LkDjpr*yQ)tpMz&crO!XyT-INJ@(NnDh833^5y8ovAarKTeU)y;Y z3;RX!pOj8AeVYX+_Uxz;n`&Vm?pd3}EC#<*osdeF_Zs^%>I`X|V-B=@H-A$@yUkE} z!DGK}MYqbTzI-I^V+JZuUm|DXk@eq2l25d#=HC?{=*+O~`=+3s&h-3tSqFm@7PzEf za#Ao%DDDPT+5VzR`h8og439Q*c;XLt&*ByS5qxdWEjrQrz>Tkw<}gKf3bMk()t-i{ zy{vaXjS@CU-eY>L#`6gR)&#Ajj6{TV^Z@%l=x+Axg&6+Hc;dVMgs>xzHu*>P@ z>(Cz_?5r!-b0>YL>Zcp@| z?W4e^ghW~Z(1sjs3eY~kX>Zf5CXZpH5F07NBxUCk^U ztURISRyKCdVl-!MT{KWTOEDTfJ{3+CS7|F-I|YAtD=mLjZ3}+~3n5Dy32~^XuP_3E zla;3#)Yr+;*+bY@jOJf>g%Rz4E_2X8{{`adAVwqk4?(EDiaJ!<#oY?Z$Ij1Y!Nn^G z6%binbGNh>)|8R^H!Z|3F&bM> zPgh|M4j&&Mb{`&g7k3*DE+HWV4sH%^ZZ-r2n}?sXreW00}(uyD6?^|W(w zhW>-m%-qGxQ;Y_|>3@mfp!rrwl1D79=0z3H>m$}`+p&TP_2r}e`x$~d2w?34+#%XS#N|G z|5nKV7TQDG&((@U)5^oe%iY3C)*Hbl?LX4E3QN0NnR&XnYrD8O{`Wws|2NA}ZiLj( zS1M)}cFz9@!Sp|Mv63f-r(Rp8xr}qO`QSyNk7*BjSdKro0qXQC6CtONgJJjhmh8U+k)=2rD{! zc$zs|SSiYg(IC{rZf9qSutouM0c!;3)`C1jY`i=?0&IfZW?XDS*8GCp-27(z{DKz$ z-CxGV!s{O+`0xIf|3CYyx!WNop_$|VF`j>>=0DaWtYGJX7+1f4PaZ8Rw|}=B?V$fM z7-2Jue`D&)WJ{ZAA8zrhh+G)p&Knz7u zMpE1N=Px5vQ|+jY+_=ReLZ~m-|ovwD=j{n&k72@$2mn5;6jd(Vy z=aV7_a{9Y2J+k#mhG7vmLMcbfD|I+NLK;mNAcdt*Qibo5Fj=#ZT5h*zHlHPf(#%v{>d! zP@z+Vn-(e_8EFd?CP10+uSR)arlasVm9|<_E^&&9(AXfYy+e-uSD6k=sHeSbl^i=c zO7ME=hu0sON(%RTrFjEhm=|?M&pHIpFj*1La=S5v7*`7tljHff>v{H3=O|r#(WLQz zTbZ<4q%-u{HZ8`pTB%^BY{!Tf-A-QcbB!wfE&5aiZuUeF1$uGhzCs7(`|e})9CX?6 zUayL@SHhy;ep%VVb9J$!tjzG_fd2f{Vq@3!=vTtrFR#zCLglc@W2Wzm3+syH3O_Wh zgbpN<#-F~a$Ngz9oXKZFMkeA3YP?_Ml0UAuiMkXu{j*k_F_CQ6z1? zdz2e6aQ{SBDkz!Yrw$|PcGrOBGk!$A?VHXAp#L_&R*wqf#O+s&k_ZX|DD?E)&z<{m ziJE{kU(Ua>Qj4_J-u6-|c>&p&k%LQo0O0u>EhxNG@Xt`aj8df*srnG(kuR?Yhb>Bg z*^xJqEl44JWdX!1)mi~7Egd+gJIA|#XYaMEM|O%prXh!}o3+M?cSBr|`<~-*ia=MK z=LG$ilUPG|QG8fY$eE!@{f;w4TNwGs;*f>Iy~tN-Trj{oKd7DvIQv7eL(VTxmubD` zqtcfOx}G_EJ5C4^xFAWhKq0{UbMNUvyele4)_G{FR}UUF8D@NZBsLkGF@Yl9W+Eek zc-;@xxXzKuxXqt%t=1W9t=1bW$Z?m}_wey{3;A+?lT?dwyN)(;8+iE6Rnw7ozE{ZK z^9UB)M!~DcNEMDG>{Dn^WGp=-qfd`lXDXuI?PY|iF|v+6Q(hWrul*IlVmN@BZ2j&a zO2a??h;X_sKV65VUs64SY^+yZET0wt2he zhZ=l2r+0U9I3p+_fRmo~aVsXq{jH8pbY*3wo0F51n7zGyZI5YpT3T9jd;3e1Hg7B& zYong79KjHrCmV^izekm@)WP$dOPlSWcPWRQTMOfnxyw|%kzihIQu`{5uZ&cd8R#oZ!c6uI#L=SO-|MbqX2}W|&{@pupb^L2N@dT}NKB5Gdi@LyN~< zY#8dr{DN`6&uCU`%9e$jPZ#zg>X&Ft;`{a7(qDl3Qvgn=5w|Q?KYB5Zig^;B8!L{+oC7BC<0=wb2 z$>jKibwUgbnpYC9;j_gueo9ggGaHR*%rYS_ouF{g+xq;|9Jj>rAVD@hw62FO3_%u_ zOBY7Zbjz~xfv$(&JjNH@kAiP22gFRHaBojgiW^%#S?Y&v_EhK$jhEgkeiE_2t4(<3 z0cQx@^RL^bFsjbJvi#hQYBQv-$m(aV|ACSUJYXc}PZkugOx|2yoLX_4bwLdQHR`k8 z^}fg0uo8{2=)KwyyED^B{GzW>vm3h6i8MNlR!4RHS|yqx^l9*g>Ce<*+u3tNUZiPF z6I~@)BHU)!fKOJ%?RT-B1|>C>fts$r-0W;@1!9MJzoB#t1KZLYK@oO3vsQ#ic(dNs zj95oKk;k(w`9Pxhm*OkWdGhc#JntCQNF;dKxwlZ&-(RETLI&i6Nf>Fb4BCUn?6Xr+ zE*T-lwpRw{8~U4SQxMjXt-Rm|yonqkq}Tr5gGM3^NLaWs%b_Ik`J{`*cg?@H<>F_b zEg+m`CkO2KgYib_0&mb!KBx=au6^%9Iz?lCZakd1n2>m0IocM%qvlwADjMTR!aqMp zbqSY1-|ToR7!DF2=?9sB^1gxEyFu~^MeAV_!#;*7XtN~`ri0koYfS>DD^tX%Vf;`! zsMgdZQ@Ak~o_giZQI594a*HZm?A%74f{9=b;I6ELc;g@TmHp$w}z_F)h zDW4cG0ORYIO`$yk$TNnqX_Mr70Qy=MQoIBOop8i{3lO@&Yl;R_*!JUB*=WX0x+3Vt z4B02b0Nn;d?{0lZgR$CMq{xwiM) z?5isxTH$wSU00C@sGAs*=lP2Aa%=m*UMz;OH{))`hJY=}ypHFV-~D4F=j^OI+D#)B+{Z~r zmTZemLNtmPp&;{>jHy*;GSG>CaOADijEjn@+OJ2JPuwKK=&VamAbxmZiU%+R$1ryf%@J4# z0+SV_w&Jltd&Y0qzjJN+{|q^hBRo*=b0FghT$G0aWTio(HwV(!6S;+R5Z8^%cg7rD zcTzNOcj$ZdbEIOTH|66x>^MuUY;5=puVl7)s>*T0?<*Qk>;<)QV|{VARHOm;ZzJo_ zxf&v-Tq#W8++}vd8MVRd;@KqRNEE{p&ab2MjYPI24=%b^9F0to;&?Pj=>&=p6hGTjG?Vd0piVmGI!scb7C-J(GAQu7o@@{ zc+-t2t(Yg|=Fx5SanDs00a{pFtaCmF5q?^dJ`2-8eZTxK_+d;s0XB=kudiD)e=p_B3BI)(~0sRUnL>lq)_dJNtl3=azA z3p8~mGgWClVnqnhUL-c%}`ZnKuY zzEPA+q{@4|$A9@{W@7{H?8)+y3%y^gEh|sI%lqePPFuT66yWS^aT8D6_xW8t%}nR6B!5> z>-;Gf*a;*|`ynpfvSVBN6y( zHwpNayZV(>?7lug5O-rP-Q)&>a8os_sc05WDfshhSDuIA0#$|!Rk;&i=SM3liRE>z zp0VS#SDC?gBK^yLQt+?qU9kD~D%JKDe)Nc{{Q@WZ;~Z0gM2G%`^C7b^H@&W#o26bT zE5^~Vczxhu_@|lA)M^_rfN3f@YvIHQm9Hs!vmfiHHF`z{NkT2ggh3RXy_;iy@r{`N zw>)*+XT01yxgm1g%?vOi-y-AefY{-D@DfAR~!jE|3n89~XBCUy`Bx?r3x6>5z?~m2i#a+GxUi2c{PUZMCha&+`P23X=Svzix?aJi5 zVpxr)>jt}M6JjyHcRk-^4T@1YbK(p`s#gAJ3gw&TC%2NUZDEDsnxptWgIg>F74R zYw+ruj7y*YV)U!QewTKL@Fk?y_kt!}9|R^5alf0fM8(l~-1(I4d!Mx|`a&G_d0*e5 zd``U`Uc)45J#)S7WdSmEVEQ3+Pw{teNSxP8pQbfr1A46x`c9b!3X*7Q$P z0bZQCL^Jyi?}?|RFC&YB{wBO)(~WyEXEX_OvMII?zTD?6V}$MU+2WiVM|G%wjEw6F zS{_UG3M!t{cT2jRlomxJ%cvp+!`XD3TKJVe|FKpW$p2y?9}lyT!}dW(1u2sW{6rhb zkG7C2D90aYh)sYzHn9rAbKxf|@!@rmj@!Ro?e#X;2J}8vNCjIQ64GTbukuee6jmNg z^{1%Id8+YVek9?2r4c<{RAQ@XKP17xg+ve2VcPnEftwPH&GkMVPsR9&d9ZTDG+3e^H$^np8GH<-pb-Z zsD73lD2cIJy;oXu%%wdK>|}IOHm8il8P0d$Y+Q_iRq6dugE11RgeQoaom{&@Vam!^ zUQsb|uqI6MX?lL<2f!c6-`~aByapaMlih-hb8S5CMbrB~{fHp@rb??dE@b(mWAX&0 zg0@>iNCiU++|ecYjq+nMh@-TE6;l~FJ)@jkfBiY{q{IO%gQ3o$^3t-!>b#q9hCxj# zWgrnL&IgVJ+}bv;k$&rlsidNqTdK$OBlSk-75D0ER<#Ll8+7Xd&5q=9_IfmlGmxyI zG39=3H>Wx(i=>+~YAGE@Zc`?<|5vS?I)jVRa;<}6TrA}@@YX?8?RFR{c4|B&S9YJJ zV0Q?hydyw_Mu@cJTJd_pL7QqM49PDUGX#<+bCQ~t=HK`!%%;<_x}m44F775!>xn*w z-%KQu>2krJgeQ@9J1y_S?(783>z*Zzmlmx@!^)d=Me>|=b35LKFrt^XF$tJmE<+n3 zuPjW3^Ml0@183O{Ma87Z2>E-hNI_YcOuRT<3ZBtNxcaW$59e=QJJpqJj4F6s&|9*L zVhA7!fCc6mY!^6<|MUY(*E=u2JpLNYjVtiQV05i&Yi!aV^&PJwZFU&+z7Y(AQFHU| z*c#X?nHsEXPnREW!r;Ys##p$CnlJF*(OkDKq;5eIzFdXuu5&a9Ts?8$R+wzO@$g$a zj!3zGhxxe+2A;FQ(WpmT5?f4mM4f%`Ev9g3E)DS(JCqzn6ZhZY^tI19Oj2IEW%!X+nN1K_=4pkXzjB{r$R*!=CXA7=9IP_~t7{g#TOQ3qeBFBQUrbPj~mWzRaK8t z!E_2r3i$}zbFXRkzfz4u1%%yL$UmfGFXtmAuD@%Yls7g!0{l%7f0DWj6{pblv1K`xfxyC?@As#iY|Fhu3>COe&i@2RdZe38FNRH!96`-)oLN`m4Wcy*5V zRUUxqbEdqH42S6T45=x{MaxHXxg~(3&~b{QuyJlNG=3xkfVRv!$?*;&1B@XWL<=FA zbT>aO?~+)?kVg0x@D|*0zc4xB+k5C?SB@m9&p^&8tx1tH4Bk6OhO~={+Ardg^mf=* zp)7jxnNk0g-zuU}f9dIs3X+L<{}Y#ttUN8+0EC^w>f5&5d|&obzF(4 z$4irsiDc4c%u>iU!Or;huN{FFgrRP8cmwDzYj6Ju*Ng1OAX%1YSZX<-@@Fe4e~bx3 zu2yQ4V3tZm1XUBjkQuvIQhZ0q`RvR?joa93#=b+{p4mBRyW_V6g1I6#%ws2ZOcS!$ zuSSpsO50U3*ycc{${usF3qZp-QfR6oUR@HersU;UgNx>OZ90YbQd z714u7YK6)qC%q5FL!8!0^<{~oSJ1J-!w4X&iO~gtVdj2+H?DeOvS@ha>koAsG51+) z0V1_38>-z&kIAy|EtFSM$|`SgM+_1$oM|xFjoaa-KEMC3p7 zz6(oKRnB->89-xDc_&wsbaQX=;xxPuVlHsY!Aszxj2ri0YqY#Vz%o=^a1FN;jIvgQcgl4qC6=Nm!o%D(lW z`v#F)9J%0Q+3l7h^qKxM4e4_sM)iw}uvdy(o~UY(ZJByWJrxpWv0a{&p57lxh_IVeLIoXY$J_`vn6sBXX}YVU*1nX-p^CY@T>WzI5jxd8 z=YMS;0H{FH^F$_my1~j65quFJh2e*+2#w!Ex!P@0wJcX<2`o8A_(Dk1sMk93zcWwKnjI{N$Oy^?TLk$Z1YsnZh8a7^ANZX zP^##@WSC0kg~s)dr{T&C{jP!au?bPN@3WjspfkX0z~0ikQ*ZQa2_K@*^o-MFX!eG3 zmn*nwfd#a{`Xgw(R3mBz&5#U`m-dz?OThfw@O7zLu2qFlE@|(Z_%dvejU+wt(m`WS zovm*+K|BbwQjk`-ofo|K&0&RYhVF%1gA`5SRNqY@M!aNWUVnY*`F?1AmekpXGy4>J zFBRyOyL;X>G)IypMO#9G+0JpME|#u$3AhHDaOrLwwN(*UQi{2_o1(4F<7f_Zw}`qt5uXgKKOZ{chzzbOwnL*B{IO_}}9XMjozUS34q#T&l%@;o3vri5FLvU1i6fVbD zK6xNRuY`6KTJeQFjoxk+)e`F2;L#%>g9dGBfZ%eVxuVl-5C}8_djgOs>m2yfZ(qM& z>UPEbto5tkS5bX?ow?nAj%Z0)VG_W~F-ovfg+)?PaV{21JZ1mtCfo?KJY)w3sMh4d z%y0|A%_rA?#`DK|+FR6PpUJ!MyvY;k{+11a9|6KZt(htDg|bg&E*Wv5+1oQ*NOG?% zH7ln(&X3hGG7!rk1%R1|bPOuk77cBqbr()cx%OG?6A?)-clqw_n~o@=bMIDhXl*=~ zcDG|o+hT}~fJj38O0hlV8t85v0+i|(%b{Ufq@oXiTi^V( zq7#&!d$=!_?qTEdH;4FmiDRC}FQx1~vN27qX&)%67k6 z71xt7^-F2@Pf9)>CKoP`Ni4P+Z&a00E4?j*)ayi>-j)C`)5TU#e-forhg z1x3*fF{(^Smvz!kE6WU-XZ(h*7@qYp7eQt@R6@41v;0=r;_)<&QXfFfr zYd)w^lvVlqAqf+R(EG5#p+9Pdxb+Gpa0Sut$cuCRdjNpAp3YhyQ>Yjl4GqD_E6%3@3*Tt{v6N!u&AaFoEGOY z*!7vG7155L(9v|Avqn9avT$^3(z;w(`XCa~>q_7K!3TKz$0W zCXoZ+3>5GI+vzFM!sFAFp7yYCe~=%`Dls)oDcsv|@6YAdz&c zMKp}v`oQCOeqGLw!!o|^jt42@*5hQf*x)r>nY(caY*KB7u%rd>rz4_fICCJo`FyTg z3J|UGjg1nwW2fY^fLEup3%miPjvm`mnrTg;%1m2w6;*L2 z15?>Sf<7nq)wdD@2f$rNX#3k@;szgrfv(fJW1ZjMlw6)lH_H!%}i=wTYv zltPZUry#O!CLrvwDG4AY!7c4Yu^|x6vUtEC%&r{$mOWj;x4~$Yuc=_@Rz?#~f>Au~ z?#I&@JuK(k2+y>(IC8Kyujn!-dQ%Pcb$_6dn)3(Uu=`dP`oN_rHHlNvNi<+{sK-#I z&rh`Rs*7*{ZA2>+jz~4jAI~RI=XY>ayYB{3M@kiw9fD=)EY>(Ce8H*_v<_jP`eXfbTT=mKFRb!zuZ`nBf%Z zcJ;NxVJkH(8hRcEEaNIWPEY^!_7%q})s%eA9}VV@uJw@m<$;bdU)+vCQ(xHDnIt1I)3g!HySh4rSrY!C6lWX77?9tRCyw8Zv>~+>|IA9h9H?z;$ zCyn_lH;S`%_ji0OCh&<}F)Q`n$UG>Utt`zcW^|_+Wehv%= z-Yne!DF0iwKl)7gSp<`nb1%{@#MKgxEA`&#-c46T&Es~8fN+~Qm0rTRVcpWA^njkG z35M$_JM1Vra~_DE?T=~3?_ggUSCbc7a%IjqPmVluwlh{nIrxAGorL{t>5V*Z0UX!A zvrO5z_|a{D@IaGQ6JUSeeUrhbcF zS`|6O*l`_V#j)tbJ9;Ccz8Z6eXI09u5XuG=k#!LcI%$p&A?- zx8yXi)K>sIdK#thQb^KN2AJelAwIrSK$rGjgO<4Go#L4Lr*LGPsB|USb5<^g-51jJ za(~A{uQHH}DTZ!Q%EPkg0D4BqoPwwmou|%_yNk#(H|??nq9$))yyE1TJiw+2?Fsas z5R)KV!KpPvMsceV?5nE)vH-ZYh|~3Y=Ym$Qn2tm3b)3Pv1XGZcL5=ZoBz>g#SG&M1 zU+&1sWIp&j9Srkj-iwVhZCXKUIMzT7ePZHkN*)F-ORossI$9%YAwkX0cjt}91(hb* z!k%;kkw<4NMwGL3z_N3POc{~fx)V2Iy5GCRsCBCJpAX+k@~(P_Iy>X*WvH&@fu*W?hpf*at&n0h4Kpm4ogC8 z)sbj$L2;UG9iLU<&f}u#cwbN1(YncEj3fsPl}*~n07Io~iQ+5ot%`YFR?XG7sR0j* zO!?k5NjSqY>QZlcpMEu&tYQI+Uu_3uL+?B+UWY4we^(91R3t_d9*3^J9G zVZA+yYbGIeGDzxLw)i{obg^aLSS-Pe9L;OZCT}!C_Q!IgMf_;9Fwjx>hyF?=@WXXi z2_r%T_%!xho(+PUfr6@V{m4^s_hA&C!I~WyE_-=8uEfc=gcO}H3 z;>NuXadTP6)&-$#%E^Pu(kSYc{-AS$u`FAXWRh37f)lb@0Lu~hYqt7h>4X7GG-26A zh?SW#4cd3xnT=vjY$^|VIR0R8!F7bN&v{)x>JPd%{_*y5wB)cgY<(`YMGfWOKRmB4 zdJo@eo%dtxuUfsQwlkH$!%Q;*OB1?B@)z*W0O&-5vjAakG_Rg37voWD1NK!BQ8!he zfD~$Dmh^1&xe6_^YUEcPQhXJ-rh~HXxQaqb6&f!=&r@Eug6~q%FtL%sz%B0^#yA~` z6wtC608h`NlCBi^rrcQ<_~i4O*v3P;iTVtCeCU6SJLK?B1o6wXilR6>nsk{&lvzR+ z7r>29@6vReym_d)-Q_N28XHmkLJ{GG1EBUt!S@$gVa9^k00Ceag*m=#^Vk> z6&O84^EVypmHL0+HdKUdC*((Q5+F~Or%hDQ(eO-x=tPD>m-n~a^|X5V?^Cu%8_sGY zxTzXjK`mjX)whh8N5ehQB-Whdup0c6&)onjq42Td=uFcdJ#V z%O@&ReQuXZ1xuunx_ZT}EuqRrn7+7$6zJ++qu*q;F(d^ZeDj4{mN@leyVg*$&rkZ% zmd`)ICyEKs#uMRL$Kd{D&pR0LmkLJ)`MItQ_lIXh#g(UVq%r_+4Ettvc!<*ox(G-h zmFf)vfTuRi&^ceQCj&HI$WE_L;5-MM84KV`@ru11`MA4k+`vP5Yg~IR38{^@xNkRyHvZQkE>S+ z=Bib+1d={#PsZ;>N|ASb*z-Dutangc}Lc@vNj&CW?B3!mfc=K@$@VsqeYNNBBar0GN`*eEA=NKj;?3Q!6Hp>*T*Kf=2Z|tA zPhQ{X z-JR>YRBLLrb4Ij0h$>13`Xag@8uWumm6$f7K!~lN=UabnPgtM}Nq!qo{J9N!UTV4n z=156Or{k~=pUX1nw6&0AX!B%=2kI-tQ1b2+xCot!3ZVTn-ppW{mJ+46x!yf6-WRjB!iAUb+$A)@q z7%sL3lWGYAV|g>3PAV?r6tm@eN+?pJ7--G6|F;!;M~K7#RpYF- zrxiFQU}{0%7x;;#hQ>oSD8*J5raz63QLfXzHXIz}(V65kcX_6?oEa@XEy3k7X* ziTxoPWxLH(ZMqhNbBg_7{0pW?+myoWBWw3Wd<1eK+uvC%! zhUHs+)-{ZT??*;PT;=>FR=$0sqPUj~yPVqmOMx_f|M&K60hJGtaYMm538sj!#=MGw zOg&QJ!mnry*!hK7!6t&i;~mlv6re+Shbi-qmuBLZTBtq@}q4df1bWRSQfMk{|%KJc#j>t0U#IwfcCDe3GZ0~dB4($xbG#1!1r%*ARGbdFc0 z!`)-ws@MItfwAMc+R0_|>Pac!yAHGLos>V;2 zU#4!E+AAmZY83l|ZxnD$LkO!x7Bk)5WN*&O&UYlWJ&)AI(1RtSes~1SXPe5!q0Y%< zLE%HWLDgmxjN5;t0_^z{jhf{Ky}&CNoV<5N$pCZFDT3-Re_wwsSTL!(4)k?uMKPZ4 zU+0UftDy2M{gzwrqd3f$y~gj#1q$xkZ&Ay~&KiE%xG9sUBNz9en zzj2h>7hj7TKQT!B9rQplK0(vi8fV#Riw1l_h6^t{BqldJ8Pi#cj|eUIUM@s1imy;t z&G_*6>T+g5V|TdbgAk5rY5Qf=AMap*2U~D<1FH+I^(O;024k5xM}Z_RtH`+ zc{c%r~03iYA+<#u51=MSRBAzHql~8ASPKq1Y5ug*PepC;9&Fu0Kt%Asw|m zt$rx-fKsK&IRkz?-5zTiHn^6AC5!eIM_W=<8b>4fdx4W0foZ6vupg%>2#N@i$-Q@B z8uOy-E_X?yf7XOwOdsMo+HJ1J^?QD}Rlh!4{YE|#3aTxObQ(@?S5=G)#<$$#^S-j3 zB|zxQy$_O3!skxc>h-%%D7pKzk$|l**E3}^#tH}i(Ok4$d-PoNS=grHVxKt{cAyhw z!3Kj@kW+W(HQLVDQV%bPj~sXub&Z6LZNuOb>oW5v7)DDK(^hc>f$T0^k5JP%F^xwd zp(S5{%HuS)gC$KB3 zhGo7Ae)`|h%rYUKY2Yc)GzXxYq_m@E~=8b zBLVPnYvpurB~~jA`4c|u9Cd~8z2n^?wz$NnEb4FdjV~vR&No6111Jsul&xnD>K4u= ziX9=y`r0lbIoFvVJ|5)!TAoVo#~@B#b6K`x?iG6w*|1At*kG1U`z#I2VcHoj4R(tw z|Hc9$LFaaPGIrq?qz7dAHS!o5;hFgVUG|JExJqKYe7N3RTYo42y(1uWPAy#R2=v}w zM`%Lqn~?il5l)d#B7o8h3sF&Yap!-#Sc@%od1YPiH>36Rcz@+LL*(F`1*K^?q%__; z1bx%oCwLBffPSn$ekZ8#Hoxm3x~=TRZl!Nj{Yq-P+naV`L6LJzio43{=dCTCf+(3W#YxSTf~myHRsJ^(`0dW z-7n3J@-p!0(`Wj`ns=B^fSzO@bI>QfYtF?+G(ENa)9J_;hzr=kt3Dq zf(d6II=?hhwe{rD*uF+6q@4c zIV7FGEW*Pi49dlYTTWT~C6^8;yPpzR^S*E_pnNw_i1vqr2c|f1<|)$Yc`4 zhoci;B}Y(|+j(?ItWTgpzEE9)PwI33nd5qb;lsv~L5Nk*j?j&Fk3ErvmKVU#i!EOW zyusEmUXz z-}KhVqWdj;@I`%_`4ZAd`kGJT`Zq)V!Y)E!sY))Kh&_WNhtqpjUgq4g zFe;Ua`h=c{g-Lp%g-!aH`2a2u;u-|PawUcs^^;e$gDdBjQ~l9B- z7e~$r8i_1qouR(=RwSw^WB-!%8+O&~Dy-e4r^j zwxC~W>4+_IUa?HD=cSLTLlOyB;vqI1Am9xUm7R4>GG4LG>JVOMUucKpQ1i{~dHp@D6zyY7n zR$~^u(ifK#qVY9GEZ*espP@^#KUI(i)o)6rR*s(&$rW4q4i!s1C$h!gpS5}b7v;nb zt+CBFruYas{}ODOmikCNTza@<(NhO-eOwLZXe~!`-T!qtSt#Nx|2-1>1D3+^eU;`* zM{2{5!&q8+A8e6GZX>KPd-DR@Q4P&R9gS!n#3*IXW#1$F^7u--;h8lXe)bk5neF`X zg|NWmd{g*3t&UcHbo?N1&Fp5B^ngkhj`uyHH;Hr3=f~YeN@1^*s$v+Dz9wg=KU)3` zNci52^sa-aRJB(=;pG5B$|02`IJu&veur%B=BGomiM>l}vOJL|!HeaN${~JdjeH@# zmW5oQrm?t8aW8bTMGmY-S~O0lAC`!keWdBXuO!DhK%+IfF-XVX8-^`W>u{o&!_T(A zN8-%=}{zW!uBPyqT4T*K)a*I*ga@1%@cY zm4qxj4*00?e8o#ZS$GUU*?QsXm*4=0c)Exr!HfCmhnW>a(Irg6weYvkTlp%_5z9f> zAZK>A*r)Y*OkR{^tJ;DJ^gIb4?6ai~a*pT@!*FG4^b_BAPhGpK^d~?EwL)OTQCV45Lpk# z;plKdQ&o&A>BxCYJ3J~0siF@}2QTY%Tp4JWIh#l+&(eOv_EUCl9%dG(0-~`s37}#o3z8J8@3N4OD`68!hx3my1!NIK`c+gpRZSV ze4jhtc}8-(F)9ACFjq*f%fKxX^1!mppdKJ+YJ~m%l_K>MnKL(qX0%QG4j|F#OpzHhIV;~^ZVqfIKBFiXfxT0?1mzQL$7Fb(F9Qn z{3aq}zaHIFx%<`BxH%!d6q`sP6q@+_fFX_p9!3fXP1T}DCPYK;5gYl@9;%ij93A{{ z5nx~usXXoY?pnt{#$&CD^zLIEuvyO&93Pnv4);htV)kk)cqLiV7#PSopKeDmokDHj zKTyW`2%EI;{9smq*dFn(-EQ52g>RIu$oSgln=o+Z=nwdQl#rENy(*g*wLQ1Ts>6rT zF4}KIb*G(38nI5}D_Zkt-bXoR(Num=s-=e&O};o9cH`1^SV`u>O(e;bWUr);%ci^&?~ z#^*ZrDw)nYJiw6_hJl9r1*c%}U>1Ek>FGm3!LV@JZw>Wtc}RHd$skBR#V8$bOmdGd zLG%}qBsT4y4gU`%I_A+S_2rgqp#v&my z!!WX{^fy4-XM%UBfz}Y)+b(IpUkl&uBZ2c5mgV5O46qEQM%=-;AWx=E5p+ z3esK+I9Mgq+JD*+#ibzzYoXOvkY8yA^pE<#)f-D`U*Bj(u>@IYJC@3PBV0lWd3|fU z^EJ&iDA`vCGAlvoEtBzRunalZ!-YvgHju%#pLK?W-&dsB*OErS(TyQz9oUwFC{k@O z`<)(KNpQNI8uk+1QG&)YFtiKQ1L6(^4XgkO-2laGgk zn~w#;#lb1a&LP0g$<4~aDa64i#LEx)_Ye9i&C}XeNJ~cU-?CosM4|TH-tI!|?0$ZJ zY<}EqZk~4RoPvU{Jh<4oxL98)SiJ&Vy)FD%UA?IPn}dvvmzAf3ySIazE94)J7M5;4 z-lEV~P5;XT7x(|Lb@lqUn_eBp?r-7F&dJ8{k4gU`w6^*Wox6{x^S^{!Td~_X+ql@c zdV9Una{h9{f~_Qtu8Ju{}JKkE$jPg#=jl%zoqul z32?Vz*Rt_)^YOH@k@bC5llmWP+=Zk)Z7jUqJaydMod3I_H2zy<2-mCC5C&BXD+kwq zj9~nqs@TX_c-x3VU)zn7mE*P1_;fgUggCi{c=?z)1cf*_{)<%A&Dy~>;J-<^I9NG& zSUEX$IC+G4IfQsP{uk2Mps}{_w)lS~wzd+ob@Ozwcs1F<#lp^p-QCp=3i*#Cg{0k_ z-8^3fziP+*KR;KLme%levvqKOE%4Hkmx3tDO7n3F^6{~9v2p%OT~$>fMOQCx3s)-} zMHx}(t9#fS9IS;bE%31-<(od}2GqYS+%aok|Re>QsI7@~8#7;=np1-=B0a ziCnHp%{^Y}pUu-BS}ZoMb~aS3cE==$MYZgXh7 zx8G+Ss0<2XAfMp8nFJry5|#Gzw3!zi@PAwjec<9yD2Sh6+KN0oU93ahhkjCd)H0~{ zwUScVN&p8guR}gP*2%H`&L4M5P2*grLfR~$v+cN7>-=Oo`*5rpsG*i8`s0s1UnFn-G-7hb^TrhW;;>~IDV>WhbnQ&Y1>w-Vi^L8ii-c3`T7TfZ4L}jgk<~hyF54PIa+AZMlF9R2K7%3#b7da&zS(gA}+!n6qQe?Z{Q^Jco#tI+O@P|U>^ALTHWtzS1<%Jq9 zXvh}=1-fR-F@ic${N5nmFBF@QKI~>+3QGi^`N+GePmVN-Cf)i-dV;WBs4erpiT!%c*e&dgc6`sv^wuBbNUK0v05RiGEU!%$@cR+^zNVrqI726B3= zW^&p7N8lf4&q2SB(zt)6zQz4jVj-=Nb4fwNNcV2K2E6kaqwxHD>!FqSMSXB+ zP{GCqp|q@7#?jGH!NkM_-qFcP(Z`3k$$KBCw4?+=$ZkqwIKTUQT{`wx!)ZgJ-%2p< z+rOYRZZDzz8Sb<+FeSUXudCTKnbx$x^t6U*p`0i5dr2Mx@wIHs9!0|YkMV7a0Dfd_ zO5F8NnMC|3LDyk*N)CBSB=(vXk&`h+5nqserLT^=NK7e+B)9@^pjhHRMgvTjN&Kw-3S!{HnK5f0COXJrYN;wfL#WH7UZrf#TkOgq@(&Y(~_- zV-@<)szM9)U~>Kyh_8GcAbfNjnic$e9QgG6pg4!7#qSs@XlSUiHmf{$BIf^*p5Q$K zuM+L=VG+spmAbbqQn<-PxQfB!GTt(jk@r-obvjNHL7Kn&YO6o%+OcrhNOWD|fT5_d5 zm&5tVD{DY)v&7ZgZ!fjE?mnLnvuNxl3ne-pQ2**x5r%vxYTJ^PkEwe5@gw$&6-r`y zYKekkTo6T116Pp%%nBn)!!nLwuAX}T&qQot5na-(SxB_Co!!%R)C!c?(dpM|$0@xs z#m}6meV#(nbmX^Tgf#V{!-cjOBayPjsW~dANuC2|RFMlWR>&My_bx8h7lX~>Ec*wCiaDF)pRNY(;n$(K#FS`peGjn&$Xg*hzTujhUOYv{i(0+sq`af zoszmLomc ze(KJ;QWyGoa&8Vj=%c<)GzMli(QyI$!f*GskKc_OQnz+?o>3wQ(_0CaZY$4|dC`wa%C!1rAl)pNe(fA<1r~<49u#nkkNC)vfE>&i z>WX`Y02a6xy%v5fgIyqViX81xTJ@g8o|4r0e4Bv$&<{=i3?+LR3@>5lnPxh;6eiTCVl zT3ylCeuLibC(QS3ow}!xy?dVvn@Xfbruwk!;s7^blUwK}sRH(QZHKtO#74Zt(LZAS z%DB);(WXo|KM*o;Hg&}ea9@X)Az6!6a|@DBh0sAeMrEor@jj6924A~O(LFpKBf}1c zHs_18(cdDYEHGj0_0t)boY5-43B?O}EZKYBu5~$S6(5N&7``>{=@=oi6C5ITT&bC7 zdG9~Ul~_w6tx>Lk(6#dc1fT(|VF5a@6ac~B!f>xCZ~%z_$oLrJnb*-B#oSGZi;PA- zPF$BZ)FTQ%&h4zV$e)bc6h%LbT>qHT;JbCtz@eh0GYvJ+2fZ|=&3?$L@mq7#XWCr}I6%5qbvHt+-h#eza)vZ3y%beiJ=FWKj zu={JkN`uQdB}*XSwRaQ#Q?VKmi~30AlB*_IXbn%hOD<@%eXVrudbroXZ4-${V&T(ptOw`1PDL@ zydb~Z7)khKGZvwZu+05Jo3P!GQIH$6sY-On=w%k-kbRM342$(g0OR zFArxnr-bcPzC7BXoIU+<6BU2Fc?c=!xoXOH%`&JeR=FO)uJRkx`P;K1I%_^3z>Uhb zXj}5saiHBHpZ?yf%UBy#0dPx+`)a>=1uHyFVeyRfUV`W9uZurQA}6}B+*PfQI1{D# z&fP;|?opvY-~0*c7}reh3(S491OTa+L+#0OW6*$X)Hb5`Uz_Xfg^;jdQO{oze5Rqi zrXr|A-ET$=Gix>kO*w{~`-Msby^x1#1HvTXQeiU@VQ6~QM1zw7+RKC`;6Vc-4}6-r zvvYYGA(b&9>^CJ5Kp_G=PL+AXKwW}YLKHJz7;)6eXWOL`QvT8hKgJ(rZm36VVV4zkF&gu zMi4Kb#n8?O4JRFmaB@P=7aXgW2KuMTpo*}|ygo~%2z+JANZp|?-9H8>!#@pC_>roC z3|h~`8Z8K)Loh%FH!BMz*~_?C2)-Vd!RQwyXT{4xNqG2#rXOO>Vlf&QDSw8^LYr~au4 zN5|Bgst(i~w_Eh=fCEK?Mqk#}my;IPuD1YnNYGN376|{oN132@H~NNMLna2%ZGUGf z+};|eUn?-L(Q=HPk~RsV~lXxcER^O_*JLgCX>>3h({+A|W>^uj3pf`OBB7calbq zv;k;trHf81L&WwaESB9(Nyr-d?s(|Y#jwWddxJ8i)6L=j+K-Zwjg+gwSF|W-lQ>Cc zgbjEfg`#m@D!&!$vE^Le#i%bD;{wG@J62eO|DF$2;~bB8mFqXQg>^08Or5EbjMrOC z$O#2Lq@~d>V;%bo#X+ZyoXS@#N5yHY7>7YtGkvdbBDi)wf*d62P}?vklWldPg$5?A z+r3DiVd6qD0+R^2dY3pk-LwAksv@z6OYIkOI1P-mTj~V#-tod@wdEg*( zc3-cDl|ntK)x(u2rN@eM8Mzkt9IPa?kPNXR8-!HK#vr@HBfARKlj zBC;D|d0syT-1uQ#!=_5iu79ijit59k7vinO!kg?@@+s&X<-GP<&LGnDs>NO1?G?h#+cYgXK|?Zdd1 zzO#(MyCeKWz&)1oe$GW9Z}Wbqp)c%L{*N{CxwHac^XJC^R!bm``E)ha*c85q&efD% z;D*m>g^%(r*>g^<;ULQlZSCEH7DwK=2^eND2)WrBJKL-r;qcIaE{VG_JvJW--~Rou zn|m2dXUacNnYCLTtGi{A1b8_-p1o_bgH3NqE=LdVph#2^j>KPSfzNiD)mmqM5L9LO z4d4-BAaZ%#^E8ybtN7NyE>w}l?PUAXG&gjbElzmS43~+_v3b3(-#aY)t!KJ4rKNm3 zU#ZzVJUcG??7CDtZr<8c{R(%*yp+wvNZ%9DUt0D(h{Kr4V%EE64_$=E3;A-gF}98f zg~m}x(#%9&qY>Vq!5!%Q;0PaF6H2c08Gqvjt?2Tg?F|Z7cJMFmDAQ(uIB~HPW|7dZrX218PB;t~RI9?qi2B=Ab ze02k?a3eiKf)!J?WATMuiv|C<_Tl(U@Vt%7MWlT343WS`ZOzC2j*w=~Dunc>o7>lT z_#N2!kF@I;#q*);#_W9Tv3_BDniTZMrP`sO-{V(JZ$;BucW7tl^AnrAoLMe937>r@ z177f_g_`;Wtey^f{Yv+svxvs@P!9ZU23DH@a|SUqy;cit8_wx9)!7}Jd=eH!FGKJ& zL9TD7sg{;I0Y!%&iM5Px_!}gd?yc}7HPEj(4apyBsEm?adwKd2M6}ZKV;$;xm`)fv z6pI%~z_aHH**UhpT~c#qA*|NlJc#78aDEG4PXfreAdw?ob2oY*>^3D2WZ@crGT*J5 z0=8IOV17XXo3yoz06GOvuC|KxoL-B^kgn2lHe-|(2DP4nRq zkkyI<$N272ius`Uh=dubry8J?E#m!r?0TLdbQ`LR@$8A4L7i&^lXXazxLA7*`J$4} zLD8Q;{DEi_L=V^7*KZs%_tjoORWZ9zpLt04qalOPlCr4!jh-DUU3t?Er;}=u$*s=Z zNN1uSs)7wwYPKi{Ym69!cp?;|+fNqO;bfYfFD)G2;tq=MGc@m_a2pjzv7L^)s@6!+ z(1TIA=Y4G?YHX_2T6U~SEX8%Rh?H`+D&v94M8{I>tu|H#_l{F}tHH^1GX9215i($VKYnrgX@NW_tjTGH{ftjr_IYtezR_QgaupMg zm)=icv-d{g?{yx3E5_#~PGl}i_ze6|E4ZaN#)XEE_R;6&GHhqU+Ah6xFG03^X@fUq zy}qIc{n@xbf@am?r-isuMuLH*g`Xj0mMz5%R0^tJ8xEj>)%+dS@RTDvYUfz1^$FE5$w%&0DjF?;9m(2++ z^z=RoQaCiUG#mn8h@e;la2a)K81H&>)`vloh>ig(zXZ%g15m?_4YTk^^}RT;ldY-?Gc&9!@)^u}8G+p*0KN z#T2+Mudjv~LCk`m8I^svf5}Tkq)skH&3(O8dhn#^sJ0c z$s!$#z(nf&sfOa#rLLxi26w;DFp*|qa0~XlJDrfsE_)a3?>-2KOTLRxV~ns6l`_}r z(D+$&N(p(XcFr2a1ki`Ae60G=sgKW#MjBGdme$41yYz#Af#DZ*>06J&6hGkxcaUm; z$yYIZO-o}unqE0BfdqINnK>{8&|_kvjmJ|FJmM(n^>;krg)($*Q|pNu=x?RF&8%k` zxzjo2asJ5i({IpxufhTA2{%FlXAa}O51~txI^TS!)BPta^$}%SC9f=dOb{JLRC;El z1e;BG#hJiH6t2L1@OlZm9WS|OA0Xwo!t27El88pD5$&S)Ad*vRi*_=uFwMByN2*X< zxG3&k8950ZgT0;-KF8AI8;3*17>pxzB_$=}#ZXfQPW){dgL35wr?x$ivM!3Y+b-4< zNHYu2Uu3U&W~8C;NX4iEUrMy_VUOZ6VRSwt*BQ%CeGdbnBMj{^y2pIv>B)2J2)@ht z=TMDeJpy-U%CfNE1TUP-bUigGp#;=ef?k}HQwS5Mz$yy`o0wn_8=qhY_q*1S6kN4V z84u~K*O=Icc&^4)&6JZiKE1I1Pum5%>AUgon)xykp^ zU-eL)pPyfD3r8Y1@UbT`AmX_7FmqtuH?pV&y3U)pkun+m@?S;7zsBf$kT4{0Pr9xh zbJg`pNxlcsuyK)D*3=4vAyA&)kBN!MS;ol&bRo^=3{-!qgE+q1JDiI7D2%fij^G;> z-2H{KN6jlELRCMBZ5b$%X4iS5#w%*&ctp;Pmj^d!J=>QYdNu(;K{63+Gk> zS64pDq0_Lg`emixch_hGkj3-newy` z2l>ft!q={wl8Be8^n&yAm6qz^n@uWKhMzl{pjeq2*|8QX2wOQzNFL9y?k_!Y{-wA; zSJW3Q^#1Gpk6G5&bEX>JC&=)N=TFj;JedVcQQlPOtmto3em z&06LSJzUh@G!JQGf*#ewhlS8mGyTc>NdEcib*s;_{WIeU6?@*|;iFglkfAox5O3q| zsIR%j`QC$+S4xIeVac!ox)>yUaZ%n&rJOOMWn3n;V{X3}4 zUZW}b5J8bHX4WpOQ>-ZnhA31x6BcT&gqL;1hyErN9RN!sCP%=1xp>Yr=V-NRa{or& z*DFEP?BbG2%L)TVAv4v$>`g5md<#)oS zU&u2AhOSk=q9G9*hd|LIu8jNj3E6G&$g!xibU1~ERWNBrVSJ7|eHWn|-zy6xcM9bA z>F?_R>Vza1{W-x#BWS5e=mOLC37Q(OzTOB?rq6FcbTny9yR4qrg!QLck4KR z^uAnE)G95MW5lmr?){jLrYiX*N7hVzeKkCy9jEzyb6wmawtmEF>noJMF zP$$uSX)#^bio*_&G|_De5fqWX;SmZjF;|3d$pEsH z1d-MK({Xaq0$^}?4l&kj$8bK& z8lqlH6n}qz3B5|uvL7f7--sBn-$a+U!LwHyj_~~*=Z*Yc5^e0+p9WC6^L~V6j65|u zNo%ES{QiwQ@Fw%2NU$XlaEg{W^IpDQT@u8cDR@a56#!SL*unz{8~3rcK2t_bYjaO& z_VK4&4`Kx&C*AVDr%7U`X$jZ6zL)j(ifO*O`K{8mW-sa!C4BRC0w9~&jE1+fD>w{T z#j9b6RLMhNmY79-et4>vIa>X+jaNM@jAJJZmMBPM&o7#>XCx?NLcBg4`G#l0_DuEp zQ^XAVm4v0lPLlLmEC~4%A|H%wwLD(4Jk(kC)a{)}?^*flODDTSa1U(x7IcXN2DQL4 z)=eO$n|}HMVb@oWafOf>OwTRuyR4^fn59-eND0joHcmdG=cMf|IS^D{4(_VzSm^zO za4Xz!QSiMs6B!l}0A$aNFGDiIb9gKPvsg902TPM~ekO!GTDD0ZM60v_>a)`3kL-nTPISG%}38p)W=_B>6f( z*<3ey26h&>2+?f*^o;J5#(6QY(aKlp>!TG}DQ`!uj`9aCIGN~u{(LJ9`5A87&dc8^ zhJtwdB(QR(v^~7^wHh@FQS=p47m2rODx&Xx_?2A=ZM)4DgnteE^llf45#Pm_txiIc zKlU4FoJz5t&ArGg%7OQsXvDZ#?Is44XggbJibL^T10b7d93!J4;enYBe1GhTgWUbn zv>@-lcC$Kv&1v+r+jOoB1&Cu+5Sok6yDuf>2xsrWn_fRD%hf0}b+;;qJk2L4k>In@ zi*n`)56d%!|5cU2W8v5bobhp_IpBz9v@vrvY|2ex#&e zznpss*yS~AldHc8M9FDTuT#6KwV4gPx;>%lOvHhU?CI4o^6yG&k+6TaYg{osEE14R z(0IJSKsWeE8?bJazQ_WC`y1GLfdnK5n0_-HYlFeiO+^-`~4O7ci)42E{ zAEbaXTQMT!qa>9e&dHlM@?vuOZHraR($ z;IiKIL<_6obMIzh<>(AUuEfCGAaDjT32Z>fZwnw30JsGEIRMCajK8LG=ywe^nmSp% zjWcGW!?l#kb`|mTrR~k{B#lJv5QNjXO!khcBk1hkBW?TUMbbDB!`j9MN?hPwd=%3} zGYty$kq>+XPZ;f$Uq>!!$2!DtR$F{sM%QaPDwUPBc7G9b?tpCukAmEFCT#aj9R6S# zsiqL;pexii(rtabJ#rtUS zD`3!7qdE-9Jvz<&xA+RW8jDvfKpLq*0?;Ox_WR(E2JUzzYq{rkAT*908^qRDily;2 z0NvIH-T;ruv6B*Ro-qOKk28*9Z$!h4sGJO*7AGa?_hq?;j_-c_=+0`ltl+gW1^0s) zZ1Vn79F(jhu;Ac8bC{dXt!NLAnIY;dvulb^gag;Z-hP|{FZv(lyeKtf!vhJ4PpjzJ z9`1y$gOuHMkGzx9D)Hm^z603M1?N1zFAIFTLUBg^Gn%WrbWUO2EYjhCmu#Bs? zi+P;x2bW@5;JcT~RXYeq9L)|uVlGrMGDUR+3mk$hd8vdkMzO!Uum5u67+5U`z?pXW zfHI}gCv*A-`2no2FyH*z759 z&kY?X84G7Ljph?-rwZ(*d~-Vvi@_#xu=n%3)uO7MGdfgd8AxtHi=R;DZVuULxMfi410NXLvrW2)7@d zJUzKpfR)qJ=CttVw!`^3GZJmA$d5pk4I-f!cU~{RYzkoF_6bjX)s|O^8&utM7J2!5 z;y1NC9U}Y4IOA+FbJ1c90;6~ftC;nEIW7_=wY7XtND(t!1}%Wx#HPEd4ch8o9#BxO z3SD(um5Z20{k&2<-vr3g3Qf>s8!b+e6tjoBFDkAHY^&UfNCdD$u|z=H7(58eC_3Jc zfTei^5@h!TF>FM}=f|S8^JkOSG;dlpc?;0IebS+ki2sFk`2(;w8;6_<=;38MErril z_DUMk2$*HYk>JpSRWujU{tBo)kNu1eFqwYnv&+E|WF2`UPCPvgGA9c{D_g#}lorBH z#U_=-q!im51dZp%Q+h43`F|-iKVX%WFeMr^TylpiCHZN>6VQWa&(pT87=qKyy>()sB6U+7&{NUrOg2YcH zQEfCsYUuGp?7=%xNv*+calw*UzZoZ*`q8z#lR4^0dz?(+S&(7rigj4V%-rMa+*?46 zCwQ+0{PG5L%gly@kpwbT0|d|$ZOoCd8we6&%el+gdvwNp-2HCU5FZWm&P3W)DQ~3k zqd*#WxFU%@q7OQ_sVpq7;t#mV;*aaIY`0_rG?QoUJjn)8$}%ahD`7h_<*m)Oap-T& zvjB0nOkTX<>aK}C)FX-_3KLMIP&|y$b(Z)7=8?i`!v>6}xL6*^Ozi{KpNj^SxUBAV zCi5`nq&2*&HUF#%J^+MN?Rel%9XNUNaHWpVhsyO>Xb~ePbhVJtaLnV)`8q#I_%q{W ziNJ9S1D9~2j-LTZbJ&hGQKRKk(fH2I*MYo$>v7i9FSN%qd1yQFWC@Al+d9?~BLSbA zKd(h6-5yrzArRu5LZlHdX~p)Igg-}q%9p0J}U}GrsF`QnPY$qk6yx!;Tnc& zn?ZzY*7mVC&Ct#~_>S5@Y4SGF;|KWdN#`~_(W19lxo1*(@$$Z@oBqM{0cV|boB zmyG&`7WO1_?9m6xSqhB(-&tYFYfCF`xpwknwkT)nIH9RLXIyYJKG#vT9WN?}NC3`1 zun(2@E8NzB=jS~aioP_6Q%jk&W|PZUOm&p|=*kf6eG}*XRwS8>NN^e+ za>(q;9y;|*PUih1e{KJ=l)+8}(RaL%^ilw*xSZp{uz|Lk>(o0`KrM36-b5)zrfCOe zl=Oq7#AydbgR-nXNZ%-zI+Hi&rvZB4*%}as6fE}&sFQVuf96!Y6?hTFD947qUSF9URgATGPMO*V@!FRV_Wjm8Brq`V~ zu4+!2qOFv>y#nn~og)R3l2=%aeV0c?E#Z z|HLE*w zKDOLtnE_sU^j+7SutOgaKURf+rGy&W9D(QMYwGe-7n3)YNI7>9w6W`*xeyNlDS&5iy zw|CIBGUPQiHy`9-`1Ohgw-sc$Ug|*Ty+C5rI7CRG{h`b!;gEiGxx2icEH?&$r(r56w(qswO*}bLOLxx;GUof6~EZARO*(;g< z6T)E3apUl$B$B2}<0nTlA<@Ua-JE(NOup|}5NllhLaMA$0MA~TUg5X`-*9aSB^X%k2^toZM@u9v4Zh=epBU?b^OWy*$Ffy5n)26dWEe5( zEJ|Js9krwur+Y!U?obJ59Ub@$%lyT>;#)UIMEeGg19Y`4o+Ki4pGvyeB)BD7Wt&i} zlVRZ6D7h!rE*5-_4`?*>2?hOdx^f0&KY3W1t}fepxHq3ET^)h>l& zmc>Gz@dUE&#QY)$qzCy3^ERElS}g;h$?p zI0Rz|!zshzr31P|ZX;`^c)fmprmhbk!GWPr!vCGh5Ca{~_ryt?I{@W)iZFhOV(v_m zRhV<{wkTmMl3$03zKFbk^7(En0YTAYPAHFOOr|ngGMOL%d zt-(S*NRa-pKF=p82p2y}s2DZ~4rRy*nmpm%&Gi0YEUXsxAb9Bz1FA%ZRkcvrQmMr< z$ad%2hyCp?`Plv3(iSx-zj)TaO_AwBdvauFuDX5(nyP@e}$b5fr*=SXmJf4+;Z_5d%TT2ua5*bMk8y9 z{7BfdFLu4u^US%)BSxKp^z%0g&x`_3BgJ2ma%th#yyTgJD~vPWA7q(jPNY5hej$|1 z!yTnsVn%K=Tyy)mA-`a-lCjsrgG}+oF|sF)LRB?9czp3Q;|QL$Ij)})&c;rB^|4#?W0 z&fGo%EVwk^g9M&>FxLj(b!LXZeJ{ogAHg6}8VxUMfQxHtW*&RP-=wYiGe1Amy6^`2 zOdj&}SRP|kK6-!lM)7oiu|*%t9-SWU@^tZm3w?O+5KKXK8KwX@5UusB(ah`Gx-3P3fq)oQ@NyPZ|+6BA8g8p4N=@UO$G#;&>9TA?@A4s41vSTK|o_IaN=B{ z#;)woT6uY1@G}ey`&7_fZbZkZs(dWwd)E2oD<)3jV%02Fc;CE)Lj> z&VHT2NF?HC5mKBvE(@h$g-=%0d%+Xj+~&-gIa(rU_HzKe7kaFZa}X^YP5zYc^E{Xw z;n3%G8FG-qMcVdqj|s$RAamYC-CK7PtSC_n`dWf+|I^lpcv|lbrrtCl9zc)90rav< zh+xGvv!ehlI!vb`U*8{ruN(7-cqLQ~d3?X6oYeU_?8_#vP)Aoh)#{^P5aQ%h^WW@$ z`jdtlEFot`|B6-Fn41`}9PCALNwz9j`SIl=F0N*y8jVglwBzY-`5T%wgvxYo;uAZ^ zb{;}dokLhjNV5SA*3njV1}qTnOb`Anx^C2+)>tHrWYltLsW=UX%U=~`!btPa;bw4E z&}mgHf-3MHA5Q2?s}+DT1_w5}&o2+t6au&C1&&;jR1h65j(_Kb%M`~nwm}KJ8|XY+ zmaE1$lc;q^jE}tzU&m$ze}VseoYD1o=*mHmdF04#ys&1<86aiq@5=k;T2;8LVDe30 z;`38K=H4O_BGxaMq(VghE9hcNv61IJV+!45SxV9#nsIgcCyCE;zWsHjEO&`-XsV6I zzECVr&3YaHm_Lo`nu6NP$ONQ$iNaP88p!J2Z_B>UI3uDzzMsWi>voU>G+)FGn0oIt z=1|S!rsH%{T0P*S-FpifUb<%}+dovalFP{uZ$R^LG!Oj~LiH#yrVBj0$EPjoc397c ziAhiOVWD!-RxkzAj*q`H@^|&H=L-nTqa!%JIdLPDXN)rCPD<)kkpu30vrrg)@nQW0l4VTRG%OI?( z5;UrV6tn!gu=GG=!1OM>y!>^V(pp3C^Di3Y>tXKRt)#%k!x;9aW=CH1hs6hsgsb=S zye%eSmyd4pc_T9MvX|1A5`|Rv5JW0DsH^l+q(PiPlt1!0gC+h3LhoVREH*Q?pL5#& zGPM$QenEs4czyc(70_wz0EiQ=vznEu`d~`k_8JMSxaK|EAco&aL;VSP-`2LQS8Ull z3KU2W<8Fp?l$+~rEJascANoGMqJfTu1Gn6XMDqh)?S1^|!D@N5H(}YP7t)_(pGZrh zpnk0O_u^tHsUw7*YQVQTu5vf^ILwM~T=GZ5M@_l09b4oo`r!b($qL&|hZWVC4u{QM z8Hzpm-sL0h_Ddfn%cUR&?gUM0xz10ocm@mU*oL_Eo|vu*-2P=Yj6Fy73Fx)md-N|X zuj>k_Gb75hdfB_HJMs1HOGTDP_`03*!<}j6XFh_ez97E$;h*K<%faEd$x zKr?rJY%368-Lc7EvdqG3zi`m`ZfC^E52dPwq+X)_>8XsXn)nb#ug6hvs-1R+W-L;T z;n^ONP!u`f3-odbWpU$shGI)CX+AYI2^L~0KW+5&Gp9nsaK3Bqh+K|ajwJV?t#=W; z8jIjJ;skzmH&Ozd;dHyaR`+hctI6B}jw2!sFw`DZ%wvjb3x)oSZpGR^shq{xZw8c$ zf)haDKl&F59QuOa5PF4hwA7C8d%T1KzK7Vyel(D0casR{dc{*AEp|L0`W`I-gNPc; z=+3A+j>AoaeLF`_8+)?JS9|Cwzen=jzG0VxI~M#_;y!Tyjh#BvHG~` zX;Zd{F{1~$nM>4 zcJ$1c*gt^4vpH$3=H3Ejf}U60sC@JR55gqO)%j}@CO=)DTn&qks|w+w6B=q}IKNI+ zjjnE8haMbqW~V9cD2#(|X?i^pDG9m7{B$L&8VO;Lz=)5RQpVkSxw|9a*^{+1W9`B1 zIWl4uLH_z>5LM8`7yV$$Kx6ICLOFG3*Ky+0_8a&8qb_&6M*@9WDpK)37Q^ZTwtEZ} zq61#^lO1mN{qz(;({}T?30;%)6fR&jkiY!|JX?+R`qNogvro18^HNjY&7ywVJ4Jpo zH!g=q11(*X?Mg;->gdehw_J9-zO59X>OZ_9I7;xY(V?COfXbY}xzN z_!*Fik6;+P&eV0oVp~0_uq}j>d-O}KSM!#fOtC?R z<<9D5rOh-jaNF~AM!h2H9~3y+D+>JSXsV)nidnytnZ12ny*TyLgYZ`c7x1#~<9v+z zQajZEoi;^Q&+7TrTo@cs0COZe&yms83C!HQ|81jK`@HTQ+VZBlRY>|skymVkwS-)b z%S+&Tz<$b54osK9;Qk1+@)hXKW`;;v5!7+q^PX@N$LH9(Kn(an5`F?tCN?&ib=8%w zB8cNk9|kgTtt>zCl*U=pVw^+gUH5-f*Ep-Ma3T-?Zrq&e!)4e8|B+5Wrd;UZw>)Kk z+s){rD_XlD+)pRC14)$X=hZ{=kty|il?;({MIm{K0Yv*~r801`W&W>G^3{|-Vj7)3 zdgUI>sNMkl0}N?^_9ra2?@jL;4L`1*x|{^wUnSV(1SUrZYRPvYaRxq?gfPGEi4)pu zapMnaefX0xD+jYtrgS3bh&QMYW(*2_oQQqwPoOx*Jbu#Qoj@zXRMK+hxeI;pE4;$O zV;nPCW9jGBMX)Q<4H$S?0dbfr_1N1{#{}qZFsz={&YCmlu3=Xx;4{6;cZNA{KvwT18<%;K=)sNP{I#=V1>xh+fwqK+@i z!P)&VREZt_6n)_wqg2&F4s>MZQP|M;@6wCoOsM#jm2Kd{_rr4@_q()ap71ySQi8S9 z){upWFWF|x&o8E-;AcCKy~^vZ)v@rmn{vwX31kgP{og)jF|ep%U`-}r{i&eVe_C_# zi5O{4*mdSk1hj|wz|Eo8GmTmXenxH}EBvwT=-TwHA$=(Ulakz_$hX7v)_dwCSr&`M z)PGe}j9u)8<*^8v!=c;Yt8maa=cAH!GmWmPaH!F@s2^Vfzt^FQ?*2e`QO>~hr$*i7 zr|C3d@V+vO2OK8?-e!)RF}c}f@UiL--7IC2ak)&ge!md#yyw8tP(6FyH9{bIv8A+% zbkaup9QX>QMcf%_!nIY&p;P?${x0!HeY?`9*F6ZN70ox50fp1A&;sT(P@ctDxw(|2 z$(rEd3l;F58fdKmNO%DPTLV_g@Q-U{aUwGX-)_T?NRM^n4Q^wf&JYTG~C;stTEW9yG63$r;P9zfa z6CQ;FJ30df+KUIBIU@iUsDWDwuxtewIAVk^f%{MEh!>gaph4X3j|q$#4002Q{`@Y= z@86@)8`iI54em(ZcUH3Mkao-o2KAAQklpInx7D%FL*4JQMrh};Hhs_5Tin)SmFDd~ zXXYjiC2#B`t<0>(%Je5P`c9Jz2eP+aq_tSXO+567GrXH77>oqnyhv#sa+k?v#T$^QsU2DAA^jgp7&)$#T@T6d@E-FjFo zR+hoDch3|yrCUF%M@=Wb7J7A0-a7Zr8S#rBxVO!H_uK`4O9D)qFb?L@@I=VsDsDmRM6>DN<`M~3330r-nNeA~Mq@%k+j>s7SQ|+Py&;{m7U9*+?vC53er~{u&P|_~N2MvIe7} z*zi!&rGpm{q1snFv?6v6J}gLce$=p`(=&RfdrzM_1#Tz;3>`EOO~S;sw~QGxJ~A}; zm`vnnqXi>alz?i_ucHrN0vh(~ z)4>kvA5c|@zb89)?qroq+}V`+S9@427INXj1+s4KT9VN#jV;-xUcG(IzM~k1B+!PM z@LWcp^m(I(5B0fy+Ekc~ZFIvEpjS$F=-;;w^y}Mus7|HWt&)oC9Lt$m_(L5bY8z=! z89Qn@bD^!(Un}P@+=89y@xJyzu-Q(jy_BZPcXUFS|cBLdxdxIgLzG9v%|B zcF=%+TDlVpBPRGq5g?;aZ|I$#24TTL-sm7Mz>w#F7Ky;twj&8>T2q>=k|wC|X$vGZ zF_x@Y_Bh$Ka|bEN%VTBH|JRyR8srO7t{zeG zePE_W-FSihORxNIgB+JInc(XsZZM>Xlnc3 zJyOU$^XHM5UwVOT*}R#pP^KXzt$j=#NotEJFW^*3JH>r{sb+jN((Ad6k&Ll zopqiZJ9d=p+xH8ru|o%G-g)y)Rz!2gv?*k8MjsL%70&iUFaqK^X?2zQ)liI}GJ8Hg zwDsj$rR-?1M)_=HsJ<_1wYCwVA%Fycn*?x!0O%aCEG2ra24CyF!a{-)^jg(ItxC2V zRck&rc#W3&38)e5HE_fjN_Jx*7VU{Nvl*>@M3+s`*+>=o5~xvF3H9XIXr?s=XY?gQ z2WGH}ngjdxCTS^2th8E0Xb>x~L?e>GE}d8}?i5c-8ZxY}t2@s|aX5R_YC}~6bMudbU2dPz$qMH2{iExG% zKtSzaqFbXF8SF)o*t`Q>LM6h`i%zXlPn@PrrE?LqfMVwkUaXX=9bWA3F8Dqr4V{po zf~EsCRIRJ)$>tEc+Ftdv7SB_Ne;-ABpx3EqMuh2u@Si6VfL-rR0$gVTga&CLG9(m& zHENda5__s1RqYg=LbgUHm+sQYrKgY(c_^4m2#cmsFLhMRY`|sJJn8$5zSoFM=!{BC zjA-^E+bD3^My*2r7@nUdUJgX{&x9Qw>bTaM1o&?dKuHPUfduf_TWE zTe6hbUTDF8^uBM`_kGv*-@du#oaZ^u^Sgid^4#}*uInUPTbXjOim(Czz+rA?WJ4Rt z2iGAc+PzH&-Afx-h-PO<0Kmq3aDjl#Y#{((6v5j$1vsHjA~6JCMKqS+j#CWrCDPCU zprso^L}R>h0bqBWC*Dt6a%Qz!w*Q28a0i_>qty+LFI`k+k{2GE@@$3liY1EvbK?5bT7q1{)ImabOiiRR{*I ztPWPwP=u>0!<1ANzz7&z0}4}z!j&K}I1;9cR8a%}^^>G&^T&E1ZH!F*(nWjHmh=h; zAR?jA;NW1zU?oL@zb6!~p+Vz7KoJNC4FMsA`URjvAbupNzZr~hB#b|v7=S1Efe#qb z?u5VqZAqG?f4bmH{7vge`YTMdfI&mhL?~PlcHq)4AQtl*M-24$`K26-f#Q5{zBs=C z5)BLgjU{>!0th58!hb{kd;6ah&_ats{nqhsZSnQ}t%4L_97J>DuYmkpG|4WMh=bbT zNQ6Lt49+-+W~S7EH$%UC~BWPZOWl?Ah-tWMP6aOHBGeQU8 zv?Xch28Y0uAaGSXI2@_0j8uWi!!(dE*q=}o0gLwt{TqsaL14-dINT1dj8s-bDyjVw zl$JDDbO8E)1!FNt4}!lhn&vXz7ww6I68$_S!M_8EG$i;C{Ar45)+znty1AjDwLifF z??c-l*_axD&5aFJ;To!{5QHN9m$@hu(%g>}fcC@S%#E}qY2i`C;I5>P@4JBgQES@6X?G>`D^bV3jA+4t!o@if43{z!|#TN^P_b+ ze_A_Jv43L)06aqGM*4OkW6L?A5q9=>IyFH&baIfVrH-qkcdjvDkK8)ODi?$1b|&IM zrj_w44yLAf$%2AwKAfLr8)Z2zG`Ni;Cphyin;R)&2t+ z3q7$x5i?+n&fLF-evDqQYt&`$;oj!c4Z>Oab=1!8IZl8aa-v%L#E2!?E*}b`6QWOM zUSW)4(_;VFqrzQxc<1)d2r(FI9cveZBBSE1sT`mh0dmWp$dQhn5bjE&>toDioTP68 zZGmulj6|^#y65CmiqPphdG^4Z9;gE3#ID7x#dPwz+h^9F1{bcW>SeQ#7aQ&5!Q5s{ z-@7Kh^R0m@m`iRMjAljfNdJg_Kn-Ad3L?uzza2Ak^HK$_H)wV zAbX;22|O_5?51sPIeh%_$f0ItCX3a3H7w*y_S=g1At;m8;Lk4l2*@l)#)eaZ_YiPs z>}zDSCs|65Jdc{ATE2Q}o>Hg^Qw9nL9)f}a2{qopDJKej6MYm%A>ezK4=B8suGm)j zt=d_RK|KBR$%#~kHjrNKcH%~CvBlK02e!x7OodPXkzd^4B@39%xPsei2YLn7K8>WU zl_nEqmTvnX*a583V<}?e$-$kI8F^{1El-(^(lf*XWWFc_QNDu0==5#ol~qPyb?nm7 zb}_(ary1}|H|M=6V)_HyA0v<=lW+p~n6Xa^Esyl>`SKk9c!c4o1al>m-i^NLM=+c) zV)+Js-qPSAH--uLv=GcV&zt2ONh+v8T(~lLdWBG zUUYzeN*KX?BNTw^0Q-&dMDxmf65O?S$4&SF|802$(%!r0sK-b8c{4#RolL+CC|4bb zG91)t=dW%r0rXb(QWBCfRb=)#?lBfg`NJ*_0ebTJqHAg0;cbHgpRUy;GSOZAxw^Qrla=FX_Y0x&9ho>IidEWlTUSs%~CmU>gI;;FHnc7L;E`=oGEYqvJ z7pzil-}$ipBP)B*n^LA>XH6ZmFLSDM(}DJ9ws?~ksvdHIYEEPW?^A{q$+SzrbzcrZ@y0Y&H3v4c-ElNnDeL({F7I;;f~_RVZcIIuDGoOBT)>SH;sZO9N3j_T` z8I4*;NCShci!2dEoi`mE?AG>{*y;+x1(hMfSw+*<{MnQk+nQ|Wp#B)j32dF%$$=cR znKY?5rj|sC++?UPXgB<;UJdI#O{=EV5ViOsH^&zi!*(*Ly7U~)r?!rQTTNq$vS43c z7C;(&<1pil4$k<~>3J;;MpTXale0vMgEI%i6!D!M8-;?1`a>v1OvlHJt1i`X?GN6Y zr|tR2g{cwWdGa(LvO3t$bY!>qOjN_2x$Gn}&&wFo0n1zy5K3EUt4J{ariaz6jW>dG zG^RH*g3~1DtfGC(g&P0;E9-W5ZVOxN1nNl8Af4{RAD_smea^|=sIz@25T&6N|9qmM z6%>4A&8ShcL=8ZK>jPsWoEU#bHMv%brS~SfqhubWu_GA)HmkB2kKvHb*!|arxX&i8 z5p_c6ELg4Vxd%dXdu3ANip4_{u5MINwIA!qC8SKW&T=ikoR8ncJ~;N`edsvkt)2Ja zvydjRHPMSTBS)!gsFVk8;%5aOovF<3OIH4aqJlNiuxF#WIZ?D;?CB}eDzM|z?f%N<{L#zbN5aosbmA}4 z)ye4Txvsr@{%c%*F5o4F=^r;#y)GvoY9(U!5+*!n2T^~>dY>R<mOTELs+2bdYX-_Yi({|=qmN^7fJq85G+G=41parGUq zEif`N0t*{Cl$~j>24FRmRU;Y{#ZI}!u#bIWB0r^`Pw417divr;Zo|hC1zBr3l-P$O ze6JsVlY3g8H{QP}YC7|Z1qd27pcJjWJ1!9~7Jq}gh3yN2p60Y#LeQt9r_t!6ZT?>v z^*pX7jg~pQP+cQ^xKa)fy#$;yV=p!ByOa#+>lRpT8fb$C#dn~sviow1a_)=QThO8w zEoG!IR&S1cke_%c{Rv7zujV_u*Z>r6`EvzX`+WU!J!qQF4S_Bs%lD+L5AHNr2fE)f zv=hl>=WMwbtDSzN5Z&2|vQy^AJ+I@$N&>%~vwn)}0roTVg-@7a-yK(fsDf_IOz7sX~0WGV#6x^JuFD-DEFHZ@g z;?>E+_bT0onBxvH%=wP@;4*Srr*<6}(p3|tP^j;eo9(fsaDi`F5uA4ZeQ+=r2e8zfiK|Y7os4AK~rc|Xc zSlNyn=zctBOM&dsvc23h84u4j_E-zIe7MXkbaB^BxvXZW`;psB86{Ss_*$(%UvYgs z@_pyf?x8yCdI7W}gnlp`K}Vm zWe?KEP+t03l<8LwwNy0HKYVc4CO>N64gtAejaL*%A6S~p8qmeGQ+<3gDY z1PyWy<(br)iw6b<{;VTN9M&c3YHNCSeMnHc44$}makC`t;jaI&(b3Vk@Reot&tBNO#8p>G7E&S_3UY>&$#F~f4RgbC@9=I=dCdM`gK)guZK&f zQkJHlNH88t8kU7kteaZxoL#qeap-7?p?m5(+4pjJylY^f zxUldF?{O=m4~_2;K2w}I!!3qf&pd7>qmEIb+$>t>*$l#@OlmT*K0T(@H->A#(wsx7 z9{0Lk+lJpz`4`Xd$RrtPTI|0P+i~*=v-Nwe-UzbJBowq$4j3- z&raX}V#>6&YMH8-L0e91ZIv?NZIXaT{&dYW;caoKOr*lV&^v^^w%3oGb0!X_7hRvJ zQu?~yDXSf3CvkIZX|C;9QuoTRC~d?2$ql7NkszNbfrgQQ>12;L=yu#-q1P5`is=`& zyS$bd;?f<+anQ6K+lYn z^;kU_Pa&w~mGKv~#$VX4zRS+1y_U6pPSY2@ZP%txFCqp8=}w0JK&2dEi}XSJ`a&Kz z*OPk@!>6KFZ%0BsX>D_MQ&^7QpAL@wUf|TnmXN;ilCAO;olairsB1aaZNKSwjo#J% zo;mFX_`HTW6f~U@y1?5c^<6{(sOSRUrSR$&vNNp|mcPD>*ZVK{-p^2p6xk2i*KRu7coik8Rzd0vqNzEq3r+3MQmtHjq$YIfjZ;`ax7M;| zSm07?YwaZL?1``=Yo6ewJR!)bTOA$807nqKv3R+h^5Mx`Z{70z=|as@HQuGQ)3(cU zXYh&#V${7zVbzaFKXHwiEI*-_qwjXREJ!&~+{U(xcSaKh^DbGb4H%e?B?|GRVXr)k zzL@ZBYx@NfxwszkwhpbGy0^kzzW8ppka6|GU}M>y=LyxNGrFs98jh1(Bcclm4DOfi zooz1>zeeigN?`?J3NGhxV_sr%r1 zJ-<(g>pd{$t`=Uhtl7DHeU{%u!PGVS?ohT{^~{jD4P+TcmmZ-5?X<9b*R;%h2m0gI zw@nw1p8Ge2NIxEZWUr3{?fw`nc05coFUPyQXMW{k@Wz5}-&_OX&1n}Gzmb)X$Ec0e z7ugse9|eFb^{$jU%jHovT94NfKYT#id`V(F>TS>W)n?4ZSk1$84R;_}YNU#Ma%QH8 zl&fB8oRs+l3fHIu zUimJ=m^6rsfljm_>1&W}hv$Q~gt5Jc#8`qXz;J}w!HvBjer$Y5XIp=4*^(uppcT9vvZq?gM+tEbPiBa6{AMK73Zu z0F+Clx&U$gY85=*wDsaMP<;;vw;)JdR1nbbxECj6x~U48eN}q+;)^|<{NZoyTUked zn5u;3Xz^Fu6GJC?&7;~GUuIDC@N6|HFv7LNWTp%w{-K%Aq3vJF<_lN$uP)Xs8cFwk zD4-4((UZZxgAOLtm0Ks}y<4~RRVvtr>edk%(n+b3x>*8%NL|u0H_l_0+-!DjkgvRu z`D4od@im|TH~u2cVY{Cd;2;ljnwh@86mB(kNQH?n;?Wlmmz$r)R}wOJUk_Pt_wxW8 zDRI*6k?k$iHfjT+b~^FiU?U>p2ltw6ftM8O!ea-f>2sTa9z9sPE9Z9NV!~~g@LHq!zygrVq*} zq#jSE-w5_5+Zb)e4m>!5I5MQLDI-iR6$~qLD*YzFMt JRBqrF^Iu$8TZ{kz literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taikohitcircleoverlay-0.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taikohitcircleoverlay-0.png new file mode 100644 index 0000000000000000000000000000000000000000..272c6bcaf75c18a985b971284b11162a13a2cca0 GIT binary patch literal 20284 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 pu7$$YDiL Date: Sat, 11 Apr 2020 15:01:09 +0900 Subject: [PATCH 0980/2376] Add support for taikobigcircle and fix exception on missing layers --- osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs index af10944ee9..43d45ea1c9 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs @@ -6,6 +6,8 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Skinning; using osuTK; using osuTK.Graphics; @@ -26,13 +28,24 @@ namespace osu.Game.Rulesets.Taiko.Skinning } [BackgroundDependencyLoader] - private void load(ISkinSource skin) + private void load(ISkinSource skin, DrawableHitObject drawableHitObject) { - InternalChildren = new[] + Drawable getDrawableFor(string lookup) { - backgroundLayer = skin.GetAnimation("taikohitcircle", true, false), - skin.GetAnimation("taikohitcircleoverlay", true, false), - }; + const string normal_hit = "taikohit"; + const string big_hit = "taikobig"; + + string prefix = ((drawableHitObject as DrawableTaikoHitObject)?.HitObject.IsStrong ?? false) ? big_hit : normal_hit; + + return skin.GetAnimation($"{prefix}{lookup}", true, false) ?? skin.GetAnimation($"{normal_hit}{lookup}", true, false); + } + + // backgroundLayer is guaranteed to exist due to the pre-check in TaikoLegacySkinTransformer + AddInternal(backgroundLayer = getDrawableFor("circle")); + + var foregroundLayer = getDrawableFor("circleoverlay"); + if (foregroundLayer != null) + AddInternal(foregroundLayer); // animations in taiko skins are used in a custom way (>150 combo and animating in time with beat). // for now just stop at first frame for sanity. From 3d5a622db7b0dbf8e4984c1ea57dba56c1d7393f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 11 Apr 2020 15:04:58 +0900 Subject: [PATCH 0981/2376] Tidy up comments --- osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs index 43d45ea1c9..80bf97936d 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs @@ -37,18 +37,20 @@ namespace osu.Game.Rulesets.Taiko.Skinning string prefix = ((drawableHitObject as DrawableTaikoHitObject)?.HitObject.IsStrong ?? false) ? big_hit : normal_hit; - return skin.GetAnimation($"{prefix}{lookup}", true, false) ?? skin.GetAnimation($"{normal_hit}{lookup}", true, false); + return skin.GetAnimation($"{prefix}{lookup}", true, false) ?? + // fallback to regular size if "big" version doesn't exist. + skin.GetAnimation($"{normal_hit}{lookup}", true, false); } - // backgroundLayer is guaranteed to exist due to the pre-check in TaikoLegacySkinTransformer + // backgroundLayer is guaranteed to exist due to the pre-check in TaikoLegacySkinTransformer. AddInternal(backgroundLayer = getDrawableFor("circle")); var foregroundLayer = getDrawableFor("circleoverlay"); if (foregroundLayer != null) AddInternal(foregroundLayer); - // animations in taiko skins are used in a custom way (>150 combo and animating in time with beat). - // for now just stop at first frame for sanity. + // Animations in taiko skins are used in a custom way (>150 combo and animating in time with beat). + // For now just stop at first frame for sanity. foreach (var c in InternalChildren) { (c as IFramedAnimation)?.Stop(); @@ -66,8 +68,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning { base.Update(); - // not all skins (including the default osu-stable) have similar sizes for hitcircle and hitcircleoverlay. - // this ensures they are scaled relative to each other but also match the expected DrawableHit size. + // Not all skins (including the default osu-stable) have similar sizes for "hitcircle" and "hitcircleoverlay". + // This ensures they are scaled relative to each other but also match the expected DrawableHit size. foreach (var c in InternalChildren) c.Scale = new Vector2(DrawWidth / 128); } From e206df479b1636496a95f96711b6ccfa6a52696f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 11 Apr 2020 15:13:20 +0900 Subject: [PATCH 0982/2376] 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 aaac6ec427..5b200ee104 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 3e2c2b1599..7cf1272611 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -22,7 +22,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 7903d964ce..c58a431e80 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -79,7 +79,7 @@ - + From 12c21cba7e0150d0d14c3a5d5906b5e21409b132 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 11 Apr 2020 15:20:27 +0900 Subject: [PATCH 0983/2376] Add missing masking specification --- .../Edit/Blueprints/HoldNoteSelectionBlueprint.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs index f1750f4a01..d569d68b59 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs @@ -45,6 +45,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints new Container { RelativeSizeAxes = Axes.Both, + Masking = true, BorderThickness = 1, BorderColour = colours.Yellow, Child = new Box From eb1fbdacde77c9f7f29634d9f8953d7eb0e55dd7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 11 Apr 2020 15:29:52 +0900 Subject: [PATCH 0984/2376] Remove unintentional edge effect --- osu.Game/Overlays/Music/CollectionsDropdown.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/osu.Game/Overlays/Music/CollectionsDropdown.cs b/osu.Game/Overlays/Music/CollectionsDropdown.cs index 4f59b053b6..5bd321f31e 100644 --- a/osu.Game/Overlays/Music/CollectionsDropdown.cs +++ b/osu.Game/Overlays/Music/CollectionsDropdown.cs @@ -29,14 +29,8 @@ namespace osu.Game.Overlays.Music { public CollectionsMenu() { + Masking = true; CornerRadius = 5; - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Shadow, - Colour = Color4.Black.Opacity(0.3f), - Radius = 3, - Offset = new Vector2(0f, 1f), - }; } [BackgroundDependencyLoader] From a843793957583694bd12bcb765068da79c9388eb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 11 Apr 2020 16:41:11 +0900 Subject: [PATCH 0985/2376] Un-nest class --- osu.Game/Screens/Select/BeatmapCarousel.cs | 4 +- .../Screens/Select/DifficultyRecommender.cs | 75 +++++++++++++++++++ osu.Game/Screens/Select/SongSelect.cs | 62 --------------- 3 files changed, 78 insertions(+), 63 deletions(-) create mode 100644 osu.Game/Screens/Select/DifficultyRecommender.cs diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 3e619a1f80..555c74fb44 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -117,8 +117,10 @@ namespace osu.Game.Screens.Select private readonly Stack randomSelectedBeatmaps = new Stack(); protected List Items = new List(); + private CarouselRoot root; - public SongSelect.DifficultyRecommender DifficultyRecommender; + + public DifficultyRecommender DifficultyRecommender; public BeatmapCarousel() { diff --git a/osu.Game/Screens/Select/DifficultyRecommender.cs b/osu.Game/Screens/Select/DifficultyRecommender.cs new file mode 100644 index 0000000000..d89d505f61 --- /dev/null +++ b/osu.Game/Screens/Select/DifficultyRecommender.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 System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Graphics; +using osu.Game.Beatmaps; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Rulesets; + +namespace osu.Game.Screens.Select +{ + public class DifficultyRecommender : Component + { + [Resolved] + private IAPIProvider api { get; set; } + + [Resolved] + private RulesetStore rulesets { get; set; } + + private readonly Dictionary recommendedStarDifficulty = new Dictionary(); + + private int pendingAPIRequests; + + [BackgroundDependencyLoader] + private void load() + { + updateRecommended(); + } + + private void updateRecommended() + { + if (pendingAPIRequests > 0) + return; + if (api.LocalUser.Value is GuestUser) + return; + + rulesets.AvailableRulesets.ForEach(rulesetInfo => + { + var req = new GetUserRequest(api.LocalUser.Value.Id, rulesetInfo); + + req.Success += result => + { + // algorithm taken from https://github.com/ppy/osu-web/blob/e6e2825516449e3d0f3f5e1852c6bdd3428c3437/app/Models/User.php#L1505 + recommendedStarDifficulty[rulesetInfo] = Math.Pow((double)(result.Statistics.PP ?? 0), 0.4) * 0.195; + pendingAPIRequests--; + }; + + req.Failure += _ => pendingAPIRequests--; + + pendingAPIRequests++; + api.Queue(req); + }); + } + + public BeatmapInfo GetRecommendedBeatmap(IEnumerable beatmaps, RulesetInfo currentRuleset) + { + if (!recommendedStarDifficulty.ContainsKey(currentRuleset)) + { + updateRecommended(); + return null; + } + + return beatmaps.OrderBy(b => + { + var difference = b.StarDifficulty - recommendedStarDifficulty[currentRuleset]; + return difference >= 0 ? difference * 2 : difference * -1; // prefer easier over harder + }).FirstOrDefault(); + } + } +} diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index d6bc20df39..9897515615 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -36,9 +36,6 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; using osu.Game.Overlays.Notifications; using osu.Game.Scoring; -using osu.Game.Online.API; -using osu.Framework.Extensions.IEnumerableExtensions; -using osu.Game.Online.API.Requests; namespace osu.Game.Screens.Select { @@ -787,65 +784,6 @@ namespace osu.Game.Screens.Select return base.OnKeyDown(e); } - public class DifficultyRecommender : Component - { - [Resolved] - private IAPIProvider api { get; set; } - - [Resolved] - private RulesetStore rulesets { get; set; } - - private readonly Dictionary recommendedStarDifficulty = new Dictionary(); - - private int pendingAPIRequests; - - [BackgroundDependencyLoader] - private void load() - { - updateRecommended(); - } - - private void updateRecommended() - { - if (pendingAPIRequests > 0) - return; - if (api.LocalUser.Value is GuestUser) - return; - - rulesets.AvailableRulesets.ForEach(rulesetInfo => - { - var req = new GetUserRequest(api.LocalUser.Value.Id, rulesetInfo); - - req.Success += result => - { - // algorithm taken from https://github.com/ppy/osu-web/blob/e6e2825516449e3d0f3f5e1852c6bdd3428c3437/app/Models/User.php#L1505 - recommendedStarDifficulty[rulesetInfo] = Math.Pow((double)(result.Statistics.PP ?? 0), 0.4) * 0.195; - pendingAPIRequests--; - }; - - req.Failure += _ => pendingAPIRequests--; - - pendingAPIRequests++; - api.Queue(req); - }); - } - - public BeatmapInfo GetRecommendedBeatmap(IEnumerable beatmaps, RulesetInfo currentRuleset) - { - if (!recommendedStarDifficulty.ContainsKey(currentRuleset)) - { - updateRecommended(); - return null; - } - - return beatmaps.OrderBy(b => - { - var difference = b.StarDifficulty - recommendedStarDifficulty[currentRuleset]; - return difference >= 0 ? difference * 2 : difference * -1; // prefer easier over harder - }).FirstOrDefault(); - } - } - private class VerticalMaskingContainer : Container { private const float panel_overflow = 1.2f; From 7f753f6b4d79a2bbee5c169cdd10e39f4dd158e0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 11 Apr 2020 16:43:09 +0900 Subject: [PATCH 0986/2376] Remove current ruleset from function call --- osu.Game/Screens/Select/BeatmapCarousel.cs | 2 +- osu.Game/Screens/Select/DifficultyRecommender.cs | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 555c74fb44..7139b804b0 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -588,7 +588,7 @@ namespace osu.Game.Screens.Select BeatmapInfo recommender(IEnumerable beatmaps) { - return DifficultyRecommender?.GetRecommendedBeatmap(beatmaps, decoupledRuleset.Value); + return DifficultyRecommender?.GetRecommendedBeatmap(beatmaps); } var set = new CarouselBeatmapSet(beatmapSet, recommender); diff --git a/osu.Game/Screens/Select/DifficultyRecommender.cs b/osu.Game/Screens/Select/DifficultyRecommender.cs index d89d505f61..fb67d63818 100644 --- a/osu.Game/Screens/Select/DifficultyRecommender.cs +++ b/osu.Game/Screens/Select/DifficultyRecommender.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Game.Beatmaps; @@ -22,6 +23,9 @@ namespace osu.Game.Screens.Select [Resolved] private RulesetStore rulesets { get; set; } + [Resolved] + private Bindable ruleset { get; set; } + private readonly Dictionary recommendedStarDifficulty = new Dictionary(); private int pendingAPIRequests; @@ -57,9 +61,9 @@ namespace osu.Game.Screens.Select }); } - public BeatmapInfo GetRecommendedBeatmap(IEnumerable beatmaps, RulesetInfo currentRuleset) + public BeatmapInfo GetRecommendedBeatmap(IEnumerable beatmaps) { - if (!recommendedStarDifficulty.ContainsKey(currentRuleset)) + if (!recommendedStarDifficulty.ContainsKey(ruleset.Value)) { updateRecommended(); return null; @@ -67,7 +71,7 @@ namespace osu.Game.Screens.Select return beatmaps.OrderBy(b => { - var difference = b.StarDifficulty - recommendedStarDifficulty[currentRuleset]; + var difference = b.StarDifficulty - recommendedStarDifficulty[ruleset.Value]; return difference >= 0 ? difference * 2 : difference * -1; // prefer easier over harder }).FirstOrDefault(); } From a84fe2525ba17ac331c198e5c1ec80c061f1066f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 11 Apr 2020 16:53:45 +0900 Subject: [PATCH 0987/2376] Fix nested hitobjects potentially indirectly masked away --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 0011faefbb..8fa0c041d4 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -375,7 +375,7 @@ namespace osu.Game.Rulesets.Objects.Drawables } } - protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => AllJudged && base.ComputeIsMaskedAway(maskingBounds); + public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) => AllJudged && base.UpdateSubTreeMasking(source, maskingBounds); protected override void UpdateAfterChildren() { From abea7b5299a5fc38c12742aad9d741dec3be7f3a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 11 Apr 2020 16:58:13 +0900 Subject: [PATCH 0988/2376] Tidy up function passing, naming, ordering etc. --- osu.Game/Screens/Select/BeatmapCarousel.cs | 15 +++---- .../Select/Carousel/CarouselBeatmapSet.cs | 13 +++--- .../Screens/Select/DifficultyRecommender.cs | 42 +++++++++++-------- osu.Game/Screens/Select/SongSelect.cs | 8 ++-- 4 files changed, 42 insertions(+), 36 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 7139b804b0..3e3bb4dbc5 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -49,6 +49,11 @@ namespace osu.Game.Screens.Select /// public BeatmapSetInfo SelectedBeatmapSet => selectedBeatmapSet?.BeatmapSet; + /// + /// A function to optionally decide on a recommended difficulty from a beatmap set. + /// + public Func, BeatmapInfo> GetRecommendedBeatmap; + private CarouselBeatmapSet selectedBeatmapSet; /// @@ -120,8 +125,6 @@ namespace osu.Game.Screens.Select private CarouselRoot root; - public DifficultyRecommender DifficultyRecommender; - public BeatmapCarousel() { root = new CarouselRoot(this); @@ -586,12 +589,10 @@ namespace osu.Game.Screens.Select b.Metadata = beatmapSet.Metadata; } - BeatmapInfo recommender(IEnumerable beatmaps) + var set = new CarouselBeatmapSet(beatmapSet) { - return DifficultyRecommender?.GetRecommendedBeatmap(beatmaps); - } - - var set = new CarouselBeatmapSet(beatmapSet, recommender); + GetRecommendedBeatmap = beatmaps => GetRecommendedBeatmap?.Invoke(beatmaps) + }; foreach (var c in set.Beatmaps) { diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs index 99ded4c58e..92ccfde14b 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs @@ -12,13 +12,13 @@ namespace osu.Game.Screens.Select.Carousel { public class CarouselBeatmapSet : CarouselGroupEagerSelect { - private readonly Func, BeatmapInfo> getRecommendedBeatmap; - public IEnumerable Beatmaps => InternalChildren.OfType(); public BeatmapSetInfo BeatmapSet; - public CarouselBeatmapSet(BeatmapSetInfo beatmapSet, Func, BeatmapInfo> getRecommendedBeatmap) + public Func, BeatmapInfo> GetRecommendedBeatmap; + + public CarouselBeatmapSet(BeatmapSetInfo beatmapSet) { BeatmapSet = beatmapSet ?? throw new ArgumentNullException(nameof(beatmapSet)); @@ -26,8 +26,6 @@ namespace osu.Game.Screens.Select.Carousel .Where(b => !b.Hidden) .Select(b => new CarouselBeatmap(b)) .ForEach(AddChild); - - this.getRecommendedBeatmap = getRecommendedBeatmap; } protected override DrawableCarouselItem CreateDrawableRepresentation() => new DrawableCarouselBeatmapSet(this); @@ -36,9 +34,8 @@ namespace osu.Game.Screens.Select.Carousel { if (LastSelected == null) { - var recommendedBeatmapInfo = getRecommendedBeatmap(Children.OfType().Where(b => !b.Filtered.Value).Select(b => b.Beatmap)); - if (recommendedBeatmapInfo != null) - return Children.OfType().First(b => b.Beatmap == recommendedBeatmapInfo); + 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); } return base.GetNextToSelect(); diff --git a/osu.Game/Screens/Select/DifficultyRecommender.cs b/osu.Game/Screens/Select/DifficultyRecommender.cs index fb67d63818..47838ebd6d 100644 --- a/osu.Game/Screens/Select/DifficultyRecommender.cs +++ b/osu.Game/Screens/Select/DifficultyRecommender.cs @@ -33,10 +33,33 @@ namespace osu.Game.Screens.Select [BackgroundDependencyLoader] private void load() { - updateRecommended(); + calculateRecommendedDifficulties(); } - private void updateRecommended() + /// + /// Find the recommended difficulty from a selection of available difficulties for the current local user. + /// + /// + /// This requires the user to be online for now. + /// + /// A collection of beatmaps to select a difficulty from. + /// The recommended difficulty, or null if a recommendation could not be provided. + public BeatmapInfo GetRecommendedBeatmap(IEnumerable beatmaps) + { + if (!recommendedStarDifficulty.ContainsKey(ruleset.Value)) + { + calculateRecommendedDifficulties(); + return null; + } + + return beatmaps.OrderBy(b => + { + var difference = b.StarDifficulty - recommendedStarDifficulty[ruleset.Value]; + return difference >= 0 ? difference * 2 : difference * -1; // prefer easier over harder + }).FirstOrDefault(); + } + + private void calculateRecommendedDifficulties() { if (pendingAPIRequests > 0) return; @@ -60,20 +83,5 @@ namespace osu.Game.Screens.Select api.Queue(req); }); } - - public BeatmapInfo GetRecommendedBeatmap(IEnumerable beatmaps) - { - if (!recommendedStarDifficulty.ContainsKey(ruleset.Value)) - { - updateRecommended(); - return null; - } - - return beatmaps.OrderBy(b => - { - var difference = b.StarDifficulty - recommendedStarDifficulty[ruleset.Value]; - return difference >= 0 ? difference * 2 : difference * -1; // prefer easier over harder - }).FirstOrDefault(); - } } } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 9897515615..f164056ede 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -80,7 +80,8 @@ namespace osu.Game.Screens.Select protected override BackgroundScreen CreateBackground() => new BackgroundScreenBeatmap(Beatmap.Value); protected BeatmapCarousel Carousel { get; private set; } - private DifficultyRecommender difficultyRecommender; + + private DifficultyRecommender recommender; private BeatmapInfoWedge beatmapInfoWedge; private DialogOverlay dialogOverlay; @@ -108,10 +109,9 @@ namespace osu.Game.Screens.Select // initial value transfer is required for FilterControl (it uses our re-cached bindables in its async load for the initial filter). transferRulesetValue(); - AddInternal(difficultyRecommender = new DifficultyRecommender()); - AddRangeInternal(new Drawable[] { + recommender = new DifficultyRecommender(), new ResetScrollContainer(() => Carousel.ScrollToSelected()) { RelativeSizeAxes = Axes.Y, @@ -159,7 +159,7 @@ namespace osu.Game.Screens.Select RelativeSizeAxes = Axes.Both, SelectionChanged = updateSelectedBeatmap, BeatmapSetsChanged = carouselBeatmapsLoaded, - DifficultyRecommender = difficultyRecommender, + GetRecommendedBeatmap = recommender.GetRecommendedBeatmap, }, } }, From 310cf830d47a4618f33b2bc1fe646aa516823ea8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 11 Apr 2020 17:07:08 +0900 Subject: [PATCH 0989/2376] Simplify api request logic --- .../Screens/Select/DifficultyRecommender.cs | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/osu.Game/Screens/Select/DifficultyRecommender.cs b/osu.Game/Screens/Select/DifficultyRecommender.cs index 47838ebd6d..595bfd6122 100644 --- a/osu.Game/Screens/Select/DifficultyRecommender.cs +++ b/osu.Game/Screens/Select/DifficultyRecommender.cs @@ -15,7 +15,7 @@ using osu.Game.Rulesets; namespace osu.Game.Screens.Select { - public class DifficultyRecommender : Component + public class DifficultyRecommender : Component, IOnlineComponent { [Resolved] private IAPIProvider api { get; set; } @@ -28,12 +28,10 @@ namespace osu.Game.Screens.Select private readonly Dictionary recommendedStarDifficulty = new Dictionary(); - private int pendingAPIRequests; - [BackgroundDependencyLoader] private void load() { - calculateRecommendedDifficulties(); + api.Register(this); } /// @@ -48,7 +46,6 @@ namespace osu.Game.Screens.Select { if (!recommendedStarDifficulty.ContainsKey(ruleset.Value)) { - calculateRecommendedDifficulties(); return null; } @@ -61,11 +58,6 @@ namespace osu.Game.Screens.Select private void calculateRecommendedDifficulties() { - if (pendingAPIRequests > 0) - return; - if (api.LocalUser.Value is GuestUser) - return; - rulesets.AvailableRulesets.ForEach(rulesetInfo => { var req = new GetUserRequest(api.LocalUser.Value.Id, rulesetInfo); @@ -74,14 +66,27 @@ namespace osu.Game.Screens.Select { // algorithm taken from https://github.com/ppy/osu-web/blob/e6e2825516449e3d0f3f5e1852c6bdd3428c3437/app/Models/User.php#L1505 recommendedStarDifficulty[rulesetInfo] = Math.Pow((double)(result.Statistics.PP ?? 0), 0.4) * 0.195; - pendingAPIRequests--; }; - req.Failure += _ => pendingAPIRequests--; - - pendingAPIRequests++; api.Queue(req); }); } + + public void APIStateChanged(IAPIProvider api, APIState state) + { + switch (state) + { + case APIState.Online: + calculateRecommendedDifficulties(); + break; + } + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + api.Unregister(this); + } } } From 7aac0e59a8c02ab765b53b196177a79f43bb294e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 11 Apr 2020 17:08:07 +0900 Subject: [PATCH 0990/2376] Reduce dictionary lookups --- osu.Game/Screens/Select/DifficultyRecommender.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Select/DifficultyRecommender.cs b/osu.Game/Screens/Select/DifficultyRecommender.cs index 595bfd6122..20cdca858a 100644 --- a/osu.Game/Screens/Select/DifficultyRecommender.cs +++ b/osu.Game/Screens/Select/DifficultyRecommender.cs @@ -44,16 +44,16 @@ namespace osu.Game.Screens.Select /// The recommended difficulty, or null if a recommendation could not be provided. public BeatmapInfo GetRecommendedBeatmap(IEnumerable beatmaps) { - if (!recommendedStarDifficulty.ContainsKey(ruleset.Value)) + if (recommendedStarDifficulty.TryGetValue(ruleset.Value, out var stars)) { - return null; + return beatmaps.OrderBy(b => + { + var difference = b.StarDifficulty - stars; + return difference >= 0 ? difference * 2 : difference * -1; // prefer easier over harder + }).FirstOrDefault(); } - return beatmaps.OrderBy(b => - { - var difference = b.StarDifficulty - recommendedStarDifficulty[ruleset.Value]; - return difference >= 0 ? difference * 2 : difference * -1; // prefer easier over harder - }).FirstOrDefault(); + return null; } private void calculateRecommendedDifficulties() From c0c1f2c0235c9e49c8c44541ad1d266a182bb017 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 11 Apr 2020 17:17:18 +0900 Subject: [PATCH 0991/2376] Add test coverage --- .../SongSelect/TestSceneBeatmapCarousel.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 76a8ee9914..f68ed4154b 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -54,6 +54,35 @@ namespace osu.Game.Tests.Visual.SongSelect this.rulesets = rulesets; } + [Test] + public void TestRecommendedSelection() + { + loadBeatmaps(); + + AddStep("set recommendation function", () => carousel.GetRecommendedBeatmap = beatmaps => beatmaps.LastOrDefault()); + + // check recommended was selected + advanceSelection(direction: 1, diff: false); + waitForSelection(1, 3); + + // change away from recommended + advanceSelection(direction: -1, diff: true); + waitForSelection(1, 2); + + // next set, check recommended + advanceSelection(direction: 1, diff: false); + waitForSelection(2, 3); + + // next set, check recommended + advanceSelection(direction: 1, diff: false); + waitForSelection(3, 3); + + // go back to first set and ensure user selection was retained + advanceSelection(direction: -1, diff: false); + advanceSelection(direction: -1, diff: false); + waitForSelection(1, 2); + } + /// /// Test keyboard traversal /// From 73a3f1fe65d6eb17ca9eef1a5b3cd8a843f0e3eb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 11 Apr 2020 17:30:34 +0900 Subject: [PATCH 0992/2376] Remove unnecessary DI --- osu.Game/Screens/Select/BeatmapCarousel.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 3e3bb4dbc5..a8225ba1ec 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -23,7 +23,6 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; using osu.Game.Input.Bindings; using osu.Game.Screens.Select.Carousel; -using osu.Game.Rulesets; namespace osu.Game.Screens.Select { @@ -146,9 +145,6 @@ namespace osu.Game.Screens.Select [Resolved] private BeatmapManager beatmaps { get; set; } - [Resolved] - private Bindable decoupledRuleset { get; set; } - [BackgroundDependencyLoader(permitNulls: true)] private void load(OsuConfigManager config) { From 832822858ca2c17eac82ab669f4c10634978c58c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 11 Apr 2020 17:47:51 +0900 Subject: [PATCH 0993/2376] Add basic request / response support --- .../Online/TestDummyAPIRequestHandling.cs | 39 +++++++++++++++++++ osu.Game/Online/API/APIRequest.cs | 2 + osu.Game/Online/API/DummyAPIAccess.cs | 7 ++++ 3 files changed, 48 insertions(+) create mode 100644 osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs diff --git a/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs b/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs new file mode 100644 index 0000000000..bf3e1204d7 --- /dev/null +++ b/osu.Game.Tests/Online/TestDummyAPIRequestHandling.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.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Tests.Visual; + +namespace osu.Game.Tests.Online +{ + public class TestDummyAPIRequestHandling : OsuTestScene + { + public TestDummyAPIRequestHandling() + { + AddStep("register request handling", () => ((DummyAPIAccess)API).HandleRequest = req => + { + switch (req) + { + case CommentVoteRequest cRequest: + cRequest.TriggerSuccess(new CommentBundle()); + break; + } + }); + + CommentVoteRequest request = null; + CommentBundle response = null; + + AddStep("fire request", () => + { + response = null; + request = new CommentVoteRequest(1, CommentVoteAction.Vote); + request.Success += res => response = res; + API.Queue(request); + }); + + AddAssert("got response", () => response != null); + } + } +} diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs index 6a6c7b72a8..1f0eae4965 100644 --- a/osu.Game/Online/API/APIRequest.cs +++ b/osu.Game/Online/API/APIRequest.cs @@ -30,6 +30,8 @@ namespace osu.Game.Online.API /// This will be scheduled to the API's internal scheduler (run on update thread automatically). /// public new event APISuccessHandler Success; + + internal void TriggerSuccess(T result) => Success?.Invoke(result); } /// diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index a1c3475fd9..fa5ad115d2 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.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.Threading; using System.Threading.Tasks; @@ -30,6 +31,11 @@ namespace osu.Game.Online.API private readonly List components = new List(); + /// + /// Provide handling logic for an arbitrary API request. + /// + public Action HandleRequest; + public APIState State { get => state; @@ -55,6 +61,7 @@ namespace osu.Game.Online.API public virtual void Queue(APIRequest request) { + HandleRequest?.Invoke(request); } public void Perform(APIRequest request) { } From 415adecdf68c07d8882ca7ecdb2c099d6749243a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 11 Apr 2020 18:02:43 +0900 Subject: [PATCH 0994/2376] Add support for Result fetching --- osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs | 8 ++++++-- osu.Game/Online/API/APIRequest.cs | 8 +++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs b/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs index bf3e1204d7..5b169cccdf 100644 --- a/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs +++ b/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs @@ -10,7 +10,8 @@ namespace osu.Game.Tests.Online { public class TestDummyAPIRequestHandling : OsuTestScene { - public TestDummyAPIRequestHandling() + [Test] + public void TestGenericRequestHandling() { AddStep("register request handling", () => ((DummyAPIAccess)API).HandleRequest = req => { @@ -33,7 +34,10 @@ namespace osu.Game.Tests.Online API.Queue(request); }); - AddAssert("got response", () => response != null); + AddAssert("response event fired", () => response != null); + + AddAssert("request has response", () => request.Result == response); + } } } } diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs index 1f0eae4965..34b69b3c09 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 System.Diagnostics; using Newtonsoft.Json; using osu.Framework.IO.Network; using osu.Framework.Logging; @@ -98,10 +99,15 @@ namespace osu.Game.Online.API { if (cancelled) return; - Success?.Invoke(); + TriggerSuccess(); }); } + internal void TriggerSuccess() + { + Success?.Invoke(); + } + public void Cancel() => Fail(new OperationCanceledException(@"Request cancelled")); public void Fail(Exception e) From c96df9758674b58df599ca8f9153f9f5c1d3e206 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 11 Apr 2020 18:02:49 +0900 Subject: [PATCH 0995/2376] Add support for non-generic requests --- .../Online/TestDummyAPIRequestHandling.cs | 29 +++++++++++++++++++ osu.Game/Online/API/APIRequest.cs | 15 ++++++---- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs b/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs index 5b169cccdf..b00b63f6d5 100644 --- a/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs +++ b/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs @@ -1,10 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using NUnit.Framework; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Chat; using osu.Game.Tests.Visual; +using osu.Game.Users; namespace osu.Game.Tests.Online { @@ -38,6 +41,32 @@ namespace osu.Game.Tests.Online AddAssert("request has response", () => request.Result == response); } + + [Test] + public void TestRequestHandling() + { + AddStep("register request handling", () => ((DummyAPIAccess)API).HandleRequest = req => + { + switch (req) + { + case LeaveChannelRequest cRequest: + cRequest.TriggerSuccess(); + break; + } + }); + + LeaveChannelRequest request; + bool gotResponse = false; + + AddStep("fire request", () => + { + gotResponse = false; + request = new LeaveChannelRequest(new Channel(), new User()); + request.Success += () => gotResponse = true; + API.Queue(request); + }); + + AddAssert("response event fired", () => gotResponse); } } } diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs index 34b69b3c09..6abb388c01 100644 --- a/osu.Game/Online/API/APIRequest.cs +++ b/osu.Game/Online/API/APIRequest.cs @@ -17,22 +17,27 @@ namespace osu.Game.Online.API { protected override WebRequest CreateWebRequest() => new OsuJsonWebRequest(Uri); - public T Result => ((OsuJsonWebRequest)WebRequest)?.ResponseObject; + public T Result { get; private set; } protected APIRequest() { - base.Success += onSuccess; + base.Success += () => TriggerSuccess(((OsuJsonWebRequest)WebRequest)?.ResponseObject); } - private void onSuccess() => Success?.Invoke(Result); - /// /// Invoked on successful completion of an API request. /// This will be scheduled to the API's internal scheduler (run on update thread automatically). /// public new event APISuccessHandler Success; - internal void TriggerSuccess(T result) => Success?.Invoke(result); + internal void TriggerSuccess(T result) + { + // disallow calling twice + Debug.Assert(Result == null); + + Result = result; + Success?.Invoke(result); + } } /// From 1c0ad13d82bec928abb9f0cdb9e826f0cf23c7f8 Mon Sep 17 00:00:00 2001 From: Tim Oliver Date: Sat, 11 Apr 2020 17:20:37 +0800 Subject: [PATCH 0996/2376] Added ignore hit object --- osu.Game.Rulesets.Taiko/Objects/IgnoreHit.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 osu.Game.Rulesets.Taiko/Objects/IgnoreHit.cs diff --git a/osu.Game.Rulesets.Taiko/Objects/IgnoreHit.cs b/osu.Game.Rulesets.Taiko/Objects/IgnoreHit.cs new file mode 100644 index 0000000000..302f940ef4 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Objects/IgnoreHit.cs @@ -0,0 +1,12 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Judgements; + +namespace osu.Game.Rulesets.Taiko.Objects +{ + public class IgnoreHit : Hit + { + public override Judgement CreateJudgement() => new IgnoreJudgement(); + } +} From 3ad36c7b84e920a13e48b160888cab9874bac20d Mon Sep 17 00:00:00 2001 From: Tim Oliver Date: Sat, 11 Apr 2020 17:20:52 +0800 Subject: [PATCH 0997/2376] Moved flying objects to use ignore hit judgements --- osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs | 3 ++- osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs index 86e885239f..4468c1e3fb 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs @@ -5,6 +5,7 @@ using osu.Framework.Allocation; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; @@ -35,7 +36,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables } public DrawableFlyingCentreHit(double time, bool isStrong = false) - : base(new Hit { StartTime = time, IsStrong = isStrong }) + : base(new IgnoreHit { StartTime = time, IsStrong = isStrong }) { HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs index ad9872b21f..e5cfc0562b 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables } public DrawableFlyingRimHit(double time, bool isStrong = false) - : base(new Hit { StartTime = time, IsStrong = isStrong }) + : base(new IgnoreHit { StartTime = time, IsStrong = isStrong }) { HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); } From df76636ffc5be7d5d817a7a943ec56a505339855 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sat, 11 Apr 2020 14:08:16 +0300 Subject: [PATCH 0998/2376] Implement "prefer no video" option --- osu.Game/Configuration/OsuConfigManager.cs | 2 ++ osu.Game/Overlays/Direct/PanelDownloadButton.cs | 10 ++++++---- .../Overlays/Settings/Sections/Online/WebSettings.cs | 5 +++++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 41f6747b74..89eb084262 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -49,6 +49,7 @@ namespace osu.Game.Configuration }; Set(OsuSetting.ExternalLinkWarning, true); + Set(OsuSetting.PreferNoVideo, false); // Audio Set(OsuSetting.VolumeInactive, 0.25, 0, 1, 0.01); @@ -212,6 +213,7 @@ namespace osu.Game.Configuration IncreaseFirstObjectVisibility, ScoreDisplayMode, ExternalLinkWarning, + PreferNoVideo, Scaling, ScalingPositionX, ScalingPositionY, diff --git a/osu.Game/Overlays/Direct/PanelDownloadButton.cs b/osu.Game/Overlays/Direct/PanelDownloadButton.cs index 1b3657f010..f09586c571 100644 --- a/osu.Game/Overlays/Direct/PanelDownloadButton.cs +++ b/osu.Game/Overlays/Direct/PanelDownloadButton.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online; @@ -14,12 +15,12 @@ namespace osu.Game.Overlays.Direct { protected bool DownloadEnabled => button.Enabled.Value; - private readonly bool noVideo; + private readonly bool? noVideo; private readonly ShakeContainer shakeContainer; private readonly DownloadButton button; - public PanelDownloadButton(BeatmapSetInfo beatmapSet, bool noVideo = false) + public PanelDownloadButton(BeatmapSetInfo beatmapSet, bool? noVideo = null) : base(beatmapSet) { this.noVideo = noVideo; @@ -43,7 +44,7 @@ namespace osu.Game.Overlays.Direct } [BackgroundDependencyLoader(true)] - private void load(OsuGame game, BeatmapManager beatmaps) + private void load(OsuGame game, BeatmapManager beatmaps, OsuConfigManager osuConfig) { if (BeatmapSet.Value?.OnlineInfo?.Availability?.DownloadDisabled ?? false) { @@ -66,7 +67,8 @@ namespace osu.Game.Overlays.Direct break; default: - beatmaps.Download(BeatmapSet.Value, noVideo); + var minimiseDownloadSize = noVideo ?? osuConfig.GetBindable(OsuSetting.PreferNoVideo).Value; + beatmaps.Download(BeatmapSet.Value, minimiseDownloadSize); break; } }; diff --git a/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs b/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs index a8b3e45a83..da3176aca8 100644 --- a/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs @@ -21,6 +21,11 @@ namespace osu.Game.Overlays.Settings.Sections.Online LabelText = "Warn about opening external links", Bindable = config.GetBindable(OsuSetting.ExternalLinkWarning) }, + new SettingsCheckbox + { + LabelText = "Prefer no-video downloads", + Bindable = config.GetBindable(OsuSetting.PreferNoVideo) + }, }; } } From fc1d497a864c48adf3d702beb1c0b7c93d23ddcd Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sat, 11 Apr 2020 14:21:28 +0300 Subject: [PATCH 0999/2376] Change PlaylistDownloadButton default --- osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs index ed3f9af8e2..d58218b6b5 100644 --- a/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs @@ -212,7 +212,7 @@ namespace osu.Game.Screens.Multi private class PlaylistDownloadButton : PanelDownloadButton { - public PlaylistDownloadButton(BeatmapSetInfo beatmapSet, bool noVideo = false) + public PlaylistDownloadButton(BeatmapSetInfo beatmapSet, bool? noVideo = null) : base(beatmapSet, noVideo) { Alpha = 0; From 97340da2f296eb1d88ac70c7addfb5cf2dd76a23 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 12 Apr 2020 02:24:36 +0300 Subject: [PATCH 1000/2376] Add null-conditional to acesses in dispose methods Such a terrible mistake, the finalizer may be called while the dependencies have an instance but the local itself doesn't have a value yet. --- osu.Game/Rulesets/UI/DrawableRuleset.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 265c6a7319..06f8715929 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -343,7 +343,7 @@ namespace osu.Game.Rulesets.UI } // Dispose the components created by this dependency container. - dependencies.Dispose(); + dependencies?.Dispose(); } } From f274ec297ce23e79eff1d2a64f745e1396b0c280 Mon Sep 17 00:00:00 2001 From: Fire937 Date: Sun, 12 Apr 2020 01:33:25 +0200 Subject: [PATCH 1001/2376] Add positional sound support for all rulesets The SamplePlaybackBalance is calculated in a way that the balance remains between -0.4 and 0.4. Positional sound is not supported in osu!taiko. --- .../Drawables/DrawableCatchHitObject.cs | 2 ++ .../Drawables/DrawableManiaHitObject.cs | 16 ++++++++++++ .../Objects/Drawables/DrawableOsuHitObject.cs | 2 ++ .../Objects/Drawables/DrawableHitObject.cs | 26 ++++++------------- 4 files changed, 28 insertions(+), 18 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs index 6844be5941..e726d6eff5 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs @@ -70,6 +70,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables public float DisplayRadius => DrawSize.X / 2 * Scale.X * HitObject.Scale; + protected override float SamplePlaybackBalance => 0.8f * HitObject.X - 0.4f; + protected DrawableCatchHitObject(CatchHitObject hitObject) : base(hitObject) { diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs index 5bfa07bd14..76e9695855 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs @@ -5,8 +5,10 @@ using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Rulesets.Mania.UI; namespace osu.Game.Rulesets.Mania.Objects.Drawables { @@ -24,6 +26,20 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables protected readonly IBindable Direction = new Bindable(); + protected override float SamplePlaybackBalance + { + get + { + CompositeDrawable stage = this; + while (!(stage is Stage)) + stage = stage.Parent; + + var columnCount = ((Stage)stage).Columns.Count; + var columnIndex = HitObject.Column; + return 0.8f * columnIndex / (columnCount - 1) - 0.4f; + } + } + protected DrawableManiaHitObject(ManiaHitObject hitObject) : base(hitObject) { diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index a677cb6a72..c5a4491b2d 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -16,6 +16,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables // Must be set to update IsHovered as it's used in relax mdo to detect osu hit objects. public override bool HandlePositionalInput => true; + protected override float SamplePlaybackBalance => (HitObject.X / 512f - 0.5f) * 0.8f; + protected DrawableOsuHitObject(OsuHitObject hitObject) : base(hitObject) { diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 30a9106ddc..ef32dc1560 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -33,11 +33,6 @@ namespace osu.Game.Rulesets.Objects.Drawables /// public readonly Bindable AccentColour = new Bindable(Color4.Gray); - /// - /// The stereo balance of the samples if the Positional hitsounds setting is set. - /// - private readonly BindableDouble positionalSoundAdjustment = new BindableDouble(); - protected SkinnableSound Samples { get; private set; } protected virtual IEnumerable GetSamples() => HitObject.Samples; @@ -94,7 +89,9 @@ namespace osu.Game.Rulesets.Objects.Drawables /// /// The stereo balance of the samples played if Positional hitsounds is set. /// - protected virtual float PositionalSound => (Position.X / 512f - 0.5f) * 0.8f; + protected virtual float SamplePlaybackBalance => 0; + + private readonly BindableDouble samplePlaybackBalanceAdjustment = new BindableDouble(); private BindableList samplesBindable; private Bindable startTimeBindable; @@ -119,6 +116,7 @@ namespace osu.Game.Rulesets.Objects.Drawables [BackgroundDependencyLoader] private void load(OsuConfigManager config) { + userPositionalHitSounds = config.GetBindable(OsuSetting.PositionalHitSounds); var judgement = HitObject.CreateJudgement(); Result = CreateResult(judgement); @@ -126,16 +124,6 @@ namespace osu.Game.Rulesets.Objects.Drawables throw new InvalidOperationException($"{GetType().ReadableName()} must provide a {nameof(JudgementResult)} through {nameof(CreateResult)}."); loadSamples(); - - userPositionalHitSounds = config.GetBindable(OsuSetting.PositionalHitSounds); - userPositionalHitSounds.BindValueChanged(positional => - { - if (positional.NewValue) - Samples?.AddAdjustment(AdjustableProperty.Balance, positionalSoundAdjustment); - else - Samples?.RemoveAdjustment(AdjustableProperty.Balance, positionalSoundAdjustment); - }); - userPositionalHitSounds.TriggerChange(); } protected override void LoadComplete() @@ -179,7 +167,9 @@ namespace osu.Game.Rulesets.Objects.Drawables + $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}."); } - AddInternal(Samples = new SkinnableSound(samples.Select(s => HitObject.SampleControlPoint.ApplyTo(s)))); + Samples = new SkinnableSound(samples.Select(s => HitObject.SampleControlPoint.ApplyTo(s))); + Samples.AddAdjustment(AdjustableProperty.Balance, samplePlaybackBalanceAdjustment); + AddInternal(Samples); } private void onDefaultsApplied() => apply(HitObject); @@ -378,7 +368,7 @@ namespace osu.Game.Rulesets.Objects.Drawables /// public virtual void PlaySamples() { - positionalSoundAdjustment.Value = PositionalSound; + samplePlaybackBalanceAdjustment.Value = userPositionalHitSounds.Value ? SamplePlaybackBalance : 0; Samples?.Play(); } From 162a85042a6e15be69f57388e50669e174f81792 Mon Sep 17 00:00:00 2001 From: Tim Oliver Date: Sun, 12 Apr 2020 10:38:22 +0800 Subject: [PATCH 1002/2376] Removed un-needed using --- osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs index 4468c1e3fb..b6f6f04821 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs @@ -5,7 +5,6 @@ using osu.Framework.Allocation; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; -using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; From 7a9ee907bf4d862c1e49188a8c835a2412977fb9 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 12 Apr 2020 07:34:58 +0300 Subject: [PATCH 1003/2376] Fix incorrect button state in some cases --- osu.Game/Overlays/OverlayScrollContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/OverlayScrollContainer.cs b/osu.Game/Overlays/OverlayScrollContainer.cs index a6c687f28f..a8f2a0ce6c 100644 --- a/osu.Game/Overlays/OverlayScrollContainer.cs +++ b/osu.Game/Overlays/OverlayScrollContainer.cs @@ -60,7 +60,7 @@ namespace osu.Game.Overlays return; currentTarget = Target; - Button.State = Current > button_scroll_position ? Visibility.Visible : Visibility.Hidden; + Button.State = Target > button_scroll_position ? Visibility.Visible : Visibility.Hidden; } public class ScrollToTopButton : OsuHoverContainer From f8b728f9e86e7bb0c8e2a6686859201dc7b26f49 Mon Sep 17 00:00:00 2001 From: Tim Oliver Date: Tue, 7 Apr 2020 16:39:41 +0800 Subject: [PATCH 1004/2376] =?UTF-8?q?Added=20=E2=80=9Cinstant=20fly?= =?UTF-8?q?=E2=80=9D=20variant=20of=20hit=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Objects/Drawables/DrawableCentreHit.cs | 17 +++++++++++++++++ .../Objects/Drawables/DrawableRimHit.cs | 17 +++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs index 4979135f50..08df05e719 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs @@ -2,7 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; namespace osu.Game.Rulesets.Taiko.Objects.Drawables @@ -23,4 +26,18 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables MainPiece.AccentColour = colours.PinkDarker; } } + + public class DrawableFlyingCentreHit : DrawableCentreHit + { + protected override void CheckForResult(bool userTriggered, double timeOffset) + { + ApplyResult(r => r.Type = HitResult.Good); + } + + public DrawableFlyingCentreHit(double time) + : base(new Hit { StartTime = time }) + { + HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + } + } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs index 5a12d71cea..0c2c9fbdef 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs @@ -2,7 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; namespace osu.Game.Rulesets.Taiko.Objects.Drawables @@ -23,4 +26,18 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables MainPiece.AccentColour = colours.BlueDarker; } } + + public class DrawableFlyingRimHit : DrawableRimHit + { + protected override void CheckForResult(bool userTriggered, double timeOffset) + { + ApplyResult(r => r.Type = HitResult.Good); + } + + public DrawableFlyingRimHit(double time) + : base(new Hit { StartTime = time }) + { + HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + } + } } From a52130857bdefc35656ff4357ff8ee3ee1783174 Mon Sep 17 00:00:00 2001 From: Tim Oliver Date: Tue, 7 Apr 2020 16:40:01 +0800 Subject: [PATCH 1005/2376] Added arbitrary hit handler to drum roll object --- .../Objects/Drawables/DrawableDrumRoll.cs | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index 5806c90115..3e7b6dfd31 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -34,6 +34,14 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private Color4 colourIdle; private Color4 colourEngaged; + private bool judgingStarted; + + /// + /// A handler action for when the drumroll has been hit, + /// regardless of any judgement. + /// + public Action OnHit; + public DrawableDrumRoll(DrumRoll drumRoll) : base(drumRoll) { @@ -86,15 +94,27 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected override TaikoPiece CreateMainPiece() => new ElongatedCirclePiece(); - public override bool OnPressed(TaikoAction action) => false; + public override bool OnPressed(TaikoAction action) + { + if (judgingStarted) + OnHit.Invoke(action); + + return false; + } private void onNewResult(DrawableHitObject obj, JudgementResult result) { if (!(obj is DrawableDrumRollTick)) return; + DrawableDrumRollTick drumRollTick = (DrawableDrumRollTick)obj; + if (result.Type > HitResult.Miss) + { + OnHit.Invoke(drumRollTick.JudgedAction); + judgingStarted = true; rollingHits++; + } else rollingHits--; @@ -113,8 +133,11 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables return; int countHit = NestedHitObjects.Count(o => o.IsHit); + if (countHit >= HitObject.RequiredGoodHits) + { ApplyResult(r => r.Type = countHit >= HitObject.RequiredGreatHits ? HitResult.Great : HitResult.Good); + } else ApplyResult(r => r.Type = HitResult.Miss); } From 81a514ee6a38b37cb1b9a64e947f2087eba287cd Mon Sep 17 00:00:00 2001 From: Tim Oliver Date: Tue, 7 Apr 2020 16:40:18 +0800 Subject: [PATCH 1006/2376] Added judgement forwarder to drumroll tick object --- .../Objects/Drawables/DrawableDrumRollTick.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs index 25b6141a0e..9961cb6ea2 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs @@ -11,6 +11,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { public class DrawableDrumRollTick : DrawableTaikoHitObject { + /// + /// The action type that the user took which caused this tick to + /// have been judged as "hit" + /// + public TaikoAction JudgedAction; + public DrawableDrumRollTick(DrumRollTick tick) : base(tick) { @@ -49,7 +55,11 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables } } - public override bool OnPressed(TaikoAction action) => UpdateResult(true); + public override bool OnPressed(TaikoAction action) + { + JudgedAction = action; + return UpdateResult(true); + } protected override DrawableStrongNestedHit CreateStrongHit(StrongHitObject hitObject) => new StrongNestedHit(hitObject, this); From fbcfc7d278a7ed779839c7ade8a395db9b4bc53a Mon Sep 17 00:00:00 2001 From: Tim Oliver Date: Tue, 7 Apr 2020 16:40:32 +0800 Subject: [PATCH 1007/2376] Added separate scrolling track to display drum roll notes --- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 25 ++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index bde9085c23..b32e7b53da 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -42,6 +42,7 @@ namespace osu.Game.Rulesets.Taiko.UI private readonly Container hitExplosionContainer; private readonly Container kiaiExplosionContainer; private readonly JudgementContainer judgementContainer; + private readonly ScrollingHitObjectContainer drumRollHitContainer; internal readonly HitTarget HitTarget; private readonly ProxyContainer topLevelHitContainer; @@ -135,6 +136,14 @@ namespace osu.Game.Rulesets.Taiko.UI Margin = new MarginPadding { Left = HIT_TARGET_OFFSET }, Blending = BlendingParameters.Additive }, + drumRollHitContainer = new ScrollingHitObjectContainer + { + Name = "Drumroll hit", + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Stretch, + Margin = new MarginPadding { Left = HIT_TARGET_OFFSET }, + Width = 1.0f + } } }, overlayBackgroundContainer = new Container @@ -212,12 +221,28 @@ namespace osu.Game.Rulesets.Taiko.UI barlineContainer.Add(barline.CreateProxy()); break; + case DrawableDrumRoll drumRoll: + drumRoll.OnHit += onDrumrollArbitraryHit; + break; + case DrawableTaikoHitObject taikoObject: topLevelHitContainer.Add(taikoObject.CreateProxiedContent()); break; } } + private void onDrumrollArbitraryHit(TaikoAction action) + { + DrawableHit drawableHit; + + if (action == TaikoAction.LeftRim || action == TaikoAction.RightRim) + drawableHit = new DrawableFlyingRimHit(Time.Current); + else + drawableHit = new DrawableFlyingCentreHit(Time.Current); + + drumRollHitContainer.Add(drawableHit); + } + internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result) { if (!DisplayJudgements.Value) From c6d996030a83763d4b03ff1e5a819e6958d9f973 Mon Sep 17 00:00:00 2001 From: Tim Oliver Date: Tue, 7 Apr 2020 17:25:47 +0800 Subject: [PATCH 1008/2376] Added logic to allow strong notes --- .../Objects/Drawables/DrawableCentreHit.cs | 4 ++-- .../Objects/Drawables/DrawableDrumRoll.cs | 6 +++--- osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs | 4 ++-- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs index 08df05e719..86e885239f 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs @@ -34,8 +34,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables ApplyResult(r => r.Type = HitResult.Good); } - public DrawableFlyingCentreHit(double time) - : base(new Hit { StartTime = time }) + public DrawableFlyingCentreHit(double time, bool isStrong = false) + : base(new Hit { StartTime = time, IsStrong = isStrong }) { HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index 3e7b6dfd31..64be870262 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables /// A handler action for when the drumroll has been hit, /// regardless of any judgement. /// - public Action OnHit; + public Action OnHit; public DrawableDrumRoll(DrumRoll drumRoll) : base(drumRoll) @@ -97,7 +97,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables public override bool OnPressed(TaikoAction action) { if (judgingStarted) - OnHit.Invoke(action); + OnHit.Invoke(action, HitObject.IsStrong); return false; } @@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (result.Type > HitResult.Miss) { - OnHit.Invoke(drumRollTick.JudgedAction); + OnHit.Invoke(drumRollTick.JudgedAction, HitObject.IsStrong); judgingStarted = true; rollingHits++; } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs index 0c2c9fbdef..ad9872b21f 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs @@ -34,8 +34,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables ApplyResult(r => r.Type = HitResult.Good); } - public DrawableFlyingRimHit(double time) - : base(new Hit { StartTime = time }) + public DrawableFlyingRimHit(double time, bool isStrong = false) + : base(new Hit { StartTime = time, IsStrong = isStrong }) { HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); } diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index b32e7b53da..59cf9193b5 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -231,14 +231,14 @@ namespace osu.Game.Rulesets.Taiko.UI } } - private void onDrumrollArbitraryHit(TaikoAction action) + private void onDrumrollArbitraryHit(TaikoAction action, bool isStrong) { DrawableHit drawableHit; if (action == TaikoAction.LeftRim || action == TaikoAction.RightRim) - drawableHit = new DrawableFlyingRimHit(Time.Current); + drawableHit = new DrawableFlyingRimHit(Time.Current, isStrong); else - drawableHit = new DrawableFlyingCentreHit(Time.Current); + drawableHit = new DrawableFlyingCentreHit(Time.Current, isStrong); drumRollHitContainer.Add(drawableHit); } From 9d0d2ef68aaff860b62c2c3de247f2a34ecb2961 Mon Sep 17 00:00:00 2001 From: Tim Oliver Date: Wed, 8 Apr 2020 12:12:59 +0800 Subject: [PATCH 1009/2376] Added content proxying to drull roll elements --- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 59cf9193b5..e947795fe5 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -241,6 +241,7 @@ namespace osu.Game.Rulesets.Taiko.UI drawableHit = new DrawableFlyingCentreHit(Time.Current, isStrong); drumRollHitContainer.Add(drawableHit); + topLevelHitContainer.Add(drawableHit.CreateProxiedContent()); } internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result) From 78492dbba8a23fba4e07606afce6b3f1ae336227 Mon Sep 17 00:00:00 2001 From: Tim Oliver Date: Sat, 11 Apr 2020 17:20:37 +0800 Subject: [PATCH 1010/2376] Added ignore hit object --- osu.Game.Rulesets.Taiko/Objects/IgnoreHit.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 osu.Game.Rulesets.Taiko/Objects/IgnoreHit.cs diff --git a/osu.Game.Rulesets.Taiko/Objects/IgnoreHit.cs b/osu.Game.Rulesets.Taiko/Objects/IgnoreHit.cs new file mode 100644 index 0000000000..302f940ef4 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Objects/IgnoreHit.cs @@ -0,0 +1,12 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Judgements; + +namespace osu.Game.Rulesets.Taiko.Objects +{ + public class IgnoreHit : Hit + { + public override Judgement CreateJudgement() => new IgnoreJudgement(); + } +} From 940d85cfa6f22df86ce1a55a752959579998028b Mon Sep 17 00:00:00 2001 From: Tim Oliver Date: Sat, 11 Apr 2020 17:20:52 +0800 Subject: [PATCH 1011/2376] Moved flying objects to use ignore hit judgements --- osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs | 3 ++- osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs index 86e885239f..4468c1e3fb 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs @@ -5,6 +5,7 @@ using osu.Framework.Allocation; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; @@ -35,7 +36,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables } public DrawableFlyingCentreHit(double time, bool isStrong = false) - : base(new Hit { StartTime = time, IsStrong = isStrong }) + : base(new IgnoreHit { StartTime = time, IsStrong = isStrong }) { HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs index ad9872b21f..e5cfc0562b 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables } public DrawableFlyingRimHit(double time, bool isStrong = false) - : base(new Hit { StartTime = time, IsStrong = isStrong }) + : base(new IgnoreHit { StartTime = time, IsStrong = isStrong }) { HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); } From 442992dc5bd9bb1e4fcdbeee0ab39a170af263f5 Mon Sep 17 00:00:00 2001 From: Tim Oliver Date: Sun, 12 Apr 2020 10:38:22 +0800 Subject: [PATCH 1012/2376] Removed un-needed using --- osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs index 4468c1e3fb..b6f6f04821 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs @@ -5,7 +5,6 @@ using osu.Framework.Allocation; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; -using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; From c3f0475748f910172c5161db4dbd62afc3eb0c28 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 12 Apr 2020 17:40:22 +0900 Subject: [PATCH 1013/2376] Make CirclePiece abstract --- .../Objects/Drawables/Pieces/CirclePiece.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs index 70fe4b7bb2..6ca77e666d 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces /// for a usage example. /// /// - public class CirclePiece : BeatSyncedContainer + public abstract class CirclePiece : BeatSyncedContainer { public const float SYMBOL_SIZE = 0.45f; public const float SYMBOL_BORDER = 8; @@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces public Box FlashBox; - public CirclePiece() + protected CirclePiece() { RelativeSizeAxes = Axes.Both; From c5d6c7728a512ab6a85857e391f7841afedb255f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 12 Apr 2020 18:29:25 +0900 Subject: [PATCH 1014/2376] 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 5b200ee104..723844155f 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 7cf1272611..0732e6090d 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -23,7 +23,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index c58a431e80..d7006761be 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + From 63a1686dfbe8e984cc8e3e5ad32ce1bfa8931e41 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sun, 12 Apr 2020 12:42:52 +0300 Subject: [PATCH 1015/2376] Scroll to screen middle --- osu.Game/Screens/Select/BeatmapCarousel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 59dddc2baa..a5379e9649 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -632,7 +632,7 @@ namespace osu.Game.Screens.Select case DrawableCarouselBeatmap beatmap: { if (beatmap.Item.State.Value == CarouselItemState.Selected) - scrollTarget = currentY + beatmap.DrawHeight / 2 - DrawHeight / 2; + scrollTarget = currentY + beatmap.DrawHeight / 2 - (Parent.DrawHeight / 2 - Parent.Padding.Top); void performMove(float y, float? startY = null) { From 07dc2773218e4e65e6c388e7210a49a8a6a8f8ac Mon Sep 17 00:00:00 2001 From: TheWildTree Date: Sun, 12 Apr 2020 14:55:42 +0200 Subject: [PATCH 1016/2376] Remove unused changelog comments class --- .../Online/TestSceneChangelogOverlay.cs | 1 - osu.Game/Overlays/Changelog/Comments.cs | 79 ------------------- 2 files changed, 80 deletions(-) delete mode 100644 osu.Game/Overlays/Changelog/Comments.cs diff --git a/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs index 864fd31a0f..22d20f7098 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs @@ -24,7 +24,6 @@ namespace osu.Game.Tests.Visual.Online typeof(ChangelogListing), typeof(ChangelogSingleBuild), typeof(ChangelogBuild), - typeof(Comments), }; protected override bool UseOnlineAPI => true; diff --git a/osu.Game/Overlays/Changelog/Comments.cs b/osu.Game/Overlays/Changelog/Comments.cs deleted file mode 100644 index 4cf39e7b44..0000000000 --- a/osu.Game/Overlays/Changelog/Comments.cs +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Game.Graphics; -using osu.Game.Graphics.Containers; -using osu.Game.Online.API.Requests.Responses; -using osuTK.Graphics; - -namespace osu.Game.Overlays.Changelog -{ - public class Comments : CompositeDrawable - { - private readonly APIChangelogBuild build; - - public Comments(APIChangelogBuild build) - { - this.build = build; - - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; - - Padding = new MarginPadding - { - Horizontal = 50, - Vertical = 20, - }; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - LinkFlowContainer text; - - InternalChildren = new Drawable[] - { - new Container - { - RelativeSizeAxes = Axes.Both, - Masking = true, - CornerRadius = 10, - Child = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colours.GreyVioletDarker - }, - }, - text = new LinkFlowContainer(t => - { - t.Colour = colours.PinkLighter; - t.Font = OsuFont.Default.With(size: 14); - }) - { - Padding = new MarginPadding(20), - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - } - }; - - text.AddParagraph("Got feedback?", t => - { - t.Colour = Color4.White; - t.Font = OsuFont.Default.With(italics: true, size: 20); - t.Padding = new MarginPadding { Bottom = 20 }; - }); - - text.AddParagraph("We would love to hear what you think of this update! "); - text.AddIcon(FontAwesome.Regular.GrinHearts); - - text.AddParagraph("Please visit the "); - text.AddLink("web version", $"{build.Url}#comments"); - text.AddText(" of this changelog to leave any comments."); - } - } -} From ecd25e567d8803c548e273b11aac15f333e55da6 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sun, 12 Apr 2020 16:00:05 +0300 Subject: [PATCH 1017/2376] Present selected difficulty --- osu.Game/OsuGame.cs | 12 ++++++++---- osu.Game/Overlays/BeatmapSet/Header.cs | 3 ++- osu.Game/Overlays/Direct/PanelDownloadButton.cs | 10 +++++++++- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 1b2fd658f4..113e9bbe24 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -315,8 +315,12 @@ namespace osu.Game /// The user should have already requested this interactively. /// /// The beatmap to select. - public void PresentBeatmap(BeatmapSetInfo beatmap) + /// Predicate used to find a difficulty to select + public void PresentBeatmap(BeatmapSetInfo beatmap, Predicate findPredicate = null) { + // Use this predicate if non was provided. This will try to find some difficulty from current ruleset so we wouldn't have to change rulesets + findPredicate ??= b => b.Ruleset.Equals(Ruleset.Value); + var databasedSet = beatmap.OnlineBeatmapSetID != null ? BeatmapManager.QueryBeatmapSet(s => s.OnlineBeatmapSetID == beatmap.OnlineBeatmapSetID) : BeatmapManager.QueryBeatmapSet(s => s.Hash == beatmap.Hash); @@ -334,13 +338,13 @@ namespace osu.Game menuScreen.LoadToSolo(); // we might even already be at the song - if (Beatmap.Value.BeatmapSetInfo.Hash == databasedSet.Hash) + if (Beatmap.Value.BeatmapSetInfo.Hash == databasedSet.Hash && findPredicate(Beatmap.Value.BeatmapInfo)) { return; } - // Use first beatmap available for current ruleset, else switch ruleset. - var first = databasedSet.Beatmaps.Find(b => b.Ruleset.Equals(Ruleset.Value)) ?? databasedSet.Beatmaps.First(); + // Find first beatmap that matches our predicate. + var first = databasedSet.Beatmaps.Find(findPredicate) ?? databasedSet.Beatmaps.First(); Ruleset.Value = first.Ruleset; Beatmap.Value = BeatmapManager.GetWorkingBeatmap(first); diff --git a/osu.Game/Overlays/BeatmapSet/Header.cs b/osu.Game/Overlays/BeatmapSet/Header.cs index 29c259b7f8..a60b9e04b1 100644 --- a/osu.Game/Overlays/BeatmapSet/Header.cs +++ b/osu.Game/Overlays/BeatmapSet/Header.cs @@ -277,7 +277,8 @@ namespace osu.Game.Overlays.BeatmapSet downloadButtonsContainer.Child = new PanelDownloadButton(BeatmapSet.Value) { Width = 50, - RelativeSizeAxes = Axes.Y + RelativeSizeAxes = Axes.Y, + CurrentBeatmap = Picker.Beatmap.GetBoundCopy() }; break; diff --git a/osu.Game/Overlays/Direct/PanelDownloadButton.cs b/osu.Game/Overlays/Direct/PanelDownloadButton.cs index 1b3657f010..eae2f3353c 100644 --- a/osu.Game/Overlays/Direct/PanelDownloadButton.cs +++ b/osu.Game/Overlays/Direct/PanelDownloadButton.cs @@ -1,7 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Graphics.Containers; @@ -16,6 +18,8 @@ namespace osu.Game.Overlays.Direct private readonly bool noVideo; + public Bindable CurrentBeatmap = new Bindable(); + private readonly ShakeContainer shakeContainer; private readonly DownloadButton button; @@ -62,7 +66,11 @@ namespace osu.Game.Overlays.Direct break; case DownloadState.LocallyAvailable: - game?.PresentBeatmap(BeatmapSet.Value); + Predicate findPredicate = null; + if (CurrentBeatmap.Value != null) + findPredicate = b => b.OnlineBeatmapID == CurrentBeatmap.Value.OnlineBeatmapID; + + game?.PresentBeatmap(BeatmapSet.Value, findPredicate); break; default: From ed28e8c8f5f6d92f7f06034800fd9df82f688ef7 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sun, 12 Apr 2020 19:38:09 +0300 Subject: [PATCH 1018/2376] Rename param --- osu.Game/OsuGame.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 113e9bbe24..5e93d760e3 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -315,11 +315,14 @@ namespace osu.Game /// The user should have already requested this interactively. /// /// The beatmap to select. - /// Predicate used to find a difficulty to select - public void PresentBeatmap(BeatmapSetInfo beatmap, Predicate findPredicate = null) + /// + /// Optional predicate used to try and find a difficulty to select. + /// If omitted, this will try to present the first beatmap from the current ruleset. + /// In case of failure the first difficulty of the set will be presented, ignoring the predicate. + /// + public void PresentBeatmap(BeatmapSetInfo beatmap, Predicate difficultyCriteria = null) { - // Use this predicate if non was provided. This will try to find some difficulty from current ruleset so we wouldn't have to change rulesets - findPredicate ??= b => b.Ruleset.Equals(Ruleset.Value); + difficultyCriteria ??= b => b.Ruleset.Equals(Ruleset.Value); var databasedSet = beatmap.OnlineBeatmapSetID != null ? BeatmapManager.QueryBeatmapSet(s => s.OnlineBeatmapSetID == beatmap.OnlineBeatmapSetID) @@ -338,13 +341,13 @@ namespace osu.Game menuScreen.LoadToSolo(); // we might even already be at the song - if (Beatmap.Value.BeatmapSetInfo.Hash == databasedSet.Hash && findPredicate(Beatmap.Value.BeatmapInfo)) + if (Beatmap.Value.BeatmapSetInfo.Hash == databasedSet.Hash && difficultyCriteria(Beatmap.Value.BeatmapInfo)) { return; } // Find first beatmap that matches our predicate. - var first = databasedSet.Beatmaps.Find(findPredicate) ?? databasedSet.Beatmaps.First(); + var first = databasedSet.Beatmaps.Find(difficultyCriteria) ?? databasedSet.Beatmaps.First(); Ruleset.Value = first.Ruleset; Beatmap.Value = BeatmapManager.GetWorkingBeatmap(first); From 3b9e0fa67def20a8e9177088147cbfff4536cc07 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sun, 12 Apr 2020 19:42:28 +0300 Subject: [PATCH 1019/2376] Use readonly IBindable --- osu.Game/Overlays/BeatmapSet/Header.cs | 7 ++++--- osu.Game/Overlays/Direct/PanelDownloadButton.cs | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/Header.cs b/osu.Game/Overlays/BeatmapSet/Header.cs index a60b9e04b1..4e57bfa688 100644 --- a/osu.Game/Overlays/BeatmapSet/Header.cs +++ b/osu.Game/Overlays/BeatmapSet/Header.cs @@ -274,12 +274,13 @@ namespace osu.Game.Overlays.BeatmapSet { case DownloadState.LocallyAvailable: // temporary for UX until new design is implemented. - downloadButtonsContainer.Child = new PanelDownloadButton(BeatmapSet.Value) + PanelDownloadButton panelButton; + downloadButtonsContainer.Child = panelButton = new PanelDownloadButton(BeatmapSet.Value) { Width = 50, - RelativeSizeAxes = Axes.Y, - CurrentBeatmap = Picker.Beatmap.GetBoundCopy() + RelativeSizeAxes = Axes.Y }; + panelButton.CurrentBeatmap.BindTo(Picker.Beatmap); break; case DownloadState.Downloading: diff --git a/osu.Game/Overlays/Direct/PanelDownloadButton.cs b/osu.Game/Overlays/Direct/PanelDownloadButton.cs index eae2f3353c..6fe174438b 100644 --- a/osu.Game/Overlays/Direct/PanelDownloadButton.cs +++ b/osu.Game/Overlays/Direct/PanelDownloadButton.cs @@ -18,7 +18,7 @@ namespace osu.Game.Overlays.Direct private readonly bool noVideo; - public Bindable CurrentBeatmap = new Bindable(); + public readonly IBindable CurrentBeatmap = new Bindable(); private readonly ShakeContainer shakeContainer; private readonly DownloadButton button; From 1cf240b5fff1c269f9e668970a4046d963521b41 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sun, 12 Apr 2020 20:04:25 +0300 Subject: [PATCH 1020/2376] Test new predicate behaviour --- .../Navigation/TestScenePresentBeatmap.cs | 57 +++++++++++++++---- 1 file changed, 45 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs index 909409835c..27f5b29738 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs @@ -20,26 +20,30 @@ namespace osu.Game.Tests.Visual.Navigation public void TestFromMainMenu() { var firstImport = importBeatmap(1); + var secondimport = importBeatmap(3); + presentAndConfirm(firstImport); - - AddStep("return to menu", () => Game.ScreenStack.CurrentScreen.Exit()); - AddUntilStep("wait for menu", () => Game.ScreenStack.CurrentScreen is MainMenu); - - var secondimport = importBeatmap(2); + returnToMenu(); presentAndConfirm(secondimport); + returnToMenu(); + presentSecondDifficultyAndConfirm(firstImport, 1); + returnToMenu(); + presentSecondDifficultyAndConfirm(secondimport, 3); } [Test] public void TestFromMainMenuDifferentRuleset() { var firstImport = importBeatmap(1); + var secondimport = importBeatmap(3, new ManiaRuleset().RulesetInfo); + presentAndConfirm(firstImport); - - AddStep("return to menu", () => Game.ScreenStack.CurrentScreen.Exit()); - AddUntilStep("wait for menu", () => Game.ScreenStack.CurrentScreen is MainMenu); - - var secondimport = importBeatmap(2, new ManiaRuleset().RulesetInfo); + returnToMenu(); presentAndConfirm(secondimport); + returnToMenu(); + presentSecondDifficultyAndConfirm(firstImport, 1); + returnToMenu(); + presentSecondDifficultyAndConfirm(secondimport, 3); } [Test] @@ -48,8 +52,11 @@ namespace osu.Game.Tests.Visual.Navigation var firstImport = importBeatmap(1); presentAndConfirm(firstImport); - var secondimport = importBeatmap(2); + var secondimport = importBeatmap(3); presentAndConfirm(secondimport); + + presentSecondDifficultyAndConfirm(firstImport, 1); + presentSecondDifficultyAndConfirm(secondimport, 3); } [Test] @@ -58,8 +65,17 @@ namespace osu.Game.Tests.Visual.Navigation var firstImport = importBeatmap(1); presentAndConfirm(firstImport); - var secondimport = importBeatmap(2, new ManiaRuleset().RulesetInfo); + var secondimport = importBeatmap(3, new ManiaRuleset().RulesetInfo); presentAndConfirm(secondimport); + + presentSecondDifficultyAndConfirm(firstImport, 1); + presentSecondDifficultyAndConfirm(secondimport, 3); + } + + private void returnToMenu() + { + AddStep("return to menu", () => Game.ScreenStack.CurrentScreen.Exit()); + AddUntilStep("wait for menu", () => Game.ScreenStack.CurrentScreen is MainMenu); } private Func importBeatmap(int i, RulesetInfo ruleset = null) @@ -89,6 +105,13 @@ namespace osu.Game.Tests.Visual.Navigation BaseDifficulty = difficulty, Ruleset = ruleset ?? new OsuRuleset().RulesetInfo }, + new BeatmapInfo + { + OnlineBeatmapID = i * 2048, + Metadata = metadata, + BaseDifficulty = difficulty, + Ruleset = ruleset ?? new OsuRuleset().RulesetInfo + }, } }).Result; }); @@ -106,5 +129,15 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("correct beatmap displayed", () => Game.Beatmap.Value.BeatmapSetInfo.ID == getImport().ID); AddAssert("correct ruleset selected", () => Game.Ruleset.Value.ID == getImport().Beatmaps.First().Ruleset.ID); } + + private void presentSecondDifficultyAndConfirm(Func getImport, int importedID) + { + Predicate pred = b => b.OnlineBeatmapID == importedID * 2048; + AddStep("present difficulty", () => Game.PresentBeatmap(getImport(), pred)); + + AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect); + AddUntilStep("correct beatmap displayed", () => Game.Beatmap.Value.BeatmapInfo.OnlineBeatmapID == importedID * 2048); + AddAssert("correct ruleset selected", () => Game.Ruleset.Value.ID == getImport().Beatmaps.First().Ruleset.ID); + } } } From b475316a4e0a34161450ab8eed126d4087866cab Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sun, 12 Apr 2020 20:40:08 +0300 Subject: [PATCH 1021/2376] Simplify and comment --- osu.Game/Screens/Select/BeatmapCarousel.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index a5379e9649..e13511a02c 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -632,7 +632,11 @@ namespace osu.Game.Screens.Select case DrawableCarouselBeatmap beatmap: { if (beatmap.Item.State.Value == CarouselItemState.Selected) - scrollTarget = currentY + beatmap.DrawHeight / 2 - (Parent.DrawHeight / 2 - Parent.Padding.Top); + // scroll position at currentY makes the set panel appear at the very top of the carousel in screen space + // move down by half of parent height (which is the height of the carousel's visible extent, including semi-transparent areas) + // then reapply parent's padding from the top by adding it + // and finally add half of the panel's own height to achieve vertical centering of the panel itself + scrollTarget = currentY - Parent.DrawHeight / 2 + Parent.Padding.Top + beatmap.DrawHeight / 2; void performMove(float y, float? startY = null) { From 3efb4aba25803c36a2f50401808a389f3b082f29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 12 Apr 2020 19:48:15 +0200 Subject: [PATCH 1022/2376] Use BindTarget --- osu.Game/Overlays/BeatmapSet/Header.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/Header.cs b/osu.Game/Overlays/BeatmapSet/Header.cs index 4e57bfa688..a03613f955 100644 --- a/osu.Game/Overlays/BeatmapSet/Header.cs +++ b/osu.Game/Overlays/BeatmapSet/Header.cs @@ -274,13 +274,12 @@ namespace osu.Game.Overlays.BeatmapSet { case DownloadState.LocallyAvailable: // temporary for UX until new design is implemented. - PanelDownloadButton panelButton; - downloadButtonsContainer.Child = panelButton = new PanelDownloadButton(BeatmapSet.Value) + downloadButtonsContainer.Child = new PanelDownloadButton(BeatmapSet.Value) { Width = 50, - RelativeSizeAxes = Axes.Y + RelativeSizeAxes = Axes.Y, + CurrentBeatmap = { BindTarget = Picker.Beatmap } }; - panelButton.CurrentBeatmap.BindTo(Picker.Beatmap); break; case DownloadState.Downloading: From 633b969017515da564a14036e7ad3eb0cbb5a141 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sun, 12 Apr 2020 21:57:35 +0300 Subject: [PATCH 1023/2376] Apply review suggestions --- .../Visual/Online/TestSceneDirectDownloadButton.cs | 4 ++-- osu.Game/Overlays/Direct/PanelDownloadButton.cs | 13 ++++++------- .../Settings/Sections/Online/WebSettings.cs | 3 ++- osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneDirectDownloadButton.cs b/osu.Game.Tests/Visual/Online/TestSceneDirectDownloadButton.cs index 5b0c2d3c67..f612992bf6 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneDirectDownloadButton.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneDirectDownloadButton.cs @@ -149,8 +149,8 @@ namespace osu.Game.Tests.Visual.Online public DownloadState DownloadState => State.Value; - public TestDownloadButton(BeatmapSetInfo beatmapSet, bool noVideo = false) - : base(beatmapSet, noVideo) + public TestDownloadButton(BeatmapSetInfo beatmapSet) + : base(beatmapSet) { } } diff --git a/osu.Game/Overlays/Direct/PanelDownloadButton.cs b/osu.Game/Overlays/Direct/PanelDownloadButton.cs index f09586c571..51f5b2ae4f 100644 --- a/osu.Game/Overlays/Direct/PanelDownloadButton.cs +++ b/osu.Game/Overlays/Direct/PanelDownloadButton.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Configuration; @@ -15,16 +16,13 @@ namespace osu.Game.Overlays.Direct { protected bool DownloadEnabled => button.Enabled.Value; - private readonly bool? noVideo; - private readonly ShakeContainer shakeContainer; private readonly DownloadButton button; + private readonly BindableBool noVideoSetting = new BindableBool(); - public PanelDownloadButton(BeatmapSetInfo beatmapSet, bool? noVideo = null) + public PanelDownloadButton(BeatmapSetInfo beatmapSet) : base(beatmapSet) { - this.noVideo = noVideo; - InternalChild = shakeContainer = new ShakeContainer { RelativeSizeAxes = Axes.Both, @@ -53,6 +51,8 @@ namespace osu.Game.Overlays.Direct return; } + noVideoSetting.BindTo(osuConfig.GetBindable(OsuSetting.PreferNoVideo)); + button.Action = () => { switch (State.Value) @@ -67,8 +67,7 @@ namespace osu.Game.Overlays.Direct break; default: - var minimiseDownloadSize = noVideo ?? osuConfig.GetBindable(OsuSetting.PreferNoVideo).Value; - beatmaps.Download(BeatmapSet.Value, minimiseDownloadSize); + beatmaps.Download(BeatmapSet.Value, noVideoSetting.Value); break; } }; diff --git a/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs b/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs index da3176aca8..23513eade8 100644 --- a/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs @@ -23,7 +23,8 @@ namespace osu.Game.Overlays.Settings.Sections.Online }, new SettingsCheckbox { - LabelText = "Prefer no-video downloads", + LabelText = "Prefer downloads without video", + Keywords = new[] { "no-video" }, Bindable = config.GetBindable(OsuSetting.PreferNoVideo) }, }; diff --git a/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs index d58218b6b5..d7dcca9809 100644 --- a/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs @@ -212,8 +212,8 @@ namespace osu.Game.Screens.Multi private class PlaylistDownloadButton : PanelDownloadButton { - public PlaylistDownloadButton(BeatmapSetInfo beatmapSet, bool? noVideo = null) - : base(beatmapSet, noVideo) + public PlaylistDownloadButton(BeatmapSetInfo beatmapSet) + : base(beatmapSet) { Alpha = 0; } From 65b96079a05f46de50dd1f0f191bed8389cdf62f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 13 Apr 2020 13:00:03 +0900 Subject: [PATCH 1024/2376] Move dampening to base implementation and change range to 0..1 --- .../Objects/Drawables/DrawableCatchHitObject.cs | 2 +- .../Objects/Drawables/DrawableManiaHitObject.cs | 2 +- .../Objects/Drawables/DrawableOsuHitObject.cs | 3 ++- .../Objects/Drawables/DrawableHitObject.cs | 16 +++++++++++----- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs index e726d6eff5..b12cdd4ccb 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs @@ -70,7 +70,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables public float DisplayRadius => DrawSize.X / 2 * Scale.X * HitObject.Scale; - protected override float SamplePlaybackBalance => 0.8f * HitObject.X - 0.4f; + protected override float SamplePlaybackPosition => HitObject.X; protected DrawableCatchHitObject(CatchHitObject hitObject) : base(hitObject) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs index 76e9695855..ce56fd222c 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables protected readonly IBindable Direction = new Bindable(); - protected override float SamplePlaybackBalance + protected override float SamplePlaybackPosition { get { diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index 068d666c8e..8308c0c576 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Graphics.Containers; +using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects.Drawables @@ -18,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables // Must be set to update IsHovered as it's used in relax mdo to detect osu hit objects. public override bool HandlePositionalInput => true; - protected override float SamplePlaybackBalance => (HitObject.X / 512f - 0.5f) * 0.8f; + protected override float SamplePlaybackPosition => HitObject.X / OsuPlayfield.BASE_SIZE.X; /// /// Whether this can be hit. diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index d6e231424b..b14927bcd5 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -87,11 +87,15 @@ namespace osu.Game.Rulesets.Objects.Drawables public JudgementResult Result { get; private set; } /// - /// The stereo balance of the samples played if Positional hitsounds is set. + /// The relative X position of this hit object for sample playback balance adjustment. /// - protected virtual float SamplePlaybackBalance => 0; + /// + /// This is a range of 0..1 (0 for far-left, 0.5 for centre, 1 for far-right). + /// Dampening is post-applied to ensure the effect is not too intense. + /// + protected virtual float SamplePlaybackPosition => 0.5f; - private readonly BindableDouble samplePlaybackBalanceAdjustment = new BindableDouble(); + private readonly BindableDouble balanceAdjust = new BindableDouble(); private BindableList samplesBindable; private Bindable startTimeBindable; @@ -168,7 +172,7 @@ namespace osu.Game.Rulesets.Objects.Drawables } Samples = new SkinnableSound(samples.Select(s => HitObject.SampleControlPoint.ApplyTo(s))); - Samples.AddAdjustment(AdjustableProperty.Balance, samplePlaybackBalanceAdjustment); + Samples.AddAdjustment(AdjustableProperty.Balance, balanceAdjust); AddInternal(Samples); } @@ -368,7 +372,9 @@ namespace osu.Game.Rulesets.Objects.Drawables /// public virtual void PlaySamples() { - samplePlaybackBalanceAdjustment.Value = userPositionalHitSounds.Value ? SamplePlaybackBalance : 0; + const float balance_adjust_amount = 0.4f; + + balanceAdjust.Value = balance_adjust_amount * (userPositionalHitSounds.Value ? SamplePlaybackPosition - 0.5f : 0); Samples?.Play(); } From cdff6060d3eccdf3ed36df6ce0d3aa26dc411a17 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 13 Apr 2020 13:01:27 +0900 Subject: [PATCH 1025/2376] Remove recursive hierarchy traversal for mania sample balance --- .../Objects/Drawables/DrawableManiaHitObject.cs | 16 +++++++++------- osu.Game/Rulesets/UI/Playfield.cs | 1 + 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs index ce56fd222c..a708adb493 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs @@ -5,10 +5,10 @@ using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Mania.Objects.Drawables { @@ -26,17 +26,19 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables protected readonly IBindable Direction = new Bindable(); + [Resolved(canBeNull: true)] + private Playfield playfield { get; set; } + protected override float SamplePlaybackPosition { get { - CompositeDrawable stage = this; - while (!(stage is Stage)) - stage = stage.Parent; + var columns = (playfield as ManiaPlayfield)?.TotalColumns; - var columnCount = ((Stage)stage).Columns.Count; - var columnIndex = HitObject.Column; - return 0.8f * columnIndex / (columnCount - 1) - 0.4f; + if (columns == null) + return base.SamplePlaybackPosition; + + return (float)HitObject.Column / columns.Value; } } diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index c52183f3f2..fc6906560b 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -15,6 +15,7 @@ using osuTK; namespace osu.Game.Rulesets.UI { + [Cached] public abstract class Playfield : CompositeDrawable { /// From c51bad0e35e4a08d3e0e159e103884bbf709e85d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 13 Apr 2020 13:42:21 +0900 Subject: [PATCH 1026/2376] Cache ManiaPlayfield instead --- .../Objects/Drawables/DrawableManiaHitObject.cs | 9 +++------ osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs | 2 ++ osu.Game/Rulesets/UI/Playfield.cs | 1 - 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs index a708adb493..88888001b4 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.Mania.UI; -using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Mania.Objects.Drawables { @@ -27,18 +26,16 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables protected readonly IBindable Direction = new Bindable(); [Resolved(canBeNull: true)] - private Playfield playfield { get; set; } + private ManiaPlayfield playfield { get; set; } protected override float SamplePlaybackPosition { get { - var columns = (playfield as ManiaPlayfield)?.TotalColumns; - - if (columns == null) + if (playfield == null) return base.SamplePlaybackPosition; - return (float)HitObject.Column / columns.Value; + return (float)HitObject.Column / playfield.TotalColumns; } } diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs index c2eb48b774..2dec468654 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics.Containers; using System; using System.Collections.Generic; using System.Linq; +using osu.Framework.Allocation; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Objects.Drawables; @@ -14,6 +15,7 @@ using osuTK; namespace osu.Game.Rulesets.Mania.UI { + [Cached] public class ManiaPlayfield : ScrollingPlayfield { private readonly List stages = new List(); diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index fc6906560b..c52183f3f2 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -15,7 +15,6 @@ using osuTK; namespace osu.Game.Rulesets.UI { - [Cached] public abstract class Playfield : CompositeDrawable { /// From f38b64d20177c2010b415f8a3bae39fbf29e0303 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 13 Apr 2020 13:57:15 +0900 Subject: [PATCH 1027/2376] Fix placement blueprints handling double clicks --- osu.Game/Rulesets/Edit/PlacementBlueprint.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index ea77a6091a..fb1eb7adbf 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -106,6 +106,9 @@ namespace osu.Game.Rulesets.Edit case ScrollEvent _: return false; + case DoubleClickEvent _: + return false; + case MouseButtonEvent _: return true; From e17d5bdbaf3a6ad45b4a60af9ab9a168cf0d0406 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 13 Apr 2020 13:57:40 +0900 Subject: [PATCH 1028/2376] Improve red slider control point placement logic --- .../Sliders/SliderPlacementBlueprint.cs | 46 +++++++++++-------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index a780653796..be43515269 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -1,10 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Diagnostics; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Input; using osu.Framework.Input.Events; +using osu.Framework.Logging; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; @@ -23,6 +26,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private SliderBodyPiece bodyPiece; private HitCirclePiece headCirclePiece; private HitCirclePiece tailCirclePiece; + private PathControlPointVisualiser controlPointVisualiser; private InputManager inputManager; @@ -51,7 +55,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders bodyPiece = new SliderBodyPiece(), headCirclePiece = new HitCirclePiece(), tailCirclePiece = new HitCirclePiece(), - new PathControlPointVisualiser(HitObject, false) + controlPointVisualiser = new PathControlPointVisualiser(HitObject, false) }; setState(PlacementState.Initial); @@ -91,17 +95,29 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders break; case PlacementState.Body: - switch (e.Button) - { - case MouseButton.Left: - ensureCursor(); + if (e.Button != MouseButton.Left) + break; - // Detatch the cursor - cursor = null; - break; + // Find the last non-cursor control point and the respective drawable piece + var lastPoint = HitObject.Path.ControlPoints.LastOrDefault(p => p != cursor); + var lastPiece = controlPointVisualiser.Pieces.Single(p => p.ControlPoint == lastPoint); + + if (lastPiece?.IsHovered == true) + { + Debug.Assert(lastPoint != null); + + segmentStart = lastPoint; + segmentStart.Type.Value = PathType.Linear; + + currentSegmentLength = 1; + } + else + { + ensureCursor(); + cursor = null; // Detatch the cursor } - break; + return true; } return true; @@ -114,16 +130,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders base.OnMouseUp(e); } - protected override bool OnDoubleClick(DoubleClickEvent e) - { - // Todo: This should all not occur on double click, but rather if the previous control point is hovered. - segmentStart = HitObject.Path.ControlPoints[^1]; - segmentStart.Type.Value = PathType.Linear; - - currentSegmentLength = 1; - return true; - } - private void beginCurve() { BeginPlacement(commitStart: true); @@ -169,6 +175,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders currentSegmentLength++; updatePathType(); + + Logger.Log("Set cursor"); } } From 99fa1458470fe7fc031e3ae4e422cf2f1ad0d73b Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 13 Apr 2020 08:38:34 +0300 Subject: [PATCH 1029/2376] Add test for potential failing case --- .../UserInterface/TestSceneOverlayScrollContainer.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs index 4205d65100..fd3b6ed3ab 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs @@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.UserInterface [SetUp] public void SetUp() => Schedule(() => { - Add(scroll = new OverlayScrollContainer + Child = scroll = new OverlayScrollContainer { RelativeSizeAxes = Axes.Both, Child = new Container @@ -45,7 +45,7 @@ namespace osu.Game.Tests.Visual.UserInterface Colour = Color4.Gray } } - }); + }; invocationCount = 0; @@ -62,6 +62,10 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("scroll to start", () => scroll.ScrollToStart(false)); AddAssert("button is hidden", () => scroll.Button.State == Visibility.Hidden); + + AddStep("scroll to 250", () => scroll.ScrollTo(500)); + AddUntilStep("scrolled back to start", () => Precision.AlmostEquals(scroll.Current, 500, 0.1f)); + AddAssert("button is visible", () => scroll.Button.State == Visibility.Visible); } [Test] From 142cddfb10d46a1392dda590ac5fb1864c82bf7f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 13 Apr 2020 15:13:35 +0900 Subject: [PATCH 1030/2376] Rename CurrentBeatmap to SelectedBeatmap --- osu.Game/Overlays/BeatmapSet/Header.cs | 2 +- osu.Game/Overlays/Direct/PanelDownloadButton.cs | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/Header.cs b/osu.Game/Overlays/BeatmapSet/Header.cs index a03613f955..11dc424183 100644 --- a/osu.Game/Overlays/BeatmapSet/Header.cs +++ b/osu.Game/Overlays/BeatmapSet/Header.cs @@ -278,7 +278,7 @@ namespace osu.Game.Overlays.BeatmapSet { Width = 50, RelativeSizeAxes = Axes.Y, - CurrentBeatmap = { BindTarget = Picker.Beatmap } + SelectedBeatmap = { BindTarget = Picker.Beatmap } }; break; diff --git a/osu.Game/Overlays/Direct/PanelDownloadButton.cs b/osu.Game/Overlays/Direct/PanelDownloadButton.cs index 6fe174438b..08e3ed9b38 100644 --- a/osu.Game/Overlays/Direct/PanelDownloadButton.cs +++ b/osu.Game/Overlays/Direct/PanelDownloadButton.cs @@ -18,7 +18,10 @@ namespace osu.Game.Overlays.Direct private readonly bool noVideo; - public readonly IBindable CurrentBeatmap = new Bindable(); + /// + /// Currently selected beatmap. Used to present the correct difficulty after completing a download. + /// + public readonly IBindable SelectedBeatmap = new Bindable(); private readonly ShakeContainer shakeContainer; private readonly DownloadButton button; @@ -67,8 +70,8 @@ namespace osu.Game.Overlays.Direct case DownloadState.LocallyAvailable: Predicate findPredicate = null; - if (CurrentBeatmap.Value != null) - findPredicate = b => b.OnlineBeatmapID == CurrentBeatmap.Value.OnlineBeatmapID; + if (SelectedBeatmap.Value != null) + findPredicate = b => b.OnlineBeatmapID == SelectedBeatmap.Value.OnlineBeatmapID; game?.PresentBeatmap(BeatmapSet.Value, findPredicate); break; From 2c20328a70cfd3f5f2722b226346feeabad633ec Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 13 Apr 2020 15:31:46 +0900 Subject: [PATCH 1031/2376] Rework control point placement for better progression --- .../Sliders/SliderPlacementBlueprint.cs | 69 +++++++++++++------ 1 file changed, 47 insertions(+), 22 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index be43515269..9af972dbce 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -3,11 +3,11 @@ using System.Diagnostics; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Input; using osu.Framework.Input.Events; -using osu.Framework.Logging; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; @@ -77,11 +77,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders break; case PlacementState.Body: - ensureCursor(); - - // The given screen-space position may have been externally snapped, but the unsnapped position from the input manager - // is used instead since snapping control points doesn't make much sense - cursor.Position.Value = ToLocalSpace(inputManager.CurrentState.Mouse.Position) - HitObject.Position; + updateCursor(); break; } } @@ -98,12 +94,15 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders if (e.Button != MouseButton.Left) break; - // Find the last non-cursor control point and the respective drawable piece - var lastPoint = HitObject.Path.ControlPoints.LastOrDefault(p => p != cursor); - var lastPiece = controlPointVisualiser.Pieces.Single(p => p.ControlPoint == lastPoint); - - if (lastPiece?.IsHovered == true) + if (canPlaceNewControlPoint(out var lastPoint)) { + // Place a new point by detatching the current cursor. + updateCursor(); + cursor = null; + } + else + { + // Transform the last point into a new segment. Debug.Assert(lastPoint != null); segmentStart = lastPoint; @@ -111,11 +110,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders currentSegmentLength = 1; } - else - { - ensureCursor(); - cursor = null; // Detatch the cursor - } return true; } @@ -167,17 +161,48 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders } } - private void ensureCursor() + private void updateCursor() { - if (cursor == null) + if (canPlaceNewControlPoint(out _)) { - HitObject.Path.ControlPoints.Add(cursor = new PathControlPoint { Position = { Value = Vector2.Zero } }); - currentSegmentLength++; + // The cursor does not overlap a previous control point, so it can be added if not already existing. + if (cursor == null) + { + HitObject.Path.ControlPoints.Add(cursor = new PathControlPoint { Position = { Value = Vector2.Zero } }); - updatePathType(); + // The path type should be adjusted in the progression of updatePathType() (Linear -> PC -> Bezier). + currentSegmentLength++; + updatePathType(); + } - Logger.Log("Set cursor"); + // Update the cursor position. + cursor.Position.Value = ToLocalSpace(inputManager.CurrentState.Mouse.Position) - HitObject.Position; } + else if (cursor != null) + { + // The cursor overlaps a previous control point, so it's removed. + HitObject.Path.ControlPoints.Remove(cursor); + cursor = null; + + // The path type should be adjusted in the reverse progression of updatePathType() (Bezier -> PC -> Linear). + currentSegmentLength--; + updatePathType(); + } + } + + /// + /// Whether a new control point can be placed at the current mouse position. + /// + /// The last-placed control point. May be null, but is not null if false is returned. + /// Whether a new control point can be placed at the current position. + private bool canPlaceNewControlPoint([CanBeNull] out PathControlPoint lastPoint) + { + // We cannot rely on the ordering of drawable pieces, so find the respective drawable piece by searching for the last non-cursor control point. + var last = HitObject.Path.ControlPoints.LastOrDefault(p => p != cursor); + var lastPiece = controlPointVisualiser.Pieces.Single(p => p.ControlPoint == last); + + lastPoint = last; + return lastPiece?.IsHovered != true; } private void updateSlider() From bde0b259c1c03d2022124c6125dd2cc45a292807 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 13 Apr 2020 15:31:54 +0900 Subject: [PATCH 1032/2376] Improve slider placement test scene --- .../TestSceneSliderPlacementBlueprint.cs | 267 ++++++++++++++++++ .../Visual/PlacementBlueprintTestScene.cs | 28 +- 2 files changed, 282 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderPlacementBlueprint.cs index 0522260150..9fc479953e 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderPlacementBlueprint.cs @@ -1,18 +1,285 @@ // 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.Utils; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Tests.Visual; +using osuTK; +using osuTK.Input; namespace osu.Game.Rulesets.Osu.Tests { public class TestSceneSliderPlacementBlueprint : PlacementBlueprintTestScene { + [SetUp] + public void Setup() => Schedule(() => + { + HitObjectContainer.Clear(); + ResetPlacement(); + }); + + [Test] + public void TestBeginPlacementWithoutFinishing() + { + addMovementStep(new Vector2(200)); + addClickStep(MouseButton.Left); + + assertPlaced(false); + } + + [Test] + public void TestPlaceWithoutMovingMouse() + { + addMovementStep(new Vector2(200)); + addClickStep(MouseButton.Left); + addClickStep(MouseButton.Right); + + assertPlaced(true); + assertLength(0); + assertControlPointType(0, PathType.Linear); + } + + [Test] + public void TestPlaceWithMouseMovement() + { + addMovementStep(new Vector2(200)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(400, 200)); + addClickStep(MouseButton.Right); + + assertPlaced(true); + assertLength(200); + assertControlPointCount(2); + assertControlPointType(0, PathType.Linear); + } + + [Test] + public void TestPlaceNormalControlPoint() + { + addMovementStep(new Vector2(200)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(300, 200)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(300)); + addClickStep(MouseButton.Right); + + assertPlaced(true); + assertControlPointCount(3); + assertControlPointPosition(1, new Vector2(100, 0)); + assertControlPointType(0, PathType.PerfectCurve); + } + + [Test] + public void TestPlaceTwoNormalControlPoints() + { + addMovementStep(new Vector2(200)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(300, 200)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(300)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(400, 300)); + addClickStep(MouseButton.Right); + + assertPlaced(true); + assertControlPointCount(4); + assertControlPointPosition(1, new Vector2(100, 0)); + assertControlPointPosition(2, new Vector2(100, 100)); + assertControlPointType(0, PathType.Bezier); + } + + [Test] + public void TestPlaceSegmentControlPoint() + { + addMovementStep(new Vector2(200)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(300, 200)); + addClickStep(MouseButton.Left); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(300)); + addClickStep(MouseButton.Right); + + assertPlaced(true); + assertControlPointCount(3); + assertControlPointPosition(1, new Vector2(100, 0)); + assertControlPointType(0, PathType.Linear); + assertControlPointType(1, PathType.Linear); + } + + [Test] + public void TestMoveToPerfectCurveThenPlaceLinear() + { + addMovementStep(new Vector2(200)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(300, 200)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(300)); + addMovementStep(new Vector2(300, 200)); + addClickStep(MouseButton.Right); + + assertPlaced(true); + assertControlPointCount(2); + assertControlPointType(0, PathType.Linear); + assertLength(100); + } + + [Test] + public void TestMoveToBezierThenPlacePerfectCurve() + { + addMovementStep(new Vector2(200)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(300, 200)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(300)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(400, 300)); + addMovementStep(new Vector2(300)); + addClickStep(MouseButton.Right); + + assertPlaced(true); + assertControlPointCount(3); + assertControlPointType(0, PathType.PerfectCurve); + } + + [Test] + public void TestMoveToFourthOrderBezierThenPlaceThirdOrderBezier() + { + addMovementStep(new Vector2(200)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(300, 200)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(300)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(400, 300)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(400)); + addMovementStep(new Vector2(400, 300)); + addClickStep(MouseButton.Right); + + assertPlaced(true); + assertControlPointCount(4); + assertControlPointType(0, PathType.Bezier); + } + + [Test] + public void TestPlaceLinearSegmentThenPlaceLinearSegment() + { + addMovementStep(new Vector2(200)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(300, 200)); + addClickStep(MouseButton.Left); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(300, 300)); + addClickStep(MouseButton.Right); + + assertPlaced(true); + assertControlPointCount(3); + assertControlPointPosition(1, new Vector2(100, 0)); + assertControlPointPosition(2, new Vector2(100)); + assertControlPointType(0, PathType.Linear); + assertControlPointType(1, PathType.Linear); + } + + [Test] + public void TestPlaceLinearSegmentThenPlacePerfectCurveSegment() + { + addMovementStep(new Vector2(200)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(300, 200)); + addClickStep(MouseButton.Left); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(300, 300)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(400, 300)); + addClickStep(MouseButton.Right); + + assertPlaced(true); + assertControlPointCount(4); + assertControlPointPosition(1, new Vector2(100, 0)); + assertControlPointPosition(2, new Vector2(100)); + assertControlPointType(0, PathType.Linear); + assertControlPointType(1, PathType.PerfectCurve); + } + + [Test] + public void TestPlacePerfectCurveSegmentThenPlacePerfectCurveSegment() + { + addMovementStep(new Vector2(200)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(300, 200)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(300, 300)); + addClickStep(MouseButton.Left); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(400, 300)); + addClickStep(MouseButton.Left); + + addMovementStep(new Vector2(400)); + addClickStep(MouseButton.Right); + + assertPlaced(true); + assertControlPointCount(5); + assertControlPointPosition(1, new Vector2(100, 0)); + assertControlPointPosition(2, new Vector2(100)); + assertControlPointPosition(3, new Vector2(200, 100)); + assertControlPointPosition(4, new Vector2(200)); + assertControlPointType(0, PathType.PerfectCurve); + assertControlPointType(2, PathType.PerfectCurve); + } + + private void addMovementStep(Vector2 position) => AddStep($"move mouse to {position}", () => InputManager.MoveMouseTo(InputManager.ToScreenSpace(position))); + + private void addClickStep(MouseButton button) + { + AddStep($"press {button}", () => InputManager.PressButton(button)); + AddStep($"release {button}", () => InputManager.ReleaseButton(button)); + } + + private void assertPlaced(bool expected) => AddAssert($"slider {(expected ? "placed" : "not placed")}", () => (getSlider() != null) == expected); + + private void assertLength(double expected) => AddAssert($"slider length is {expected}", () => Precision.AlmostEquals(expected, getSlider().Distance, 1)); + + private void assertControlPointCount(int expected) => AddAssert($"has {expected} control points", () => getSlider().Path.ControlPoints.Count == expected); + + private void assertControlPointType(int index, PathType type) => AddAssert($"control point {index} is {type}", () => getSlider().Path.ControlPoints[index].Type.Value == type); + + private void assertControlPointPosition(int index, Vector2 position) => + AddAssert($"control point {index} at {position}", () => Precision.AlmostEquals(position, getSlider().Path.ControlPoints[index].Position.Value, 1)); + + private Slider getSlider() => HitObjectContainer.Count > 0 ? (Slider)((DrawableSlider)HitObjectContainer[0]).HitObject : null; + protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableSlider((Slider)hitObject); protected override PlacementBlueprint CreateBlueprint() => new SliderPlacementBlueprint(); } diff --git a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs index ce95dfa62f..dc67d28f63 100644 --- a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs +++ b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs @@ -4,8 +4,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Input; -using osu.Framework.Input.Events; using osu.Framework.Timing; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; @@ -15,13 +13,11 @@ using osu.Game.Screens.Edit.Compose; namespace osu.Game.Tests.Visual { [Cached(Type = typeof(IPlacementHandler))] - public abstract class PlacementBlueprintTestScene : OsuTestScene, IPlacementHandler + public abstract class PlacementBlueprintTestScene : OsuManualInputManagerTestScene, IPlacementHandler { - protected Container HitObjectContainer; + protected readonly Container HitObjectContainer; private PlacementBlueprint currentBlueprint; - private InputManager inputManager; - protected PlacementBlueprintTestScene() { Add(HitObjectContainer = CreateHitObjectContainer().With(c => c.Clock = new FramedClock(new StopwatchClock()))); @@ -45,8 +41,7 @@ namespace osu.Game.Tests.Visual { base.LoadComplete(); - inputManager = GetContainingInputManager(); - Add(currentBlueprint = CreateBlueprint()); + ResetPlacement(); } public void BeginPlacement(HitObject hitObject) @@ -58,7 +53,13 @@ namespace osu.Game.Tests.Visual if (commit) AddHitObject(CreateHitObject(hitObject)); - Remove(currentBlueprint); + ResetPlacement(); + } + + protected void ResetPlacement() + { + if (currentBlueprint != null) + Remove(currentBlueprint); Add(currentBlueprint = CreateBlueprint()); } @@ -66,10 +67,11 @@ namespace osu.Game.Tests.Visual { } - protected override bool OnMouseMove(MouseMoveEvent e) + protected override void Update() { - currentBlueprint.UpdatePosition(e.ScreenSpaceMousePosition); - return true; + base.Update(); + + currentBlueprint.UpdatePosition(InputManager.CurrentState.Mouse.Position); } public override void Add(Drawable drawable) @@ -79,7 +81,7 @@ namespace osu.Game.Tests.Visual if (drawable is PlacementBlueprint blueprint) { blueprint.Show(); - blueprint.UpdatePosition(inputManager.CurrentState.Mouse.Position); + blueprint.UpdatePosition(InputManager.CurrentState.Mouse.Position); } } From 0eaff00787293a93f945bdf0f5866b5285e2626f Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 13 Apr 2020 09:45:49 +0300 Subject: [PATCH 1033/2376] Fix typo in test --- .../Visual/UserInterface/TestSceneOverlayScrollContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs index fd3b6ed3ab..3ef0adcd9d 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs @@ -63,7 +63,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("scroll to start", () => scroll.ScrollToStart(false)); AddAssert("button is hidden", () => scroll.Button.State == Visibility.Hidden); - AddStep("scroll to 250", () => scroll.ScrollTo(500)); + AddStep("scroll to 500", () => scroll.ScrollTo(500)); AddUntilStep("scrolled back to start", () => Precision.AlmostEquals(scroll.Current, 500, 0.1f)); AddAssert("button is visible", () => scroll.Button.State == Visibility.Visible); } From bdce79ed5b85b01de2f49e24e22aa8156a518f3c Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 13 Apr 2020 09:57:05 +0300 Subject: [PATCH 1034/2376] Fix incorrect test step name --- .../Visual/UserInterface/TestSceneOverlayScrollContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs index 3ef0adcd9d..6a09fecc0a 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs @@ -64,7 +64,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("button is hidden", () => scroll.Button.State == Visibility.Hidden); AddStep("scroll to 500", () => scroll.ScrollTo(500)); - AddUntilStep("scrolled back to start", () => Precision.AlmostEquals(scroll.Current, 500, 0.1f)); + AddUntilStep("scrolled to 500", () => Precision.AlmostEquals(scroll.Current, 500, 0.1f)); AddAssert("button is visible", () => scroll.Button.State == Visibility.Visible); } From 9a65aa18d78407bbba9658e0fc98810fc2940c69 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 13 Apr 2020 16:13:14 +0900 Subject: [PATCH 1035/2376] Fix connections hidden due to overlapping controlpoints --- .../TestScenePathControlPointVisualiser.cs | 64 +++++++++++++++++++ .../PathControlPointConnectionPiece.cs | 16 +++-- .../Components/PathControlPointVisualiser.cs | 57 +++++++++-------- 3 files changed, 103 insertions(+), 34 deletions(-) create mode 100644 osu.Game.Rulesets.Osu.Tests/TestScenePathControlPointVisualiser.cs diff --git a/osu.Game.Rulesets.Osu.Tests/TestScenePathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu.Tests/TestScenePathControlPointVisualiser.cs new file mode 100644 index 0000000000..cbe14ff4d2 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestScenePathControlPointVisualiser.cs @@ -0,0 +1,64 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using Humanizer; +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Tests.Visual; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests +{ + public class TestScenePathControlPointVisualiser : OsuTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(StringHumanizeExtensions), + typeof(PathControlPointPiece), + typeof(PathControlPointConnectionPiece) + }; + + private Slider slider; + private PathControlPointVisualiser visualiser; + + [SetUp] + public void Setup() => Schedule(() => + { + slider = new Slider(); + slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + }); + + [Test] + public void TestAddOverlappingControlPoints() + { + createVisualiser(true); + + addControlPointStep(new Vector2(200)); + addControlPointStep(new Vector2(300)); + addControlPointStep(new Vector2(300)); + addControlPointStep(new Vector2(500, 300)); + + AddAssert("last connection displayed", () => + { + var lastConnection = visualiser.Connections.Last(c => c.ControlPoint.Position.Value == new Vector2(300)); + return lastConnection.DrawWidth > 50; + }); + } + + private void createVisualiser(bool allowSelection) => AddStep("create visualiser", () => Child = visualiser = new PathControlPointVisualiser(slider, allowSelection) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }); + + private void addControlPointStep(Vector2 position) => AddStep($"add control point {position}", () => slider.Path.ControlPoints.Add(new PathControlPoint(position))); + } +} diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs index 0fc441fec6..9c620ecb2f 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs @@ -16,22 +16,25 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components /// public class PathControlPointConnectionPiece : CompositeDrawable { - public PathControlPoint ControlPoint; + public readonly PathControlPoint ControlPoint; private readonly Path path; private readonly Slider slider; + private readonly int controlPointIndex; private IBindable sliderPosition; private IBindable pathVersion; - public PathControlPointConnectionPiece(Slider slider, PathControlPoint controlPoint) + public PathControlPointConnectionPiece(Slider slider, int controlPointIndex) { this.slider = slider; - ControlPoint = controlPoint; + this.controlPointIndex = controlPointIndex; Origin = Anchor.Centre; AutoSizeAxes = Axes.Both; + ControlPoint = slider.Path.ControlPoints[controlPointIndex]; + InternalChild = path = new SmoothPath { Anchor = Anchor.Centre, @@ -61,13 +64,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components path.ClearVertices(); - int index = slider.Path.ControlPoints.IndexOf(ControlPoint) + 1; - - if (index == 0 || index == slider.Path.ControlPoints.Count) + int nextIndex = controlPointIndex + 1; + if (nextIndex == 0 || nextIndex == slider.Path.ControlPoints.Count) return; path.AddVertex(Vector2.Zero); - path.AddVertex(slider.Path.ControlPoints[index].Position.Value - ControlPoint.Position.Value); + path.AddVertex(slider.Path.ControlPoints[nextIndex].Position.Value - ControlPoint.Position.Value); path.OriginPosition = path.PositionInBoundingBox(Vector2.Zero); } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index e293eba9d7..f6354bc612 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; using System.Linq; using Humanizer; using osu.Framework.Bindables; @@ -24,17 +25,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components public class PathControlPointVisualiser : CompositeDrawable, IKeyBindingHandler, IHasContextMenu { internal readonly Container Pieces; + internal readonly Container Connections; - private readonly Container connections; - + private readonly IBindableList controlPoints = new BindableList(); private readonly Slider slider; - private readonly bool allowSelection; private InputManager inputManager; - private IBindableList controlPoints; - public Action> RemoveControlPointsRequested; public PathControlPointVisualiser(Slider slider, bool allowSelection) @@ -46,7 +44,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components InternalChildren = new Drawable[] { - connections = new Container { RelativeSizeAxes = Axes.Both }, + Connections = new Container { RelativeSizeAxes = Axes.Both }, Pieces = new Container { RelativeSizeAxes = Axes.Both } }; } @@ -57,33 +55,38 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components inputManager = GetContainingInputManager(); - controlPoints = slider.Path.ControlPoints.GetBoundCopy(); - controlPoints.ItemsAdded += addControlPoints; - controlPoints.ItemsRemoved += removeControlPoints; - - addControlPoints(controlPoints); + controlPoints.CollectionChanged += onControlPointsChanged; + controlPoints.BindTo(slider.Path.ControlPoints); } - private void addControlPoints(IEnumerable controlPoints) + private void onControlPointsChanged(object sender, NotifyCollectionChangedEventArgs e) { - foreach (var point in controlPoints) + switch (e.Action) { - Pieces.Add(new PathControlPointPiece(slider, point).With(d => - { - if (allowSelection) - d.RequestSelection = selectPiece; - })); + case NotifyCollectionChangedAction.Add: + for (int i = 0; i < e.NewItems.Count; i++) + { + var point = (PathControlPoint)e.NewItems[i]; - connections.Add(new PathControlPointConnectionPiece(slider, point)); - } - } + Pieces.Add(new PathControlPointPiece(slider, point).With(d => + { + if (allowSelection) + d.RequestSelection = selectPiece; + })); - private void removeControlPoints(IEnumerable controlPoints) - { - foreach (var point in controlPoints) - { - Pieces.RemoveAll(p => p.ControlPoint == point); - connections.RemoveAll(c => c.ControlPoint == point); + Connections.Add(new PathControlPointConnectionPiece(slider, e.NewStartingIndex + i)); + } + + break; + + case NotifyCollectionChangedAction.Remove: + foreach (var point in e.OldItems.Cast()) + { + Pieces.RemoveAll(p => p.ControlPoint == point); + Connections.RemoveAll(c => c.ControlPoint == point); + } + + break; } } From 29dd2252054ed45fbac8be5f43318bdae45552f5 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 13 Apr 2020 10:45:15 +0300 Subject: [PATCH 1036/2376] Make button protected --- .../UserInterface/TestSceneOverlayScrollContainer.cs | 9 +++++++-- osu.Game/Overlays/OverlayScrollContainer.cs | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs index 6a09fecc0a..e9e63613c0 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs @@ -25,14 +25,14 @@ namespace osu.Game.Tests.Visual.UserInterface [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); - private OverlayScrollContainer scroll; + private TestScrollContainer scroll; private int invocationCount; [SetUp] public void SetUp() => Schedule(() => { - Child = scroll = new OverlayScrollContainer + Child = scroll = new TestScrollContainer { RelativeSizeAxes = Axes.Both, Child = new Container @@ -104,5 +104,10 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("invocation count is 1", () => invocationCount == 1); } + + private class TestScrollContainer : OverlayScrollContainer + { + public new ScrollToTopButton Button => base.Button; + } } } diff --git a/osu.Game/Overlays/OverlayScrollContainer.cs b/osu.Game/Overlays/OverlayScrollContainer.cs index a8f2a0ce6c..9af09f0f6a 100644 --- a/osu.Game/Overlays/OverlayScrollContainer.cs +++ b/osu.Game/Overlays/OverlayScrollContainer.cs @@ -26,7 +26,7 @@ namespace osu.Game.Overlays /// private const int button_scroll_position = 200; - public ScrollToTopButton Button { get; } + protected readonly ScrollToTopButton Button; private float currentTarget; From b8ecc41667301cccff0d3e81bccdc95f6946a69f Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 13 Apr 2020 10:52:34 +0300 Subject: [PATCH 1037/2376] Add comment --- osu.Game/Overlays/OverlayScrollContainer.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Overlays/OverlayScrollContainer.cs b/osu.Game/Overlays/OverlayScrollContainer.cs index 9af09f0f6a..e95a6379f1 100644 --- a/osu.Game/Overlays/OverlayScrollContainer.cs +++ b/osu.Game/Overlays/OverlayScrollContainer.cs @@ -56,6 +56,7 @@ namespace osu.Game.Overlays return; } + // Clicking on button should immediately cause it's disappearance, so we don't want to override it's state until we have a new target. if (Target == currentTarget) return; From 1e3251e3e936f0ce0eca7a77dd7ae5dbffab27eb Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 13 Apr 2020 10:59:53 +0300 Subject: [PATCH 1038/2376] Remove excessive logic --- osu.Game/Overlays/OverlayScrollContainer.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/osu.Game/Overlays/OverlayScrollContainer.cs b/osu.Game/Overlays/OverlayScrollContainer.cs index e95a6379f1..e7415e6f74 100644 --- a/osu.Game/Overlays/OverlayScrollContainer.cs +++ b/osu.Game/Overlays/OverlayScrollContainer.cs @@ -28,8 +28,6 @@ namespace osu.Game.Overlays protected readonly ScrollToTopButton Button; - private float currentTarget; - public OverlayScrollContainer() { AddInternal(Button = new ScrollToTopButton @@ -40,7 +38,6 @@ namespace osu.Game.Overlays Action = () => { ScrollToStart(); - currentTarget = Target; Button.State = Visibility.Hidden; } }); @@ -56,11 +53,6 @@ namespace osu.Game.Overlays return; } - // Clicking on button should immediately cause it's disappearance, so we don't want to override it's state until we have a new target. - if (Target == currentTarget) - return; - - currentTarget = Target; Button.State = Target > button_scroll_position ? Visibility.Visible : Visibility.Hidden; } From bb53f96c717e9034c5a2e1b2411c3474d076fc9a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 13 Apr 2020 17:18:50 +0900 Subject: [PATCH 1039/2376] Store states as byte[] instead of Streams --- .../Editor/LegacyEditorBeatmapDifferTest.cs | 19 +++++++-------- osu.Game/Screens/Edit/EditorChangeHandler.cs | 18 ++++++++------ .../Screens/Edit/LegacyEditorBeatmapDiffer.cs | 24 +++++++------------ 3 files changed, 29 insertions(+), 32 deletions(-) diff --git a/osu.Game.Tests/Editor/LegacyEditorBeatmapDifferTest.cs b/osu.Game.Tests/Editor/LegacyEditorBeatmapDifferTest.cs index d70a112b7f..ecd3799cb1 100644 --- a/osu.Game.Tests/Editor/LegacyEditorBeatmapDifferTest.cs +++ b/osu.Game.Tests/Editor/LegacyEditorBeatmapDifferTest.cs @@ -315,26 +315,25 @@ namespace osu.Game.Tests.Editor differ.Patch(encode(current), encode(patch)); // Convert beatmaps to strings for assertion purposes. - string currentStr = Encoding.ASCII.GetString(encode(current).ToArray()); - string patchStr = Encoding.ASCII.GetString(encode(patch).ToArray()); + string currentStr = Encoding.ASCII.GetString(encode(current)); + string patchStr = Encoding.ASCII.GetString(encode(patch)); Assert.That(currentStr, Is.EqualTo(patchStr)); } - private MemoryStream encode(IBeatmap beatmap) + private byte[] encode(IBeatmap beatmap) { - var encoded = new MemoryStream(); - + using (var encoded = new MemoryStream()) using (var sw = new StreamWriter(encoded, leaveOpen: true)) + { new LegacyBeatmapEncoder(beatmap).Encode(sw); - - return encoded; + return encoded.ToArray(); + } } - private IBeatmap decode(Stream stream) + private IBeatmap decode(byte[] state) { - stream.Seek(0, SeekOrigin.Begin); - + using (var stream = new MemoryStream(state)) using (var reader = new LineBufferedReader(stream, true)) return Decoder.GetDecoder(reader).Decode(reader); } diff --git a/osu.Game/Screens/Edit/EditorChangeHandler.cs b/osu.Game/Screens/Edit/EditorChangeHandler.cs index 7e372926ba..22f076d939 100644 --- a/osu.Game/Screens/Edit/EditorChangeHandler.cs +++ b/osu.Game/Screens/Edit/EditorChangeHandler.cs @@ -16,7 +16,9 @@ namespace osu.Game.Screens.Edit public class EditorChangeHandler : IEditorChangeHandler { private readonly LegacyEditorBeatmapDiffer differ; - private readonly List savedStates = new List(); + + private readonly List savedStates = new List(); + private int currentState = -1; private readonly EditorBeatmap editorBeatmap; @@ -69,15 +71,17 @@ namespace osu.Game.Screens.Edit if (isRestoring) return; - var stream = new MemoryStream(); - - using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) - new LegacyBeatmapEncoder(editorBeatmap).Encode(sw); - if (currentState < savedStates.Count - 1) savedStates.RemoveRange(currentState + 1, savedStates.Count - currentState - 1); - savedStates.Add(stream); + using (var stream = new MemoryStream()) + { + using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) + new LegacyBeatmapEncoder(editorBeatmap).Encode(sw); + + savedStates.Add(stream.ToArray()); + } + currentState = savedStates.Count - 1; } diff --git a/osu.Game/Screens/Edit/LegacyEditorBeatmapDiffer.cs b/osu.Game/Screens/Edit/LegacyEditorBeatmapDiffer.cs index 8d2f577a1d..c62f21bb39 100644 --- a/osu.Game/Screens/Edit/LegacyEditorBeatmapDiffer.cs +++ b/osu.Game/Screens/Edit/LegacyEditorBeatmapDiffer.cs @@ -4,12 +4,13 @@ using System; using System.Collections.Generic; using System.IO; +using System.Text; using DiffPlex; using osu.Framework.Audio.Track; using osu.Framework.Graphics.Textures; using osu.Game.Beatmaps; -using osu.Game.Beatmaps.Formats; using osu.Game.IO; +using Decoder = osu.Game.Beatmaps.Formats.Decoder; namespace osu.Game.Screens.Edit { @@ -22,7 +23,7 @@ namespace osu.Game.Screens.Edit this.editorBeatmap = editorBeatmap; } - public void Patch(Stream currentState, Stream newState) + public void Patch(byte[] currentState, byte[] newState) { // Diff the beatmaps var result = new Differ().CreateLineDiffs(readString(currentState), readString(newState), true, false); @@ -36,7 +37,7 @@ namespace osu.Game.Screens.Edit foreach (var block in result.DiffBlocks) { - // Removed hitobject + // Removed hitobjects for (int i = 0; i < block.DeleteCountA; i++) { int hoIndex = block.DeleteStartA + i - oldHitObjectsIndex - 1; @@ -47,7 +48,7 @@ namespace osu.Game.Screens.Edit toRemove.Add(hoIndex); } - // Added hitobject + // Added hitobjects for (int i = 0; i < block.InsertCountB; i++) { int hoIndex = block.InsertStartB + i - newHitObjectsIndex - 1; @@ -74,18 +75,11 @@ namespace osu.Game.Screens.Edit } } - private string readString(Stream stream) + private string readString(byte[] state) => Encoding.UTF8.GetString(state); + + private IBeatmap readBeatmap(byte[] state) { - stream.Seek(0, SeekOrigin.Begin); - - using (var sr = new StreamReader(stream, System.Text.Encoding.UTF8, true, 1024, true)) - return sr.ReadToEnd(); - } - - private IBeatmap readBeatmap(Stream stream) - { - stream.Seek(0, SeekOrigin.Begin); - + using (var stream = new MemoryStream(state)) using (var reader = new LineBufferedReader(stream, true)) return new PassThroughWorkingBeatmap(Decoder.GetDecoder(reader).Decode(reader)).GetPlayableBeatmap(editorBeatmap.BeatmapInfo.Ruleset); } From 6aab19413c793bce5650fa1aa6a95a82a3048e4c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 13 Apr 2020 17:20:01 +0900 Subject: [PATCH 1040/2376] Rename differ to patcher, add xmldoc --- ...mapDifferTest.cs => LegacyEditorBeatmapPatcherTest.cs} | 8 ++++---- osu.Game/Screens/Edit/EditorChangeHandler.cs | 6 +++--- ...itorBeatmapDiffer.cs => LegacyEditorBeatmapPatcher.cs} | 7 +++++-- 3 files changed, 12 insertions(+), 9 deletions(-) rename osu.Game.Tests/Editor/{LegacyEditorBeatmapDifferTest.cs => LegacyEditorBeatmapPatcherTest.cs} (97%) rename osu.Game/Screens/Edit/{LegacyEditorBeatmapDiffer.cs => LegacyEditorBeatmapPatcher.cs} (93%) diff --git a/osu.Game.Tests/Editor/LegacyEditorBeatmapDifferTest.cs b/osu.Game.Tests/Editor/LegacyEditorBeatmapPatcherTest.cs similarity index 97% rename from osu.Game.Tests/Editor/LegacyEditorBeatmapDifferTest.cs rename to osu.Game.Tests/Editor/LegacyEditorBeatmapPatcherTest.cs index ecd3799cb1..11c6399d19 100644 --- a/osu.Game.Tests/Editor/LegacyEditorBeatmapDifferTest.cs +++ b/osu.Game.Tests/Editor/LegacyEditorBeatmapPatcherTest.cs @@ -20,15 +20,15 @@ using Decoder = osu.Game.Beatmaps.Formats.Decoder; namespace osu.Game.Tests.Editor { [TestFixture] - public class LegacyEditorBeatmapDifferTest + public class LegacyEditorBeatmapPatcherTest { - private LegacyEditorBeatmapDiffer differ; + private LegacyEditorBeatmapPatcher patcher; private EditorBeatmap current; [SetUp] public void Setup() { - differ = new LegacyEditorBeatmapDiffer(current = new EditorBeatmap(new OsuBeatmap + patcher = new LegacyEditorBeatmapPatcher(current = new EditorBeatmap(new OsuBeatmap { BeatmapInfo = { @@ -312,7 +312,7 @@ namespace osu.Game.Tests.Editor patch = decode(encode(patch)); // Apply the patch. - differ.Patch(encode(current), encode(patch)); + patcher.Patch(encode(current), encode(patch)); // Convert beatmaps to strings for assertion purposes. string currentStr = Encoding.ASCII.GetString(encode(current)); diff --git a/osu.Game/Screens/Edit/EditorChangeHandler.cs b/osu.Game/Screens/Edit/EditorChangeHandler.cs index 22f076d939..00a27801f4 100644 --- a/osu.Game/Screens/Edit/EditorChangeHandler.cs +++ b/osu.Game/Screens/Edit/EditorChangeHandler.cs @@ -15,7 +15,7 @@ namespace osu.Game.Screens.Edit /// public class EditorChangeHandler : IEditorChangeHandler { - private readonly LegacyEditorBeatmapDiffer differ; + private readonly LegacyEditorBeatmapPatcher patcher; private readonly List savedStates = new List(); @@ -37,7 +37,7 @@ namespace osu.Game.Screens.Edit editorBeatmap.HitObjectRemoved += hitObjectRemoved; editorBeatmap.HitObjectUpdated += hitObjectUpdated; - differ = new LegacyEditorBeatmapDiffer(editorBeatmap); + patcher = new LegacyEditorBeatmapPatcher(editorBeatmap); // Initial state. SaveState(); @@ -103,7 +103,7 @@ namespace osu.Game.Screens.Edit isRestoring = true; - differ.Patch(savedStates[currentState], savedStates[newState]); + patcher.Patch(savedStates[currentState], savedStates[newState]); currentState = newState; isRestoring = false; diff --git a/osu.Game/Screens/Edit/LegacyEditorBeatmapDiffer.cs b/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs similarity index 93% rename from osu.Game/Screens/Edit/LegacyEditorBeatmapDiffer.cs rename to osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs index c62f21bb39..17eba87076 100644 --- a/osu.Game/Screens/Edit/LegacyEditorBeatmapDiffer.cs +++ b/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs @@ -14,11 +14,14 @@ using Decoder = osu.Game.Beatmaps.Formats.Decoder; namespace osu.Game.Screens.Edit { - public class LegacyEditorBeatmapDiffer + /// + /// Patches an based on the difference between two legacy (.osu) states. + /// + public class LegacyEditorBeatmapPatcher { private readonly EditorBeatmap editorBeatmap; - public LegacyEditorBeatmapDiffer(EditorBeatmap editorBeatmap) + public LegacyEditorBeatmapPatcher(EditorBeatmap editorBeatmap) { this.editorBeatmap = editorBeatmap; } From dd949a3fe09ebaa45143be64075efe89aafc08f7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 13 Apr 2020 17:52:04 +0900 Subject: [PATCH 1041/2376] Fix test writer flush happening too late --- osu.Game.Tests/Editor/LegacyEditorBeatmapPatcherTest.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Editor/LegacyEditorBeatmapPatcherTest.cs b/osu.Game.Tests/Editor/LegacyEditorBeatmapPatcherTest.cs index 11c6399d19..c24418d688 100644 --- a/osu.Game.Tests/Editor/LegacyEditorBeatmapPatcherTest.cs +++ b/osu.Game.Tests/Editor/LegacyEditorBeatmapPatcherTest.cs @@ -324,9 +324,10 @@ namespace osu.Game.Tests.Editor private byte[] encode(IBeatmap beatmap) { using (var encoded = new MemoryStream()) - using (var sw = new StreamWriter(encoded, leaveOpen: true)) { - new LegacyBeatmapEncoder(beatmap).Encode(sw); + using (var sw = new StreamWriter(encoded)) + new LegacyBeatmapEncoder(beatmap).Encode(sw); + return encoded.ToArray(); } } @@ -334,7 +335,7 @@ namespace osu.Game.Tests.Editor private IBeatmap decode(byte[] state) { using (var stream = new MemoryStream(state)) - using (var reader = new LineBufferedReader(stream, true)) + using (var reader = new LineBufferedReader(stream)) return Decoder.GetDecoder(reader).Decode(reader); } } From 409cda3cc0d5a8edeb8470a88648499bab9d511d Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2020 08:53:21 +0000 Subject: [PATCH 1042/2376] Bump BenchmarkDotNet from 0.12.0 to 0.12.1 Bumps [BenchmarkDotNet](https://github.com/dotnet/BenchmarkDotNet) from 0.12.0 to 0.12.1. - [Release notes](https://github.com/dotnet/BenchmarkDotNet/releases) - [Commits](https://github.com/dotnet/BenchmarkDotNet/compare/v0.12.0...v0.12.1) Signed-off-by: dependabot-preview[bot] --- osu.Game.Benchmarks/osu.Game.Benchmarks.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj index f2e1c0ec3b..88fe8f1150 100644 --- a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj +++ b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj @@ -7,7 +7,7 @@ - + From b741e359cd5abf64d3428c3ff0da0e2ae1f1e436 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 13 Apr 2020 12:23:28 +0300 Subject: [PATCH 1043/2376] Use OverlayScrollContainer for overlays --- .../Graphics/Containers/SectionsContainer.cs | 27 ++++++++++++------- osu.Game/Overlays/BeatmapListingOverlay.cs | 2 +- osu.Game/Overlays/BeatmapSetOverlay.cs | 4 +-- osu.Game/Overlays/ChangelogOverlay.cs | 2 +- osu.Game/Overlays/NewsOverlay.cs | 2 +- osu.Game/Overlays/RankingsOverlay.cs | 4 +-- .../SearchableList/SearchableListOverlay.cs | 2 +- osu.Game/Overlays/UserProfileOverlay.cs | 2 ++ 8 files changed, 27 insertions(+), 18 deletions(-) diff --git a/osu.Game/Graphics/Containers/SectionsContainer.cs b/osu.Game/Graphics/Containers/SectionsContainer.cs index 07a50c39e1..24c61ad11c 100644 --- a/osu.Game/Graphics/Containers/SectionsContainer.cs +++ b/osu.Game/Graphics/Containers/SectionsContainer.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -19,7 +20,7 @@ namespace osu.Game.Graphics.Containers private Drawable expandableHeader, fixedHeader, footer, headerBackground; private readonly OsuScrollContainer scrollContainer; private readonly Container headerBackgroundContainer; - private readonly FlowContainer scrollContentContainer; + private FlowContainer scrollContentContainer; protected override Container Content => scrollContentContainer; @@ -125,20 +126,26 @@ namespace osu.Game.Graphics.Containers public SectionsContainer() { - AddInternal(scrollContainer = new OsuScrollContainer + AddRangeInternal(new Drawable[] { - RelativeSizeAxes = Axes.Both, - Masking = true, - ScrollbarVisible = false, - Children = new Drawable[] { scrollContentContainer = CreateScrollContentContainer() } - }); - AddInternal(headerBackgroundContainer = new Container - { - RelativeSizeAxes = Axes.X + scrollContainer = CreateScrollContainer().With(s => + { + s.RelativeSizeAxes = Axes.Both; + s.Masking = true; + s.ScrollbarVisible = false; + s.Children = new Drawable[] { scrollContentContainer = CreateScrollContentContainer() }; + }), + headerBackgroundContainer = new Container + { + RelativeSizeAxes = Axes.X + } }); originalSectionsMargin = scrollContentContainer.Margin; } + [NotNull] + protected virtual OsuScrollContainer CreateScrollContainer() => new OsuScrollContainer(); + public void ScrollTo(Drawable section) => scrollContainer.ScrollTo(scrollContainer.GetChildPosInContent(section) - (FixedHeader?.BoundingBox.Height ?? 0)); public void ScrollToTop() => scrollContainer.ScrollTo(0); diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index 5bac5a5402..b450f33ee1 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -54,7 +54,7 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.Both, Colour = ColourProvider.Background6 }, - new BasicScrollContainer + new OverlayScrollContainer { RelativeSizeAxes = Axes.Both, ScrollbarVisible = false, diff --git a/osu.Game/Overlays/BeatmapSetOverlay.cs b/osu.Game/Overlays/BeatmapSetOverlay.cs index 0d16c4842d..3e23442023 100644 --- a/osu.Game/Overlays/BeatmapSetOverlay.cs +++ b/osu.Game/Overlays/BeatmapSetOverlay.cs @@ -39,7 +39,7 @@ namespace osu.Game.Overlays public BeatmapSetOverlay() : base(OverlayColourScheme.Blue) { - OsuScrollContainer scroll; + OverlayScrollContainer scroll; Info info; CommentsSection comments; @@ -49,7 +49,7 @@ namespace osu.Game.Overlays { RelativeSizeAxes = Axes.Both }, - scroll = new OsuScrollContainer + scroll = new OverlayScrollContainer { RelativeSizeAxes = Axes.Both, ScrollbarVisible = false, diff --git a/osu.Game/Overlays/ChangelogOverlay.cs b/osu.Game/Overlays/ChangelogOverlay.cs index d13ac5c2de..726be9e194 100644 --- a/osu.Game/Overlays/ChangelogOverlay.cs +++ b/osu.Game/Overlays/ChangelogOverlay.cs @@ -50,7 +50,7 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.Both, Colour = ColourProvider.Background4, }, - new OsuScrollContainer + new OverlayScrollContainer { RelativeSizeAxes = Axes.Both, ScrollbarVisible = false, diff --git a/osu.Game/Overlays/NewsOverlay.cs b/osu.Game/Overlays/NewsOverlay.cs index 71c205ff63..6c9477cbc4 100644 --- a/osu.Game/Overlays/NewsOverlay.cs +++ b/osu.Game/Overlays/NewsOverlay.cs @@ -36,7 +36,7 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.Both, Colour = colours.PurpleDarkAlternative }, - new OsuScrollContainer + new OverlayScrollContainer { RelativeSizeAxes = Axes.Both, Child = new FillFlowContainer diff --git a/osu.Game/Overlays/RankingsOverlay.cs b/osu.Game/Overlays/RankingsOverlay.cs index afb23883ac..7b200d4226 100644 --- a/osu.Game/Overlays/RankingsOverlay.cs +++ b/osu.Game/Overlays/RankingsOverlay.cs @@ -23,7 +23,7 @@ namespace osu.Game.Overlays protected Bindable Scope => header.Current; - private readonly BasicScrollContainer scrollFlow; + private readonly OverlayScrollContainer scrollFlow; private readonly Container contentContainer; private readonly LoadingLayer loading; private readonly Box background; @@ -44,7 +44,7 @@ namespace osu.Game.Overlays { RelativeSizeAxes = Axes.Both }, - scrollFlow = new BasicScrollContainer + scrollFlow = new OverlayScrollContainer { RelativeSizeAxes = Axes.Both, ScrollbarVisible = false, diff --git a/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs b/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs index d6174e0733..ebd12913f5 100644 --- a/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs +++ b/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs @@ -72,7 +72,7 @@ namespace osu.Game.Overlays.SearchableList { RelativeSizeAxes = Axes.Both, Masking = true, - Child = new OsuScrollContainer + Child = new OverlayScrollContainer { RelativeSizeAxes = Axes.Both, ScrollbarVisible = false, diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs index 6ec30f7707..b4c8a2d3ca 100644 --- a/osu.Game/Overlays/UserProfileOverlay.cs +++ b/osu.Game/Overlays/UserProfileOverlay.cs @@ -195,6 +195,8 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.Both; } + protected override OsuScrollContainer CreateScrollContainer() => new OverlayScrollContainer(); + protected override FlowContainer CreateScrollContentContainer() => new FillFlowContainer { Direction = FillDirection.Vertical, From 4c5d01a611ddc88cd847a2d91db27ff57bc3e037 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 13 Apr 2020 12:34:51 +0300 Subject: [PATCH 1044/2376] Remove unused usings --- osu.Game/Overlays/NewsOverlay.cs | 1 - osu.Game/Overlays/SearchableList/SearchableListOverlay.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/osu.Game/Overlays/NewsOverlay.cs b/osu.Game/Overlays/NewsOverlay.cs index 6c9477cbc4..46d692d44d 100644 --- a/osu.Game/Overlays/NewsOverlay.cs +++ b/osu.Game/Overlays/NewsOverlay.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; -using osu.Game.Graphics.Containers; using osu.Game.Overlays.News; namespace osu.Game.Overlays diff --git a/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs b/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs index ebd12913f5..4ab2de06b6 100644 --- a/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs +++ b/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Game.Graphics.Backgrounds; -using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; namespace osu.Game.Overlays.SearchableList From cee4b005e6a5ceb3e4af6380f73321205b9d9403 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 13 Apr 2020 20:00:06 +0900 Subject: [PATCH 1045/2376] Fix custom sample set 0 not falling back to default samples --- osu.Game/Beatmaps/Formats/LegacyDecoder.cs | 2 +- .../Objects/Legacy/ConvertHitObjectParser.cs | 2 +- osu.Game/Skinning/LegacyBeatmapSkin.cs | 13 +++++++++++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index 561707f9ef..90a5d0dcba 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -168,7 +168,7 @@ namespace osu.Game.Beatmaps.Formats { var baseInfo = base.ApplyTo(hitSampleInfo); - if (string.IsNullOrEmpty(baseInfo.Suffix) && CustomSampleBank > 1) + if (string.IsNullOrEmpty(baseInfo.Suffix) && CustomSampleBank > 0) baseInfo.Suffix = CustomSampleBank.ToString(); return baseInfo; diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 8d3ad5984f..8580cdede3 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -415,7 +415,7 @@ namespace osu.Game.Rulesets.Objects.Legacy { set { - if (value > 1) + if (value > 0) Suffix = value.ToString(); } } diff --git a/osu.Game/Skinning/LegacyBeatmapSkin.cs b/osu.Game/Skinning/LegacyBeatmapSkin.cs index 1190a330fe..c4636f46f5 100644 --- a/osu.Game/Skinning/LegacyBeatmapSkin.cs +++ b/osu.Game/Skinning/LegacyBeatmapSkin.cs @@ -2,8 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.IO.Stores; +using osu.Game.Audio; using osu.Game.Beatmaps; namespace osu.Game.Skinning @@ -33,6 +35,17 @@ namespace osu.Game.Skinning return base.GetConfig(lookup); } + public override SampleChannel GetSample(ISampleInfo sampleInfo) + { + if (sampleInfo is HitSampleInfo hsi && string.IsNullOrEmpty(hsi.Suffix)) + { + // When no custom sample set is provided, always fall-back to the default samples. + return null; + } + + return base.GetSample(sampleInfo); + } + private static SkinInfo createSkinInfo(BeatmapInfo beatmap) => new SkinInfo { Name = beatmap.ToString(), Creator = beatmap.Metadata.Author.ToString() }; } From 58a7313091812597a622ff0f616fcd23a271febd Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 13 Apr 2020 20:09:17 +0900 Subject: [PATCH 1046/2376] Fix fallback for file hit samples --- osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 8580cdede3..1dca4a5c02 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -425,6 +425,12 @@ namespace osu.Game.Rulesets.Objects.Legacy { public string Filename; + public FileHitSampleInfo() + { + // Has no effect since LookupNames is overridden, however prompts LegacyBeatmapSkin to not fallback. + Suffix = "0"; + } + public override IEnumerable LookupNames => new[] { Filename, From 0be2dc9b2dee3eecb9fdac94a9f8e64745230d72 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 13 Apr 2020 20:12:51 +0900 Subject: [PATCH 1047/2376] Tidy up SectionsContainer class layout/ordering --- .../Graphics/Containers/SectionsContainer.cs | 82 ++++++++++--------- 1 file changed, 43 insertions(+), 39 deletions(-) diff --git a/osu.Game/Graphics/Containers/SectionsContainer.cs b/osu.Game/Graphics/Containers/SectionsContainer.cs index 24c61ad11c..a3125614aa 100644 --- a/osu.Game/Graphics/Containers/SectionsContainer.cs +++ b/osu.Game/Graphics/Containers/SectionsContainer.cs @@ -17,12 +17,7 @@ namespace osu.Game.Graphics.Containers public class SectionsContainer : Container where T : Drawable { - private Drawable expandableHeader, fixedHeader, footer, headerBackground; - private readonly OsuScrollContainer scrollContainer; - private readonly Container headerBackgroundContainer; - private FlowContainer scrollContentContainer; - - protected override Container Content => scrollContentContainer; + public Bindable SelectedSection { get; } = new Bindable(); public Drawable ExpandableHeader { @@ -84,6 +79,7 @@ namespace osu.Game.Graphics.Containers headerBackgroundContainer.Clear(); headerBackground = value; + if (value == null) return; headerBackgroundContainer.Add(headerBackground); @@ -92,37 +88,17 @@ namespace osu.Game.Graphics.Containers } } - public Bindable SelectedSection { get; } = new Bindable(); + protected override Container Content => scrollContentContainer; - protected virtual FlowContainer CreateScrollContentContainer() - => new FillFlowContainer - { - Direction = FillDirection.Vertical, - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - }; - - public override void Add(T drawable) - { - base.Add(drawable); - lastKnownScroll = float.NaN; - headerHeight = float.NaN; - footerHeight = float.NaN; - } + private readonly OsuScrollContainer scrollContainer; + private readonly Container headerBackgroundContainer; + private readonly MarginPadding originalSectionsMargin; + private Drawable expandableHeader, fixedHeader, footer, headerBackground; + private FlowContainer scrollContentContainer; private float headerHeight, footerHeight; - private readonly MarginPadding originalSectionsMargin; - private void updateSectionsMargin() - { - if (!Children.Any()) return; - - var newMargin = originalSectionsMargin; - newMargin.Top += headerHeight; - newMargin.Bottom += footerHeight; - - scrollContentContainer.Margin = newMargin; - } + private float lastKnownScroll; public SectionsContainer() { @@ -133,22 +109,41 @@ namespace osu.Game.Graphics.Containers s.RelativeSizeAxes = Axes.Both; s.Masking = true; s.ScrollbarVisible = false; - s.Children = new Drawable[] { scrollContentContainer = CreateScrollContentContainer() }; + s.Child = scrollContentContainer = CreateScrollContentContainer(); }), headerBackgroundContainer = new Container { RelativeSizeAxes = Axes.X } }); + originalSectionsMargin = scrollContentContainer.Margin; } + public override void Add(T drawable) + { + base.Add(drawable); + lastKnownScroll = float.NaN; + headerHeight = float.NaN; + footerHeight = float.NaN; + } + + public void ScrollTo(Drawable section) => + scrollContainer.ScrollTo(scrollContainer.GetChildPosInContent(section) - (FixedHeader?.BoundingBox.Height ?? 0)); + + public void ScrollToTop() => scrollContainer.ScrollTo(0); + [NotNull] protected virtual OsuScrollContainer CreateScrollContainer() => new OsuScrollContainer(); - public void ScrollTo(Drawable section) => scrollContainer.ScrollTo(scrollContainer.GetChildPosInContent(section) - (FixedHeader?.BoundingBox.Height ?? 0)); - - public void ScrollToTop() => scrollContainer.ScrollTo(0); + [NotNull] + protected virtual FlowContainer CreateScrollContentContainer() => + new FillFlowContainer + { + Direction = FillDirection.Vertical, + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + }; protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source) { @@ -163,8 +158,6 @@ namespace osu.Game.Graphics.Containers return result; } - private float lastKnownScroll; - protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); @@ -215,5 +208,16 @@ namespace osu.Game.Graphics.Containers SelectedSection.Value = bestMatch; } } + + private void updateSectionsMargin() + { + if (!Children.Any()) return; + + var newMargin = originalSectionsMargin; + newMargin.Top += headerHeight; + newMargin.Bottom += footerHeight; + + scrollContentContainer.Margin = newMargin; + } } } From 2388799acfb8a781894d2c41de06c7f25b92bccb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 13 Apr 2020 20:34:18 +0900 Subject: [PATCH 1048/2376] Limit upper number of editor beatmap states saved to 50 --- .../Editor/EditorChangeHandlerTest.cs | 71 +++++++++++++++++++ osu.Game/Screens/Edit/EditorChangeHandler.cs | 7 ++ 2 files changed, 78 insertions(+) create mode 100644 osu.Game.Tests/Editor/EditorChangeHandlerTest.cs diff --git a/osu.Game.Tests/Editor/EditorChangeHandlerTest.cs b/osu.Game.Tests/Editor/EditorChangeHandlerTest.cs new file mode 100644 index 0000000000..ef16976130 --- /dev/null +++ b/osu.Game.Tests/Editor/EditorChangeHandlerTest.cs @@ -0,0 +1,71 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Screens.Edit; + +namespace osu.Game.Tests.Editor +{ + [TestFixture] + public class EditorChangeHandlerTest + { + [Test] + public void TestSaveRestoreState() + { + var handler = new EditorChangeHandler(new EditorBeatmap(new Beatmap())); + + Assert.That(handler.HasUndoState, Is.False); + + handler.SaveState(); + + Assert.That(handler.HasUndoState, Is.True); + + handler.RestoreState(-1); + + Assert.That(handler.HasUndoState, Is.False); + } + + [Test] + public void TestMaxStatesSaved() + { + var handler = new EditorChangeHandler(new EditorBeatmap(new Beatmap())); + + Assert.That(handler.HasUndoState, Is.False); + + for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES; i++) + handler.SaveState(); + + Assert.That(handler.HasUndoState, Is.True); + + for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES; i++) + { + Assert.That(handler.HasUndoState, Is.True); + handler.RestoreState(-1); + } + + Assert.That(handler.HasUndoState, Is.False); + } + + [Test] + public void TestMaxStatesExceeded() + { + var handler = new EditorChangeHandler(new EditorBeatmap(new Beatmap())); + + Assert.That(handler.HasUndoState, Is.False); + + for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES * 2; i++) + handler.SaveState(); + + Assert.That(handler.HasUndoState, Is.True); + + for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES; i++) + { + Assert.That(handler.HasUndoState, Is.True); + handler.RestoreState(-1); + } + + Assert.That(handler.HasUndoState, Is.False); + } + } +} diff --git a/osu.Game/Screens/Edit/EditorChangeHandler.cs b/osu.Game/Screens/Edit/EditorChangeHandler.cs index 00a27801f4..a8204715cd 100644 --- a/osu.Game/Screens/Edit/EditorChangeHandler.cs +++ b/osu.Game/Screens/Edit/EditorChangeHandler.cs @@ -25,6 +25,8 @@ namespace osu.Game.Screens.Edit private int bulkChangesStarted; private bool isRestoring; + public const int MAX_SAVED_STATES = 50; + /// /// Creates a new . /// @@ -43,6 +45,8 @@ namespace osu.Game.Screens.Edit SaveState(); } + public bool HasUndoState => currentState > 0; + private void hitObjectAdded(HitObject obj) => SaveState(); private void hitObjectRemoved(HitObject obj) => SaveState(); @@ -74,6 +78,9 @@ namespace osu.Game.Screens.Edit if (currentState < savedStates.Count - 1) savedStates.RemoveRange(currentState + 1, savedStates.Count - currentState - 1); + if (savedStates.Count > MAX_SAVED_STATES) + savedStates.RemoveAt(0); + using (var stream = new MemoryStream()) { using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) From 1c8a71b2842dd90ff1cdfc1c4d4cd3595a618925 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 13 Apr 2020 21:24:47 +0900 Subject: [PATCH 1049/2376] Exception instead of assert --- osu.Game/Online/API/APIRequest.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs index 6abb388c01..47600e4f68 100644 --- a/osu.Game/Online/API/APIRequest.cs +++ b/osu.Game/Online/API/APIRequest.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Diagnostics; using Newtonsoft.Json; using osu.Framework.IO.Network; using osu.Framework.Logging; @@ -32,8 +31,8 @@ namespace osu.Game.Online.API internal void TriggerSuccess(T result) { - // disallow calling twice - Debug.Assert(Result == null); + if (Result != null) + throw new InvalidOperationException("Attempted to trigger success more than once"); Result = result; Success?.Invoke(result); From 89d806358809b62ca0c8b81ec77abbc412ae8cbe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 13 Apr 2020 21:35:35 +0900 Subject: [PATCH 1050/2376] Add support for Perform/PerformAsync --- .../Online/TestDummyAPIRequestHandling.cs | 63 ++++++++++++++++--- osu.Game/Online/API/DummyAPIAccess.cs | 8 ++- 2 files changed, 59 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs b/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs index b00b63f6d5..5ef01d5702 100644 --- a/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs +++ b/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs @@ -43,17 +43,9 @@ namespace osu.Game.Tests.Online } [Test] - public void TestRequestHandling() + public void TestQueueRequestHandling() { - AddStep("register request handling", () => ((DummyAPIAccess)API).HandleRequest = req => - { - switch (req) - { - case LeaveChannelRequest cRequest: - cRequest.TriggerSuccess(); - break; - } - }); + registerHandler(); LeaveChannelRequest request; bool gotResponse = false; @@ -68,5 +60,56 @@ namespace osu.Game.Tests.Online AddAssert("response event fired", () => gotResponse); } + + [Test] + public void TestPerformRequestHandling() + { + registerHandler(); + + LeaveChannelRequest request; + bool gotResponse = false; + + AddStep("fire request", () => + { + gotResponse = false; + request = new LeaveChannelRequest(new Channel(), new User()); + request.Success += () => gotResponse = true; + API.Perform(request); + }); + + AddAssert("response event fired", () => gotResponse); + } + + [Test] + public void TestPerformAsyncRequestHandling() + { + registerHandler(); + + LeaveChannelRequest request; + bool gotResponse = false; + + AddStep("fire request", () => + { + gotResponse = false; + request = new LeaveChannelRequest(new Channel(), new User()); + request.Success += () => gotResponse = true; + API.PerformAsync(request); + }); + + AddAssert("response event fired", () => gotResponse); + } + + private void registerHandler() + { + AddStep("register request handling", () => ((DummyAPIAccess)API).HandleRequest = req => + { + switch (req) + { + case LeaveChannelRequest cRequest: + cRequest.TriggerSuccess(); + break; + } + }); + } } } diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index fa5ad115d2..7800241904 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -64,9 +64,13 @@ namespace osu.Game.Online.API HandleRequest?.Invoke(request); } - public void Perform(APIRequest request) { } + public void Perform(APIRequest request) => HandleRequest?.Invoke(request); - public Task PerformAsync(APIRequest request) => Task.CompletedTask; + public Task PerformAsync(APIRequest request) + { + HandleRequest?.Invoke(request); + return Task.CompletedTask; + } public void Register(IOnlineComponent component) { From 4cfc6866835d4d92f2c6f03cd4bfeaa47c845c76 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 13 Apr 2020 21:41:18 +0900 Subject: [PATCH 1051/2376] Fix excption with 0 control points --- .../Sliders/Components/PathControlPointConnectionPiece.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs index 9c620ecb2f..ba1d35c35c 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs @@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components path.ClearVertices(); int nextIndex = controlPointIndex + 1; - if (nextIndex == 0 || nextIndex == slider.Path.ControlPoints.Count) + if (nextIndex == 0 || nextIndex >= slider.Path.ControlPoints.Count) return; path.AddVertex(Vector2.Zero); From 2b2ab2bf1929d3b6d730d579b73b1bc39e2220e8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 13 Apr 2020 21:59:23 +0900 Subject: [PATCH 1052/2376] Show new segments as red points even when hovered --- .../Blueprints/Sliders/Components/PathControlPointPiece.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs index 092a13cca5..fed149b5c5 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs @@ -4,6 +4,7 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -51,6 +52,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components this.slider = slider; ControlPoint = controlPoint; + controlPoint.Type.BindValueChanged(_ => updateMarkerDisplay()); + Origin = Anchor.Centre; AutoSizeAxes = Axes.Both; @@ -183,8 +186,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components markerRing.Alpha = IsSelected.Value ? 1 : 0; Color4 colour = ControlPoint.Type.Value != null ? colours.Red : colours.Yellow; + if (IsHovered || IsSelected.Value) - colour = Color4.White; + colour = colour.Lighten(1); + marker.Colour = colour; } } From 13812fef4c1af988e1d292e43d96e20dc17b0784 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Mon, 13 Apr 2020 17:28:02 +0300 Subject: [PATCH 1053/2376] Replace BindTo with setting the bindable --- osu.Game/Overlays/Direct/PanelDownloadButton.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Direct/PanelDownloadButton.cs b/osu.Game/Overlays/Direct/PanelDownloadButton.cs index 51f5b2ae4f..dfcaf5ded6 100644 --- a/osu.Game/Overlays/Direct/PanelDownloadButton.cs +++ b/osu.Game/Overlays/Direct/PanelDownloadButton.cs @@ -18,7 +18,7 @@ namespace osu.Game.Overlays.Direct private readonly ShakeContainer shakeContainer; private readonly DownloadButton button; - private readonly BindableBool noVideoSetting = new BindableBool(); + private Bindable noVideoSetting; public PanelDownloadButton(BeatmapSetInfo beatmapSet) : base(beatmapSet) @@ -51,7 +51,7 @@ namespace osu.Game.Overlays.Direct return; } - noVideoSetting.BindTo(osuConfig.GetBindable(OsuSetting.PreferNoVideo)); + noVideoSetting = osuConfig.GetBindable(OsuSetting.PreferNoVideo); button.Action = () => { From 3e48c26bc24eedfdc2fe0a8a6ac01f5377648ca4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 14 Apr 2020 00:54:02 +0200 Subject: [PATCH 1054/2376] Add failing tests --- .../Rulesets/Scoring/ScoreProcessorTest.cs | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs new file mode 100644 index 0000000000..64d1024efb --- /dev/null +++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.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.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Judgements; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Scoring; +using osu.Game.Tests.Beatmaps; + +namespace osu.Game.Tests.Rulesets.Scoring +{ + public class ScoreProcessorTest + { + private ScoreProcessor scoreProcessor; + private IBeatmap beatmap; + + [SetUp] + public void SetUp() + { + scoreProcessor = new ScoreProcessor(); + beatmap = new TestBeatmap(new RulesetInfo()) + { + HitObjects = new List + { + new HitCircle() + } + }; + } + + [TestCase(ScoringMode.Standardised, HitResult.Meh, 750_000)] + [TestCase(ScoringMode.Standardised, HitResult.Good, 800_000)] + [TestCase(ScoringMode.Standardised, HitResult.Great, 1_000_000)] + [TestCase(ScoringMode.Classic, HitResult.Meh, 50)] + [TestCase(ScoringMode.Classic, HitResult.Good, 100)] + [TestCase(ScoringMode.Classic, HitResult.Great, 300)] + public void TestSingleOsuHit(ScoringMode scoringMode, HitResult hitResult, int expectedScore) + { + scoreProcessor.Mode.Value = scoringMode; + scoreProcessor.ApplyBeatmap(beatmap); + + var judgementResult = new JudgementResult(beatmap.HitObjects.Single(), new OsuJudgement()) + { + Type = hitResult + }; + scoreProcessor.ApplyResult(judgementResult); + + Assert.IsTrue(Precision.AlmostEquals(expectedScore, scoreProcessor.TotalScore.Value)); + } + } +} From 13c81db0cf90fcb1db1909a20aa753c23884faea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 14 Apr 2020 00:56:37 +0200 Subject: [PATCH 1055/2376] Fix incorrect classic score formula Upon closer inspection the classic score formula was subtly wrong. The version given in the wiki is: Score = Hit Value + (Hit Value * ((Combo multiplier * Difficulty multiplier * Mod multiplier) / 25)) The code previously used: bonusScore + baseScore * ((1 + Math.Max(0, HighestCombo.Value - 1) * scoreMultiplier) / 25); which is not equivalent to the version on the wiki. The error is in the 1 factor, as in the above version it is being divided by 25, while it should be outside the division to keep parity with the previous formula. The tests attached in the previous commit demonstrate that this change causes a single hit without combo to increase total score by its exact numeric value. --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 8eafaa88ec..1f40f44dce 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -174,7 +174,7 @@ namespace osu.Game.Rulesets.Scoring case ScoringMode.Classic: // should emulate osu-stable's scoring as closely as we can (https://osu.ppy.sh/help/wiki/Score/ScoreV1) - return bonusScore + baseScore * ((1 + Math.Max(0, HighestCombo.Value - 1) * scoreMultiplier) / 25); + return bonusScore + baseScore * (1 + Math.Max(0, HighestCombo.Value - 1) * scoreMultiplier / 25); } } From 5f13dc81bed4d90e9fd43c5a3e96573de00951dd Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 14 Apr 2020 04:38:18 +0300 Subject: [PATCH 1056/2376] Remove no longer necessary extensions --- .../Skinning/CatchSkinExtensions.cs | 20 ------------------- .../Skinning/LegacyFruitPiece.cs | 9 ++++++--- 2 files changed, 6 insertions(+), 23 deletions(-) delete mode 100644 osu.Game.Rulesets.Catch/Skinning/CatchSkinExtensions.cs diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchSkinExtensions.cs b/osu.Game.Rulesets.Catch/Skinning/CatchSkinExtensions.cs deleted file mode 100644 index 718b22a0fb..0000000000 --- a/osu.Game.Rulesets.Catch/Skinning/CatchSkinExtensions.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using JetBrains.Annotations; -using osu.Framework.Bindables; -using osu.Game.Rulesets.Catch.UI; -using osu.Game.Skinning; -using osuTK.Graphics; - -namespace osu.Game.Rulesets.Catch.Skinning -{ - internal static class CatchSkinExtensions - { - [NotNull] - public static IBindable GetHyperDashFruitColour(this ISkin skin) - => skin.GetConfig(CatchSkinColour.HyperDashFruit) ?? - skin.GetConfig(CatchSkinColour.HyperDash) ?? - new Bindable(Catcher.DEFAULT_HYPER_DASH_COLOUR); - } -} diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs index 470c12559e..5be54d3882 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Catch.Objects.Drawables; +using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Skinning; using osuTK; @@ -55,14 +56,16 @@ namespace osu.Game.Rulesets.Catch.Skinning { var hyperDash = new Sprite { - Texture = skin.GetTexture(lookupName), - Colour = skin.GetHyperDashFruitColour().Value, Anchor = Anchor.Centre, Origin = Anchor.Centre, Blending = BlendingParameters.Additive, Depth = 1, Alpha = 0.7f, - Scale = new Vector2(1.2f) + Scale = new Vector2(1.2f), + Texture = skin.GetTexture(lookupName), + Colour = skin.GetConfig(CatchSkinColour.HyperDashFruit)?.Value ?? + skin.GetConfig(CatchSkinColour.HyperDash)?.Value ?? + Catcher.DEFAULT_HYPER_DASH_COLOUR, }; AddInternal(hyperDash); From c5f8bbb25fe55fd89af091ce27b8f5ba27aaa9f9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Apr 2020 11:56:37 +0900 Subject: [PATCH 1057/2376] Fix beatmap background not displaying when video is present --- osu.Game/Storyboards/Storyboard.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Storyboards/Storyboard.cs b/osu.Game/Storyboards/Storyboard.cs index a1ddafbacf..d13c874ee2 100644 --- a/osu.Game/Storyboards/Storyboard.cs +++ b/osu.Game/Storyboards/Storyboard.cs @@ -47,9 +47,6 @@ namespace osu.Game.Storyboards if (backgroundPath == null) return false; - if (GetLayer("Video").Elements.Any()) - return true; - return GetLayer("Background").Elements.Any(e => e.Path.ToLowerInvariant() == backgroundPath); } } From 3c5fb7982351a86964b3fa65c515c61f36b4d9e8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 14 Apr 2020 14:51:09 +0900 Subject: [PATCH 1058/2376] Mark dummy api test scene as headless --- osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs b/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs index 5ef01d5702..1e77d50115 100644 --- a/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs +++ b/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; +using osu.Framework.Testing; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; @@ -11,6 +12,7 @@ using osu.Game.Users; namespace osu.Game.Tests.Online { + [HeadlessTest] public class TestDummyAPIRequestHandling : OsuTestScene { [Test] From 9619fb9f6a5fa140b3bbf0226cf12c879619c66e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 14 Apr 2020 15:00:36 +0900 Subject: [PATCH 1059/2376] Remove bind in Player --- osu.Game/Screens/Play/Player.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index f1df69c5db..4597ae760c 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -27,7 +27,6 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Scoring; -using osu.Game.Screens.Play.HUD; using osu.Game.Scoring.Legacy; using osu.Game.Screens.Ranking; using osu.Game.Skinning; @@ -207,9 +206,6 @@ namespace osu.Game.Screens.Play foreach (var mod in Mods.Value.OfType()) mod.ApplyToHealthProcessor(HealthProcessor); - foreach (var overlay in DrawableRuleset.Overlays.OfType()) - overlay.BindHealthProcessor(HealthProcessor); - breakTracker.IsBreakTime.BindValueChanged(onBreakTimeChanged, true); } From 7d2d0785fd0cda708e8cfe0ecf867c7eb6214bb6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 14 Apr 2020 15:07:32 +0900 Subject: [PATCH 1060/2376] Fix potential unsafe ordering of binds --- osu.Game/Screens/Play/HUD/FailingLayer.cs | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/FailingLayer.cs b/osu.Game/Screens/Play/HUD/FailingLayer.cs index a1188343ac..aa15c1fd45 100644 --- a/osu.Game/Screens/Play/HUD/FailingLayer.cs +++ b/osu.Game/Screens/Play/HUD/FailingLayer.cs @@ -26,7 +26,8 @@ namespace osu.Game.Screens.Play.HUD private const int fade_time = 400; - private Bindable enabled; + private readonly Bindable enabled = new Bindable(); + private Bindable configEnabled; /// /// The threshold under which the current player life should be considered low and the layer should start fading in. @@ -36,6 +37,7 @@ namespace osu.Game.Screens.Play.HUD private const float gradient_size = 0.3f; private readonly Container boxes; + private HealthProcessor healthProcessor; public FailingLayer() { @@ -73,16 +75,29 @@ namespace osu.Game.Screens.Play.HUD { boxes.Colour = color.Red; - enabled = config.GetBindable(OsuSetting.FadePlayfieldWhenHealthLow); + configEnabled = config.GetBindable(OsuSetting.FadePlayfieldWhenHealthLow); enabled.BindValueChanged(e => this.FadeTo(e.NewValue ? 1 : 0, fade_time, Easing.OutQuint), true); + + updateBindings(); } public override void BindHealthProcessor(HealthProcessor processor) { base.BindHealthProcessor(processor); - // don't display ever if the ruleset is not using a draining health display. - if (!(processor is DrainingHealthProcessor)) + healthProcessor = processor; + updateBindings(); + } + + private void updateBindings() + { + if (configEnabled == null || healthProcessor == null) + return; + + // Don't display ever if the ruleset is not using a draining health display. + if (healthProcessor is DrainingHealthProcessor) + enabled.BindTo(configEnabled); + else { enabled.UnbindBindings(); enabled.Value = false; From 3183827329e477c8edb138f0f3930fcc450bf8a8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 14 Apr 2020 15:09:31 +0900 Subject: [PATCH 1061/2376] Reorder fields --- osu.Game/Screens/Play/HUD/FailingLayer.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/FailingLayer.cs b/osu.Game/Screens/Play/HUD/FailingLayer.cs index aa15c1fd45..cea85af112 100644 --- a/osu.Game/Screens/Play/HUD/FailingLayer.cs +++ b/osu.Game/Screens/Play/HUD/FailingLayer.cs @@ -23,20 +23,18 @@ namespace osu.Game.Screens.Play.HUD public class FailingLayer : HealthDisplay { private const float max_alpha = 0.4f; - private const int fade_time = 400; - - private readonly Bindable enabled = new Bindable(); - private Bindable configEnabled; + private const float gradient_size = 0.3f; /// /// The threshold under which the current player life should be considered low and the layer should start fading in. /// public double LowHealthThreshold = 0.20f; - private const float gradient_size = 0.3f; - + private readonly Bindable enabled = new Bindable(); private readonly Container boxes; + + private Bindable configEnabled; private HealthProcessor healthProcessor; public FailingLayer() From b8b334ca27d853b81bf86eeb93d29a91d3ca4f34 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 14 Apr 2020 15:21:56 +0900 Subject: [PATCH 1062/2376] Always unbind bindings --- osu.Game/Screens/Play/HUD/FailingLayer.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/FailingLayer.cs b/osu.Game/Screens/Play/HUD/FailingLayer.cs index cea85af112..cb8b5c1a9d 100644 --- a/osu.Game/Screens/Play/HUD/FailingLayer.cs +++ b/osu.Game/Screens/Play/HUD/FailingLayer.cs @@ -92,14 +92,13 @@ namespace osu.Game.Screens.Play.HUD if (configEnabled == null || healthProcessor == null) return; + enabled.UnbindBindings(); + // Don't display ever if the ruleset is not using a draining health display. if (healthProcessor is DrainingHealthProcessor) enabled.BindTo(configEnabled); else - { - enabled.UnbindBindings(); enabled.Value = false; - } } protected override void Update() From 59728ffebddcdb47c121d7eaf3b1f80687f0e63a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 14 Apr 2020 15:24:34 +0900 Subject: [PATCH 1063/2376] Fix up/improve test scene --- .../Visual/Gameplay/TestSceneFailingLayer.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs index d831ea1835..a95e806862 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs @@ -20,7 +20,12 @@ namespace osu.Game.Tests.Visual.Gameplay [SetUpSteps] public void SetUpSteps() { - AddStep("create layer", () => Child = layer = new FailingLayer()); + AddStep("create layer", () => + { + Child = layer = new FailingLayer(); + layer.BindHealthProcessor(new DrainingHealthProcessor(1)); + }); + AddStep("enable layer", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, true)); AddUntilStep("layer is visible", () => layer.IsPresent); } @@ -44,6 +49,7 @@ namespace osu.Game.Tests.Visual.Gameplay public void TestLayerDisabledViaConfig() { AddStep("disable layer", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, false)); + AddStep("set health to 0.10", () => layer.Current.Value = 0.1); AddUntilStep("layer is not visible", () => !layer.IsPresent); } @@ -51,6 +57,7 @@ namespace osu.Game.Tests.Visual.Gameplay public void TestLayerVisibilityWithAccumulatingProcessor() { AddStep("bind accumulating processor", () => layer.BindHealthProcessor(new AccumulatingHealthProcessor(1))); + AddStep("set health to 0.10", () => layer.Current.Value = 0.1); AddUntilStep("layer is not visible", () => !layer.IsPresent); } @@ -58,6 +65,7 @@ namespace osu.Game.Tests.Visual.Gameplay public void TestLayerVisibilityWithDrainingProcessor() { AddStep("bind accumulating processor", () => layer.BindHealthProcessor(new DrainingHealthProcessor(1))); + AddStep("set health to 0.10", () => layer.Current.Value = 0.1); AddWaitStep("wait for potential fade", 10); AddAssert("layer is still visible", () => layer.IsPresent); } From f3dbddd75ca735dfb75fb34efb15a2f14370a440 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 14 Apr 2020 15:52:38 +0900 Subject: [PATCH 1064/2376] Update bindings in LoadComplete() --- osu.Game/Screens/Play/HUD/FailingLayer.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/FailingLayer.cs b/osu.Game/Screens/Play/HUD/FailingLayer.cs index cb8b5c1a9d..a49aa89a7c 100644 --- a/osu.Game/Screens/Play/HUD/FailingLayer.cs +++ b/osu.Game/Screens/Play/HUD/FailingLayer.cs @@ -75,7 +75,11 @@ namespace osu.Game.Screens.Play.HUD configEnabled = config.GetBindable(OsuSetting.FadePlayfieldWhenHealthLow); enabled.BindValueChanged(e => this.FadeTo(e.NewValue ? 1 : 0, fade_time, Easing.OutQuint), true); + } + protected override void LoadComplete() + { + base.LoadComplete(); updateBindings(); } @@ -89,7 +93,7 @@ namespace osu.Game.Screens.Play.HUD private void updateBindings() { - if (configEnabled == null || healthProcessor == null) + if (LoadState < LoadState.Ready) return; enabled.UnbindBindings(); From 7f95418262d84096ea2afb38896d674170f9942e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Apr 2020 16:52:17 +0900 Subject: [PATCH 1065/2376] Fix osu!mania replays actuating incorrect keys when multiple stages are involved --- .../ManiaLegacyReplayTest.cs | 51 ++++++++++++ .../Replays/ManiaReplayFrame.cs | 83 ++++++++++++++++--- 2 files changed, 121 insertions(+), 13 deletions(-) create mode 100644 osu.Game.Rulesets.Mania.Tests/ManiaLegacyReplayTest.cs diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaLegacyReplayTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyReplayTest.cs new file mode 100644 index 0000000000..40bb83aece --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyReplayTest.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 NUnit.Framework; +using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Rulesets.Mania.Replays; + +namespace osu.Game.Rulesets.Mania.Tests +{ + [TestFixture] + public class ManiaLegacyReplayTest + { + [TestCase(ManiaAction.Key1)] + [TestCase(ManiaAction.Key1, ManiaAction.Key2)] + [TestCase(ManiaAction.Special1)] + [TestCase(ManiaAction.Key8)] + public void TestEncodeDecodeSingleStage(params ManiaAction[] actions) + { + var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 9 }); + + var frame = new ManiaReplayFrame(0, actions); + var legacyFrame = frame.ToLegacy(beatmap); + + var decodedFrame = new ManiaReplayFrame(); + decodedFrame.FromLegacy(legacyFrame, beatmap); + + Assert.That(decodedFrame.Actions, Is.EquivalentTo(frame.Actions)); + } + + [TestCase(ManiaAction.Key1)] + [TestCase(ManiaAction.Key1, ManiaAction.Key2)] + [TestCase(ManiaAction.Special1)] + [TestCase(ManiaAction.Special2)] + [TestCase(ManiaAction.Special1, ManiaAction.Special2)] + [TestCase(ManiaAction.Special1, ManiaAction.Key5)] + [TestCase(ManiaAction.Key8)] + public void TestEncodeDecodeDualStage(params ManiaAction[] actions) + { + var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 5 }); + beatmap.Stages.Add(new StageDefinition { Columns = 5 }); + + var frame = new ManiaReplayFrame(0, actions); + var legacyFrame = frame.ToLegacy(beatmap); + + var decodedFrame = new ManiaReplayFrame(); + decodedFrame.FromLegacy(legacyFrame, beatmap); + + Assert.That(decodedFrame.Actions, Is.EquivalentTo(frame.Actions)); + } + } +} diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs b/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs index 8c73c36e99..0059a78a44 100644 --- a/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs +++ b/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs @@ -1,8 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; -using System.Linq; using osu.Game.Beatmaps; using osu.Game.Replays.Legacy; using osu.Game.Rulesets.Mania.Beatmaps; @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Replays while (activeColumns > 0) { - var isSpecial = maniaBeatmap.Stages.First().IsSpecialColumn(counter); + bool isSpecial = isColumnAtIndexSpecial(maniaBeatmap, counter); if ((activeColumns & 1) > 0) Actions.Add(isSpecial ? specialAction : normalAction); @@ -58,33 +58,90 @@ namespace osu.Game.Rulesets.Mania.Replays int keys = 0; - var specialColumns = new List(); - - for (int i = 0; i < maniaBeatmap.TotalColumns; i++) - { - if (maniaBeatmap.Stages.First().IsSpecialColumn(i)) - specialColumns.Add(i); - } - foreach (var action in Actions) { switch (action) { case ManiaAction.Special1: - keys |= 1 << specialColumns[0]; + keys |= 1 << getSpecialColumnIndex(maniaBeatmap, 0); break; case ManiaAction.Special2: - keys |= 1 << specialColumns[1]; + keys |= 1 << getSpecialColumnIndex(maniaBeatmap, 1); break; default: - keys |= 1 << (action - ManiaAction.Key1); + // the index in lazer, which doesn't include special keys. + int nonSpecialKeyIndex = action - ManiaAction.Key1; + + int overallIndex = 0; + + // iterate to find the index including special keys. + while (true) + { + if (!isColumnAtIndexSpecial(maniaBeatmap, overallIndex)) + { + // found a non-special column we could use. + if (nonSpecialKeyIndex == 0) + break; + + // found a non-special column but not ours. + nonSpecialKeyIndex--; + } + + overallIndex++; + } + + keys |= 1 << overallIndex; break; } } return new LegacyReplayFrame(Time, keys, null, ReplayButtonState.None); } + + /// + /// Find the overall index (across all stages) for a specified special key. + /// + /// The beatmap. + /// The special key offset (0 is S1). + /// The overall index for the special column. + private int getSpecialColumnIndex(ManiaBeatmap maniaBeatmap, int specialOffset) + { + for (int i = 0; i < maniaBeatmap.TotalColumns; i++) + { + if (isColumnAtIndexSpecial(maniaBeatmap, i)) + { + if (specialOffset == 0) + return i; + + specialOffset--; + } + } + + throw new InvalidOperationException("Special key index too high"); + } + + /// + /// Check whether the column at an overall index (across all stages) is a special column. + /// + /// The beatmap. + /// The overall index to check. + /// + private bool isColumnAtIndexSpecial(ManiaBeatmap beatmap, int index) + { + foreach (var stage in beatmap.Stages) + { + for (int stageIndex = 0; stageIndex < stage.Columns; stageIndex++) + { + if (index == 0) + return stage.IsSpecialColumn(stageIndex); + + index--; + } + } + + return false; + } } } From 69352214637b9b5ef6f7e9d56da30da98455584a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 14 Apr 2020 21:05:07 +0900 Subject: [PATCH 1066/2376] Improve logic for CSB transfer --- osu.Game/Beatmaps/Formats/LegacyDecoder.cs | 8 ++++-- .../Objects/Legacy/ConvertHitObjectParser.cs | 28 ++++++++++++++++--- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index 90a5d0dcba..5b2b213322 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -8,6 +8,7 @@ using osu.Framework.Logging; using osu.Game.Audio; using osu.Game.Beatmaps.ControlPoints; using osu.Game.IO; +using osu.Game.Rulesets.Objects.Legacy; using osuTK.Graphics; namespace osu.Game.Beatmaps.Formats @@ -168,8 +169,11 @@ namespace osu.Game.Beatmaps.Formats { var baseInfo = base.ApplyTo(hitSampleInfo); - if (string.IsNullOrEmpty(baseInfo.Suffix) && CustomSampleBank > 0) - baseInfo.Suffix = CustomSampleBank.ToString(); + if (baseInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacy + && legacy.CustomSampleBank == 0) + { + legacy.CustomSampleBank = CustomSampleBank; + } return baseInfo; } diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 1dca4a5c02..95cbf3ab40 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -409,26 +409,46 @@ namespace osu.Game.Rulesets.Objects.Legacy public SampleBankInfo Clone() => (SampleBankInfo)MemberwiseClone(); } - private class LegacyHitSampleInfo : HitSampleInfo + internal class LegacyHitSampleInfo : HitSampleInfo { + private int customSampleBank; + public int CustomSampleBank { + get => customSampleBank; set { + customSampleBank = value; + + // A 0 custom sample bank should cause LegacyBeatmapSkin to always fall back to the user skin. This is done by giving a null suffix. if (value > 0) Suffix = value.ToString(); } } + + public override IEnumerable LookupNames + { + get + { + // The lookup should only contain the suffix for custom sample bank 2 and beyond. + // For custom sample bank 1 and 0, the lookup should not contain the suffix as only the lookup source (beatmap or user skin) is changed. + if (CustomSampleBank >= 2) + yield return $"{Bank}-{Name}{Suffix}"; + + yield return $"{Bank}-{Name}"; + } + } } - private class FileHitSampleInfo : HitSampleInfo + private class FileHitSampleInfo : LegacyHitSampleInfo { public string Filename; public FileHitSampleInfo() { - // Has no effect since LookupNames is overridden, however prompts LegacyBeatmapSkin to not fallback. - Suffix = "0"; + // Make sure that the LegacyBeatmapSkin does not fall back to the user skin. + // Note that this does not change the lookup names, as they are overridden locally. + CustomSampleBank = 1; } public override IEnumerable LookupNames => new[] From b29957798f38c28f3940649ecc13be0ca1ee5b8e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 14 Apr 2020 21:05:42 +0900 Subject: [PATCH 1067/2376] Fix no audiomanager in test scene working beatmap --- osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs | 6 ++++-- osu.Game/Tests/Visual/OsuTestScene.cs | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs b/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs index 6db34af20c..8f8afb87d4 100644 --- a/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs +++ b/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.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.Audio; using osu.Framework.Audio.Track; using osu.Framework.Graphics.Textures; using osu.Game.Beatmaps; @@ -18,8 +19,9 @@ namespace osu.Game.Tests.Beatmaps /// /// The beatmap. /// An optional storyboard. - public TestWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) - : base(beatmap.BeatmapInfo, null) + /// The . + public TestWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null, AudioManager audioManager = null) + : base(beatmap.BeatmapInfo, audioManager) { this.beatmap = beatmap; this.storyboard = storyboard; diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index d1d8059cb1..5dc8714c07 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -179,7 +179,7 @@ namespace osu.Game.Tests.Visual /// Audio manager. Required if a reference clock isn't provided. /// The length of the returned virtual track. public ClockBackedTestWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock referenceClock, AudioManager audio, double length = 60000) - : base(beatmap, storyboard) + : base(beatmap, storyboard, audio) { if (referenceClock != null) { From 00d564d29cafc4b4319e7165de2d5a5210c9d083 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 14 Apr 2020 21:05:54 +0900 Subject: [PATCH 1068/2376] Add tests --- .../Gameplay/TestSceneHitObjectSamples.cs | 344 ++++++++++++++++++ .../controlpoint-beatmap-custom-sample.osu | 7 + .../controlpoint-beatmap-sample.osu | 7 + .../controlpoint-skin-sample.osu | 7 + .../SampleLookups/file-beatmap-sample.osu | 4 + ...tobject-beatmap-custom-sample-override.osu | 7 + .../hitobject-beatmap-custom-sample.osu | 4 + .../hitobject-beatmap-sample.osu | 4 + .../SampleLookups/hitobject-skin-sample.osu | 4 + 9 files changed, 388 insertions(+) create mode 100644 osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs create mode 100644 osu.Game.Tests/Resources/SampleLookups/controlpoint-beatmap-custom-sample.osu create mode 100644 osu.Game.Tests/Resources/SampleLookups/controlpoint-beatmap-sample.osu create mode 100644 osu.Game.Tests/Resources/SampleLookups/controlpoint-skin-sample.osu create mode 100644 osu.Game.Tests/Resources/SampleLookups/file-beatmap-sample.osu create mode 100644 osu.Game.Tests/Resources/SampleLookups/hitobject-beatmap-custom-sample-override.osu create mode 100644 osu.Game.Tests/Resources/SampleLookups/hitobject-beatmap-custom-sample.osu create mode 100644 osu.Game.Tests/Resources/SampleLookups/hitobject-beatmap-sample.osu create mode 100644 osu.Game.Tests/Resources/SampleLookups/hitobject-skin-sample.osu diff --git a/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs new file mode 100644 index 0000000000..f80ea3ae88 --- /dev/null +++ b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs @@ -0,0 +1,344 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.IO.Stores; +using osu.Framework.Timing; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Formats; +using osu.Game.IO; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Osu; +using osu.Game.Skinning; +using osu.Game.Storyboards; +using osu.Game.Tests.Resources; +using osu.Game.Tests.Visual; +using osu.Game.Users; + +namespace osu.Game.Tests.Gameplay +{ + public class TestSceneHitObjectSamples : PlayerTestScene + { + private readonly SkinInfo userSkinInfo = new SkinInfo(); + + private readonly BeatmapInfo beatmapInfo = new BeatmapInfo + { + BeatmapSet = new BeatmapSetInfo(), + Metadata = new BeatmapMetadata + { + Author = User.SYSTEM_USER + } + }; + + private readonly TestResourceStore userSkinResourceStore = new TestResourceStore(); + private readonly TestResourceStore beatmapSkinResourceStore = new TestResourceStore(); + + protected override bool HasCustomSteps => true; + + public TestSceneHitObjectSamples() + : base(new OsuRuleset()) + { + } + + private SkinSourceDependencyContainer dependencies; + + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + => new DependencyContainer(dependencies = new SkinSourceDependencyContainer(base.CreateChildDependencies(parent))); + + /// + /// Tests that a hitobject which provides no custom sample set retrieves samples from the user skin. + /// + [Test] + public void TestDefaultSampleFromUserSkin() + { + const string expected_sample = "normal-hitnormal"; + + setupSkins(expected_sample, expected_sample); + + createTestWithBeatmap("hitobject-skin-sample.osu"); + + assertUserLookup(expected_sample); + } + + /// + /// Tests that a hitobject which provides a sample set of 1 retrieves samples from the beatmap skin. + /// + [Test] + public void TestDefaultSampleFromBeatmap() + { + const string expected_sample = "normal-hitnormal"; + + setupSkins(expected_sample, expected_sample); + + createTestWithBeatmap("hitobject-beatmap-sample.osu"); + + assertBeatmapLookup(expected_sample); + } + + /// + /// Tests that a hitobject which provides a sample set of 1 retrieves samples from the user skin when the beatmap does not contain the sample. + /// + [Test] + public void TestDefaultSampleFromUserSkinFallback() + { + const string expected_sample = "normal-hitnormal"; + + setupSkins(null, expected_sample); + + createTestWithBeatmap("hitobject-beatmap-sample.osu"); + + assertUserLookup(expected_sample); + } + + /// + /// Tests that a hitobject which provides a custom sample set of 2 retrieves the following samples from the beatmap skin: + /// normal-hitnormal2 + /// normal-hitnormal + /// + [TestCase("normal-hitnormal2")] + [TestCase("normal-hitnormal")] + public void TestDefaultCustomSampleFromBeatmap(string expectedSample) + { + setupSkins(expectedSample, expectedSample); + + createTestWithBeatmap("hitobject-beatmap-custom-sample.osu"); + + assertBeatmapLookup(expectedSample); + } + + /// + /// Tests that a hitobject which provides a custom sample set of 2 retrieves the following samples from the user skin when the beatmap does not contain the sample: + /// normal-hitnormal2 + /// normal-hitnormal + /// + [TestCase("normal-hitnormal2")] + [TestCase("normal-hitnormal")] + public void TestDefaultCustomSampleFromUserSkinFallback(string expectedSample) + { + setupSkins(string.Empty, expectedSample); + + createTestWithBeatmap("hitobject-beatmap-custom-sample.osu"); + + assertUserLookup(expectedSample); + } + + /// + /// Tests that a hitobject which provides a sample file retrieves the sample file from the beatmap skin. + /// + [Test] + public void TestFileSampleFromBeatmap() + { + const string expected_sample = "hit_1.wav"; + + setupSkins(expected_sample, expected_sample); + + createTestWithBeatmap("file-beatmap-sample.osu"); + + assertBeatmapLookup(expected_sample); + } + + /// + /// Tests that a default hitobject and control point causes . + /// + [Test] + public void TestControlPointSampleFromSkin() + { + const string expected_sample = "normal-hitnormal"; + + setupSkins(expected_sample, expected_sample); + + createTestWithBeatmap("controlpoint-skin-sample.osu"); + + assertUserLookup(expected_sample); + } + + /// + /// Tests that a control point that provides a custom sample set of 1 causes . + /// + [Test] + public void TestControlPointSampleFromBeatmap() + { + const string expected_sample = "normal-hitnormal"; + + setupSkins(expected_sample, expected_sample); + + createTestWithBeatmap("controlpoint-beatmap-sample.osu"); + + assertBeatmapLookup(expected_sample); + } + + /// + /// Tests that a control point that provides a custom sample of 2 causes . + /// + [TestCase("normal-hitnormal2")] + [TestCase("normal-hitnormal")] + public void TestControlPointCustomSampleFromBeatmap(string sampleName) + { + setupSkins(sampleName, sampleName); + + createTestWithBeatmap("controlpoint-beatmap-custom-sample.osu"); + + assertBeatmapLookup(sampleName); + } + + /// + /// Tests that a hitobject's custom sample overrides the control point's. + /// + [Test] + public void TestHitObjectCustomSampleOverride() + { + const string expected_sample = "normal-hitnormal3"; + + setupSkins(expected_sample, expected_sample); + + createTestWithBeatmap("hitobject-beatmap-custom-sample-override.osu"); + + assertBeatmapLookup(expected_sample); + } + + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => currentTestBeatmap; + + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) + => new TestWorkingBeatmap(beatmapInfo, beatmapSkinResourceStore, beatmap, storyboard, Clock, Audio); + + private IBeatmap currentTestBeatmap; + + private void createTestWithBeatmap(string filename) + { + CreateTest(() => + { + AddStep("clear performed lookups", () => + { + userSkinResourceStore.PerformedLookups.Clear(); + beatmapSkinResourceStore.PerformedLookups.Clear(); + }); + + AddStep($"load {filename}", () => + { + using (var reader = new LineBufferedReader(TestResources.OpenResource($"SampleLookups/{filename}"))) + currentTestBeatmap = Decoder.GetDecoder(reader).Decode(reader); + }); + }); + } + + private void setupSkins(string beatmapFile, string userFile) + { + AddStep("setup skins", () => + { + userSkinInfo.Files = new List + { + new SkinFileInfo + { + Filename = userFile, + FileInfo = new IO.FileInfo { Hash = userFile } + } + }; + + beatmapInfo.BeatmapSet.Files = new List + { + new BeatmapSetFileInfo + { + Filename = beatmapFile, + FileInfo = new IO.FileInfo { Hash = beatmapFile } + } + }; + + // Need to refresh the cached skin source to refresh the skin resource store. + dependencies.SkinSource = new SkinProvidingContainer(new LegacySkin(userSkinInfo, userSkinResourceStore, dependencies.Get())); + }); + } + + private void assertBeatmapLookup(string name) => AddAssert($"\"{name}\" looked up from beatmap skin", + () => !userSkinResourceStore.PerformedLookups.Contains(name) && beatmapSkinResourceStore.PerformedLookups.Contains(name)); + + private void assertUserLookup(string name) => AddAssert($"\"{name}\" looked up from user skin", + () => !beatmapSkinResourceStore.PerformedLookups.Contains(name) && userSkinResourceStore.PerformedLookups.Contains(name)); + + private class SkinSourceDependencyContainer : IReadOnlyDependencyContainer + { + public ISkinSource SkinSource; + + private readonly IReadOnlyDependencyContainer fallback; + + public SkinSourceDependencyContainer(IReadOnlyDependencyContainer fallback) + { + this.fallback = fallback; + } + + public object Get(Type type) + { + if (type == typeof(ISkinSource)) + return SkinSource; + + return fallback.Get(type); + } + + public object Get(Type type, CacheInfo info) + { + if (type == typeof(ISkinSource)) + return SkinSource; + + return fallback.Get(type); + } + + public void Inject(T instance) where T : class + { + // Never used directly + } + } + + private class TestResourceStore : IResourceStore + { + public readonly List PerformedLookups = new List(); + + public byte[] Get(string name) + { + markLookup(name); + return Array.Empty(); + } + + public Task GetAsync(string name) + { + markLookup(name); + return Task.FromResult(Array.Empty()); + } + + public Stream GetStream(string name) + { + markLookup(name); + return new MemoryStream(); + } + + private void markLookup(string name) => PerformedLookups.Add(name.Substring(name.LastIndexOf('/') + 1)); + + public IEnumerable GetAvailableResources() => Enumerable.Empty(); + + public void Dispose() + { + } + } + + private class TestWorkingBeatmap : ClockBackedTestWorkingBeatmap + { + private readonly BeatmapInfo skinBeatmapInfo; + private readonly IResourceStore resourceStore; + + public TestWorkingBeatmap(BeatmapInfo skinBeatmapInfo, IResourceStore resourceStore, IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock referenceClock, AudioManager audio, + double length = 60000) + : base(beatmap, storyboard, referenceClock, audio, length) + { + this.skinBeatmapInfo = skinBeatmapInfo; + this.resourceStore = resourceStore; + } + + protected override ISkin GetSkin() => new LegacyBeatmapSkin(skinBeatmapInfo, resourceStore, AudioManager); + } + } +} diff --git a/osu.Game.Tests/Resources/SampleLookups/controlpoint-beatmap-custom-sample.osu b/osu.Game.Tests/Resources/SampleLookups/controlpoint-beatmap-custom-sample.osu new file mode 100644 index 0000000000..91dbc6a60e --- /dev/null +++ b/osu.Game.Tests/Resources/SampleLookups/controlpoint-beatmap-custom-sample.osu @@ -0,0 +1,7 @@ +osu file format v14 + +[TimingPoints] +0,300,4,0,2,100,1,0 + +[HitObjects] +444,320,1000,5,0,0:0:0:0: \ No newline at end of file diff --git a/osu.Game.Tests/Resources/SampleLookups/controlpoint-beatmap-sample.osu b/osu.Game.Tests/Resources/SampleLookups/controlpoint-beatmap-sample.osu new file mode 100644 index 0000000000..3274820100 --- /dev/null +++ b/osu.Game.Tests/Resources/SampleLookups/controlpoint-beatmap-sample.osu @@ -0,0 +1,7 @@ +osu file format v14 + +[TimingPoints] +0,300,4,0,1,100,1,0 + +[HitObjects] +444,320,1000,5,0,0:0:0:0: \ No newline at end of file diff --git a/osu.Game.Tests/Resources/SampleLookups/controlpoint-skin-sample.osu b/osu.Game.Tests/Resources/SampleLookups/controlpoint-skin-sample.osu new file mode 100644 index 0000000000..c53ec465fb --- /dev/null +++ b/osu.Game.Tests/Resources/SampleLookups/controlpoint-skin-sample.osu @@ -0,0 +1,7 @@ +osu file format v14 + +[TimingPoints] +0,300,4,0,0,100,1,0 + +[HitObjects] +444,320,1000,5,0,0:0:0:0: \ No newline at end of file diff --git a/osu.Game.Tests/Resources/SampleLookups/file-beatmap-sample.osu b/osu.Game.Tests/Resources/SampleLookups/file-beatmap-sample.osu new file mode 100644 index 0000000000..65b5ea8707 --- /dev/null +++ b/osu.Game.Tests/Resources/SampleLookups/file-beatmap-sample.osu @@ -0,0 +1,4 @@ +osu file format v14 + +[HitObjects] +255,193,2170,1,0,0:0:0:0:hit_1.wav diff --git a/osu.Game.Tests/Resources/SampleLookups/hitobject-beatmap-custom-sample-override.osu b/osu.Game.Tests/Resources/SampleLookups/hitobject-beatmap-custom-sample-override.osu new file mode 100644 index 0000000000..13dc2faab1 --- /dev/null +++ b/osu.Game.Tests/Resources/SampleLookups/hitobject-beatmap-custom-sample-override.osu @@ -0,0 +1,7 @@ +osu file format v14 + +[TimingPoints] +0,300,4,0,2,100,1,0 + +[HitObjects] +444,320,1000,5,0,0:0:3:0: \ No newline at end of file diff --git a/osu.Game.Tests/Resources/SampleLookups/hitobject-beatmap-custom-sample.osu b/osu.Game.Tests/Resources/SampleLookups/hitobject-beatmap-custom-sample.osu new file mode 100644 index 0000000000..4ab672dbb0 --- /dev/null +++ b/osu.Game.Tests/Resources/SampleLookups/hitobject-beatmap-custom-sample.osu @@ -0,0 +1,4 @@ +osu file format v14 + +[HitObjects] +444,320,1000,5,0,0:0:2:0: \ No newline at end of file diff --git a/osu.Game.Tests/Resources/SampleLookups/hitobject-beatmap-sample.osu b/osu.Game.Tests/Resources/SampleLookups/hitobject-beatmap-sample.osu new file mode 100644 index 0000000000..33bc34949a --- /dev/null +++ b/osu.Game.Tests/Resources/SampleLookups/hitobject-beatmap-sample.osu @@ -0,0 +1,4 @@ +osu file format v14 + +[HitObjects] +444,320,1000,5,0,0:0:1:0: \ No newline at end of file diff --git a/osu.Game.Tests/Resources/SampleLookups/hitobject-skin-sample.osu b/osu.Game.Tests/Resources/SampleLookups/hitobject-skin-sample.osu new file mode 100644 index 0000000000..47f5b44c90 --- /dev/null +++ b/osu.Game.Tests/Resources/SampleLookups/hitobject-skin-sample.osu @@ -0,0 +1,4 @@ +osu file format v14 + +[HitObjects] +444,320,1000,5,0,0:0:0:0: \ No newline at end of file From 44981431c5470c457d1708ad9227e4ec1dd60566 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 14 Apr 2020 21:33:32 +0900 Subject: [PATCH 1069/2376] Remove suffix hackery --- .../Objects/Legacy/ConvertHitObjectParser.cs | 16 +--------------- osu.Game/Skinning/LegacyBeatmapSkin.cs | 5 +++-- 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 95cbf3ab40..9a60a0a75c 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -420,24 +420,10 @@ namespace osu.Game.Rulesets.Objects.Legacy { customSampleBank = value; - // A 0 custom sample bank should cause LegacyBeatmapSkin to always fall back to the user skin. This is done by giving a null suffix. - if (value > 0) + if (value >= 2) Suffix = value.ToString(); } } - - public override IEnumerable LookupNames - { - get - { - // The lookup should only contain the suffix for custom sample bank 2 and beyond. - // For custom sample bank 1 and 0, the lookup should not contain the suffix as only the lookup source (beatmap or user skin) is changed. - if (CustomSampleBank >= 2) - yield return $"{Bank}-{Name}{Suffix}"; - - yield return $"{Bank}-{Name}"; - } - } } private class FileHitSampleInfo : LegacyHitSampleInfo diff --git a/osu.Game/Skinning/LegacyBeatmapSkin.cs b/osu.Game/Skinning/LegacyBeatmapSkin.cs index c4636f46f5..21533e58cd 100644 --- a/osu.Game/Skinning/LegacyBeatmapSkin.cs +++ b/osu.Game/Skinning/LegacyBeatmapSkin.cs @@ -7,6 +7,7 @@ using osu.Framework.Bindables; using osu.Framework.IO.Stores; using osu.Game.Audio; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Objects.Legacy; namespace osu.Game.Skinning { @@ -37,9 +38,9 @@ namespace osu.Game.Skinning public override SampleChannel GetSample(ISampleInfo sampleInfo) { - if (sampleInfo is HitSampleInfo hsi && string.IsNullOrEmpty(hsi.Suffix)) + if (sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacy && legacy.CustomSampleBank == 0) { - // When no custom sample set is provided, always fall-back to the default samples. + // When no custom sample bank is provided, always fall-back to the default samples. return null; } From 64d44dedcd643cef2e971a968154e0fd17e2a6b2 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 14 Apr 2020 22:39:51 +0900 Subject: [PATCH 1070/2376] Make testscene headless --- osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs index f80ea3ae88..a8bd902117 100644 --- a/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs @@ -10,6 +10,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.IO.Stores; +using osu.Framework.Testing; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; @@ -24,6 +25,7 @@ using osu.Game.Users; namespace osu.Game.Tests.Gameplay { + [HeadlessTest] public class TestSceneHitObjectSamples : PlayerTestScene { private readonly SkinInfo userSkinInfo = new SkinInfo(); From 10486a0ad2b2aa1809d1d4f18d532cf545ebcb38 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 14 Apr 2020 23:10:14 +0900 Subject: [PATCH 1071/2376] Fix potential dependency-related issues --- osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs index a8bd902117..366437a771 100644 --- a/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs @@ -253,7 +253,7 @@ namespace osu.Game.Tests.Gameplay }; // Need to refresh the cached skin source to refresh the skin resource store. - dependencies.SkinSource = new SkinProvidingContainer(new LegacySkin(userSkinInfo, userSkinResourceStore, dependencies.Get())); + dependencies.SkinSource = new SkinProvidingContainer(new LegacySkin(userSkinInfo, userSkinResourceStore, Audio)); }); } @@ -287,7 +287,7 @@ namespace osu.Game.Tests.Gameplay if (type == typeof(ISkinSource)) return SkinSource; - return fallback.Get(type); + return fallback.Get(type, info); } public void Inject(T instance) where T : class From d47e414fb142e7aa504814494d496a3d08528a46 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Apr 2020 12:35:43 +0900 Subject: [PATCH 1072/2376] Apply review feedback (unroll inner loop / xml fixes) --- .../Replays/ManiaReplayFrame.cs | 35 +++++++++---------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs b/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs index 0059a78a44..da4b0c943c 100644 --- a/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs +++ b/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs @@ -74,22 +74,20 @@ namespace osu.Game.Rulesets.Mania.Replays // the index in lazer, which doesn't include special keys. int nonSpecialKeyIndex = action - ManiaAction.Key1; + // the index inclusive of special keys. int overallIndex = 0; // iterate to find the index including special keys. - while (true) + for (; overallIndex < maniaBeatmap.TotalColumns; overallIndex++) { - if (!isColumnAtIndexSpecial(maniaBeatmap, overallIndex)) - { - // found a non-special column we could use. - if (nonSpecialKeyIndex == 0) - break; - - // found a non-special column but not ours. - nonSpecialKeyIndex--; - } - - overallIndex++; + // skip over special columns. + if (isColumnAtIndexSpecial(maniaBeatmap, overallIndex)) + continue; + // found a non-special column to use. + if (nonSpecialKeyIndex == 0) + break; + // found a non-special column but not ours. + nonSpecialKeyIndex--; } keys |= 1 << overallIndex; @@ -127,21 +125,20 @@ namespace osu.Game.Rulesets.Mania.Replays /// /// The beatmap. /// The overall index to check. - /// private bool isColumnAtIndexSpecial(ManiaBeatmap beatmap, int index) { foreach (var stage in beatmap.Stages) { - for (int stageIndex = 0; stageIndex < stage.Columns; stageIndex++) + if (index >= stage.Columns) { - if (index == 0) - return stage.IsSpecialColumn(stageIndex); - - index--; + index -= stage.Columns; + continue; } + + return stage.IsSpecialColumn(index); } - return false; + throw new ArgumentException("Column index is too high.", nameof(index)); } } } From f4b5a17b650264a9b0fda00c1a59c94cfde58fec Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 15 Apr 2020 07:00:38 +0300 Subject: [PATCH 1073/2376] Fix typo in DrawableTaikoHitObject --- .../Objects/Drawables/DrawableTaikoHitObject.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs index 397888bb11..2f90f3b96c 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs @@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables /// /// Moves to a layer proxied above the playfield. - /// Does nothing is content is already proxied. + /// Does nothing if content is already proxied. /// protected void ProxyContent() { From e534d59c807ecd1350e2ced30c4595e49fc6af4a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 15 Apr 2020 13:08:15 +0900 Subject: [PATCH 1074/2376] Use another argument exception --- osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs b/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs index da4b0c943c..dbab54d1d0 100644 --- a/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs +++ b/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs @@ -117,7 +117,7 @@ namespace osu.Game.Rulesets.Mania.Replays } } - throw new InvalidOperationException("Special key index too high"); + throw new ArgumentException("Special key index is too high.", nameof(specialOffset)); } /// From 72707a9973f41f1961bd030c66d84a7627e4be81 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 15 Apr 2020 13:54:23 +0900 Subject: [PATCH 1075/2376] Fix OS-dependent substring --- osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs index 366437a771..f611f2717e 100644 --- a/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs @@ -318,7 +318,7 @@ namespace osu.Game.Tests.Gameplay return new MemoryStream(); } - private void markLookup(string name) => PerformedLookups.Add(name.Substring(name.LastIndexOf('/') + 1)); + private void markLookup(string name) => PerformedLookups.Add(name.Substring(name.LastIndexOf(Path.DirectorySeparatorChar) + 1)); public IEnumerable GetAvailableResources() => Enumerable.Empty(); From 019e777d7da8022678efa7e4a60026d6d6440be7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Apr 2020 16:01:49 +0900 Subject: [PATCH 1076/2376] Move taiko skinning tests to own namespace --- .../{ => Skinning}/TaikoSkinnableTestScene.cs | 2 +- .../{ => Skinning}/TestSceneDrawableHit.cs | 2 +- .../{ => Skinning}/TestSceneInputDrum.cs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) rename osu.Game.Rulesets.Taiko.Tests/{ => Skinning}/TaikoSkinnableTestScene.cs (92%) rename osu.Game.Rulesets.Taiko.Tests/{ => Skinning}/TestSceneDrawableHit.cs (97%) rename osu.Game.Rulesets.Taiko.Tests/{ => Skinning}/TestSceneInputDrum.cs (96%) diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoSkinnableTestScene.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TaikoSkinnableTestScene.cs similarity index 92% rename from osu.Game.Rulesets.Taiko.Tests/TaikoSkinnableTestScene.cs rename to osu.Game.Rulesets.Taiko.Tests/Skinning/TaikoSkinnableTestScene.cs index 6db2a6907f..161154b1a7 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoSkinnableTestScene.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TaikoSkinnableTestScene.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; using osu.Game.Rulesets.Taiko.Skinning; using osu.Game.Tests.Visual; -namespace osu.Game.Rulesets.Taiko.Tests +namespace osu.Game.Rulesets.Taiko.Tests.Skinning { public abstract class TaikoSkinnableTestScene : SkinnableTestScene { diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrawableHit.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs similarity index 97% rename from osu.Game.Rulesets.Taiko.Tests/TestSceneDrawableHit.cs rename to osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs index 301295253d..a3832b010c 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrawableHit.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs @@ -13,7 +13,7 @@ using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.Taiko.Skinning; -namespace osu.Game.Rulesets.Taiko.Tests +namespace osu.Game.Rulesets.Taiko.Tests.Skinning { [TestFixture] public class TestSceneDrawableHit : TaikoSkinnableTestScene diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneInputDrum.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneInputDrum.cs similarity index 96% rename from osu.Game.Rulesets.Taiko.Tests/TestSceneInputDrum.cs rename to osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneInputDrum.cs index 1928e9f66f..412027ca61 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneInputDrum.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneInputDrum.cs @@ -6,14 +6,14 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; -using osuTK; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Taiko.Skinning; using osu.Game.Rulesets.Taiko.UI; +using osuTK; -namespace osu.Game.Rulesets.Taiko.Tests +namespace osu.Game.Rulesets.Taiko.Tests.Skinning { [TestFixture] public class TestSceneInputDrum : TaikoSkinnableTestScene From 102c1d9095d4189731f6d7ac547abd0c0e5b7527 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 15 Apr 2020 15:50:19 +0900 Subject: [PATCH 1077/2376] Add disabled state to menu items --- .../Visual/UserInterface/TestSceneOsuMenu.cs | 91 +++++++++++++++++++ .../UserInterface/DrawableOsuMenuItem.cs | 26 +++++- .../Graphics/UserInterface/OsuMenuItem.cs | 6 ++ 3 files changed, 118 insertions(+), 5 deletions(-) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneOsuMenu.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuMenu.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuMenu.cs new file mode 100644 index 0000000000..cdda1969ca --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuMenu.cs @@ -0,0 +1,91 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Testing; +using osu.Game.Graphics.UserInterface; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public class TestSceneOsuMenu : OsuManualInputManagerTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(OsuMenu), + typeof(DrawableOsuMenuItem) + }; + + private OsuMenu menu; + private bool actionPeformed; + + [SetUp] + public void Setup() => Schedule(() => + { + actionPeformed = false; + + Child = menu = new OsuMenu(Direction.Vertical, true) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Items = new[] + { + new OsuMenuItem("standard", MenuItemType.Standard, performAction), + new OsuMenuItem("highlighted", MenuItemType.Highlighted, performAction), + new OsuMenuItem("destructive", MenuItemType.Destructive, performAction), + } + }; + }); + + [Test] + public void TestClickEnabledMenuItem() + { + AddStep("move to first menu item", () => InputManager.MoveMouseTo(menu.ChildrenOfType().First())); + AddStep("click", () => InputManager.Click(MouseButton.Left)); + + AddAssert("action performed", () => actionPeformed); + } + + [Test] + public void TestDisableMenuItemsAndClick() + { + AddStep("disable menu items", () => + { + foreach (var item in menu.Items) + ((OsuMenuItem)item).Enabled.Value = false; + }); + + AddStep("move to first menu item", () => InputManager.MoveMouseTo(menu.ChildrenOfType().First())); + AddStep("click", () => InputManager.Click(MouseButton.Left)); + + AddAssert("action not performed", () => !actionPeformed); + } + + [Test] + public void TestEnableMenuItemsAndClick() + { + AddStep("disable menu items", () => + { + foreach (var item in menu.Items) + ((OsuMenuItem)item).Enabled.Value = false; + }); + + AddStep("enable menu items", () => + { + foreach (var item in menu.Items) + ((OsuMenuItem)item).Enabled.Value = true; + }); + + AddStep("move to first menu item", () => InputManager.MoveMouseTo(menu.ChildrenOfType().First())); + AddStep("click", () => InputManager.Click(MouseButton.Left)); + + AddAssert("action performed", () => actionPeformed); + } + + private void performAction() => actionPeformed = true; + } +} diff --git a/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs b/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs index a3ca851341..abaae7b43c 100644 --- a/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs +++ b/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs @@ -42,6 +42,8 @@ namespace osu.Game.Graphics.UserInterface BackgroundColourHover = Color4Extensions.FromHex(@"172023"); updateTextColour(); + + Item.Action.BindDisabledChanged(_ => updateState(), true); } private void updateTextColour() @@ -65,19 +67,33 @@ namespace osu.Game.Graphics.UserInterface protected override bool OnHover(HoverEvent e) { - sampleHover.Play(); - text.BoldText.FadeIn(transition_length, Easing.OutQuint); - text.NormalText.FadeOut(transition_length, Easing.OutQuint); + updateState(); return base.OnHover(e); } protected override void OnHoverLost(HoverLostEvent e) { - text.BoldText.FadeOut(transition_length, Easing.OutQuint); - text.NormalText.FadeIn(transition_length, Easing.OutQuint); + updateState(); base.OnHoverLost(e); } + private void updateState() + { + Alpha = Item.Action.Disabled ? 0.2f : 1; + + if (IsHovered && !Item.Action.Disabled) + { + sampleHover.Play(); + text.BoldText.FadeIn(transition_length, Easing.OutQuint); + text.NormalText.FadeOut(transition_length, Easing.OutQuint); + } + else + { + text.BoldText.FadeOut(transition_length, Easing.OutQuint); + text.NormalText.FadeIn(transition_length, Easing.OutQuint); + } + } + protected override bool OnClick(ClickEvent e) { sampleClick.Play(); diff --git a/osu.Game/Graphics/UserInterface/OsuMenuItem.cs b/osu.Game/Graphics/UserInterface/OsuMenuItem.cs index 0fe41937ce..36122ca0b2 100644 --- a/osu.Game/Graphics/UserInterface/OsuMenuItem.cs +++ b/osu.Game/Graphics/UserInterface/OsuMenuItem.cs @@ -2,12 +2,15 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Bindables; using osu.Framework.Graphics.UserInterface; namespace osu.Game.Graphics.UserInterface { public class OsuMenuItem : MenuItem { + public readonly Bindable Enabled = new Bindable(true); + public readonly MenuItemType Type; public OsuMenuItem(string text, MenuItemType type = MenuItemType.Standard) @@ -19,6 +22,9 @@ namespace osu.Game.Graphics.UserInterface : base(text, action) { Type = type; + + Enabled.BindValueChanged(enabled => Action.Disabled = !enabled.NewValue); + Action.BindDisabledChanged(disabled => Enabled.Value = !disabled); } } } From e8c955ed9b0a253ba2bfa6bd1e1af737a4e34440 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 15 Apr 2020 15:50:43 +0900 Subject: [PATCH 1078/2376] Add CanUndo/CanRedo bindables --- .../Editor/EditorChangeHandlerTest.cs | 25 +++++++++++-------- osu.Game/Screens/Edit/EditorChangeHandler.cs | 17 ++++++++++--- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Editor/EditorChangeHandlerTest.cs b/osu.Game.Tests/Editor/EditorChangeHandlerTest.cs index ef16976130..9613f250c4 100644 --- a/osu.Game.Tests/Editor/EditorChangeHandlerTest.cs +++ b/osu.Game.Tests/Editor/EditorChangeHandlerTest.cs @@ -15,15 +15,18 @@ namespace osu.Game.Tests.Editor { var handler = new EditorChangeHandler(new EditorBeatmap(new Beatmap())); - Assert.That(handler.HasUndoState, Is.False); + Assert.That(handler.CanUndo.Value, Is.False); + Assert.That(handler.CanRedo.Value, Is.False); handler.SaveState(); - Assert.That(handler.HasUndoState, Is.True); + Assert.That(handler.CanUndo.Value, Is.True); + Assert.That(handler.CanRedo.Value, Is.False); handler.RestoreState(-1); - Assert.That(handler.HasUndoState, Is.False); + Assert.That(handler.CanUndo.Value, Is.False); + Assert.That(handler.CanRedo.Value, Is.True); } [Test] @@ -31,20 +34,20 @@ namespace osu.Game.Tests.Editor { var handler = new EditorChangeHandler(new EditorBeatmap(new Beatmap())); - Assert.That(handler.HasUndoState, Is.False); + Assert.That(handler.CanUndo.Value, Is.False); for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES; i++) handler.SaveState(); - Assert.That(handler.HasUndoState, Is.True); + Assert.That(handler.CanUndo.Value, Is.True); for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES; i++) { - Assert.That(handler.HasUndoState, Is.True); + Assert.That(handler.CanUndo.Value, Is.True); handler.RestoreState(-1); } - Assert.That(handler.HasUndoState, Is.False); + Assert.That(handler.CanUndo.Value, Is.False); } [Test] @@ -52,20 +55,20 @@ namespace osu.Game.Tests.Editor { var handler = new EditorChangeHandler(new EditorBeatmap(new Beatmap())); - Assert.That(handler.HasUndoState, Is.False); + Assert.That(handler.CanUndo.Value, Is.False); for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES * 2; i++) handler.SaveState(); - Assert.That(handler.HasUndoState, Is.True); + Assert.That(handler.CanUndo.Value, Is.True); for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES; i++) { - Assert.That(handler.HasUndoState, Is.True); + Assert.That(handler.CanUndo.Value, Is.True); handler.RestoreState(-1); } - Assert.That(handler.HasUndoState, Is.False); + Assert.That(handler.CanUndo.Value, Is.False); } } } diff --git a/osu.Game/Screens/Edit/EditorChangeHandler.cs b/osu.Game/Screens/Edit/EditorChangeHandler.cs index a8204715cd..1553c2d2ef 100644 --- a/osu.Game/Screens/Edit/EditorChangeHandler.cs +++ b/osu.Game/Screens/Edit/EditorChangeHandler.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Text; +using osu.Framework.Bindables; using osu.Game.Beatmaps.Formats; using osu.Game.Rulesets.Objects; @@ -15,8 +16,10 @@ namespace osu.Game.Screens.Edit /// public class EditorChangeHandler : IEditorChangeHandler { - private readonly LegacyEditorBeatmapPatcher patcher; + public readonly Bindable CanUndo = new Bindable(); + public readonly Bindable CanRedo = new Bindable(); + private readonly LegacyEditorBeatmapPatcher patcher; private readonly List savedStates = new List(); private int currentState = -1; @@ -45,8 +48,6 @@ namespace osu.Game.Screens.Edit SaveState(); } - public bool HasUndoState => currentState > 0; - private void hitObjectAdded(HitObject obj) => SaveState(); private void hitObjectRemoved(HitObject obj) => SaveState(); @@ -90,6 +91,8 @@ namespace osu.Game.Screens.Edit } currentState = savedStates.Count - 1; + + updateBindables(); } /// @@ -114,6 +117,14 @@ namespace osu.Game.Screens.Edit currentState = newState; isRestoring = false; + + updateBindables(); + } + + private void updateBindables() + { + CanUndo.Value = savedStates.Count > 0 && currentState > 0; + CanRedo.Value = currentState < savedStates.Count - 1; } } } From ce21cfbb035b16bb42b473614edbe372b5ace04b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 15 Apr 2020 16:17:34 +0900 Subject: [PATCH 1079/2376] Use bindables in menu items --- osu.Game/Screens/Edit/Editor.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 14a227eb07..ad17498d93 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -155,8 +155,8 @@ namespace osu.Game.Screens.Edit { Items = new[] { - new EditorMenuItem("Undo", MenuItemType.Standard, undo), - new EditorMenuItem("Redo", MenuItemType.Standard, redo) + new EditorMenuItem("Undo", MenuItemType.Standard, undo) { Enabled = { BindTarget = changeHandler.CanUndo } }, + new EditorMenuItem("Redo", MenuItemType.Standard, redo) { Enabled = { BindTarget = changeHandler.CanRedo } } } } } From 18c28390ef6f441acd11acd318cffa057331fa4e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Apr 2020 16:29:39 +0900 Subject: [PATCH 1080/2376] Setup drumroll testing --- .../Skinning/TestSceneDrawableDrumRoll.cs | 84 +++++++++++++++++++ .../Tests/Visual/ScrollingTestContainer.cs | 4 +- 2 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRoll.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRoll.cs new file mode 100644 index 0000000000..388be5bbc4 --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRoll.cs @@ -0,0 +1,84 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Objects.Drawables; +using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Taiko.Tests.Skinning +{ + [TestFixture] + public class TestSceneDrawableDrumRoll : TaikoSkinnableTestScene + { + public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] + { + typeof(DrawableDrumRoll), + typeof(DrawableDrumRollTick), + }).ToList(); + + [Cached(typeof(IScrollingInfo))] + private ScrollingTestContainer.TestScrollingInfo info = new ScrollingTestContainer.TestScrollingInfo + { + Direction = { Value = ScrollingDirection.Left }, + TimeRange = { Value = 5000 }, + }; + + [BackgroundDependencyLoader] + private void load() + { + AddStep("Drum roll", () => SetContents(() => + { + var hoc = new ScrollingHitObjectContainer(); + + hoc.Add(new DrawableDrumRoll(createDrumRollAtCurrentTime()) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 500, + }); + + return hoc; + })); + + AddStep("Drum roll (strong)", () => SetContents(() => + { + var hoc = new ScrollingHitObjectContainer(); + + hoc.Add(new DrawableDrumRoll(createDrumRollAtCurrentTime(true)) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 500, + }); + + return hoc; + })); + } + + private DrumRoll createDrumRollAtCurrentTime(bool strong = false) + { + var drumroll = new DrumRoll + { + IsStrong = strong, + StartTime = Time.Current + 1000, + Duration = 4000, + }; + + var cpi = new ControlPointInfo(); + cpi.Add(0, new TimingControlPoint { BeatLength = 500 }); + + drumroll.ApplyDefaults(cpi, new BeatmapDifficulty()); + + return drumroll; + } + } +} diff --git a/osu.Game/Tests/Visual/ScrollingTestContainer.cs b/osu.Game/Tests/Visual/ScrollingTestContainer.cs index 18326a78ad..3b741fcf1d 100644 --- a/osu.Game/Tests/Visual/ScrollingTestContainer.cs +++ b/osu.Game/Tests/Visual/ScrollingTestContainer.cs @@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual public void Flip() => scrollingInfo.Direction.Value = scrollingInfo.Direction.Value == ScrollingDirection.Up ? ScrollingDirection.Down : ScrollingDirection.Up; - private class TestScrollingInfo : IScrollingInfo + public class TestScrollingInfo : IScrollingInfo { public readonly Bindable Direction = new Bindable(); IBindable IScrollingInfo.Direction => Direction; @@ -54,7 +54,7 @@ namespace osu.Game.Tests.Visual IScrollAlgorithm IScrollingInfo.Algorithm => Algorithm; } - private class TestScrollAlgorithm : IScrollAlgorithm + public class TestScrollAlgorithm : IScrollAlgorithm { public readonly SortedList ControlPoints = new SortedList(); From eb165840cb4e202846dfbc11b2da997af6a814fd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Apr 2020 16:54:50 +0900 Subject: [PATCH 1081/2376] Add remaining taiko hitobject skinnables and expose as SkinnableDrawable for safety --- .../Objects/Drawables/DrawableCentreHit.cs | 3 +-- .../Objects/Drawables/DrawableDrumRoll.cs | 27 +++++++++++++------ .../Objects/Drawables/DrawableDrumRollTick.cs | 8 +++--- .../Objects/Drawables/DrawableHit.cs | 2 +- .../Objects/Drawables/DrawableRimHit.cs | 3 +-- .../Objects/Drawables/DrawableSwell.cs | 16 ++++++----- .../Objects/Drawables/DrawableSwellTick.cs | 5 ++-- .../Drawables/DrawableTaikoHitObject.cs | 5 ++-- .../Objects/Drawables/Pieces/CirclePiece.cs | 3 ++- .../TaikoSkinComponents.cs | 5 +++- 10 files changed, 46 insertions(+), 31 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs index f3f4c59a62..a87da44415 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; using osu.Game.Skinning; @@ -16,7 +15,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { } - protected override CompositeDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.CentreHit), + protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.CentreHit), _ => new CentreHitCirclePiece(), confineMode: ConfineMode.ScaleToFit); } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index 0627eb95fd..5c3433cbf4 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -14,6 +14,7 @@ using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { @@ -29,25 +30,29 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables /// private int rollingHits; - private readonly Container tickContainer; + private Container tickContainer; private Color4 colourIdle; private Color4 colourEngaged; - private ElongatedCirclePiece elongatedPiece; - public DrawableDrumRoll(DrumRoll drumRoll) : base(drumRoll) { RelativeSizeAxes = Axes.Y; - elongatedPiece.Add(tickContainer = new Container { RelativeSizeAxes = Axes.Both }); } [BackgroundDependencyLoader] private void load(OsuColour colours) { - elongatedPiece.AccentColour = colourIdle = colours.YellowDark; + colourIdle = colours.YellowDark; colourEngaged = colours.YellowDarker; + + updateColour(); + + ((Container)MainPiece.Drawable).Add(tickContainer = new Container { RelativeSizeAxes = Axes.Both }); + + if (MainPiece.Drawable is IHasAccentColour accentMain) + accentMain.AccentColour = colourIdle; } protected override void LoadComplete() @@ -86,7 +91,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables return base.CreateNestedHitObject(hitObject); } - protected override CompositeDrawable CreateMainPiece() => elongatedPiece = new ElongatedCirclePiece(); + protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.DrumRollBody), + _ => new ElongatedCirclePiece()); public override bool OnPressed(TaikoAction action) => false; @@ -102,8 +108,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables rollingHits = Math.Clamp(rollingHits, 0, rolling_hits_for_engaged_colour); - Color4 newColour = Interpolation.ValueAt((float)rollingHits / rolling_hits_for_engaged_colour, colourIdle, colourEngaged, 0, 1); - (MainPiece as IHasAccentColour)?.FadeAccent(newColour, 100); + updateColour(); } protected override void CheckForResult(bool userTriggered, double timeOffset) @@ -151,5 +156,11 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables public override bool OnPressed(TaikoAction action) => false; } + + private void updateColour() + { + Color4 newColour = Interpolation.ValueAt((float)rollingHits / rolling_hits_for_engaged_colour, colourIdle, colourEngaged, 0, 1); + (MainPiece.Drawable as IHasAccentColour)?.FadeAccent(newColour, 100); + } } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs index fea3eea6a9..e11e019826 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs @@ -3,10 +3,10 @@ using System; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { @@ -20,10 +20,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables public override bool DisplayResult => false; - protected override CompositeDrawable CreateMainPiece() => new TickPiece - { - Filled = HitObject.FirstTick - }; + protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.DrumRollTick), + _ => new TickPiece()); protected override void CheckForResult(bool userTriggered, double timeOffset) { diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index 85dfc8d5e0..9333e5f144 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -116,7 +116,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables // If we're far enough away from the left stage, we should bring outselves in front of it ProxyContent(); - var flash = (MainPiece as CirclePiece)?.FlashBox; + var flash = (MainPiece.Drawable as CirclePiece)?.FlashBox; flash?.FadeTo(0.9f).FadeOut(300); const float gravity_time = 300; diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs index 463a8b746c..f767403c65 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; using osu.Game.Skinning; @@ -16,7 +15,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { } - protected override CompositeDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.RimHit), + protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.RimHit), _ => new RimHitCirclePiece(), confineMode: ConfineMode.ScaleToFit); } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 3a2e44038f..32f7acadc8 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -14,6 +14,7 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { @@ -114,12 +115,13 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables targetRing.BorderColour = colours.YellowDark.Opacity(0.25f); } - protected override CompositeDrawable CreateMainPiece() => new SwellCirclePiece - { - // to allow for rotation transform - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }; + protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.Swell), + _ => new SwellCirclePiece + { + // to allow for rotation transform + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); protected override void LoadComplete() { @@ -184,7 +186,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables .Then() .FadeTo(completion / 8, 2000, Easing.OutQuint); - MainPiece.RotateTo((float)(completion * HitObject.Duration / 8), 4000, Easing.OutQuint); + MainPiece.Drawable.RotateTo((float)(completion * HitObject.Duration / 8), 4000, Easing.OutQuint); expandingRing.ScaleTo(1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 260, Easing.OutQuint); diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs index 5a954addfb..1685576f0d 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs @@ -2,9 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { @@ -31,6 +31,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables public override bool OnPressed(TaikoAction action) => false; - protected override CompositeDrawable CreateMainPiece() => new TickPiece(); + protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.DrumRollTick), + _ => new TickPiece()); } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs index 2f90f3b96c..1be04f1760 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs @@ -11,6 +11,7 @@ using System.Collections.Generic; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; using osu.Game.Rulesets.Objects; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { @@ -115,7 +116,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables public new TObject HitObject; protected readonly Vector2 BaseSize; - protected readonly CompositeDrawable MainPiece; + protected readonly SkinnableDrawable MainPiece; private readonly Container strongHitContainer; @@ -167,7 +168,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables // Normal and clap samples are handled by the drum protected override IEnumerable GetSamples() => HitObject.Samples.Where(s => s.Name != HitSampleInfo.HIT_NORMAL && s.Name != HitSampleInfo.HIT_CLAP); - protected abstract CompositeDrawable CreateMainPiece(); + protected abstract SkinnableDrawable CreateMainPiece(); /// /// Creates the handler for this 's . diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs index 6ca77e666d..b5471e6976 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs @@ -10,6 +10,7 @@ using osuTK.Graphics; using osu.Game.Beatmaps.ControlPoints; using osu.Framework.Audio.Track; using osu.Framework.Graphics.Effects; +using osu.Game.Graphics; using osu.Game.Graphics.Containers; namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces @@ -21,7 +22,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces /// for a usage example. /// /// - public abstract class CirclePiece : BeatSyncedContainer + public abstract class CirclePiece : BeatSyncedContainer, IHasAccentColour { public const float SYMBOL_SIZE = 0.45f; public const float SYMBOL_BORDER = 8; diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs index babf21b6a9..156ea71c16 100644 --- a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs +++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs @@ -7,6 +7,9 @@ namespace osu.Game.Rulesets.Taiko { InputDrum, CentreHit, - RimHit + RimHit, + DrumRollBody, + DrumRollTick, + Swell } } From 45d88b70f8de3cf146a1e28e99d07dabf6d511ce Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Apr 2020 17:50:37 +0900 Subject: [PATCH 1082/2376] Split out base logic from LegacyHit into LegacyCirclePiece --- .../Skinning/TestSceneDrawableHit.cs | 1 + .../Skinning/LegacyCirclePiece.cs | 96 +++++++++++++++++++ osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs | 69 +------------ 3 files changed, 99 insertions(+), 67 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/Skinning/LegacyCirclePiece.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs index a3832b010c..6d6da1fb5b 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs @@ -24,6 +24,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning typeof(DrawableCentreHit), typeof(DrawableRimHit), typeof(LegacyHit), + typeof(LegacyCirclePiece), }).ToList(); [BackgroundDependencyLoader] diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyCirclePiece.cs new file mode 100644 index 0000000000..bfcf268c3d --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyCirclePiece.cs @@ -0,0 +1,96 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Animations; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Taiko.Objects.Drawables; +using osu.Game.Skinning; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Taiko.Skinning +{ + public class LegacyCirclePiece : CompositeDrawable, IHasAccentColour + { + private Drawable backgroundLayer; + + public LegacyCirclePiece() + { + RelativeSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load(ISkinSource skin, DrawableHitObject drawableHitObject) + { + Drawable getDrawableFor(string lookup) + { + const string normal_hit = "taikohit"; + const string big_hit = "taikobig"; + + string prefix = ((drawableHitObject as DrawableTaikoHitObject)?.HitObject.IsStrong ?? false) ? big_hit : normal_hit; + + return skin.GetAnimation($"{prefix}{lookup}", true, false) ?? + // fallback to regular size if "big" version doesn't exist. + skin.GetAnimation($"{normal_hit}{lookup}", true, false); + } + + // backgroundLayer is guaranteed to exist due to the pre-check in TaikoLegacySkinTransformer. + AddInternal(backgroundLayer = getDrawableFor("circle")); + + var foregroundLayer = getDrawableFor("circleoverlay"); + if (foregroundLayer != null) + AddInternal(foregroundLayer); + + // Animations in taiko skins are used in a custom way (>150 combo and animating in time with beat). + // For now just stop at first frame for sanity. + foreach (var c in InternalChildren) + { + (c as IFramedAnimation)?.Stop(); + + c.Anchor = Anchor.Centre; + c.Origin = Anchor.Centre; + } + } + + protected override void LoadComplete() + { + base.LoadComplete(); + updateAccentColour(); + } + + protected override void Update() + { + base.Update(); + + // Not all skins (including the default osu-stable) have similar sizes for "hitcircle" and "hitcircleoverlay". + // This ensures they are scaled relative to each other but also match the expected DrawableHit size. + foreach (var c in InternalChildren) + c.Scale = new Vector2(DrawHeight / 128); + } + + private Color4 accentColour; + + public Color4 AccentColour + { + get => accentColour; + set + { + if (value == accentColour) + return; + + accentColour = value; + if (IsLoaded) + updateAccentColour(); + } + } + + private void updateAccentColour() + { + backgroundLayer.Colour = accentColour; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs index 80bf97936d..656728f6e4 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs @@ -2,90 +2,25 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Animations; -using osu.Framework.Graphics.Containers; -using osu.Game.Graphics; -using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Taiko.Objects.Drawables; -using osu.Game.Skinning; -using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Taiko.Skinning { - public class LegacyHit : CompositeDrawable, IHasAccentColour + public class LegacyHit : LegacyCirclePiece { private readonly TaikoSkinComponents component; - private Drawable backgroundLayer; - public LegacyHit(TaikoSkinComponents component) { this.component = component; - - RelativeSizeAxes = Axes.Both; } [BackgroundDependencyLoader] - private void load(ISkinSource skin, DrawableHitObject drawableHitObject) + private void load() { - Drawable getDrawableFor(string lookup) - { - const string normal_hit = "taikohit"; - const string big_hit = "taikobig"; - - string prefix = ((drawableHitObject as DrawableTaikoHitObject)?.HitObject.IsStrong ?? false) ? big_hit : normal_hit; - - return skin.GetAnimation($"{prefix}{lookup}", true, false) ?? - // fallback to regular size if "big" version doesn't exist. - skin.GetAnimation($"{normal_hit}{lookup}", true, false); - } - - // backgroundLayer is guaranteed to exist due to the pre-check in TaikoLegacySkinTransformer. - AddInternal(backgroundLayer = getDrawableFor("circle")); - - var foregroundLayer = getDrawableFor("circleoverlay"); - if (foregroundLayer != null) - AddInternal(foregroundLayer); - - // Animations in taiko skins are used in a custom way (>150 combo and animating in time with beat). - // For now just stop at first frame for sanity. - foreach (var c in InternalChildren) - { - (c as IFramedAnimation)?.Stop(); - - c.Anchor = Anchor.Centre; - c.Origin = Anchor.Centre; - } - AccentColour = component == TaikoSkinComponents.CentreHit ? new Color4(235, 69, 44, 255) : new Color4(67, 142, 172, 255); } - - protected override void Update() - { - base.Update(); - - // Not all skins (including the default osu-stable) have similar sizes for "hitcircle" and "hitcircleoverlay". - // This ensures they are scaled relative to each other but also match the expected DrawableHit size. - foreach (var c in InternalChildren) - c.Scale = new Vector2(DrawWidth / 128); - } - - private Color4 accentColour; - - public Color4 AccentColour - { - get => accentColour; - set - { - if (value == accentColour) - return; - - backgroundLayer.Colour = accentColour = value; - } - } } } From 313741799468b34fb5abb903b9513b9bbafbe4a0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Apr 2020 17:50:57 +0900 Subject: [PATCH 1083/2376] Add drumroll skinning --- .../Skinning/TestSceneDrawableDrumRoll.cs | 2 + .../Skinning/LegacyDrumRoll.cs | 110 ++++++++++++++++++ .../Skinning/TaikoLegacySkinTransformer.cs | 6 + 3 files changed, 118 insertions(+) create mode 100644 osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRoll.cs index 388be5bbc4..554894bf68 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRoll.cs @@ -11,6 +11,7 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects.Drawables; +using osu.Game.Rulesets.Taiko.Skinning; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Tests.Visual; @@ -23,6 +24,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning { typeof(DrawableDrumRoll), typeof(DrawableDrumRollTick), + typeof(LegacyDrumRoll), }).ToList(); [Cached(typeof(IScrollingInfo))] diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs new file mode 100644 index 0000000000..d3579fbbbd --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs @@ -0,0 +1,110 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Skinning; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Taiko.Skinning +{ + public class LegacyDrumRoll : Container, IHasAccentColour + { + protected override Container Content => content; + + private Container content; + + private LegacyCirclePiece headCircle; + + private Sprite body; + + private Sprite end; + + public LegacyDrumRoll() + { + RelativeSizeAxes = Axes.Y; + } + + [BackgroundDependencyLoader] + private void load(ISkinSource skin) + { + InternalChildren = new Drawable[] + { + content = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + headCircle = new LegacyCirclePiece + { + Depth = float.MinValue, + RelativeSizeAxes = Axes.Y, + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + }, + body = new Sprite + { + RelativeSizeAxes = Axes.Both, + Texture = skin.GetTexture("taiko-roll-middle"), + }, + end = new Sprite + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Both, + Texture = skin.GetTexture("taiko-roll-end"), + FillMode = FillMode.Fit, + }, + }, + }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + updateAccentColour(); + } + + protected override void Update() + { + base.Update(); + + var padding = Content.DrawHeight * Content.Width / 2; + + Content.Padding = new MarginPadding + { + Left = padding, + Right = padding, + }; + + Width = Parent.DrawSize.X + DrawHeight; + } + + private Color4 accentColour; + + public Color4 AccentColour + { + get => accentColour; + set + { + if (value == accentColour) + return; + + accentColour = value; + if (IsLoaded) + updateAccentColour(); + } + } + + private void updateAccentColour() + { + headCircle.AccentColour = accentColour; + body.Colour = accentColour; + end.Colour = accentColour; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs index 9cd625c35f..86e3945021 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs @@ -27,6 +27,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning switch (taikoComponent.Component) { + case TaikoSkinComponents.DrumRollBody: + if (GetTexture("taiko-roll-middle") != null) + return new LegacyDrumRoll(); + + return null; + case TaikoSkinComponents.InputDrum: if (GetTexture("taiko-bar-left") != null) return new LegacyInputDrum(); From 07632cd1e53a9e297861b5f152f5e74e7d2552bb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Apr 2020 18:44:12 +0900 Subject: [PATCH 1084/2376] Remove unnecessary container logic --- .../Objects/Drawables/DrawableDrumRoll.cs | 16 ++--- .../Drawables/Pieces/ElongatedCirclePiece.cs | 17 +++-- .../Skinning/LegacyDrumRoll.cs | 65 ++++++------------- 3 files changed, 35 insertions(+), 63 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index 5c3433cbf4..0a6f462607 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables /// private int rollingHits; - private Container tickContainer; + private Container tickContainer; private Color4 colourIdle; private Color4 colourEngaged; @@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables updateColour(); - ((Container)MainPiece.Drawable).Add(tickContainer = new Container { RelativeSizeAxes = Axes.Both }); + Content.Add(tickContainer = new Container { RelativeSizeAxes = Axes.Both }); if (MainPiece.Drawable is IHasAccentColour accentMain) accentMain.AccentColour = colourIdle; @@ -139,6 +139,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected override DrawableStrongNestedHit CreateStrongHit(StrongHitObject hitObject) => new StrongNestedHit(hitObject, this); + private void updateColour() + { + Color4 newColour = Interpolation.ValueAt((float)rollingHits / rolling_hits_for_engaged_colour, colourIdle, colourEngaged, 0, 1); + (MainPiece.Drawable as IHasAccentColour)?.FadeAccent(newColour, 100); + } + private class StrongNestedHit : DrawableStrongNestedHit { public StrongNestedHit(StrongHitObject strong, DrawableDrumRoll drumRoll) @@ -156,11 +162,5 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables public override bool OnPressed(TaikoAction action) => false; } - - private void updateColour() - { - Color4 newColour = Interpolation.ValueAt((float)rollingHits / rolling_hits_for_engaged_colour, colourIdle, colourEngaged, 0, 1); - (MainPiece.Drawable as IHasAccentColour)?.FadeAccent(newColour, 100); - } } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/ElongatedCirclePiece.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/ElongatedCirclePiece.cs index 7e3272e42b..034ab6dd21 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/ElongatedCirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/ElongatedCirclePiece.cs @@ -1,7 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Game.Graphics; namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces { @@ -12,18 +14,15 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces RelativeSizeAxes = Axes.Y; } + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + AccentColour = colours.YellowDark; + } + protected override void Update() { base.Update(); - - var padding = Content.DrawHeight * Content.Width / 2; - - Content.Padding = new MarginPadding - { - Left = padding, - Right = padding, - }; - Width = Parent.DrawSize.X + DrawHeight; } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs index d3579fbbbd..8531f3cefd 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs @@ -11,12 +11,8 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Taiko.Skinning { - public class LegacyDrumRoll : Container, IHasAccentColour + public class LegacyDrumRoll : CompositeDrawable, IHasAccentColour { - protected override Container Content => content; - - private Container content; - private LegacyCirclePiece headCircle; private Sprite body; @@ -25,42 +21,34 @@ namespace osu.Game.Rulesets.Taiko.Skinning public LegacyDrumRoll() { - RelativeSizeAxes = Axes.Y; + RelativeSizeAxes = Axes.Both; } [BackgroundDependencyLoader] - private void load(ISkinSource skin) + private void load(ISkinSource skin, OsuColour colours) { InternalChildren = new Drawable[] { - content = new Container + end = new Sprite + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Both, + Texture = skin.GetTexture("taiko-roll-end"), + FillMode = FillMode.Fit, + }, + body = new Sprite { RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - headCircle = new LegacyCirclePiece - { - Depth = float.MinValue, - RelativeSizeAxes = Axes.Y, - Anchor = Anchor.TopLeft, - Origin = Anchor.TopLeft, - }, - body = new Sprite - { - RelativeSizeAxes = Axes.Both, - Texture = skin.GetTexture("taiko-roll-middle"), - }, - end = new Sprite - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreLeft, - RelativeSizeAxes = Axes.Both, - Texture = skin.GetTexture("taiko-roll-end"), - FillMode = FillMode.Fit, - }, - }, + Texture = skin.GetTexture("taiko-roll-middle"), + }, + headCircle = new LegacyCirclePiece + { + RelativeSizeAxes = Axes.Y, }, }; + + AccentColour = colours.YellowDark; } protected override void LoadComplete() @@ -69,21 +57,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning updateAccentColour(); } - protected override void Update() - { - base.Update(); - - var padding = Content.DrawHeight * Content.Width / 2; - - Content.Padding = new MarginPadding - { - Left = padding, - Right = padding, - }; - - Width = Parent.DrawSize.X + DrawHeight; - } - private Color4 accentColour; public Color4 AccentColour From bfc0d41c0ca81f5a0cc4c3fb00e7d320fac55330 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Apr 2020 19:24:50 +0900 Subject: [PATCH 1085/2376] Add tick skinning support --- osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs index 86e3945021..3af7df07c4 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs @@ -46,6 +46,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning return new LegacyHit(taikoComponent.Component); return null; + + case TaikoSkinComponents.DrumRollTick: + return this.GetAnimation("sliderscorepoint", false, false); } return source.GetDrawableComponent(component); From f36477e39dd1bbd055d345997f91c99701ffa208 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Apr 2020 10:04:09 +0900 Subject: [PATCH 1086/2376] Add back "filled" property setting --- .../Objects/Drawables/DrawableDrumRollTick.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs index e11e019826..689a7bfa64 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs @@ -21,7 +21,10 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables public override bool DisplayResult => false; protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.DrumRollTick), - _ => new TickPiece()); + _ => new TickPiece + { + Filled = HitObject.FirstTick + }); protected override void CheckForResult(bool userTriggered, double timeOffset) { From e2b28bfe88cc94c685a8ddbe3f496190c39103c1 Mon Sep 17 00:00:00 2001 From: Joehu Date: Wed, 15 Apr 2020 18:17:12 -0700 Subject: [PATCH 1087/2376] Hide edit context menu item in multiplayer song select --- .../Select/Carousel/DrawableCarouselBeatmap.cs | 12 +++++++++--- osu.Game/Screens/Select/SongSelect.cs | 10 ---------- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index 2520c70989..a371c56101 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -41,6 +41,9 @@ namespace osu.Game.Screens.Select.Carousel [Resolved(CanBeNull = true)] private BeatmapSetOverlay beatmapOverlay { get; set; } + [Resolved(CanBeNull = true)] + private SongSelect songSelect { get; set; } + public DrawableCarouselBeatmap(CarouselBeatmap panel) : base(panel) { @@ -49,7 +52,7 @@ namespace osu.Game.Screens.Select.Carousel } [BackgroundDependencyLoader(true)] - private void load(SongSelect songSelect, BeatmapManager manager) + private void load(BeatmapManager manager) { if (songSelect != null) { @@ -190,10 +193,13 @@ namespace osu.Game.Screens.Select.Carousel List items = new List { new OsuMenuItem("Play", MenuItemType.Highlighted, () => startRequested?.Invoke(beatmap)), - new OsuMenuItem("Edit", MenuItemType.Standard, () => editRequested?.Invoke(beatmap)), - new OsuMenuItem("Hide", MenuItemType.Destructive, () => hideRequested?.Invoke(beatmap)), }; + if (songSelect.AllowEditing) + items.Add(new OsuMenuItem("Edit", MenuItemType.Standard, () => editRequested?.Invoke(beatmap))); + + items.Add(new OsuMenuItem("Hide", MenuItemType.Destructive, () => hideRequested?.Invoke(beatmap))); + if (beatmap.OnlineBeatmapID.HasValue) items.Add(new OsuMenuItem("Details", MenuItemType.Standard, () => beatmapOverlay?.FetchAndShowBeatmap(beatmap.OnlineBeatmapID.Value))); diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index f164056ede..8967628954 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -34,7 +34,6 @@ using System.Linq; using System.Threading.Tasks; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; -using osu.Game.Overlays.Notifications; using osu.Game.Scoring; namespace osu.Game.Screens.Select @@ -71,9 +70,6 @@ namespace osu.Game.Screens.Select /// public virtual bool AllowEditing => true; - [Resolved(canBeNull: true)] - private NotificationOverlay notificationOverlay { get; set; } - [Resolved] private Bindable> selectedMods { get; set; } @@ -328,12 +324,6 @@ namespace osu.Game.Screens.Select public void Edit(BeatmapInfo beatmap = null) { - if (!AllowEditing) - { - notificationOverlay?.Post(new SimpleNotification { Text = "Editing is not available from the current mode." }); - return; - } - Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap ?? beatmapNoDebounce); this.Push(new Editor()); } From 06e25091f666c8b8f2ac4e4e42f1d24d83026c37 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Apr 2020 10:44:08 +0900 Subject: [PATCH 1088/2376] Fix typo --- .../Visual/UserInterface/TestSceneOsuMenu.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuMenu.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuMenu.cs index cdda1969ca..c171e567ad 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuMenu.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuMenu.cs @@ -21,12 +21,12 @@ namespace osu.Game.Tests.Visual.UserInterface }; private OsuMenu menu; - private bool actionPeformed; + private bool actionPerformed; [SetUp] public void Setup() => Schedule(() => { - actionPeformed = false; + actionPerformed = false; Child = menu = new OsuMenu(Direction.Vertical, true) { @@ -47,7 +47,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("move to first menu item", () => InputManager.MoveMouseTo(menu.ChildrenOfType().First())); AddStep("click", () => InputManager.Click(MouseButton.Left)); - AddAssert("action performed", () => actionPeformed); + AddAssert("action performed", () => actionPerformed); } [Test] @@ -62,7 +62,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("move to first menu item", () => InputManager.MoveMouseTo(menu.ChildrenOfType().First())); AddStep("click", () => InputManager.Click(MouseButton.Left)); - AddAssert("action not performed", () => !actionPeformed); + AddAssert("action not performed", () => !actionPerformed); } [Test] @@ -83,9 +83,9 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("move to first menu item", () => InputManager.MoveMouseTo(menu.ChildrenOfType().First())); AddStep("click", () => InputManager.Click(MouseButton.Left)); - AddAssert("action performed", () => actionPeformed); + AddAssert("action performed", () => actionPerformed); } - private void performAction() => actionPeformed = true; + private void performAction() => actionPerformed = true; } } From c4caf38febbe7862589c2f33d761c005a7e6f0fa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Apr 2020 12:10:20 +0900 Subject: [PATCH 1089/2376] Simplify menu item checks (and add for other items) --- .../Carousel/DrawableCarouselBeatmap.cs | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index a371c56101..3e4798a812 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -41,9 +41,6 @@ namespace osu.Game.Screens.Select.Carousel [Resolved(CanBeNull = true)] private BeatmapSetOverlay beatmapOverlay { get; set; } - [Resolved(CanBeNull = true)] - private SongSelect songSelect { get; set; } - public DrawableCarouselBeatmap(CarouselBeatmap panel) : base(panel) { @@ -52,12 +49,13 @@ namespace osu.Game.Screens.Select.Carousel } [BackgroundDependencyLoader(true)] - private void load(BeatmapManager manager) + private void load(BeatmapManager manager, SongSelect songSelect) { if (songSelect != null) { startRequested = b => songSelect.FinaliseSelection(b); - editRequested = songSelect.Edit; + if (songSelect.AllowEditing) + editRequested = songSelect.Edit; } if (manager != null) @@ -190,18 +188,19 @@ namespace osu.Game.Screens.Select.Carousel { get { - List items = new List - { - new OsuMenuItem("Play", MenuItemType.Highlighted, () => startRequested?.Invoke(beatmap)), - }; + List items = new List(); - if (songSelect.AllowEditing) - items.Add(new OsuMenuItem("Edit", MenuItemType.Standard, () => editRequested?.Invoke(beatmap))); + if (startRequested != null) + items.Add(new OsuMenuItem("Play", MenuItemType.Highlighted, () => startRequested(beatmap))); - items.Add(new OsuMenuItem("Hide", MenuItemType.Destructive, () => hideRequested?.Invoke(beatmap))); + if (editRequested != null) + items.Add(new OsuMenuItem("Edit", MenuItemType.Standard, () => editRequested(beatmap))); - if (beatmap.OnlineBeatmapID.HasValue) - items.Add(new OsuMenuItem("Details", MenuItemType.Standard, () => beatmapOverlay?.FetchAndShowBeatmap(beatmap.OnlineBeatmapID.Value))); + if (hideRequested != null) + items.Add(new OsuMenuItem("Hide", MenuItemType.Destructive, () => hideRequested(beatmap))); + + if (beatmap.OnlineBeatmapID.HasValue && beatmapOverlay != null) + items.Add(new OsuMenuItem("Details", MenuItemType.Standard, () => beatmapOverlay.FetchAndShowBeatmap(beatmap.OnlineBeatmapID.Value))); return items.ToArray(); } From 91b13f91eaaaad87dd221bdb8daf3ed34d7166b2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Apr 2020 12:11:12 +0900 Subject: [PATCH 1090/2376] Add exception disallowing potential edit when disabled at a property level --- osu.Game/Screens/Select/SongSelect.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 8967628954..5bc2e1aa56 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -324,6 +324,9 @@ namespace osu.Game.Screens.Select public void Edit(BeatmapInfo beatmap = null) { + if (!AllowEditing) + throw new InvalidOperationException($"Attempted to edit when {nameof(AllowEditing)} is disabled"); + Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap ?? beatmapNoDebounce); this.Push(new Editor()); } From 03a74a4320db317e063130161933694d4563ca85 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Apr 2020 12:13:26 +0900 Subject: [PATCH 1091/2376] Apply same conditional check changes to DrawableCarouselBeatmapSet --- .../Select/Carousel/DrawableCarouselBeatmapSet.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index a53b74c1b8..5acb6d1946 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -46,6 +46,7 @@ namespace osu.Game.Screens.Select.Carousel private void load(BeatmapManager manager, BeatmapSetOverlay beatmapOverlay) { restoreHiddenRequested = s => s.Beatmaps.ForEach(manager.Restore); + if (beatmapOverlay != null) viewDetails = beatmapOverlay.FetchAndShowBeatmapSet; @@ -131,13 +132,14 @@ namespace osu.Game.Screens.Select.Carousel if (Item.State.Value == CarouselItemState.NotSelected) items.Add(new OsuMenuItem("Expand", MenuItemType.Highlighted, () => Item.State.Value = CarouselItemState.Selected)); - if (beatmapSet.OnlineBeatmapSetID != null) - items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => viewDetails?.Invoke(beatmapSet.OnlineBeatmapSetID.Value))); + if (beatmapSet.OnlineBeatmapSetID != null && viewDetails != null) + items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => viewDetails(beatmapSet.OnlineBeatmapSetID.Value))); if (beatmapSet.Beatmaps.Any(b => b.Hidden)) - items.Add(new OsuMenuItem("Restore all hidden", MenuItemType.Standard, () => restoreHiddenRequested?.Invoke(beatmapSet))); + items.Add(new OsuMenuItem("Restore all hidden", MenuItemType.Standard, () => restoreHiddenRequested(beatmapSet))); - items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, () => dialogOverlay?.Push(new BeatmapDeleteDialog(beatmapSet)))); + if (dialogOverlay != null) + items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, () => dialogOverlay.Push(new BeatmapDeleteDialog(beatmapSet)))); return items.ToArray(); } From 9e2be6f2f438dcc288bbe711c486a8cc112e310d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 16 Apr 2020 13:25:08 +0900 Subject: [PATCH 1092/2376] Remove bindable to promote one-way access --- osu.Game.Tests/Visual/UserInterface/TestSceneOsuMenu.cs | 6 +++--- osu.Game/Graphics/UserInterface/OsuMenuItem.cs | 6 ------ osu.Game/Screens/Edit/Editor.cs | 9 +++++++-- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuMenu.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuMenu.cs index c171e567ad..9ea76c2c7b 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuMenu.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuMenu.cs @@ -56,7 +56,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("disable menu items", () => { foreach (var item in menu.Items) - ((OsuMenuItem)item).Enabled.Value = false; + ((OsuMenuItem)item).Action.Disabled = true; }); AddStep("move to first menu item", () => InputManager.MoveMouseTo(menu.ChildrenOfType().First())); @@ -71,13 +71,13 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("disable menu items", () => { foreach (var item in menu.Items) - ((OsuMenuItem)item).Enabled.Value = false; + ((OsuMenuItem)item).Action.Disabled = true; }); AddStep("enable menu items", () => { foreach (var item in menu.Items) - ((OsuMenuItem)item).Enabled.Value = true; + ((OsuMenuItem)item).Action.Disabled = false; }); AddStep("move to first menu item", () => InputManager.MoveMouseTo(menu.ChildrenOfType().First())); diff --git a/osu.Game/Graphics/UserInterface/OsuMenuItem.cs b/osu.Game/Graphics/UserInterface/OsuMenuItem.cs index 36122ca0b2..0fe41937ce 100644 --- a/osu.Game/Graphics/UserInterface/OsuMenuItem.cs +++ b/osu.Game/Graphics/UserInterface/OsuMenuItem.cs @@ -2,15 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osu.Framework.Bindables; using osu.Framework.Graphics.UserInterface; namespace osu.Game.Graphics.UserInterface { public class OsuMenuItem : MenuItem { - public readonly Bindable Enabled = new Bindable(true); - public readonly MenuItemType Type; public OsuMenuItem(string text, MenuItemType type = MenuItemType.Standard) @@ -22,9 +19,6 @@ namespace osu.Game.Graphics.UserInterface : base(text, action) { Type = type; - - Enabled.BindValueChanged(enabled => Action.Disabled = !enabled.NewValue); - Action.BindDisabledChanged(disabled => Enabled.Value = !disabled); } } } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index ad17498d93..9a1f450dc6 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -107,6 +107,8 @@ namespace osu.Game.Screens.Edit dependencies.CacheAs(changeHandler); EditorMenuBar menuBar; + OsuMenuItem undoMenuItem; + OsuMenuItem redoMenuItem; var fileMenuItems = new List { @@ -155,8 +157,8 @@ namespace osu.Game.Screens.Edit { Items = new[] { - new EditorMenuItem("Undo", MenuItemType.Standard, undo) { Enabled = { BindTarget = changeHandler.CanUndo } }, - new EditorMenuItem("Redo", MenuItemType.Standard, redo) { Enabled = { BindTarget = changeHandler.CanRedo } } + undoMenuItem = new EditorMenuItem("Undo", MenuItemType.Standard, undo), + redoMenuItem = new EditorMenuItem("Redo", MenuItemType.Standard, redo) } } } @@ -214,6 +216,9 @@ namespace osu.Game.Screens.Edit } }); + changeHandler.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true); + changeHandler.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true); + menuBar.Mode.ValueChanged += onModeChanged; bottomBackground.Colour = colours.Gray2; From 9dda7da489918120d251c6c266272f41a2fa8671 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Apr 2020 14:11:38 +0900 Subject: [PATCH 1093/2376] Fix spinners being considered the "first object" for increased visibility in hidden --- .../Mods/TestSceneOsuModHidden.cs | 106 ++++++++++++++++++ osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs | 2 + osu.Game/Rulesets/Mods/ModHidden.cs | 14 ++- 3 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs new file mode 100644 index 0000000000..8bd3d3c7cc --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs @@ -0,0 +1,106 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using NUnit.Framework; +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.Tests.Visual; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests.Mods +{ + public class TestSceneOsuModHidden : ModTestScene + { + public TestSceneOsuModHidden() + : base(new OsuRuleset()) + { + } + + [Test] + public void TestDefaultBeatmapTest() => CreateModTest(new ModTestData + { + Mod = new OsuModHidden(), + Autoplay = true, + PassCondition = checkSomeHit + }); + + [Test] + public void FirstCircleAfterTwoSpinners() => CreateModTest(new ModTestData + { + Mod = new OsuModHidden(), + Autoplay = true, + Beatmap = new Beatmap + { + HitObjects = new List + { + new Spinner + { + Position = new Vector2(256, 192), + EndTime = 1000, + }, + new Spinner + { + Position = new Vector2(256, 192), + StartTime = 1200, + EndTime = 2200, + }, + new HitCircle + { + Position = new Vector2(300, 192), + StartTime = 3200, + }, + new HitCircle + { + Position = new Vector2(384, 192), + StartTime = 4200, + } + } + }, + PassCondition = checkSomeHit + }); + + [Test] + public void FirstSliderAfterTwoSpinners() => CreateModTest(new ModTestData + { + Mod = new OsuModHidden(), + Autoplay = true, + Beatmap = new Beatmap + { + HitObjects = new List + { + new Spinner + { + Position = new Vector2(256, 192), + EndTime = 1000, + }, + new Spinner + { + Position = new Vector2(256, 192), + StartTime = 1200, + EndTime = 2200, + }, + new Slider + { + StartTime = 3200, + Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(100, 0), }) + }, + new Slider + { + StartTime = 5200, + Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(100, 0), }) + } + } + }, + PassCondition = checkSomeHit + }); + + private bool checkSomeHit() + { + return Player.ScoreProcessor.JudgedHits >= 4; + } + } +} diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs index 91a4e049e3..fdba03f260 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs @@ -23,6 +23,8 @@ namespace osu.Game.Rulesets.Osu.Mods private const double fade_in_duration_multiplier = 0.4; private const double fade_out_duration_multiplier = 0.3; + protected override bool IsFirstHideableObject(DrawableHitObject hitObject) => !(hitObject is DrawableSpinner); + public override void ApplyToDrawableHitObjects(IEnumerable drawables) { static void adjustFadeIn(OsuHitObject h) => h.TimeFadeIn = h.TimePreempt * fade_in_duration_multiplier; diff --git a/osu.Game/Rulesets/Mods/ModHidden.cs b/osu.Game/Rulesets/Mods/ModHidden.cs index 4e4a75db82..a1915b974c 100644 --- a/osu.Game/Rulesets/Mods/ModHidden.cs +++ b/osu.Game/Rulesets/Mods/ModHidden.cs @@ -23,6 +23,13 @@ namespace osu.Game.Rulesets.Mods protected Bindable IncreaseFirstObjectVisibility = new Bindable(); + /// + /// Check whether the provided hitobject should be considered the "first" hideable object. + /// Can be used to skip spinners, for instance. + /// + /// The hitobject to check. + protected virtual bool IsFirstHideableObject(DrawableHitObject hitObject) => true; + public void ReadFromConfig(OsuConfigManager config) { IncreaseFirstObjectVisibility = config.GetBindable(OsuSetting.IncreaseFirstObjectVisibility); @@ -30,8 +37,11 @@ namespace osu.Game.Rulesets.Mods public virtual void ApplyToDrawableHitObjects(IEnumerable drawables) { - foreach (var d in drawables.Skip(IncreaseFirstObjectVisibility.Value ? 1 : 0)) - d.ApplyCustomUpdateState += ApplyHiddenState; + if (IncreaseFirstObjectVisibility.Value) + drawables = drawables.SkipWhile(h => !IsFirstHideableObject(h)).Skip(1); + + foreach (var dho in drawables) + dho.ApplyCustomUpdateState += ApplyHiddenState; } public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor) From ef0da9e3e831096674d37ba799246de1d569a786 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 16 Apr 2020 11:01:36 +0300 Subject: [PATCH 1094/2376] Basic overlay layout implementation --- .../Online/TestSceneDashboardOverlay.cs | 43 +++++++++++++ .../Dashboard/DashboardOverlayHeader.cs | 24 +++++++ osu.Game/Overlays/DashboardOverlay.cs | 62 +++++++++++++++++++ 3 files changed, 129 insertions(+) create mode 100644 osu.Game.Tests/Visual/Online/TestSceneDashboardOverlay.cs create mode 100644 osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs create mode 100644 osu.Game/Overlays/DashboardOverlay.cs diff --git a/osu.Game.Tests/Visual/Online/TestSceneDashboardOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneDashboardOverlay.cs new file mode 100644 index 0000000000..df95f24686 --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneDashboardOverlay.cs @@ -0,0 +1,43 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using NUnit.Framework; +using osu.Game.Overlays; +using osu.Game.Overlays.Dashboard; +using osu.Game.Overlays.Dashboard.Friends; + +namespace osu.Game.Tests.Visual.Online +{ + public class TestSceneDashboardOverlay : OsuTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(DashboardOverlay), + typeof(DashboardOverlayHeader), + typeof(FriendDisplay) + }; + + protected override bool UseOnlineAPI => true; + + private readonly DashboardOverlay overlay; + + public TestSceneDashboardOverlay() + { + Add(overlay = new DashboardOverlay()); + } + + [Test] + public void TestShow() + { + AddStep("Show", overlay.Show); + } + + [Test] + public void TestHide() + { + AddStep("Hide", overlay.Hide); + } + } +} diff --git a/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs b/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs new file mode 100644 index 0000000000..1c52b033a5 --- /dev/null +++ b/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.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. + +namespace osu.Game.Overlays.Dashboard +{ + public class DashboardOverlayHeader : TabControlOverlayHeader + { + protected override OverlayTitle CreateTitle() => new DashboardTitle(); + + private class DashboardTitle : OverlayTitle + { + public DashboardTitle() + { + Title = "dashboard"; + IconTexture = "Icons/changelog"; + } + } + } + + public enum HomeOverlayTabs + { + Friends + } +} diff --git a/osu.Game/Overlays/DashboardOverlay.cs b/osu.Game/Overlays/DashboardOverlay.cs new file mode 100644 index 0000000000..a1a7c9889a --- /dev/null +++ b/osu.Game/Overlays/DashboardOverlay.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 osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays.Dashboard; + +namespace osu.Game.Overlays +{ + public class DashboardOverlay : FullscreenOverlay + { + private readonly Box background; + private readonly Container content; + + public DashboardOverlay() + : base(OverlayColourScheme.Purple) + { + Children = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both + }, + new OverlayScrollContainer + { + RelativeSizeAxes = Axes.Both, + ScrollbarVisible = false, + Child = new FillFlowContainer + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new DashboardOverlayHeader + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Depth = -float.MaxValue + }, + content = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + } + } + } + }, + new LoadingLayer(content), + }; + } + + [BackgroundDependencyLoader] + private void load() + { + background.Colour = ColourProvider.Background5; + } + } +} From 2ab4a7293ec507b691fbf5fcd1208634fbe74aa2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Apr 2020 17:26:09 +0900 Subject: [PATCH 1095/2376] Clean up enum sorting attribute code --- .../API/Requests/SearchBeatmapSetsRequest.cs | 18 +------ .../BeatmapListing/BeatmapSearchFilterRow.cs | 28 ++-------- osu.Game/Utils/OrderAttribute.cs | 52 +++++++++++++++++++ 3 files changed, 56 insertions(+), 42 deletions(-) create mode 100644 osu.Game/Utils/OrderAttribute.cs diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs index aef0788b49..1206563b18 100644 --- a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs +++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs @@ -1,12 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.ComponentModel; using osu.Framework.IO.Network; using osu.Game.Overlays; using osu.Game.Overlays.Direct; using osu.Game.Rulesets; +using osu.Game.Utils; namespace osu.Game.Online.API.Requests { @@ -139,20 +139,4 @@ namespace osu.Game.Online.API.Requests [Order(5)] Italian } - - [AttributeUsage(AttributeTargets.Field)] - public class OrderAttribute : Attribute - { - public readonly int Order; - - public OrderAttribute(int order) - { - Order = order; - } - } - - [AttributeUsage(AttributeTargets.Enum)] - public class HasOrderedElementsAttribute : Attribute - { - } } diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs index bc0a011e31..64b3afcae1 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs @@ -2,7 +2,6 @@ // 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.Bindables; @@ -14,10 +13,10 @@ using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; -using osu.Game.Online.API.Requests; using osuTK; using osuTK.Graphics; using Humanizer; +using osu.Game.Utils; namespace osu.Game.Overlays.BeatmapListing { @@ -82,30 +81,9 @@ namespace osu.Game.Overlays.BeatmapListing TabContainer.Spacing = new Vector2(10, 0); - var type = typeof(T); - - if (type.IsEnum) + if (typeof(T).IsEnum) { - if (Attribute.GetCustomAttribute(type, typeof(HasOrderedElementsAttribute)) != null) - { - var enumValues = Enum.GetValues(type).Cast().ToArray(); - var enumNames = Enum.GetNames(type); - - int[] enumPositions = Array.ConvertAll(enumNames, n => - { - var orderAttr = (OrderAttribute)type.GetField(n).GetCustomAttributes(typeof(OrderAttribute), false)[0]; - return orderAttr.Order; - }); - - Array.Sort(enumPositions, enumValues); - - foreach (var val in enumValues) - AddItem(val); - - return; - } - - foreach (var val in (T[])Enum.GetValues(type)) + foreach (var val in OrderAttributeUtils.GetValuesInOrder()) AddItem(val); } } diff --git a/osu.Game/Utils/OrderAttribute.cs b/osu.Game/Utils/OrderAttribute.cs new file mode 100644 index 0000000000..4959caa726 --- /dev/null +++ b/osu.Game/Utils/OrderAttribute.cs @@ -0,0 +1,52 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace osu.Game.Utils +{ + public static class OrderAttributeUtils + { + /// + /// Get values of an enum in order. Supports custom ordering via . + /// + public static IEnumerable GetValuesInOrder() + { + var type = typeof(T); + + if (!type.IsEnum) + throw new InvalidOperationException("T must be an enum"); + + IEnumerable items = (T[])Enum.GetValues(type); + + if (Attribute.GetCustomAttribute(type, typeof(HasOrderedElementsAttribute)) == null) + return items; + + return items.OrderBy(i => + { + if (type.GetField(i.ToString()).GetCustomAttributes(typeof(OrderAttribute), false).FirstOrDefault() is OrderAttribute attr) + return attr.Order; + + return 0; + }); + } + } + + [AttributeUsage(AttributeTargets.Field)] + public class OrderAttribute : Attribute + { + public readonly int Order; + + public OrderAttribute(int order) + { + Order = order; + } + } + + [AttributeUsage(AttributeTargets.Enum)] + public class HasOrderedElementsAttribute : Attribute + { + } +} From c6aa6acc1b2f46c6c39a0d241325b1d2d8f154a5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 16 Apr 2020 17:28:06 +0900 Subject: [PATCH 1096/2376] Apply performance calculator changes --- .../Difficulty/CatchPerformanceCalculator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs index 21d4642c22..bc52f0b812 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs @@ -70,7 +70,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty if (approachRate > 9.0f) approachRateFactor += 0.1f * (approachRate - 9.0f); // 10% for each AR above 9 if (approachRate > 10.0f) - approachRateFactor += 0.2f * (float)Math.Pow(approachRate - 10.0f, 1.5f); // Additional 20% at AR 11, 40% total + approachRateFactor += 0.1f * (approachRate - 10.0f); // Additional 10% at AR 11, 30% total else if (approachRate < 8.0f) approachRateFactor += 0.025f * (8.0f - approachRate); // 2.5% for each AR below 8 From 29bea4e11c03292545a9937a149f28c3686c14c4 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 16 Apr 2020 11:42:21 +0300 Subject: [PATCH 1097/2376] Implement OverlayView component --- .../Visual/Online/TestSceneFriendDisplay.cs | 17 ++- .../Dashboard/Friends/FriendDisplay.cs | 143 ++++++++---------- osu.Game/Overlays/OverlayView.cs | 71 +++++++++ 3 files changed, 149 insertions(+), 82 deletions(-) create mode 100644 osu.Game/Overlays/OverlayView.cs diff --git a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs index cf365a7614..0b5ff1c960 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs @@ -10,6 +10,7 @@ using osu.Game.Users; using osu.Game.Overlays; using osu.Framework.Allocation; using NUnit.Framework; +using osu.Game.Online.API; namespace osu.Game.Tests.Visual.Online { @@ -27,7 +28,7 @@ namespace osu.Game.Tests.Visual.Online [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); - private FriendDisplay display; + private TestFriendDisplay display; [SetUp] public void Setup() => Schedule(() => @@ -35,7 +36,7 @@ namespace osu.Game.Tests.Visual.Online Child = new BasicScrollContainer { RelativeSizeAxes = Axes.Both, - Child = display = new FriendDisplay() + Child = display = new TestFriendDisplay() }; }); @@ -83,5 +84,17 @@ namespace osu.Game.Tests.Visual.Online LastVisit = DateTimeOffset.Now } }; + + private class TestFriendDisplay : FriendDisplay + { + public void Fetch() + { + base.APIStateChanged(API, APIState.Online); + } + + public override void APIStateChanged(IAPIProvider api, APIState state) + { + } + } } } diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs index 3c9b31daae..9764f82199 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs @@ -16,7 +16,7 @@ using osuTK; namespace osu.Game.Overlays.Dashboard.Friends { - public class FriendDisplay : CompositeDrawable + public class FriendDisplay : OverlayView> { private List users = new List(); @@ -26,15 +26,10 @@ namespace osu.Game.Overlays.Dashboard.Friends set { users = value; - onlineStreamControl.Populate(value); } } - [Resolved] - private IAPIProvider api { get; set; } - - private GetFriendsRequest request; private CancellationTokenSource cancellationToken; private Drawable currentContent; @@ -48,92 +43,85 @@ namespace osu.Game.Overlays.Dashboard.Friends public FriendDisplay() { - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; - InternalChild = new FillFlowContainer + AddRange(new Drawable[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new Drawable[] + new Container { - new Container + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new Drawable[] + controlBackground = new Box { - controlBackground = new Box + RelativeSizeAxes = Axes.Both + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { - RelativeSizeAxes = Axes.Both + Top = 20, + Horizontal = 45 }, - new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding - { - Top = 20, - Horizontal = 45 - }, - Child = onlineStreamControl = new FriendOnlineStreamControl(), - } + Child = onlineStreamControl = new FriendOnlineStreamControl(), } - }, - new Container + } + }, + new Container + { + Name = "User List", + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] { - Name = "User List", - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new Drawable[] + background = new Box { - background = new Box + RelativeSizeAxes = Axes.Both + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Margin = new MarginPadding { Bottom = 20 }, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both - }, - new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Margin = new MarginPadding { Bottom = 20 }, - Children = new Drawable[] + new Container { - new Container + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding - { - Horizontal = 40, - Vertical = 20 - }, - Child = userListToolbar = new UserListToolbar - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - } + Horizontal = 40, + Vertical = 20 }, - new Container + Child = userListToolbar = new UserListToolbar { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new Drawable[] + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + } + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + itemsPlaceholder = new Container { - itemsPlaceholder = new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Horizontal = 50 } - }, - loading = new LoadingLayer(itemsPlaceholder) - } + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Horizontal = 50 } + }, + loading = new LoadingLayer(itemsPlaceholder) } } } } } } - }; + }); } [BackgroundDependencyLoader] @@ -152,14 +140,11 @@ namespace osu.Game.Overlays.Dashboard.Friends userListToolbar.SortCriteria.BindValueChanged(_ => recreatePanels()); } - public void Fetch() - { - if (!api.IsLoggedIn) - return; + protected override APIRequest> CreateRequest() => new GetFriendsRequest(); - request = new GetFriendsRequest(); - request.Success += response => Schedule(() => Users = response); - api.Queue(request); + protected override void OnSuccess(List response) + { + Users = response; } private void recreatePanels() @@ -258,9 +243,7 @@ namespace osu.Game.Overlays.Dashboard.Friends protected override void Dispose(bool isDisposing) { - request?.Cancel(); cancellationToken?.Cancel(); - base.Dispose(isDisposing); } } diff --git a/osu.Game/Overlays/OverlayView.cs b/osu.Game/Overlays/OverlayView.cs new file mode 100644 index 0000000000..f39c6bd1b9 --- /dev/null +++ b/osu.Game/Overlays/OverlayView.cs @@ -0,0 +1,71 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Online.API; + +namespace osu.Game.Overlays +{ + /// + /// Drawable which used to represent online content in . + /// + /// Response type + public abstract class OverlayView : Container, IOnlineComponent + where T : class + { + [Resolved] + protected IAPIProvider API { get; private set; } + + protected override Container Content => content; + + private readonly FillFlowContainer content; + + protected OverlayView() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + AddInternal(content = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + }); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + API.Register(this); + } + + private APIRequest request; + + protected abstract APIRequest CreateRequest(); + + protected abstract void OnSuccess(T response); + + public virtual void APIStateChanged(IAPIProvider api, APIState state) + { + switch (state) + { + case APIState.Online: + request = CreateRequest(); + request.Success += response => Schedule(() => OnSuccess(response)); + api.Queue(request); + break; + + default: + break; + } + } + + protected override void Dispose(bool isDisposing) + { + request?.Cancel(); + API?.Unregister(this); + base.Dispose(isDisposing); + } + } +} From 894598eb220e7cc05f3fab5df81a786973f804d5 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 16 Apr 2020 12:05:51 +0300 Subject: [PATCH 1098/2376] Replace SocialOverlay with DashboardOverlay --- osu.Game.Tests/Visual/TestSceneOsuGame.cs | 2 +- osu.Game/OsuGame.cs | 8 +- .../Dashboard/DashboardOverlayHeader.cs | 4 +- osu.Game/Overlays/DashboardOverlay.cs | 94 ++++++++++++++++++- .../Overlays/Toolbar/ToolbarSocialButton.cs | 4 +- 5 files changed, 100 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/TestSceneOsuGame.cs b/osu.Game.Tests/Visual/TestSceneOsuGame.cs index 492494ada3..8793d880e3 100644 --- a/osu.Game.Tests/Visual/TestSceneOsuGame.cs +++ b/osu.Game.Tests/Visual/TestSceneOsuGame.cs @@ -48,7 +48,7 @@ namespace osu.Game.Tests.Visual typeof(OnScreenDisplay), typeof(NotificationOverlay), typeof(DirectOverlay), - typeof(SocialOverlay), + typeof(DashboardOverlay), typeof(ChannelManager), typeof(ChatOverlay), typeof(SettingsOverlay), diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 5e93d760e3..c861b84835 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -67,7 +67,7 @@ namespace osu.Game private DirectOverlay direct; - private SocialOverlay social; + private DashboardOverlay dashboard; private UserProfileOverlay userProfile; @@ -611,7 +611,7 @@ namespace osu.Game //overlay elements loadComponentSingleFile(direct = new DirectOverlay(), overlayContent.Add, true); - loadComponentSingleFile(social = new SocialOverlay(), overlayContent.Add, true); + loadComponentSingleFile(dashboard = new DashboardOverlay(), overlayContent.Add, true); var rankingsOverlay = loadComponentSingleFile(new RankingsOverlay(), overlayContent.Add, true); loadComponentSingleFile(channelManager = new ChannelManager(), AddInternal, true); loadComponentSingleFile(chatOverlay = new ChatOverlay(), overlayContent.Add, true); @@ -670,7 +670,7 @@ namespace osu.Game } // ensure only one of these overlays are open at once. - var singleDisplayOverlays = new OverlayContainer[] { chatOverlay, social, direct, changelogOverlay, rankingsOverlay }; + var singleDisplayOverlays = new OverlayContainer[] { chatOverlay, dashboard, direct, changelogOverlay, rankingsOverlay }; foreach (var overlay in singleDisplayOverlays) { @@ -842,7 +842,7 @@ namespace osu.Game return true; case GlobalAction.ToggleSocial: - social.ToggleVisibility(); + dashboard.ToggleVisibility(); return true; case GlobalAction.ResetInputSettings: diff --git a/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs b/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs index 1c52b033a5..9ee679a866 100644 --- a/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs +++ b/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs @@ -3,7 +3,7 @@ namespace osu.Game.Overlays.Dashboard { - public class DashboardOverlayHeader : TabControlOverlayHeader + public class DashboardOverlayHeader : TabControlOverlayHeader { protected override OverlayTitle CreateTitle() => new DashboardTitle(); @@ -17,7 +17,7 @@ namespace osu.Game.Overlays.Dashboard } } - public enum HomeOverlayTabs + public enum DashboardOverlayTabs { Friends } diff --git a/osu.Game/Overlays/DashboardOverlay.cs b/osu.Game/Overlays/DashboardOverlay.cs index a1a7c9889a..1e0fbc90b4 100644 --- a/osu.Game/Overlays/DashboardOverlay.cs +++ b/osu.Game/Overlays/DashboardOverlay.cs @@ -1,19 +1,29 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Threading; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; using osu.Game.Overlays.Dashboard; +using osu.Game.Overlays.Dashboard.Friends; namespace osu.Game.Overlays { public class DashboardOverlay : FullscreenOverlay { + private CancellationTokenSource cancellationToken; + private readonly Box background; private readonly Container content; + private readonly DashboardOverlayHeader header; + private readonly LoadingLayer loading; + private readonly OverlayScrollContainer scrollFlow; public DashboardOverlay() : base(OverlayColourScheme.Purple) @@ -24,7 +34,7 @@ namespace osu.Game.Overlays { RelativeSizeAxes = Axes.Both }, - new OverlayScrollContainer + scrollFlow = new OverlayScrollContainer { RelativeSizeAxes = Axes.Both, ScrollbarVisible = false, @@ -35,7 +45,7 @@ namespace osu.Game.Overlays Direction = FillDirection.Vertical, Children = new Drawable[] { - new DashboardOverlayHeader + header = new DashboardOverlayHeader { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, @@ -49,7 +59,7 @@ namespace osu.Game.Overlays } } }, - new LoadingLayer(content), + loading = new LoadingLayer(content), }; } @@ -58,5 +68,83 @@ namespace osu.Game.Overlays { background.Colour = ColourProvider.Background5; } + + protected override void LoadComplete() + { + base.LoadComplete(); + header.Current.BindValueChanged(onTabChanged); + } + + private bool displayUpdateRequired = true; + + protected override void PopIn() + { + base.PopIn(); + + // We don't want to create new display on every call, only when exiting from fully closed state. + if (displayUpdateRequired) + { + header.Current.TriggerChange(); + displayUpdateRequired = false; + } + } + + protected override void PopOutComplete() + { + base.PopOutComplete(); + loadDisplay(Empty()); + displayUpdateRequired = true; + } + + private void loadDisplay(Drawable display) + { + scrollFlow.ScrollToStart(); + + LoadComponentAsync(display, loaded => + { + loading.Hide(); + content.Child = loaded; + }, (cancellationToken = new CancellationTokenSource()).Token); + } + + private void onTabChanged(ValueChangedEvent tab) + { + cancellationToken?.Cancel(); + + loading.Show(); + + switch (tab.NewValue) + { + case DashboardOverlayTabs.Friends: + loadDisplay(new FriendDisplay()); + break; + + default: + throw new NotImplementedException($"Display for {tab.NewValue} tab is not implemented"); + } + } + + public override void APIStateChanged(IAPIProvider api, APIState state) + { + switch (state) + { + case APIState.Online: + // Will force to create a display based on visibility state + displayUpdateRequired = true; + State.TriggerChange(); + return; + + default: + content.Clear(); + loading.Show(); + return; + } + } + + protected override void Dispose(bool isDisposing) + { + cancellationToken?.Cancel(); + base.Dispose(isDisposing); + } } } diff --git a/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs b/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs index 5e353d3319..f6646eb81d 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs @@ -14,9 +14,9 @@ namespace osu.Game.Overlays.Toolbar } [BackgroundDependencyLoader(true)] - private void load(SocialOverlay chat) + private void load(DashboardOverlay dashboard) { - StateContainer = chat; + StateContainer = dashboard; } } } From eb86be0a6da6d76dfef8526eff26ddb584d8bd7b Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 16 Apr 2020 12:07:38 +0300 Subject: [PATCH 1099/2376] Adjust header content margin --- osu.Game/Overlays/OverlayHeader.cs | 4 +++- osu.Game/Overlays/TabControlOverlayHeader.cs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/OverlayHeader.cs b/osu.Game/Overlays/OverlayHeader.cs index 4ac0f697c3..dbc934bde9 100644 --- a/osu.Game/Overlays/OverlayHeader.cs +++ b/osu.Game/Overlays/OverlayHeader.cs @@ -12,6 +12,8 @@ namespace osu.Game.Overlays { public abstract class OverlayHeader : Container { + public const int CONTENT_X_MARGIN = 50; + private readonly Box titleBackground; protected readonly FillFlowContainer HeaderInfo; @@ -54,7 +56,7 @@ namespace osu.Game.Overlays AutoSizeAxes = Axes.Y, Padding = new MarginPadding { - Horizontal = UserProfileOverlay.CONTENT_X_MARGIN, + Horizontal = CONTENT_X_MARGIN, }, Children = new[] { diff --git a/osu.Game/Overlays/TabControlOverlayHeader.cs b/osu.Game/Overlays/TabControlOverlayHeader.cs index ab1a6aff78..e8e000f441 100644 --- a/osu.Game/Overlays/TabControlOverlayHeader.cs +++ b/osu.Game/Overlays/TabControlOverlayHeader.cs @@ -44,7 +44,7 @@ namespace osu.Game.Overlays }, TabControl = CreateTabControl().With(control => { - control.Margin = new MarginPadding { Left = UserProfileOverlay.CONTENT_X_MARGIN }; + control.Margin = new MarginPadding { Left = CONTENT_X_MARGIN }; control.Current = Current; }) } From 87f52b82331dc1f6ba4b198d96cb8b768a152c19 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 16 Apr 2020 12:09:44 +0300 Subject: [PATCH 1100/2376] Remove redundant switch section --- osu.Game/Overlays/OverlayView.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Overlays/OverlayView.cs b/osu.Game/Overlays/OverlayView.cs index f39c6bd1b9..e3a07fc2de 100644 --- a/osu.Game/Overlays/OverlayView.cs +++ b/osu.Game/Overlays/OverlayView.cs @@ -55,9 +55,6 @@ namespace osu.Game.Overlays request.Success += response => Schedule(() => OnSuccess(response)); api.Queue(request); break; - - default: - break; } } From d62094cd4ba1e9d20d60edc8e326198c615f8732 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Apr 2020 18:10:35 +0900 Subject: [PATCH 1101/2376] Fix carousel not correctly updating when selection changes to a new beatmap from a child screen --- osu.Game/Screens/Select/BeatmapCarousel.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index a8225ba1ec..d8178bbbbb 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -217,6 +217,9 @@ namespace osu.Game.Screens.Select /// True if a selection was made, False if it wasn't. public bool SelectBeatmap(BeatmapInfo beatmap, bool bypassFilters = true) { + // ensure that any pending events from BeatmapManager have been run before attempting a selection. + Scheduler.Update(); + if (beatmap?.Hidden != false) return false; From d7ea5432a8b1eda9d85e29de2261b0d4cfa3ccbc Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 16 Apr 2020 18:15:52 +0900 Subject: [PATCH 1102/2376] Fix incorrect combo calculation --- .../Difficulty/CatchPerformanceCalculator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs index bc52f0b812..e7ce680365 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs @@ -63,7 +63,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty // Combo scaling if (Attributes.MaxCombo > 0) - value *= Math.Min(Math.Pow(Attributes.MaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0); + value *= Math.Min(Math.Pow(Score.MaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0); float approachRate = (float)Attributes.ApproachRate; float approachRateFactor = 1.0f; From ae210d567d794f5b16587b8684bb9bdb9edfeeb2 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 16 Apr 2020 18:16:08 +0900 Subject: [PATCH 1103/2376] Add temporary solution for tick hit/miss count --- osu.Game/Rulesets/Scoring/HitResult.cs | 5 +++++ .../Scoring/Legacy/ScoreInfoExtensions.cs | 21 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/osu.Game/Rulesets/Scoring/HitResult.cs b/osu.Game/Rulesets/Scoring/HitResult.cs index 7ba88d3df8..0c895bd086 100644 --- a/osu.Game/Rulesets/Scoring/HitResult.cs +++ b/osu.Game/Rulesets/Scoring/HitResult.cs @@ -43,5 +43,10 @@ namespace osu.Game.Rulesets.Scoring /// [Description(@"Perfect")] Perfect, + + SmallTickHit, + SmallTickMiss, + LargeTickHit, + LargeTickMiss, } } diff --git a/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs b/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs index 66b1acf591..9745d1abef 100644 --- a/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs +++ b/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs @@ -66,6 +66,9 @@ namespace osu.Game.Scoring.Legacy { case 3: return scoreInfo.Statistics[HitResult.Good]; + + case 2: + return scoreInfo.Statistics[HitResult.SmallTickMiss]; } return null; @@ -78,6 +81,10 @@ namespace osu.Game.Scoring.Legacy case 3: scoreInfo.Statistics[HitResult.Good] = value; break; + + case 2: + scoreInfo.Statistics[HitResult.SmallTickMiss] = value; + break; } } @@ -91,6 +98,9 @@ namespace osu.Game.Scoring.Legacy case 3: return scoreInfo.Statistics[HitResult.Ok]; + + case 2: + return scoreInfo.Statistics[HitResult.LargeTickHit]; } return null; @@ -108,6 +118,10 @@ namespace osu.Game.Scoring.Legacy case 3: scoreInfo.Statistics[HitResult.Ok] = value; break; + + case 2: + scoreInfo.Statistics[HitResult.LargeTickHit] = value; + break; } } @@ -118,6 +132,9 @@ namespace osu.Game.Scoring.Legacy case 0: case 3: return scoreInfo.Statistics[HitResult.Meh]; + + case 2: + return scoreInfo.Statistics[HitResult.SmallTickHit]; } return null; @@ -131,6 +148,10 @@ namespace osu.Game.Scoring.Legacy case 3: scoreInfo.Statistics[HitResult.Meh] = value; break; + + case 2: + scoreInfo.Statistics[HitResult.SmallTickHit] = value; + break; } } From c5a343d3a07daf31ad95a036850a05e7007f2a41 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 16 Apr 2020 14:10:39 +0300 Subject: [PATCH 1104/2376] Fix overlay accepting state changes while hidden --- osu.Game/Overlays/DashboardOverlay.cs | 28 +++++++++++++-------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/osu.Game/Overlays/DashboardOverlay.cs b/osu.Game/Overlays/DashboardOverlay.cs index 1e0fbc90b4..86c0f3bd83 100644 --- a/osu.Game/Overlays/DashboardOverlay.cs +++ b/osu.Game/Overlays/DashboardOverlay.cs @@ -81,7 +81,7 @@ namespace osu.Game.Overlays { base.PopIn(); - // We don't want to create new display on every call, only when exiting from fully closed state. + // We don't want to create a new display on every call, only when exiting from fully closed state. if (displayUpdateRequired) { header.Current.TriggerChange(); @@ -102,7 +102,9 @@ namespace osu.Game.Overlays LoadComponentAsync(display, loaded => { - loading.Hide(); + if (API.IsLoggedIn) + loading.Hide(); + content.Child = loaded; }, (cancellationToken = new CancellationTokenSource()).Token); } @@ -110,9 +112,14 @@ namespace osu.Game.Overlays private void onTabChanged(ValueChangedEvent tab) { cancellationToken?.Cancel(); - loading.Show(); + if (!API.IsLoggedIn) + { + loadDisplay(Empty()); + return; + } + switch (tab.NewValue) { case DashboardOverlayTabs.Friends: @@ -126,19 +133,10 @@ namespace osu.Game.Overlays public override void APIStateChanged(IAPIProvider api, APIState state) { - switch (state) - { - case APIState.Online: - // Will force to create a display based on visibility state - displayUpdateRequired = true; - State.TriggerChange(); - return; + if (State.Value == Visibility.Hidden) + return; - default: - content.Clear(); - loading.Show(); - return; - } + header.Current.TriggerChange(); } protected override void Dispose(bool isDisposing) From 3daacbc2d202b3d42ac84e7242e571a045d8fa09 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 17 Apr 2020 13:34:20 +0900 Subject: [PATCH 1105/2376] Initial inefficient refactor of hitobject enumeration --- osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs | 76 ++++++++------------ osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 2 +- 2 files changed, 29 insertions(+), 49 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs index dfca2aff7b..171ce6fe61 100644 --- a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs @@ -1,9 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.UI; @@ -37,12 +37,9 @@ namespace osu.Game.Rulesets.Osu.UI DrawableHitObject blockingObject = null; // Find the last hitobject which blocks future hits. - foreach (var obj in hitObjectContainer.AliveObjects) + foreach (var obj in enumerateHitObjectsUpTo(hitObject)) { - if (obj == hitObject) - break; - - if (drawableCanBlockFutureHits(obj)) + if (hitObjectCanBlockFutureHits(obj)) blockingObject = obj; } @@ -64,64 +61,47 @@ namespace osu.Game.Rulesets.Osu.UI /// Handles a being hit to potentially miss all earlier s. /// /// The that was hit. - public void HandleHit(HitObject hitObject) + public void HandleHit(DrawableHitObject hitObject) { // Hitobjects which themselves don't block future hitobjects don't cause misses (e.g. slider ticks, spinners). if (!hitObjectCanBlockFutureHits(hitObject)) return; - double maximumTime = hitObject.StartTime; - - // Iterate through and apply miss results to all top-level and nested hitobjects which block future hits. - foreach (var obj in hitObjectContainer.AliveObjects) + foreach (var obj in enumerateHitObjectsUpTo(hitObject)) { - if (obj.Judged || obj.HitObject.StartTime >= maximumTime) + if (obj.Judged) continue; - if (hitObjectCanBlockFutureHits(obj.HitObject)) - applyMiss(obj); - - foreach (var nested in obj.NestedHitObjects) - { - if (nested.Judged || nested.HitObject.StartTime >= maximumTime) - continue; - - if (hitObjectCanBlockFutureHits(nested.HitObject)) - applyMiss(nested); - } + if (hitObjectCanBlockFutureHits(obj)) + ((DrawableOsuHitObject)obj).MissForcefully(); } - - static void applyMiss(DrawableHitObject obj) => ((DrawableOsuHitObject)obj).MissForcefully(); - } - - /// - /// Whether a blocks hits on future s until its start time is reached. - /// - /// - /// This will ONLY match on top-most s. - /// - /// The to test. - private static bool drawableCanBlockFutureHits(DrawableHitObject hitObject) - { - // Special considerations for slider tails aren't required since only top-most drawable hitobjects are being iterated over. - return hitObject is DrawableHitCircle || hitObject is DrawableSlider; } /// /// Whether a blocks hits on future s until its start time is reached. /// - /// - /// This is more rigorous and may not match on top-most s as does. - /// /// The to test. - private static bool hitObjectCanBlockFutureHits(HitObject hitObject) - { - // Unlike the above we will receive slider tails, but they do not block future hits. - if (hitObject is SliderTailCircle) - return false; + private static bool hitObjectCanBlockFutureHits(DrawableHitObject hitObject) + => hitObject is DrawableHitCircle; - // All other hitcircles continue to block future hits. - return hitObject is HitCircle; + // Todo: Inefficient + private IEnumerable enumerateHitObjectsUpTo(DrawableHitObject hitObject) + { + return enumerate(hitObjectContainer.AliveObjects); + + IEnumerable enumerate(IEnumerable list) + { + foreach (var obj in list) + { + if (obj.HitObject.StartTime >= hitObject.HitObject.StartTime) + yield break; + + yield return obj; + + foreach (var nested in enumerate(obj.NestedHitObjects)) + yield return nested; + } + } } } } diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 2f222f59b4..4b1a2ce43c 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -86,7 +86,7 @@ namespace osu.Game.Rulesets.Osu.UI private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) { // Hitobjects that block future hits should miss previous hitobjects if they're hit out-of-order. - hitPolicy.HandleHit(result.HitObject); + hitPolicy.HandleHit(judgedObject); if (!judgedObject.DisplayResult || !DisplayJudgements.Value) return; From 62f77a05befb156ac6cda6411f2dda85ffcc8b44 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 17 Apr 2020 14:00:00 +0900 Subject: [PATCH 1106/2376] Optimise by removing state machine --- osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs | 115 +++++++++++++++---- 1 file changed, 95 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs index 171ce6fe61..b55e04ec4c 100644 --- a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs @@ -1,7 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections; using System.Collections.Generic; +using System.Diagnostics; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables; @@ -36,11 +38,14 @@ namespace osu.Game.Rulesets.Osu.UI { DrawableHitObject blockingObject = null; - // Find the last hitobject which blocks future hits. - foreach (var obj in enumerateHitObjectsUpTo(hitObject)) + var enumerator = new HitObjectEnumerator(hitObjectContainer, hitObject.HitObject.StartTime); + + while (enumerator.MoveNext()) { - if (hitObjectCanBlockFutureHits(obj)) - blockingObject = obj; + Debug.Assert(enumerator.Current != null); + + if (hitObjectCanBlockFutureHits(enumerator.Current)) + blockingObject = enumerator.Current; } // If there is no previous hitobject, allow the hit. @@ -67,13 +72,17 @@ namespace osu.Game.Rulesets.Osu.UI if (!hitObjectCanBlockFutureHits(hitObject)) return; - foreach (var obj in enumerateHitObjectsUpTo(hitObject)) + var enumerator = new HitObjectEnumerator(hitObjectContainer, hitObject.HitObject.StartTime); + + while (enumerator.MoveNext()) { - if (obj.Judged) + Debug.Assert(enumerator.Current != null); + + if (enumerator.Current.Judged) continue; - if (hitObjectCanBlockFutureHits(obj)) - ((DrawableOsuHitObject)obj).MissForcefully(); + if (hitObjectCanBlockFutureHits(enumerator.Current)) + ((DrawableOsuHitObject)enumerator.Current).MissForcefully(); } } @@ -84,23 +93,89 @@ namespace osu.Game.Rulesets.Osu.UI private static bool hitObjectCanBlockFutureHits(DrawableHitObject hitObject) => hitObject is DrawableHitCircle; - // Todo: Inefficient - private IEnumerable enumerateHitObjectsUpTo(DrawableHitObject hitObject) + private struct HitObjectEnumerator : IEnumerator { - return enumerate(hitObjectContainer.AliveObjects); + private readonly IEnumerator hitObjectEnumerator; + private readonly double targetTime; - IEnumerable enumerate(IEnumerable list) + private DrawableHitObject currentTopLevel; + private int currentNestedIndex; + + public HitObjectEnumerator(HitObjectContainer hitObjectContainer, double targetTime) { - foreach (var obj in list) - { - if (obj.HitObject.StartTime >= hitObject.HitObject.StartTime) - yield break; + hitObjectEnumerator = hitObjectContainer.AliveObjects.GetEnumerator(); + this.targetTime = targetTime; - yield return obj; + currentTopLevel = null; + currentNestedIndex = -1; + Current = null; + } - foreach (var nested in enumerate(obj.NestedHitObjects)) - yield return nested; - } + /// + /// Attempts to move to the next top-level or nested hitobject. + /// Stops when no such hitobject is found or until the hitobject start time reaches . + /// + /// Whether a new hitobject was moved to. + public bool MoveNext() + { + // If we don't already have a top-level hitobject, try to get one. + if (currentTopLevel == null) + return moveNextTopLevel(); + + // If we have a top-level hitobject, try to move to the next nested hitobject or otherwise move to the next top-level hitobject. + if (!moveNextNested()) + return moveNextTopLevel(); + + // Guaranteed by moveNextNested() to have a hitobject. + return true; + } + + /// + /// Attempts to move to the next top-level hitobject. + /// + /// Whether a new top-level hitobject was found. + private bool moveNextTopLevel() + { + currentNestedIndex = -1; + + hitObjectEnumerator.MoveNext(); + currentTopLevel = hitObjectEnumerator.Current; + + Current = currentTopLevel; + + return Current?.HitObject.StartTime < targetTime; + } + + /// + /// Attempts to move to the next nested hitobject in the current top-level hitobject. + /// + /// Whether a new nested hitobject was moved to. + private bool moveNextNested() + { + currentNestedIndex++; + if (currentNestedIndex >= currentTopLevel.NestedHitObjects.Count) + return false; + + Current = currentTopLevel.NestedHitObjects[currentNestedIndex]; + Debug.Assert(Current != null); + + return Current?.HitObject.StartTime < targetTime; + } + + public void Reset() + { + hitObjectEnumerator.Reset(); + currentTopLevel = null; + currentNestedIndex = -1; + Current = null; + } + + public DrawableHitObject Current { get; set; } + + object IEnumerator.Current => Current; + + public void Dispose() + { } } } From ee5301b887a78a3bd0cabab17c306857133da794 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 17 Apr 2020 14:12:38 +0900 Subject: [PATCH 1107/2376] Fix head/tail circles not getting correct hit windows --- osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs index d6858f831e..3df51be600 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs @@ -371,6 +371,9 @@ namespace osu.Game.Rulesets.Osu.Tests { HeadCircle.HitWindows = new TestHitWindows(); TailCircle.HitWindows = new TestHitWindows(); + + HeadCircle.HitWindows.SetDifficulty(0); + TailCircle.HitWindows.SetDifficulty(0); }; } } From 08df9d49e52a968691b77a76fe360c3111eb3436 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 17 Apr 2020 14:12:43 +0900 Subject: [PATCH 1108/2376] Add failing test --- .../TestSceneOutOfOrderHits.cs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs index 3df51be600..40ee53e8f2 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs @@ -296,6 +296,44 @@ namespace osu.Game.Rulesets.Osu.Tests addJudgementAssert(hitObjects[1], HitResult.Great); } + [Test] + public void TestHitSliderHeadBeforeHitCircle() + { + const double time_circle = 1000; + const double time_slider = 1200; + Vector2 positionCircle = Vector2.Zero; + Vector2 positionSlider = new Vector2(80); + + var hitObjects = new List + { + new TestHitCircle + { + StartTime = time_circle, + Position = positionCircle + }, + new TestSlider + { + StartTime = time_slider, + Position = positionSlider, + Path = new SliderPath(PathType.Linear, new[] + { + Vector2.Zero, + new Vector2(25, 0), + }) + } + }; + + performTest(hitObjects, new List + { + new OsuReplayFrame { Time = time_circle - 100, Position = positionSlider, Actions = { OsuAction.LeftButton } }, + new OsuReplayFrame { Time = time_circle, Position = positionCircle, Actions = { OsuAction.RightButton } }, + new OsuReplayFrame { Time = time_slider, Position = positionSlider, Actions = { OsuAction.LeftButton } }, + }); + + addJudgementAssert(hitObjects[0], HitResult.Great); + addJudgementAssert(hitObjects[1], HitResult.Great); + } + private void addJudgementAssert(OsuHitObject hitObject, HitResult result) { AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judgement is {result}", From a4a782381797f927bc80f108cbbf94f410faef99 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 17 Apr 2020 14:22:03 +0900 Subject: [PATCH 1109/2376] Add fail-safe to ensure hittability after a hit --- osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs index b55e04ec4c..31edefea83 100644 --- a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.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; using System.Collections.Generic; using System.Diagnostics; @@ -72,6 +73,9 @@ namespace osu.Game.Rulesets.Osu.UI if (!hitObjectCanBlockFutureHits(hitObject)) return; + if (!IsHittable(hitObject, hitObject.HitObject.StartTime + hitObject.Result.TimeOffset)) + throw new InvalidOperationException($"A {hitObject} was hit before it become hittable!"); + var enumerator = new HitObjectEnumerator(hitObjectContainer, hitObject.HitObject.StartTime); while (enumerator.MoveNext()) From 4e4fe5cc904107ac647ffaa57d5ac17361ee073e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 17 Apr 2020 14:33:29 +0900 Subject: [PATCH 1110/2376] Fix slider heads not being blocked when hit out of order --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 522217a916..72502c02cd 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -125,7 +125,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables return new DrawableSliderTail(slider, tail); case SliderHeadCircle head: - return new DrawableSliderHead(slider, head) { OnShake = Shake }; + return new DrawableSliderHead(slider, head) + { + OnShake = Shake, + CheckHittable = (d, t) => CheckHittable?.Invoke(d, t) ?? true + }; case SliderTick tick: return new DrawableSliderTick(tick) { Position = tick.Position - slider.Position }; From 2dee5e03e30f158880a09aaa04ded47879d6f74d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 17 Apr 2020 14:40:29 +0900 Subject: [PATCH 1111/2376] Dispose enumerators for safety --- osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs | 31 +++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs index 31edefea83..4bc7da4794 100644 --- a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs @@ -39,14 +39,15 @@ namespace osu.Game.Rulesets.Osu.UI { DrawableHitObject blockingObject = null; - var enumerator = new HitObjectEnumerator(hitObjectContainer, hitObject.HitObject.StartTime); - - while (enumerator.MoveNext()) + using (var enumerator = new HitObjectEnumerator(hitObjectContainer, hitObject.HitObject.StartTime)) { - Debug.Assert(enumerator.Current != null); + while (enumerator.MoveNext()) + { + Debug.Assert(enumerator.Current != null); - if (hitObjectCanBlockFutureHits(enumerator.Current)) - blockingObject = enumerator.Current; + if (hitObjectCanBlockFutureHits(enumerator.Current)) + blockingObject = enumerator.Current; + } } // If there is no previous hitobject, allow the hit. @@ -76,17 +77,18 @@ namespace osu.Game.Rulesets.Osu.UI if (!IsHittable(hitObject, hitObject.HitObject.StartTime + hitObject.Result.TimeOffset)) throw new InvalidOperationException($"A {hitObject} was hit before it become hittable!"); - var enumerator = new HitObjectEnumerator(hitObjectContainer, hitObject.HitObject.StartTime); - - while (enumerator.MoveNext()) + using (var enumerator = new HitObjectEnumerator(hitObjectContainer, hitObject.HitObject.StartTime)) { - Debug.Assert(enumerator.Current != null); + while (enumerator.MoveNext()) + { + Debug.Assert(enumerator.Current != null); - if (enumerator.Current.Judged) - continue; + if (enumerator.Current.Judged) + continue; - if (hitObjectCanBlockFutureHits(enumerator.Current)) - ((DrawableOsuHitObject)enumerator.Current).MissForcefully(); + if (hitObjectCanBlockFutureHits(enumerator.Current)) + ((DrawableOsuHitObject)enumerator.Current).MissForcefully(); + } } } @@ -180,6 +182,7 @@ namespace osu.Game.Rulesets.Osu.UI public void Dispose() { + hitObjectEnumerator?.Dispose(); } } } From bbcbd7e3fbc790e91415bc96ffcfb93c63bcffc6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 17 Apr 2020 14:48:12 +0900 Subject: [PATCH 1112/2376] Simplify by removing custom enumerator --- osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs | 118 +++---------------- 1 file changed, 19 insertions(+), 99 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs index 4bc7da4794..cd9838e7bf 100644 --- a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs @@ -2,9 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections; using System.Collections.Generic; -using System.Diagnostics; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables; @@ -39,15 +37,10 @@ namespace osu.Game.Rulesets.Osu.UI { DrawableHitObject blockingObject = null; - using (var enumerator = new HitObjectEnumerator(hitObjectContainer, hitObject.HitObject.StartTime)) + foreach (var obj in enumerateHitObjectsUpTo(hitObject.HitObject.StartTime)) { - while (enumerator.MoveNext()) - { - Debug.Assert(enumerator.Current != null); - - if (hitObjectCanBlockFutureHits(enumerator.Current)) - blockingObject = enumerator.Current; - } + if (hitObjectCanBlockFutureHits(obj)) + blockingObject = obj; } // If there is no previous hitobject, allow the hit. @@ -77,18 +70,13 @@ namespace osu.Game.Rulesets.Osu.UI if (!IsHittable(hitObject, hitObject.HitObject.StartTime + hitObject.Result.TimeOffset)) throw new InvalidOperationException($"A {hitObject} was hit before it become hittable!"); - using (var enumerator = new HitObjectEnumerator(hitObjectContainer, hitObject.HitObject.StartTime)) + foreach (var obj in enumerateHitObjectsUpTo(hitObject.HitObject.StartTime)) { - while (enumerator.MoveNext()) - { - Debug.Assert(enumerator.Current != null); + if (obj.Judged) + continue; - if (enumerator.Current.Judged) - continue; - - if (hitObjectCanBlockFutureHits(enumerator.Current)) - ((DrawableOsuHitObject)enumerator.Current).MissForcefully(); - } + if (hitObjectCanBlockFutureHits(obj)) + ((DrawableOsuHitObject)obj).MissForcefully(); } } @@ -99,90 +87,22 @@ namespace osu.Game.Rulesets.Osu.UI private static bool hitObjectCanBlockFutureHits(DrawableHitObject hitObject) => hitObject is DrawableHitCircle; - private struct HitObjectEnumerator : IEnumerator + private IEnumerable enumerateHitObjectsUpTo(double targetTime) { - private readonly IEnumerator hitObjectEnumerator; - private readonly double targetTime; - - private DrawableHitObject currentTopLevel; - private int currentNestedIndex; - - public HitObjectEnumerator(HitObjectContainer hitObjectContainer, double targetTime) + foreach (var obj in hitObjectContainer.AliveObjects) { - hitObjectEnumerator = hitObjectContainer.AliveObjects.GetEnumerator(); - this.targetTime = targetTime; + if (obj.HitObject.StartTime >= targetTime) + yield break; - currentTopLevel = null; - currentNestedIndex = -1; - Current = null; - } + yield return obj; - /// - /// Attempts to move to the next top-level or nested hitobject. - /// Stops when no such hitobject is found or until the hitobject start time reaches . - /// - /// Whether a new hitobject was moved to. - public bool MoveNext() - { - // If we don't already have a top-level hitobject, try to get one. - if (currentTopLevel == null) - return moveNextTopLevel(); + for (int i = 0; i < obj.NestedHitObjects.Count; i++) + { + if (obj.NestedHitObjects[i].HitObject.StartTime >= targetTime) + break; - // If we have a top-level hitobject, try to move to the next nested hitobject or otherwise move to the next top-level hitobject. - if (!moveNextNested()) - return moveNextTopLevel(); - - // Guaranteed by moveNextNested() to have a hitobject. - return true; - } - - /// - /// Attempts to move to the next top-level hitobject. - /// - /// Whether a new top-level hitobject was found. - private bool moveNextTopLevel() - { - currentNestedIndex = -1; - - hitObjectEnumerator.MoveNext(); - currentTopLevel = hitObjectEnumerator.Current; - - Current = currentTopLevel; - - return Current?.HitObject.StartTime < targetTime; - } - - /// - /// Attempts to move to the next nested hitobject in the current top-level hitobject. - /// - /// Whether a new nested hitobject was moved to. - private bool moveNextNested() - { - currentNestedIndex++; - if (currentNestedIndex >= currentTopLevel.NestedHitObjects.Count) - return false; - - Current = currentTopLevel.NestedHitObjects[currentNestedIndex]; - Debug.Assert(Current != null); - - return Current?.HitObject.StartTime < targetTime; - } - - public void Reset() - { - hitObjectEnumerator.Reset(); - currentTopLevel = null; - currentNestedIndex = -1; - Current = null; - } - - public DrawableHitObject Current { get; set; } - - object IEnumerator.Current => Current; - - public void Dispose() - { - hitObjectEnumerator?.Dispose(); + yield return obj.NestedHitObjects[i]; + } } } } From 69fb984e71fae2c371b19de763cbf8a80ff861d2 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 17 Apr 2020 17:04:09 +0900 Subject: [PATCH 1113/2376] Remove EquivalentTo() and Equals() --- osu.Game/Beatmaps/ControlPoints/ControlPoint.cs | 11 +---------- .../Beatmaps/ControlPoints/DifficultyControlPoint.cs | 7 +++---- osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs | 10 +++++----- osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs | 9 ++++----- osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs | 9 ++------- osu.Game/Beatmaps/Formats/LegacyDecoder.cs | 7 ++++--- osu.Game/Graphics/Containers/BeatSyncedContainer.cs | 2 +- 7 files changed, 20 insertions(+), 35 deletions(-) diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs index 9599ad184b..f9bb3877d3 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs @@ -5,7 +5,7 @@ using System; namespace osu.Game.Beatmaps.ControlPoints { - public abstract class ControlPoint : IComparable, IEquatable + public abstract class ControlPoint : IComparable { /// /// The time at which the control point takes effect. @@ -18,13 +18,6 @@ namespace osu.Game.Beatmaps.ControlPoints public int CompareTo(ControlPoint other) => Time.CompareTo(other.Time); - /// - /// Whether this control point is equivalent to another, ignoring time. - /// - /// Another control point to compare with. - /// Whether equivalent. - public abstract bool EquivalentTo(ControlPoint other); - /// /// Whether this control point results in a meaningful change when placed after another. /// @@ -32,7 +25,5 @@ namespace osu.Game.Beatmaps.ControlPoints /// The time this control point will be placed at if it is added. /// Whether redundant. public abstract bool IsRedundant(ControlPoint existing, double time); - - public bool Equals(ControlPoint other) => Time == other?.Time && EquivalentTo(other); } } diff --git a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs index dc856b0a0a..42140462cb 100644 --- a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs @@ -27,9 +27,8 @@ namespace osu.Game.Beatmaps.ControlPoints set => SpeedMultiplierBindable.Value = value; } - public override bool EquivalentTo(ControlPoint other) => - other is DifficultyControlPoint otherTyped && otherTyped.SpeedMultiplier.Equals(SpeedMultiplier); - - public override bool IsRedundant(ControlPoint existing, double time) => EquivalentTo(existing); + public override bool IsRedundant(ControlPoint existing, double time) + => existing is DifficultyControlPoint existingDifficulty + && SpeedMultiplier == existingDifficulty.SpeedMultiplier; } } diff --git a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs index d050f44ba4..f7a232c394 100644 --- a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs @@ -35,10 +35,10 @@ namespace osu.Game.Beatmaps.ControlPoints set => KiaiModeBindable.Value = value; } - public override bool EquivalentTo(ControlPoint other) => - other is EffectControlPoint otherTyped && - KiaiMode == otherTyped.KiaiMode && OmitFirstBarLine == otherTyped.OmitFirstBarLine; - - public override bool IsRedundant(ControlPoint existing, double time) => !OmitFirstBarLine && EquivalentTo(existing); + public override bool IsRedundant(ControlPoint existing, double time) + => !OmitFirstBarLine + && existing is EffectControlPoint existingEffect + && KiaiMode == existingEffect.KiaiMode + && OmitFirstBarLine == existingEffect.OmitFirstBarLine; } } diff --git a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs index 38edbe70da..0fced16b4d 100644 --- a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs @@ -68,10 +68,9 @@ namespace osu.Game.Beatmaps.ControlPoints return newSampleInfo; } - public override bool EquivalentTo(ControlPoint other) => - other is SampleControlPoint otherTyped && - SampleBank == otherTyped.SampleBank && SampleVolume == otherTyped.SampleVolume; - - public override bool IsRedundant(ControlPoint existing, double time) => EquivalentTo(existing); + public override bool IsRedundant(ControlPoint existing, double time) + => existing is SampleControlPoint existingSample + && SampleBank == existingSample.SampleBank + && SampleVolume == existingSample.SampleVolume; } } diff --git a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs index 316c603ece..27f4662d49 100644 --- a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs @@ -48,12 +48,7 @@ namespace osu.Game.Beatmaps.ControlPoints /// public double BPM => 60000 / BeatLength; - public override bool EquivalentTo(ControlPoint other) => - other is TimingControlPoint otherTyped - && TimeSignature == otherTyped.TimeSignature && BeatLength.Equals(otherTyped.BeatLength); - - public override bool IsRedundant(ControlPoint existing, double time) => - EquivalentTo(existing) - && existing.Time == time; + // Timing points are never redundant as they can change the time signature. + public override bool IsRedundant(ControlPoint existing, double time) => false; } } diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index 561707f9ef..5fa1da111d 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -174,9 +174,10 @@ namespace osu.Game.Beatmaps.Formats return baseInfo; } - public override bool EquivalentTo(ControlPoint other) => - base.EquivalentTo(other) && other is LegacySampleControlPoint otherTyped && - CustomSampleBank == otherTyped.CustomSampleBank; + public override bool IsRedundant(ControlPoint existing, double time) + => base.IsRedundant(existing, time) + && existing is LegacySampleControlPoint existingSample + && CustomSampleBank == existingSample.CustomSampleBank; } } } diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs index f36079682e..5a613d1a54 100644 --- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs +++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs @@ -103,7 +103,7 @@ namespace osu.Game.Graphics.Containers TimeSinceLastBeat = beatLength - TimeUntilNextBeat; - if (timingPoint.Equals(lastTimingPoint) && beatIndex == lastBeat) + if (timingPoint == lastTimingPoint && beatIndex == lastBeat) return; using (BeginDelayedSequence(-TimeSinceLastBeat, true)) From 9aac98664ce9379938cea94e62dc3bb31df13a26 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 17 Apr 2020 17:06:12 +0900 Subject: [PATCH 1114/2376] Remove unnecessary time property --- osu.Game/Beatmaps/ControlPoints/ControlPoint.cs | 7 +++---- osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs | 2 +- osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs | 2 +- osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs | 2 +- osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs | 2 +- osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs | 2 +- osu.Game/Beatmaps/Formats/LegacyDecoder.cs | 4 ++-- 7 files changed, 10 insertions(+), 11 deletions(-) diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs index f9bb3877d3..a1822a1163 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs @@ -19,11 +19,10 @@ namespace osu.Game.Beatmaps.ControlPoints public int CompareTo(ControlPoint other) => Time.CompareTo(other.Time); /// - /// Whether this control point results in a meaningful change when placed after another. + /// Determines whether this results in a meaningful change when placed alongside another. /// /// An existing control point to compare with. - /// The time this control point will be placed at if it is added. - /// Whether redundant. - public abstract bool IsRedundant(ControlPoint existing, double time); + /// Whether this is redundant when placed alongside . + public abstract bool IsRedundant(ControlPoint existing); } } diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs index 37a3dbf592..8e4079f776 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs @@ -247,7 +247,7 @@ namespace osu.Game.Beatmaps.ControlPoints break; } - return newPoint.IsRedundant(existing, time); + return newPoint.IsRedundant(existing); } private void groupItemAdded(ControlPoint controlPoint) diff --git a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs index 42140462cb..2448b2b25c 100644 --- a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs @@ -27,7 +27,7 @@ namespace osu.Game.Beatmaps.ControlPoints set => SpeedMultiplierBindable.Value = value; } - public override bool IsRedundant(ControlPoint existing, double time) + public override bool IsRedundant(ControlPoint existing) => existing is DifficultyControlPoint existingDifficulty && SpeedMultiplier == existingDifficulty.SpeedMultiplier; } diff --git a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs index f7a232c394..9b69147468 100644 --- a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs @@ -35,7 +35,7 @@ namespace osu.Game.Beatmaps.ControlPoints set => KiaiModeBindable.Value = value; } - public override bool IsRedundant(ControlPoint existing, double time) + public override bool IsRedundant(ControlPoint existing) => !OmitFirstBarLine && existing is EffectControlPoint existingEffect && KiaiMode == existingEffect.KiaiMode diff --git a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs index 0fced16b4d..61851a00d7 100644 --- a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs @@ -68,7 +68,7 @@ namespace osu.Game.Beatmaps.ControlPoints return newSampleInfo; } - public override bool IsRedundant(ControlPoint existing, double time) + public override bool IsRedundant(ControlPoint existing) => existing is SampleControlPoint existingSample && SampleBank == existingSample.SampleBank && SampleVolume == existingSample.SampleVolume; diff --git a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs index 27f4662d49..1927dd6575 100644 --- a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs @@ -49,6 +49,6 @@ namespace osu.Game.Beatmaps.ControlPoints public double BPM => 60000 / BeatLength; // Timing points are never redundant as they can change the time signature. - public override bool IsRedundant(ControlPoint existing, double time) => false; + public override bool IsRedundant(ControlPoint existing) => false; } } diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index 5fa1da111d..556527bfd5 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -174,8 +174,8 @@ namespace osu.Game.Beatmaps.Formats return baseInfo; } - public override bool IsRedundant(ControlPoint existing, double time) - => base.IsRedundant(existing, time) + public override bool IsRedundant(ControlPoint existing) + => base.IsRedundant(existing) && existing is LegacySampleControlPoint existingSample && CustomSampleBank == existingSample.CustomSampleBank; } From 0fba93bf658d917286defb5b029fcf5bc0f1b566 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 17 Apr 2020 17:10:13 +0900 Subject: [PATCH 1115/2376] Add back null check --- osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs index 8e4079f776..d33a922a32 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs @@ -247,7 +247,7 @@ namespace osu.Game.Beatmaps.ControlPoints break; } - return newPoint.IsRedundant(existing); + return newPoint?.IsRedundant(existing) == true; } private void groupItemAdded(ControlPoint controlPoint) From 67bd7bfa3905aa96f21f2225a517e2e369e80540 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 17 Apr 2020 06:17:15 +0300 Subject: [PATCH 1116/2376] Add `CreateRuleset` in OsuTestScene for scenes that depend on it --- osu.Game/Tests/Visual/OsuTestScene.cs | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index 5dc8714c07..8058a074ef 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -70,7 +70,18 @@ namespace osu.Game.Tests.Visual Beatmap.SetDefault(); Ruleset = Dependencies.Ruleset; - Ruleset.SetDefault(); + + var definedRuleset = CreateRuleset()?.RulesetInfo; + + if (definedRuleset != null) + { + // Set global ruleset bindable to the ruleset defined + // for this test scene and disallow changing it. + Ruleset.Value = definedRuleset; + Ruleset.Disabled = true; + } + else + Ruleset.SetDefault(); SelectedMods = Dependencies.Mods; SelectedMods.SetDefault(); @@ -124,6 +135,14 @@ namespace osu.Game.Tests.Visual [Resolved] protected AudioManager Audio { get; private set; } + /// + /// Creates the ruleset to be used for this test scene. + /// + /// + /// When testing against ruleset-specific components, this method must be overriden to their ruleset. + /// + protected virtual Ruleset CreateRuleset() => null; + protected virtual IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset); protected WorkingBeatmap CreateWorkingBeatmap(RulesetInfo ruleset) => @@ -135,7 +154,8 @@ namespace osu.Game.Tests.Visual [BackgroundDependencyLoader] private void load(RulesetStore rulesets) { - Ruleset.Value = rulesets.AvailableRulesets.First(); + if (!Ruleset.Disabled) + Ruleset.Value = rulesets.AvailableRulesets.First(); } protected override void Dispose(bool isDisposing) From 5fa6bcb5a3f73b080873f48ee34a7dcec0a9da58 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 17 Apr 2020 11:17:14 +0300 Subject: [PATCH 1117/2376] Move `SkinnableTestScene` into using the global `CreateRuleset` method --- .../CatchSkinnableTestScene.cs | 2 +- .../Skinning/ManiaSkinnableTestScene.cs | 4 ++-- .../OsuSkinnableTestScene.cs | 2 +- .../TaikoSkinnableTestScene.cs | 2 +- osu.Game/Tests/Visual/SkinnableTestScene.cs | 13 ++++++++----- 5 files changed, 13 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/CatchSkinnableTestScene.cs b/osu.Game.Rulesets.Catch.Tests/CatchSkinnableTestScene.cs index 0c46b078b5..c0060af74a 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchSkinnableTestScene.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchSkinnableTestScene.cs @@ -16,6 +16,6 @@ namespace osu.Game.Rulesets.Catch.Tests typeof(CatchLegacySkinTransformer), }; - protected override Ruleset CreateRulesetForSkinProvider() => new CatchRuleset(); + protected override Ruleset CreateRuleset() => new CatchRuleset(); } } diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs index a3c1d518c5..f41ba4db42 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs @@ -34,8 +34,6 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning typeof(ManiaSettingsSubsection) }; - protected override Ruleset CreateRulesetForSkinProvider() => new ManiaRuleset(); - protected ManiaSkinnableTestScene() { scrollingInfo.Direction.Value = ScrollingDirection.Down; @@ -60,6 +58,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning AddStep("change direction to up", () => scrollingInfo.Direction.Value = ScrollingDirection.Up); } + protected override Ruleset CreateRuleset() => new ManiaRuleset(); + private class TestScrollingInfo : IScrollingInfo { public readonly Bindable Direction = new Bindable(); diff --git a/osu.Game.Rulesets.Osu.Tests/OsuSkinnableTestScene.cs b/osu.Game.Rulesets.Osu.Tests/OsuSkinnableTestScene.cs index 90ebbd9f04..1458270193 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuSkinnableTestScene.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuSkinnableTestScene.cs @@ -16,6 +16,6 @@ namespace osu.Game.Rulesets.Osu.Tests typeof(OsuLegacySkinTransformer), }; - protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset(); + protected override Ruleset CreateRuleset() => new OsuRuleset(); } } diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoSkinnableTestScene.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoSkinnableTestScene.cs index 6db2a6907f..98e6c2ec52 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoSkinnableTestScene.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoSkinnableTestScene.cs @@ -16,6 +16,6 @@ namespace osu.Game.Rulesets.Taiko.Tests typeof(TaikoLegacySkinTransformer), }; - protected override Ruleset CreateRulesetForSkinProvider() => new TaikoRuleset(); + protected override Ruleset CreateRuleset() => new TaikoRuleset(); } } diff --git a/osu.Game/Tests/Visual/SkinnableTestScene.cs b/osu.Game/Tests/Visual/SkinnableTestScene.cs index ace24c0d7e..d648afd504 100644 --- a/osu.Game/Tests/Visual/SkinnableTestScene.cs +++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs @@ -13,7 +13,6 @@ using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; -using osu.Game.Rulesets; using osu.Game.Skinning; using osuTK; using osuTK.Graphics; @@ -30,11 +29,15 @@ namespace osu.Game.Tests.Visual protected SkinnableTestScene() : base(2, 3) { + // avoid running silently incorrectly. + if (CreateRuleset() == null) + { + throw new InvalidOperationException( + $"No ruleset provided, override {nameof(CreateRuleset)} to the ruleset belonging to the skinnable content." + + "This is required to add the legacy skin transformer for the content to behave as expected."); + } } - // Required to be part of the per-ruleset implementation to construct the newer version of the Ruleset. - protected abstract Ruleset CreateRulesetForSkinProvider(); - [BackgroundDependencyLoader] private void load(AudioManager audio, SkinManager skinManager) { @@ -107,7 +110,7 @@ namespace osu.Game.Tests.Visual { new OutlineBox { Alpha = autoSize ? 1 : 0 }, mainProvider.WithChild( - new SkinProvidingContainer(CreateRulesetForSkinProvider().CreateLegacySkinProvider(mainProvider, beatmap)) + new SkinProvidingContainer(Ruleset.Value.CreateInstance().CreateLegacySkinProvider(mainProvider, beatmap)) { Child = created, RelativeSizeAxes = !autoSize ? Axes.Both : Axes.None, From 92df4e3a9eb8ad56c6da99b088d0159a419c8110 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 17 Apr 2020 10:32:12 +0300 Subject: [PATCH 1118/2376] Remove `PlayerTestScene` constructor and use `CreateRuleset` method instead --- .../TestSceneAutoJuiceStream.cs | 7 +------ .../TestSceneBananaShower.cs | 8 +------ .../TestSceneCatchPlayer.cs | 10 ++++++--- .../TestSceneCatchStacker.cs | 8 +------ .../TestSceneHyperDash.cs | 8 +------ .../TestSceneJuiceStream.cs | 8 +------ .../TestSceneManiaPlayer.cs | 19 +++++++++++++++++ .../TestScenePlayer.cs | 15 ------------- .../TestSceneHitCircleLongCombo.cs | 8 +------ .../TestSceneOsuPlayer.cs | 10 ++++++--- .../TestSceneSkinFallbacks.cs | 3 +-- .../TestSceneSwellJudgements.cs | 8 +------ .../TestSceneTaikoPlayer.cs | 19 +++++++++++++++++ .../TestSceneTaikoSuddenDeath.cs | 7 +------ .../Gameplay/TestSceneHitObjectSamples.cs | 10 ++------- .../Visual/Gameplay/TestPlayerTestScene.cs | 16 ++++++++++++++ .../Gameplay/TestSceneGameplayRewinding.cs | 8 +------ .../Visual/Gameplay/TestScenePause.cs | 4 +--- .../Gameplay/TestScenePauseWhenInactive.cs | 8 +------ osu.Game/Tests/Visual/PlayerTestScene.cs | 21 +++++++------------ 20 files changed, 89 insertions(+), 116 deletions(-) create mode 100644 osu.Game.Rulesets.Mania.Tests/TestSceneManiaPlayer.cs delete mode 100644 osu.Game.Rulesets.Mania.Tests/TestScenePlayer.cs create mode 100644 osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayer.cs create mode 100644 osu.Game.Tests/Visual/Gameplay/TestPlayerTestScene.cs diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs index ed7bfb9a44..7c2304694f 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs @@ -12,13 +12,8 @@ using osuTK; namespace osu.Game.Rulesets.Catch.Tests { - public class TestSceneAutoJuiceStream : PlayerTestScene + public class TestSceneAutoJuiceStream : TestSceneCatchPlayer { - public TestSceneAutoJuiceStream() - : base(new CatchRuleset()) - { - } - protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) { var beatmap = new Beatmap diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.cs index 024c4cefb0..56f94e609f 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.cs @@ -8,12 +8,11 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.UI; -using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Catch.Tests { [TestFixture] - public class TestSceneBananaShower : PlayerTestScene + public class TestSceneBananaShower : TestSceneCatchPlayer { public override IReadOnlyList RequiredTypes => new[] { @@ -26,11 +25,6 @@ namespace osu.Game.Rulesets.Catch.Tests typeof(DrawableCatchRuleset), }; - public TestSceneBananaShower() - : base(new CatchRuleset()) - { - } - [Test] public void TestBananaShower() { diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayer.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayer.cs index 9836a7811a..722f3b5a3b 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayer.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayer.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Collections.Generic; using NUnit.Framework; using osu.Game.Tests.Visual; @@ -9,9 +11,11 @@ namespace osu.Game.Rulesets.Catch.Tests [TestFixture] public class TestSceneCatchPlayer : PlayerTestScene { - public TestSceneCatchPlayer() - : base(new CatchRuleset()) + public override IReadOnlyList RequiredTypes => new[] { - } + typeof(CatchRuleset), + }; + + protected override Ruleset CreateRuleset() => new CatchRuleset(); } } diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchStacker.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchStacker.cs index 9ce46ad6ba..44672b6526 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchStacker.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchStacker.cs @@ -4,18 +4,12 @@ using NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Objects; -using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Catch.Tests { [TestFixture] - public class TestSceneCatchStacker : PlayerTestScene + public class TestSceneCatchStacker : TestSceneCatchPlayer { - public TestSceneCatchStacker() - : base(new CatchRuleset()) - { - } - protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) { var beatmap = new Beatmap diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs index 49ff9df4d7..75b8b68c14 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs @@ -10,24 +10,18 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Objects; -using osu.Game.Tests.Visual; using osuTK; namespace osu.Game.Rulesets.Catch.Tests { [TestFixture] - public class TestSceneHyperDash : PlayerTestScene + public class TestSceneHyperDash : TestSceneCatchPlayer { public override IReadOnlyList RequiredTypes => new[] { typeof(CatcherArea), }; - public TestSceneHyperDash() - : base(new CatchRuleset()) - { - } - protected override bool Autoplay => true; [Test] diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.cs index cbc87459e1..ffcf61a4bf 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.cs @@ -7,18 +7,12 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; -using osu.Game.Tests.Visual; using osuTK; namespace osu.Game.Rulesets.Catch.Tests { - public class TestSceneJuiceStream : PlayerTestScene + public class TestSceneJuiceStream : TestSceneCatchPlayer { - public TestSceneJuiceStream() - : base(new CatchRuleset()) - { - } - [Test] public void TestJuiceStreamEndingCombo() { diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaPlayer.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaPlayer.cs new file mode 100644 index 0000000000..11663605e2 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaPlayer.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 System; +using System.Collections.Generic; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Mania.Tests +{ + public class TestSceneManiaPlayer : PlayerTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(ManiaRuleset), + }; + + protected override Ruleset CreateRuleset() => new ManiaRuleset(); + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/TestScenePlayer.cs b/osu.Game.Rulesets.Mania.Tests/TestScenePlayer.cs deleted file mode 100644 index cd25d162d0..0000000000 --- a/osu.Game.Rulesets.Mania.Tests/TestScenePlayer.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Tests.Visual; - -namespace osu.Game.Rulesets.Mania.Tests -{ - public class TestScenePlayer : PlayerTestScene - { - public TestScenePlayer() - : base(new ManiaRuleset()) - { - } - } -} diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLongCombo.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLongCombo.cs index b99cd523ff..8cf29ddfbf 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLongCombo.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLongCombo.cs @@ -4,19 +4,13 @@ using NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Tests.Visual; using osuTK; namespace osu.Game.Rulesets.Osu.Tests { [TestFixture] - public class TestSceneHitCircleLongCombo : PlayerTestScene + public class TestSceneHitCircleLongCombo : TestSceneOsuPlayer { - public TestSceneHitCircleLongCombo() - : base(new OsuRuleset()) - { - } - protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) { var beatmap = new Beatmap diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuPlayer.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuPlayer.cs index 0a33b09ba8..102f8bf841 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuPlayer.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuPlayer.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Collections.Generic; using NUnit.Framework; using osu.Game.Tests.Visual; @@ -9,9 +11,11 @@ namespace osu.Game.Rulesets.Osu.Tests [TestFixture] public class TestSceneOsuPlayer : PlayerTestScene { - public TestSceneOsuPlayer() - : base(new OsuRuleset()) + public override IReadOnlyList RequiredTypes => new[] { - } + typeof(OsuRuleset), + }; + + protected override Ruleset CreateRuleset() => new OsuRuleset(); } } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs index d39e24fc1f..b357e20ee8 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs @@ -25,13 +25,12 @@ using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Osu.Tests { [TestFixture] - public class TestSceneSkinFallbacks : PlayerTestScene + public class TestSceneSkinFallbacks : TestSceneOsuPlayer { private readonly TestSource testUserSkin; private readonly TestSource testBeatmapSkin; public TestSceneSkinFallbacks() - : base(new OsuRuleset()) { testUserSkin = new TestSource("user"); testBeatmapSkin = new TestSource("beatmap"); diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneSwellJudgements.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneSwellJudgements.cs index 303f0163b1..965cde0f3f 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneSwellJudgements.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneSwellJudgements.cs @@ -5,17 +5,11 @@ using System.Linq; using NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Rulesets.Taiko.Objects; -using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Taiko.Tests { - public class TestSceneSwellJudgements : PlayerTestScene + public class TestSceneSwellJudgements : TestSceneTaikoPlayer { - public TestSceneSwellJudgements() - : base(new TaikoRuleset()) - { - } - [Test] public void TestZeroTickTimeOffsets() { diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayer.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayer.cs new file mode 100644 index 0000000000..4c5ab7eabf --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayer.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 System; +using System.Collections.Generic; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Taiko.Tests +{ + public class TestSceneTaikoPlayer : PlayerTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(TaikoRuleset) + }; + + protected override Ruleset CreateRuleset() => new TaikoRuleset(); + } +} diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs index 2ab041e191..aaa634648a 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs @@ -11,13 +11,8 @@ using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Taiko.Tests { - public class TestSceneTaikoSuddenDeath : PlayerTestScene + public class TestSceneTaikoSuddenDeath : TestSceneTaikoPlayer { - public TestSceneTaikoSuddenDeath() - : base(new TaikoRuleset()) - { - } - protected override bool AllowFail => true; protected override TestPlayer CreatePlayer(Ruleset ruleset) diff --git a/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs index f611f2717e..7d3d8b7f16 100644 --- a/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs @@ -16,17 +16,16 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; using osu.Game.IO; using osu.Game.Rulesets; -using osu.Game.Rulesets.Osu; using osu.Game.Skinning; using osu.Game.Storyboards; using osu.Game.Tests.Resources; -using osu.Game.Tests.Visual; +using osu.Game.Tests.Visual.Gameplay; using osu.Game.Users; namespace osu.Game.Tests.Gameplay { [HeadlessTest] - public class TestSceneHitObjectSamples : PlayerTestScene + public class TestSceneHitObjectSamples : TestPlayerTestScene { private readonly SkinInfo userSkinInfo = new SkinInfo(); @@ -44,11 +43,6 @@ namespace osu.Game.Tests.Gameplay protected override bool HasCustomSteps => true; - public TestSceneHitObjectSamples() - : base(new OsuRuleset()) - { - } - private SkinSourceDependencyContainer dependencies; protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) diff --git a/osu.Game.Tests/Visual/Gameplay/TestPlayerTestScene.cs b/osu.Game.Tests/Visual/Gameplay/TestPlayerTestScene.cs new file mode 100644 index 0000000000..2130171449 --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestPlayerTestScene.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 osu.Game.Rulesets; +using osu.Game.Rulesets.Osu; + +namespace osu.Game.Tests.Visual.Gameplay +{ + /// + /// A with an arbitrary ruleset value to test with. + /// + public abstract class TestPlayerTestScene : PlayerTestScene + { + protected override Ruleset CreateRuleset() => new OsuRuleset(); + } +} diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs index 310746d179..744eeed022 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs @@ -10,23 +10,17 @@ using osu.Framework.Utils; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Rulesets; -using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Storyboards; using osuTK; namespace osu.Game.Tests.Visual.Gameplay { - public class TestSceneGameplayRewinding : PlayerTestScene + public class TestSceneGameplayRewinding : TestPlayerTestScene { [Resolved] private AudioManager audioManager { get; set; } - public TestSceneGameplayRewinding() - : base(new OsuRuleset()) - { - } - private Track track; protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index 944e6ca6be..411265d600 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -10,14 +10,13 @@ using osu.Framework.Testing; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; using osu.Game.Rulesets; -using osu.Game.Rulesets.Osu; using osu.Game.Screens.Play; using osuTK; using osuTK.Input; namespace osu.Game.Tests.Visual.Gameplay { - public class TestScenePause : PlayerTestScene + public class TestScenePause : TestPlayerTestScene { protected new PausePlayer Player => (PausePlayer)base.Player; @@ -26,7 +25,6 @@ namespace osu.Game.Tests.Visual.Gameplay protected override Container Content => content; public TestScenePause() - : base(new OsuRuleset()) { base.Content.Add(content = new MenuCursorContainer { RelativeSizeAxes = Axes.Both }); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs index a83320048b..20911bfa4d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs @@ -8,12 +8,11 @@ using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Rulesets; -using osu.Game.Rulesets.Osu; namespace osu.Game.Tests.Visual.Gameplay { [HeadlessTest] // we alter unsafe properties on the game host to test inactive window state. - public class TestScenePauseWhenInactive : PlayerTestScene + public class TestScenePauseWhenInactive : TestPlayerTestScene { protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) { @@ -27,11 +26,6 @@ namespace osu.Game.Tests.Visual.Gameplay [Resolved] private GameHost host { get; set; } - public TestScenePauseWhenInactive() - : base(new OsuRuleset()) - { - } - [Test] public void TestDoesntPauseDuringIntro() { diff --git a/osu.Game/Tests/Visual/PlayerTestScene.cs b/osu.Game/Tests/Visual/PlayerTestScene.cs index 9e852719e0..f5e78fbbd1 100644 --- a/osu.Game/Tests/Visual/PlayerTestScene.cs +++ b/osu.Game/Tests/Visual/PlayerTestScene.cs @@ -19,15 +19,8 @@ namespace osu.Game.Tests.Visual /// protected virtual bool HasCustomSteps { get; } = false; - private readonly Ruleset ruleset; - protected TestPlayer Player; - protected PlayerTestScene(Ruleset ruleset) - { - this.ruleset = ruleset; - } - protected OsuConfigManager LocalConfig; [BackgroundDependencyLoader] @@ -53,7 +46,7 @@ namespace osu.Game.Tests.Visual action?.Invoke(); - AddStep(ruleset.RulesetInfo.Name, LoadPlayer); + AddStep(CreateRuleset().RulesetInfo.Name, LoadPlayer); AddUntilStep("player loaded", () => Player.IsLoaded && Player.Alpha == 1); } @@ -63,28 +56,28 @@ namespace osu.Game.Tests.Visual protected void LoadPlayer() { - var beatmap = CreateBeatmap(ruleset.RulesetInfo); + var beatmap = CreateBeatmap(Ruleset.Value); Beatmap.Value = CreateWorkingBeatmap(beatmap); - Ruleset.Value = ruleset.RulesetInfo; - SelectedMods.Value = Array.Empty(); + var rulesetInstance = Ruleset.Value.CreateInstance(); + if (!AllowFail) { - var noFailMod = ruleset.GetAllMods().FirstOrDefault(m => m is ModNoFail); + var noFailMod = rulesetInstance.GetAllMods().FirstOrDefault(m => m is ModNoFail); if (noFailMod != null) SelectedMods.Value = new[] { noFailMod }; } if (Autoplay) { - var mod = ruleset.GetAutoplayMod(); + var mod = rulesetInstance.GetAutoplayMod(); if (mod != null) SelectedMods.Value = SelectedMods.Value.Concat(mod.Yield()).ToArray(); } - Player = CreatePlayer(ruleset); + Player = CreatePlayer(rulesetInstance); LoadScreen(Player); } From 155bc8b49a08842297cf1a4eb1b4d9e36d799b55 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 17 Apr 2020 10:56:01 +0300 Subject: [PATCH 1119/2376] Remove `ModTestScene` ruleset parameter on constructor and use `CreateRuleset` instead --- .../Mods/TestSceneCatchModPerfect.cs | 4 +++- .../Mods/TestSceneManiaModPerfect.cs | 4 +++- .../Mods/TestSceneOsuModDifficultyAdjust.cs | 7 ++----- .../Mods/TestSceneOsuModDoubleTime.cs | 7 ++----- osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModPerfect.cs | 4 +++- .../TestSceneMissHitWindowJudgements.cs | 7 ++----- .../Mods/TestSceneTaikoModPerfect.cs | 4 +++- osu.Game/Tests/Visual/ModPerfectTestScene.cs | 7 ++----- osu.Game/Tests/Visual/ModTestScene.cs | 5 ----- 9 files changed, 20 insertions(+), 29 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs index 47e91e50d4..1e69a3f1b6 100644 --- a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs +++ b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods public class TestSceneCatchModPerfect : ModPerfectTestScene { public TestSceneCatchModPerfect() - : base(new CatchRuleset(), new CatchModPerfect()) + : base(new CatchModPerfect()) { } @@ -50,5 +50,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods // We only care about testing misses, hits are tested via JuiceStream [TestCase(true)] public void TestTinyDroplet(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new TinyDroplet { StartTime = 1000 }), shouldMiss); + + protected override Ruleset CreateRuleset() => new CatchRuleset(); } } diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModPerfect.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModPerfect.cs index 607d42a1bb..72ef58ec73 100644 --- a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModPerfect.cs +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModPerfect.cs @@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods public class TestSceneManiaModPerfect : ModPerfectTestScene { public TestSceneManiaModPerfect() - : base(new ManiaRuleset(), new ManiaModPerfect()) + : base(new ManiaModPerfect()) { } @@ -22,5 +22,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods [TestCase(false)] [TestCase(true)] public void TestHoldNote(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new HoldNote { StartTime = 1000, EndTime = 3000 }), shouldMiss); + + protected override Ruleset CreateRuleset() => new ManiaRuleset(); } } diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs index 69415b70e3..6c5949ca85 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs @@ -15,11 +15,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods { public class TestSceneOsuModDifficultyAdjust : ModTestScene { - public TestSceneOsuModDifficultyAdjust() - : base(new OsuRuleset()) - { - } - [Test] public void TestNoAdjustment() => CreateModTest(new ModTestData { @@ -82,5 +77,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods { return Player.ScoreProcessor.JudgedHits >= 2; } + + protected override Ruleset CreateRuleset() => new OsuRuleset(); } } diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDoubleTime.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDoubleTime.cs index dcf19ad993..c61ef2724b 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDoubleTime.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDoubleTime.cs @@ -10,11 +10,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods { public class TestSceneOsuModDoubleTime : ModTestScene { - public TestSceneOsuModDoubleTime() - : base(new OsuRuleset()) - { - } - [TestCase(0.5)] [TestCase(1.01)] [TestCase(1.5)] @@ -31,5 +26,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods Precision.AlmostEquals(Player.GameplayClockContainer.GameplayClock.Rate, mod.SpeedChange.Value) }); } + + protected override Ruleset CreateRuleset() => new OsuRuleset(); } } diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModPerfect.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModPerfect.cs index b03a894085..ddbbf9554c 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModPerfect.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModPerfect.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods public class TestSceneOsuModPerfect : ModPerfectTestScene { public TestSceneOsuModPerfect() - : base(new OsuRuleset(), new OsuModPerfect()) + : base(new OsuModPerfect()) { } @@ -48,5 +48,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods CreateHitObjectTest(new HitObjectTestData(spinner), shouldMiss); } + + protected override Ruleset CreateRuleset() => new OsuRuleset(); } } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs index 5f3596976d..13457ccaf9 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs @@ -19,11 +19,6 @@ namespace osu.Game.Rulesets.Osu.Tests { public class TestSceneMissHitWindowJudgements : ModTestScene { - public TestSceneMissHitWindowJudgements() - : base(new OsuRuleset()) - { - } - [Test] public void TestMissViaEarlyHit() { @@ -66,6 +61,8 @@ namespace osu.Game.Rulesets.Osu.Tests }); } + protected override Ruleset CreateRuleset() => new OsuRuleset(); + private class TestAutoMod : OsuModAutoplay { public override Score CreateReplayScore(IBeatmap beatmap) => new Score diff --git a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModPerfect.cs b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModPerfect.cs index 26c90ad295..a9c962bfa0 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModPerfect.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModPerfect.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Mods public class TestSceneTaikoModPerfect : ModPerfectTestScene { public TestSceneTaikoModPerfect() - : base(new TestTaikoRuleset(), new TaikoModPerfect()) + : base(new TaikoModPerfect()) { } @@ -29,6 +29,8 @@ namespace osu.Game.Rulesets.Taiko.Tests.Mods [TestCase(true)] public void TestSwell(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new Swell { StartTime = 1000, EndTime = 3000 }), shouldMiss); + protected override Ruleset CreateRuleset() => new TestTaikoRuleset(); + private class TestTaikoRuleset : TaikoRuleset { public override HealthProcessor CreateHealthProcessor(double drainStartTime) => new TestTaikoHealthProcessor(); diff --git a/osu.Game/Tests/Visual/ModPerfectTestScene.cs b/osu.Game/Tests/Visual/ModPerfectTestScene.cs index 798947eb40..3565fe751b 100644 --- a/osu.Game/Tests/Visual/ModPerfectTestScene.cs +++ b/osu.Game/Tests/Visual/ModPerfectTestScene.cs @@ -10,13 +10,10 @@ namespace osu.Game.Tests.Visual { public abstract class ModPerfectTestScene : ModTestScene { - private readonly Ruleset ruleset; private readonly ModPerfect mod; - protected ModPerfectTestScene(Ruleset ruleset, ModPerfect mod) - : base(ruleset) + protected ModPerfectTestScene(ModPerfect mod) { - this.ruleset = ruleset; this.mod = mod; } @@ -25,7 +22,7 @@ namespace osu.Game.Tests.Visual Mod = mod, Beatmap = new Beatmap { - BeatmapInfo = { Ruleset = ruleset.RulesetInfo }, + BeatmapInfo = { Ruleset = Ruleset.Value }, HitObjects = { testData.HitObject } }, Autoplay = !shouldMiss, diff --git a/osu.Game/Tests/Visual/ModTestScene.cs b/osu.Game/Tests/Visual/ModTestScene.cs index 8b41fb5075..c198d6b52c 100644 --- a/osu.Game/Tests/Visual/ModTestScene.cs +++ b/osu.Game/Tests/Visual/ModTestScene.cs @@ -19,11 +19,6 @@ namespace osu.Game.Tests.Visual typeof(ModTestScene) }; - protected ModTestScene(Ruleset ruleset) - : base(ruleset) - { - } - private ModTestData currentTestData; protected void CreateModTest(ModTestData testData) => CreateTest(() => From 7f791dcdf04d3434ea7275de20fd8d369060b062 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 17 Apr 2020 10:57:58 +0300 Subject: [PATCH 1120/2376] Re-enable ruleset bindable before setting defined ruleset in case it's disabled Happens on cases like restarting the test scene by clicking directly on it on the browser (*where it for some reason reloads the entire test scene*) --- osu.Game/Tests/Visual/OsuTestScene.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index 8058a074ef..25ac768272 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -75,6 +75,10 @@ namespace osu.Game.Tests.Visual if (definedRuleset != null) { + // re-enable the bindable in case it was disabled. + // happens when restarting current test scene. + Ruleset.Disabled = false; + // Set global ruleset bindable to the ruleset defined // for this test scene and disallow changing it. Ruleset.Value = definedRuleset; From 5833a7ac913af77a8c8d801ffd5d117b9861930c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Apr 2020 18:50:58 +0900 Subject: [PATCH 1121/2376] Fix presenting new ruleset and beatmap at once causing wedge display desync --- .../SongSelect/TestScenePlaySongSelect.cs | 62 +++++++++++++++++++ osu.Game/Screens/Select/SongSelect.cs | 6 +- 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 4405c75744..39e04ed39a 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -359,6 +359,68 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("no selection", () => songSelect.Carousel.SelectedBeatmap == null); } + [Test] + public void TestPresentNewRulesetNewBeatmap() + { + createSongSelect(); + changeRuleset(2); + + addRulesetImportStep(2); + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.RulesetID == 2); + + addRulesetImportStep(0); + addRulesetImportStep(0); + addRulesetImportStep(0); + + BeatmapInfo target = null; + + AddStep("select beatmap/ruleset externally", () => + { + target = manager.GetAllUsableBeatmapSets() + .Last(b => b.Beatmaps.Any(bi => bi.RulesetID == 0)).Beatmaps.Last(); + + Ruleset.Value = rulesets.AvailableRulesets.First(r => r.ID == 0); + Beatmap.Value = manager.GetWorkingBeatmap(target); + }); + + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.Equals(target)); + + // this is an important check, to make sure updateComponentFromBeatmap() was actually run + AddUntilStep("selection shown on wedge", () => songSelect.CurrentBeatmapDetailsBeatmap.BeatmapInfo == target); + } + + [Test] + public void TestPresentNewBeatmapNewRuleset() + { + createSongSelect(); + changeRuleset(2); + + addRulesetImportStep(2); + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.RulesetID == 2); + + addRulesetImportStep(0); + addRulesetImportStep(0); + addRulesetImportStep(0); + + BeatmapInfo target = null; + + AddStep("select beatmap/ruleset externally", () => + { + target = manager.GetAllUsableBeatmapSets() + .Last(b => b.Beatmaps.Any(bi => bi.RulesetID == 0)).Beatmaps.Last(); + + Beatmap.Value = manager.GetWorkingBeatmap(target); + Ruleset.Value = rulesets.AvailableRulesets.First(r => r.ID == 0); + }); + + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.Equals(target)); + + AddUntilStep("has correct ruleset", () => Ruleset.Value.ID == 0); + + // this is an important check, to make sure updateComponentFromBeatmap() was actually run + AddUntilStep("selection shown on wedge", () => songSelect.CurrentBeatmapDetailsBeatmap.BeatmapInfo == target); + } + [Test] public void TestRulesetChangeResetsMods() { diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 5bc2e1aa56..9d3dc58a26 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -448,8 +448,10 @@ namespace osu.Game.Screens.Select { Mods.Value = Array.Empty(); - // required to return once in order to have the carousel in a good state. - // if the ruleset changed, the rest of the selection update will happen via updateSelectedRuleset. + // the ruleset transfer may cause a deselection of the current beatmap (due to incompatibility). + // this can happen via Carousel.FlushPendingFilterOperations(). + // to ensure a good state, re-transfer no-debounce values. + performUpdateSelected(); return; } From 58a1c6e17186084345feedbd1c5b0d44c2ce8695 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Apr 2020 19:52:58 +0900 Subject: [PATCH 1122/2376] Reapply taiko visibility hack at a higher level --- .../Objects/Drawables/DrawableDrumRoll.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index 0a6f462607..99f48afff0 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -15,6 +15,7 @@ using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; +using osuTK; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { @@ -137,6 +138,14 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables } } + protected override void Update() + { + base.Update(); + + OriginPosition = new Vector2(DrawHeight); + Content.X = DrawHeight / 2; + } + protected override DrawableStrongNestedHit CreateStrongHit(StrongHitObject hitObject) => new StrongNestedHit(hitObject, this); private void updateColour() From 5f3ed3e93aab472b9d748d857e4394ec13843742 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Apr 2020 22:25:24 +0900 Subject: [PATCH 1123/2376] 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 723844155f..d2bdbc8b61 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 76f7a030f9..5facb04117 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -23,7 +23,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 7a487a6430..dda1ee5c42 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From 61e3491e603daf0a497ec988318043001a4e068a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 18 Apr 2020 12:57:09 +0900 Subject: [PATCH 1124/2376] Fix hard crash in editor on legacy modes without encoder implementation --- osu.Game/Screens/Edit/EditorChangeHandler.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/EditorChangeHandler.cs b/osu.Game/Screens/Edit/EditorChangeHandler.cs index 1553c2d2ef..ac889500b4 100644 --- a/osu.Game/Screens/Edit/EditorChangeHandler.cs +++ b/osu.Game/Screens/Edit/EditorChangeHandler.cs @@ -82,12 +82,19 @@ namespace osu.Game.Screens.Edit if (savedStates.Count > MAX_SAVED_STATES) savedStates.RemoveAt(0); - using (var stream = new MemoryStream()) + try { - using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) - new LegacyBeatmapEncoder(editorBeatmap).Encode(sw); + using (var stream = new MemoryStream()) + { + using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) + new LegacyBeatmapEncoder(editorBeatmap).Encode(sw); - savedStates.Add(stream.ToArray()); + savedStates.Add(stream.ToArray()); + } + } + catch (NotImplementedException) + { + // some rulesets don't have encoder implementations yet. } currentState = savedStates.Count - 1; From c00a386ff681a3ba4a037de6654bd1fd199d6770 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 18 Apr 2020 21:46:04 +0900 Subject: [PATCH 1125/2376] Remove exceptions instead --- .../Beatmaps/Formats/LegacyBeatmapEncoder.cs | 21 +------------------ osu.Game/Screens/Edit/EditorChangeHandler.cs | 15 ++++--------- 2 files changed, 5 insertions(+), 31 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 12f2c58e35..8d9dfc318a 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -194,20 +194,7 @@ namespace osu.Game.Beatmaps.Formats handleOsuHitObject(writer, h); break; - case 1: - foreach (var h in beatmap.HitObjects) - handleTaikoHitObject(writer, h); - break; - - case 2: - foreach (var h in beatmap.HitObjects) - handleCatchHitObject(writer, h); - break; - - case 3: - foreach (var h in beatmap.HitObjects) - handleManiaHitObject(writer, h); - break; + // TODO: implement other legacy rulesets } } @@ -328,12 +315,6 @@ namespace osu.Game.Beatmaps.Formats } } - private void handleTaikoHitObject(TextWriter writer, HitObject hitObject) => throw new NotImplementedException(); - - private void handleCatchHitObject(TextWriter writer, HitObject hitObject) => throw new NotImplementedException(); - - private void handleManiaHitObject(TextWriter writer, HitObject hitObject) => throw new NotImplementedException(); - private string getSampleBank(IList samples, bool banksOnly = false, bool zeroBanks = false) { LegacySampleBank normalBank = toLegacySampleBank(samples.SingleOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL)?.Bank); diff --git a/osu.Game/Screens/Edit/EditorChangeHandler.cs b/osu.Game/Screens/Edit/EditorChangeHandler.cs index ac889500b4..1553c2d2ef 100644 --- a/osu.Game/Screens/Edit/EditorChangeHandler.cs +++ b/osu.Game/Screens/Edit/EditorChangeHandler.cs @@ -82,19 +82,12 @@ namespace osu.Game.Screens.Edit if (savedStates.Count > MAX_SAVED_STATES) savedStates.RemoveAt(0); - try + using (var stream = new MemoryStream()) { - using (var stream = new MemoryStream()) - { - using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) - new LegacyBeatmapEncoder(editorBeatmap).Encode(sw); + using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) + new LegacyBeatmapEncoder(editorBeatmap).Encode(sw); - savedStates.Add(stream.ToArray()); - } - } - catch (NotImplementedException) - { - // some rulesets don't have encoder implementations yet. + savedStates.Add(stream.ToArray()); } currentState = savedStates.Count - 1; From 6b16908c05bafd95e069b986d5efd6d73631875f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 18 Apr 2020 21:51:37 +0900 Subject: [PATCH 1126/2376] Move todo to appease dotnet-format --- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 8d9dfc318a..fe63eec3f9 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -187,14 +187,13 @@ namespace osu.Game.Beatmaps.Formats writer.WriteLine("[HitObjects]"); + // TODO: implement other legacy rulesets switch (beatmap.BeatmapInfo.RulesetID) { case 0: foreach (var h in beatmap.HitObjects) handleOsuHitObject(writer, h); break; - - // TODO: implement other legacy rulesets } } From fc6c245de5dc362489706fbae2b1c299260f9fb8 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 19 Apr 2020 05:36:04 +0300 Subject: [PATCH 1127/2376] Replace all judged event logic with HasCompleted bindable --- .../TestSceneHoldNoteInput.cs | 8 +------- .../TestSceneOutOfOrderHits.cs | 8 +------- .../TestSceneSliderInput.cs | 8 +------- .../TestSceneSwellJudgements.cs | 2 +- osu.Game/Rulesets/Scoring/JudgementProcessor.cs | 17 +++++++++-------- osu.Game/Screens/Play/BreakTracker.cs | 2 +- osu.Game/Tests/Visual/ModPerfectTestScene.cs | 2 +- 7 files changed, 15 insertions(+), 32 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs index 7b0cf40d45..0d13b85901 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs @@ -27,7 +27,6 @@ namespace osu.Game.Rulesets.Mania.Tests private const double time_after_tail = 5250; private List judgementResults; - private bool allJudgedFired; /// /// -----[ ]----- @@ -283,20 +282,15 @@ namespace osu.Game.Rulesets.Mania.Tests { if (currentPlayer == p) judgementResults.Add(result); }; - p.ScoreProcessor.AllJudged += () => - { - if (currentPlayer == p) allJudgedFired = true; - }; }; LoadScreen(currentPlayer = p); - allJudgedFired = false; judgementResults = new List(); }); AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0); AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); - AddUntilStep("Wait for all judged", () => allJudgedFired); + AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value); } private class ScoreAccessibleReplayPlayer : ReplayPlayer diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs index d6858f831e..91d5e04f6f 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs @@ -316,7 +316,6 @@ namespace osu.Game.Rulesets.Osu.Tests private ScoreAccessibleReplayPlayer currentPlayer; private List judgementResults; - private bool allJudgedFired; private void performTest(List hitObjects, List frames) { @@ -342,20 +341,15 @@ namespace osu.Game.Rulesets.Osu.Tests { if (currentPlayer == p) judgementResults.Add(result); }; - p.ScoreProcessor.AllJudged += () => - { - if (currentPlayer == p) allJudgedFired = true; - }; }; LoadScreen(currentPlayer = p); - allJudgedFired = false; judgementResults = new List(); }); AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0); AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); - AddUntilStep("Wait for all judged", () => allJudgedFired); + AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value); } private class TestHitCircle : HitCircle diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs index 67e1b77770..b0c2e56c3e 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs @@ -47,7 +47,6 @@ namespace osu.Game.Rulesets.Osu.Tests private const double time_slider_end = 4000; private List judgementResults; - private bool allJudgedFired; /// /// Scenario: @@ -375,20 +374,15 @@ namespace osu.Game.Rulesets.Osu.Tests { if (currentPlayer == p) judgementResults.Add(result); }; - p.ScoreProcessor.AllJudged += () => - { - if (currentPlayer == p) allJudgedFired = true; - }; }; LoadScreen(currentPlayer = p); - allJudgedFired = false; judgementResults = new List(); }); AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0); AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); - AddUntilStep("Wait for all judged", () => allJudgedFired); + AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value); } private class ScoreAccessibleReplayPlayer : ReplayPlayer diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneSwellJudgements.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneSwellJudgements.cs index 303f0163b1..923e28a45e 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneSwellJudgements.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneSwellJudgements.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Taiko.Tests [Test] public void TestZeroTickTimeOffsets() { - AddUntilStep("gameplay finished", () => Player.ScoreProcessor.HasCompleted); + AddUntilStep("gameplay finished", () => Player.ScoreProcessor.HasCompleted.Value); AddAssert("all tick offsets are 0", () => Player.Results.Where(r => r.HitObject is SwellTick).All(r => r.TimeOffset == 0)); } diff --git a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs index 334b95f808..d878ef0a5c 100644 --- a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs +++ b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Bindables; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; using osu.Game.Beatmaps; @@ -12,11 +13,6 @@ namespace osu.Game.Rulesets.Scoring { public abstract class JudgementProcessor : Component { - /// - /// Invoked when all s have been judged by this . - /// - public event Action AllJudged; - /// /// Invoked when a new judgement has occurred. This occurs after the judgement has been processed by this . /// @@ -32,10 +28,12 @@ namespace osu.Game.Rulesets.Scoring /// public int JudgedHits { get; private set; } + private readonly BindableBool hasCompleted = new BindableBool(); + /// /// Whether all s have been processed. /// - public bool HasCompleted => JudgedHits == MaxHits; + public IBindable HasCompleted => hasCompleted; /// /// Applies a to this . @@ -60,8 +58,8 @@ namespace osu.Game.Rulesets.Scoring NewJudgement?.Invoke(result); - if (HasCompleted) - AllJudged?.Invoke(); + if (JudgedHits == MaxHits) + hasCompleted.Value = true; } /// @@ -72,6 +70,9 @@ namespace osu.Game.Rulesets.Scoring { JudgedHits--; + if (JudgedHits < MaxHits) + hasCompleted.Value = false; + RevertResultInternal(result); } diff --git a/osu.Game/Screens/Play/BreakTracker.cs b/osu.Game/Screens/Play/BreakTracker.cs index 64262d52b5..fcd7ed6b73 100644 --- a/osu.Game/Screens/Play/BreakTracker.cs +++ b/osu.Game/Screens/Play/BreakTracker.cs @@ -51,7 +51,7 @@ namespace osu.Game.Screens.Play isBreakTime.Value = getCurrentBreak()?.HasEffect == true || Clock.CurrentTime < gameplayStartTime - || scoreProcessor?.HasCompleted == true; + || scoreProcessor?.HasCompleted.Value == true; } private BreakPeriod getCurrentBreak() diff --git a/osu.Game/Tests/Visual/ModPerfectTestScene.cs b/osu.Game/Tests/Visual/ModPerfectTestScene.cs index 798947eb40..5948283428 100644 --- a/osu.Game/Tests/Visual/ModPerfectTestScene.cs +++ b/osu.Game/Tests/Visual/ModPerfectTestScene.cs @@ -46,7 +46,7 @@ namespace osu.Game.Tests.Visual public bool CheckFailed(bool failed) { if (!failed) - return ScoreProcessor.HasCompleted && !HealthProcessor.HasFailed; + return ScoreProcessor.HasCompleted.Value && !HealthProcessor.HasFailed; return HealthProcessor.HasFailed; } From 7e64bec94f286b4d1bbc550d4e95f9ca601ca997 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 19 Apr 2020 05:58:22 +0300 Subject: [PATCH 1128/2376] Use HasCompleted in Player --- osu.Game/Screens/Play/Player.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 4597ae760c..542e226809 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -197,7 +197,7 @@ namespace osu.Game.Screens.Play }; // Bind the judgement processors to ourselves - ScoreProcessor.AllJudged += onCompletion; + ScoreProcessor.HasCompleted.ValueChanged += updateCompletionState; HealthProcessor.Failed += onFail; foreach (var mod in Mods.Value.OfType()) @@ -412,7 +412,7 @@ namespace osu.Game.Screens.Play private ScheduledDelegate completionProgressDelegate; - private void onCompletion() + private void updateCompletionState(ValueChangedEvent completionState) { // screen may be in the exiting transition phase. if (!this.IsCurrentScreen()) From 6d276890a7dca11c37855655f8f760c4b3176314 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 19 Apr 2020 05:59:56 +0300 Subject: [PATCH 1129/2376] Fix results screen pushed after rewinding in-between push delay --- osu.Game/Screens/Play/Player.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 542e226809..c6c83e5379 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -418,6 +418,16 @@ namespace osu.Game.Screens.Play if (!this.IsCurrentScreen()) return; + // cancel push delegate in case judges reverted + // after delegate may have already been scheduled. + if (!completionState.NewValue) + { + completionProgressDelegate?.Cancel(); + completionProgressDelegate = null; + ValidForResume = true; + return; + } + // Only show the completion screen if the player hasn't failed if (HealthProcessor.HasFailed || completionProgressDelegate != null) return; From 65a8860a65812dc4de6aae708d5352508898cdb7 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 19 Apr 2020 06:01:09 +0300 Subject: [PATCH 1130/2376] Add test cases to ensure no regression in "cancelling completion" --- .../TestSceneCompletionCancellation.cs | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs new file mode 100644 index 0000000000..54e1ff5345 --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs @@ -0,0 +1,129 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Track; +using osu.Framework.Screens; +using osu.Framework.Testing; +using osu.Framework.Timing; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Storyboards; +using osuTK; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public class TestSceneCompletionCancellation : PlayerTestScene + { + private Track track; + + [Resolved] + private AudioManager audio { get; set; } + + protected override bool AllowFail => false; + + public TestSceneCompletionCancellation() + : base(new OsuRuleset()) + { + } + + [SetUpSteps] + public override void SetUpSteps() + { + base.SetUpSteps(); + + // Ensure track has actually running before attempting to seek + AddUntilStep("wait for track to start running", () => track.IsRunning); + } + + [Test] + public void TestCancelCompletionOnRewind() + { + cancelCompletionSteps(); + + AddAssert("no attempt to push ranking", () => !((FakeRankingPushPlayer)Player).GotoRankingInvoked); + } + + [Test] + public void TestReCompleteAfterCancellation() + { + cancelCompletionSteps(); + + // Attempt completing again. + AddStep("seek to completion again", () => track.Seek(5000)); + AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value); + + AddWaitStep("wait", 5); + + AddAssert("attempted to push ranking", () => ((FakeRankingPushPlayer)Player).GotoRankingInvoked); + } + + /// + /// Tests whether can still pause after cancelling completion + /// by reverting back to true. + /// + [Test] + public void TestCanPauseAfterCancellation() + { + cancelCompletionSteps(); + + AddStep("pause", () => Player.Pause()); + AddAssert("paused successfully", () => Player.GameplayClockContainer.IsPaused.Value); + } + + private void cancelCompletionSteps() + { + AddStep("seek to completion", () => track.Seek(5000)); + AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value); + + AddStep("rewind to cancel", () => track.Seek(4000)); + AddUntilStep("completion cleared by processor", () => !Player.ScoreProcessor.HasCompleted.Value); + + AddWaitStep("wait", 5); + } + + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) + { + var working = new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audio); + track = working.Track; + return working; + } + + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) + { + var beatmap = new Beatmap(); + + for (int i = 1; i <= 19; i++) + { + beatmap.HitObjects.Add(new HitCircle + { + Position = new Vector2(256, 192), + StartTime = i * 250, + }); + } + + return beatmap; + } + + protected override TestPlayer CreatePlayer(Ruleset ruleset) => new FakeRankingPushPlayer(); + + public class FakeRankingPushPlayer : TestPlayer + { + public bool GotoRankingInvoked; + + public FakeRankingPushPlayer() + : base(true, true) + { + } + + protected override void GotoRanking() + { + GotoRankingInvoked = true; + } + } + } +} From 1dd471dfcc1ae9465d17c3d21ea2577d7a2b46c0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 19 Apr 2020 15:12:36 +0900 Subject: [PATCH 1131/2376] Add /np (now playing) command support in chat --- .../Visual/Online/TestNowPlayingCommand.cs | 85 +++++++++++++++++++ osu.Game/Online/Chat/ChannelManager.cs | 6 +- osu.Game/Online/Chat/IChannelPostTarget.cs | 19 +++++ osu.Game/Online/Chat/NowPlayingCommand.cs | 55 ++++++++++++ osu.Game/Online/PollingComponent.cs | 4 +- 5 files changed, 166 insertions(+), 3 deletions(-) create mode 100644 osu.Game.Tests/Visual/Online/TestNowPlayingCommand.cs create mode 100644 osu.Game/Online/Chat/IChannelPostTarget.cs create mode 100644 osu.Game/Online/Chat/NowPlayingCommand.cs diff --git a/osu.Game.Tests/Visual/Online/TestNowPlayingCommand.cs b/osu.Game.Tests/Visual/Online/TestNowPlayingCommand.cs new file mode 100644 index 0000000000..60032ab118 --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestNowPlayingCommand.cs @@ -0,0 +1,85 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Online.Chat; +using osu.Game.Rulesets; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual.Online +{ + [HeadlessTest] + public class TestNowPlayingCommand : OsuTestScene + { + [Cached(typeof(IChannelPostTarget))] + private PostTarget postTarget { get; set; } + + public TestNowPlayingCommand() + { + Add(postTarget = new PostTarget()); + } + + [Test] + public void TestGenericActivity() + { + AddStep("Set activity", () => API.Activity.Value = new UserActivity.InLobby()); + + AddStep("Run command", () => Add(new NowPlayingCommand())); + + AddAssert("Check correct response", () => postTarget.LastMessage.Contains("is listening")); + } + + [Test] + public void TestEditActivity() + { + AddStep("Set activity", () => API.Activity.Value = new UserActivity.Editing(new BeatmapInfo())); + + AddStep("Run command", () => Add(new NowPlayingCommand())); + + AddAssert("Check correct response", () => postTarget.LastMessage.Contains("is editing")); + } + + [Test] + public void TestPlayActivity() + { + AddStep("Set activity", () => API.Activity.Value = new UserActivity.SoloGame(new BeatmapInfo(), new RulesetInfo())); + + AddStep("Run command", () => Add(new NowPlayingCommand())); + + AddAssert("Check correct response", () => postTarget.LastMessage.Contains("is playing")); + } + + [TestCase(true)] + [TestCase(false)] + public void TestLinkPresence(bool hasOnlineId) + { + AddStep("Set activity", () => API.Activity.Value = new UserActivity.InLobby()); + + AddStep("Set beatmap", () => Beatmap.Value = new DummyWorkingBeatmap(null, null) + { + BeatmapInfo = { OnlineBeatmapID = hasOnlineId ? 1234 : (int?)null } + }); + + AddStep("Run command", () => Add(new NowPlayingCommand())); + + if (hasOnlineId) + AddAssert("Check link presence", () => postTarget.LastMessage.Contains("https://osu.ppy.sh/b/1234")); + else + AddAssert("Check link not present", () => !postTarget.LastMessage.Contains("https://")); + } + + public class PostTarget : Component, IChannelPostTarget + { + public void PostMessage(string text, bool isAction = false, Channel target = null) + { + LastMessage = text; + } + + public string LastMessage { get; private set; } + } + } +} diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index 2c37216fd6..f53beefeb5 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -18,7 +18,7 @@ namespace osu.Game.Online.Chat /// /// Manages everything channel related /// - public class ChannelManager : PollingComponent + public class ChannelManager : PollingComponent, IChannelPostTarget { /// /// The channels the player joins on startup @@ -204,6 +204,10 @@ namespace osu.Game.Online.Chat switch (command) { + case "np": + AddInternal(new NowPlayingCommand()); + break; + case "me": if (string.IsNullOrWhiteSpace(content)) { diff --git a/osu.Game/Online/Chat/IChannelPostTarget.cs b/osu.Game/Online/Chat/IChannelPostTarget.cs new file mode 100644 index 0000000000..5697e918f0 --- /dev/null +++ b/osu.Game/Online/Chat/IChannelPostTarget.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.Allocation; + +namespace osu.Game.Online.Chat +{ + [Cached(typeof(IChannelPostTarget))] + public interface IChannelPostTarget + { + /// + /// Posts a message to the currently opened channel. + /// + /// The message text that is going to be posted + /// Is true if the message is an action, e.g.: user is currently eating + /// An optional target channel. If null, will be used. + void PostMessage(string text, bool isAction = false, Channel target = null); + } +} diff --git a/osu.Game/Online/Chat/NowPlayingCommand.cs b/osu.Game/Online/Chat/NowPlayingCommand.cs new file mode 100644 index 0000000000..c0b54812b6 --- /dev/null +++ b/osu.Game/Online/Chat/NowPlayingCommand.cs @@ -0,0 +1,55 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Beatmaps; +using osu.Game.Online.API; +using osu.Game.Users; + +namespace osu.Game.Online.Chat +{ + public class NowPlayingCommand : Component + { + [Resolved] + private IChannelPostTarget channelManager { get; set; } + + [Resolved] + private IAPIProvider api { get; set; } + + [Resolved] + private Bindable currentBeatmap { get; set; } + + protected override void LoadComplete() + { + base.LoadComplete(); + + string verb; + BeatmapInfo beatmap; + + switch (api.Activity.Value) + { + case UserActivity.SoloGame solo: + verb = "playing"; + beatmap = solo.Beatmap; + break; + + case UserActivity.Editing edit: + verb = "editing"; + beatmap = edit.Beatmap; + break; + + default: + verb = "listening to"; + beatmap = currentBeatmap.Value.BeatmapInfo; + break; + } + + var beatmapString = beatmap.OnlineBeatmapID.HasValue ? $"[https://osu.ppy.sh/b/{beatmap.OnlineBeatmapID} {beatmap}]" : beatmap.ToString(); + + channelManager.PostMessage($"is {verb} {beatmapString}", true); + Expire(); + } + } +} diff --git a/osu.Game/Online/PollingComponent.cs b/osu.Game/Online/PollingComponent.cs index acbb2c39f4..228f147835 100644 --- a/osu.Game/Online/PollingComponent.cs +++ b/osu.Game/Online/PollingComponent.cs @@ -3,7 +3,7 @@ using System; using System.Threading.Tasks; -using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Threading; namespace osu.Game.Online @@ -11,7 +11,7 @@ namespace osu.Game.Online /// /// A component which requires a constant polling process. /// - public abstract class PollingComponent : Component + public abstract class PollingComponent : CompositeDrawable // switch away from Component because InternalChildren are used in usages. { private double? lastTimePolled; From e4d4040afb90a990eb369ced822d65f07acd1e25 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 19 Apr 2020 16:57:47 +0900 Subject: [PATCH 1132/2376] Rename test to match other classes --- ...TestNowPlayingCommand.cs => TestSceneNowPlayingCommand.cs} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename osu.Game.Tests/Visual/Online/{TestNowPlayingCommand.cs => TestSceneNowPlayingCommand.cs} (96%) diff --git a/osu.Game.Tests/Visual/Online/TestNowPlayingCommand.cs b/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs similarity index 96% rename from osu.Game.Tests/Visual/Online/TestNowPlayingCommand.cs rename to osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs index 60032ab118..103308d34d 100644 --- a/osu.Game.Tests/Visual/Online/TestNowPlayingCommand.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs @@ -13,12 +13,12 @@ using osu.Game.Users; namespace osu.Game.Tests.Visual.Online { [HeadlessTest] - public class TestNowPlayingCommand : OsuTestScene + public class TestSceneNowPlayingCommand : OsuTestScene { [Cached(typeof(IChannelPostTarget))] private PostTarget postTarget { get; set; } - public TestNowPlayingCommand() + public TestSceneNowPlayingCommand() { Add(postTarget = new PostTarget()); } From e3e0cd149f94d033a36d7622c0a8bf91e029fde0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 19 Apr 2020 12:41:00 +0200 Subject: [PATCH 1133/2376] Refactor test code to eliminate boolean flags --- .../TestSceneHyperDashColouring.cs | 127 +++++++++--------- 1 file changed, 65 insertions(+), 62 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs index 846b17f324..a48ecb9b79 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs @@ -27,74 +27,71 @@ namespace osu.Game.Rulesets.Catch.Tests private SkinManager skins { get; set; } [Test] - public void TestHyperDashFruitColour() + public void TestDefaultFruitColour() { - DrawableFruit drawableFruit = null; + var skin = new TestSkin(); - AddStep("setup hyper-dash fruit", () => - { - var fruit = new Fruit { HyperDashTarget = new Banana() }; - fruit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - - Child = setupSkinHierarchy(drawableFruit = new DrawableFruit(fruit) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(4f), - }, false, false, false); - }); - - AddAssert("hyper-dash fruit has default colour", () => checkLegacyFruitHyperDashColour(drawableFruit, Catcher.DEFAULT_HYPER_DASH_COLOUR)); - } - - [TestCase(true)] - [TestCase(false)] - public void TestCustomHyperDashFruitColour(bool customCatcherHyperDashColour) - { - DrawableFruit drawableFruit = null; - - AddStep("setup hyper-dash fruit", () => - { - var fruit = new Fruit { HyperDashTarget = new Banana() }; - fruit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - - Child = setupSkinHierarchy(drawableFruit = new DrawableFruit(fruit) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(4f), - }, customCatcherHyperDashColour, false, true); - }); - - AddAssert("hyper-dash fruit use fruit colour from skin", () => checkLegacyFruitHyperDashColour(drawableFruit, TestSkin.CUSTOM_HYPER_DASH_FRUIT_COLOUR)); + checkHyperDashFruitColour(skin, Catcher.DEFAULT_HYPER_DASH_COLOUR); } [Test] - public void TestCustomHyperDashFruitColourFallback() + public void TestCustomFruitColour() + { + var skin = new TestSkin + { + HyperDashFruitColour = Color4.Cyan + }; + + checkHyperDashFruitColour(skin, skin.HyperDashFruitColour); + } + + [Test] + public void TestCustomFruitColourPriority() + { + var skin = new TestSkin + { + HyperDashColour = Color4.Goldenrod, + HyperDashFruitColour = Color4.Cyan + }; + + checkHyperDashFruitColour(skin, skin.HyperDashFruitColour); + } + + [Test] + public void TestFruitColourFallback() + { + var skin = new TestSkin + { + HyperDashColour = Color4.Goldenrod + }; + + checkHyperDashFruitColour(skin, skin.HyperDashColour); + } + + private void checkHyperDashFruitColour(ISkin skin, Color4 expectedColour) { DrawableFruit drawableFruit = null; - AddStep("setup hyper-dash fruit", () => + AddStep("create hyper-dash fruit", () => { var fruit = new Fruit { HyperDashTarget = new Banana() }; fruit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - Child = setupSkinHierarchy( - drawableFruit = new DrawableFruit(fruit) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(4f), - }, true, false, false); + Child = setupSkinHierarchy(drawableFruit = new DrawableFruit(fruit) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(4f), + }, skin); }); - AddAssert("hyper-dash fruit colour falls back to catcher colour from skin", () => checkLegacyFruitHyperDashColour(drawableFruit, TestSkin.CUSTOM_HYPER_DASH_COLOUR)); + AddAssert("hyper-dash colour is correct", () => checkLegacyFruitHyperDashColour(drawableFruit, expectedColour)); } - private Drawable setupSkinHierarchy(Drawable child, bool customCatcherColour, bool customAfterColour, bool customFruitColour) + private Drawable setupSkinHierarchy(Drawable child, ISkin skin) { var legacySkinProvider = new SkinProvidingContainer(skins.GetSkin(DefaultLegacySkin.Info)); - var testSkinProvider = new SkinProvidingContainer(new TestSkin(customCatcherColour, customAfterColour, customFruitColour)); + var testSkinProvider = new SkinProvidingContainer(skin); var legacySkinTransformer = new SkinProvidingContainer(new CatchLegacySkinTransformer(testSkinProvider)); return legacySkinProvider @@ -108,21 +105,27 @@ namespace osu.Game.Rulesets.Catch.Tests private class TestSkin : LegacySkin { - public static readonly Color4 CUSTOM_HYPER_DASH_COLOUR = Color4.Goldenrod; - public static readonly Color4 CUSTOM_HYPER_DASH_AFTER_COLOUR = Color4.Lime; - public static readonly Color4 CUSTOM_HYPER_DASH_FRUIT_COLOUR = Color4.Cyan; + public Color4 HyperDashColour + { + get => Configuration.CustomColours[CatchSkinColour.HyperDash.ToString()]; + set => Configuration.CustomColours[CatchSkinColour.HyperDash.ToString()] = value; + } - public TestSkin(bool customCatcherColour, bool customAfterColour, bool customFruitColour) + public Color4 HyperDashAfterImageColour + { + get => Configuration.CustomColours[CatchSkinColour.HyperDashAfterImage.ToString()]; + set => Configuration.CustomColours[CatchSkinColour.HyperDashAfterImage.ToString()] = value; + } + + public Color4 HyperDashFruitColour + { + get => Configuration.CustomColours[CatchSkinColour.HyperDashFruit.ToString()]; + set => Configuration.CustomColours[CatchSkinColour.HyperDashFruit.ToString()] = value; + } + + public TestSkin() : base(new SkinInfo(), null, null, string.Empty) { - if (customCatcherColour) - Configuration.CustomColours[CatchSkinColour.HyperDash.ToString()] = CUSTOM_HYPER_DASH_COLOUR; - - if (customAfterColour) - Configuration.CustomColours[CatchSkinColour.HyperDashAfterImage.ToString()] = CUSTOM_HYPER_DASH_AFTER_COLOUR; - - if (customFruitColour) - Configuration.CustomColours[CatchSkinColour.HyperDashFruit.ToString()] = CUSTOM_HYPER_DASH_FRUIT_COLOUR; } } } From a7179d1c8751b179ae8d676ee76cd73b8930336f Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sun, 19 Apr 2020 15:15:04 +0200 Subject: [PATCH 1134/2376] Fix comment wording MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Bartłomiej Dach --- osu.Game/Rulesets/RulesetStore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index 34da2dc2db..d4b41dcb38 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -59,7 +59,7 @@ namespace osu.Game.Rulesets // the requesting assembly may be located out of the executable's base directory, thus requiring manual resolving of its dependencies. // this assumes the only explicit dependency of the ruleset is the game core assembly. - // the ruleset dependency on the game core assembly requires manual resolving, transient dependencies should be resolved automatically + // the ruleset dependency on the game core assembly requires manual resolving, transitive dependencies should be resolved automatically if (asm.Name.Equals(typeof(OsuGame).Assembly.GetName().Name, StringComparison.Ordinal)) return Assembly.GetExecutingAssembly(); From 07b8ef83c95c209a9953013b3cadc01bc529b7aa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 19 Apr 2020 22:15:07 +0900 Subject: [PATCH 1135/2376] Add /np to help line --- osu.Game/Online/Chat/ChannelManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index f53beefeb5..822f628dd2 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -238,7 +238,7 @@ namespace osu.Game.Online.Chat break; case "help": - target.AddNewMessages(new InfoMessage("Supported commands: /help, /me [action], /join [channel]")); + target.AddNewMessages(new InfoMessage("Supported commands: /help, /me [action], /join [channel], /np")); break; default: From ba1c465edf296fe55bf4d623e5543918ab85f7af Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sun, 19 Apr 2020 15:25:21 +0200 Subject: [PATCH 1136/2376] Add comment in regards to the attaching of event handler to the assembly lookup event before call to loadUserRulesets(). --- osu.Game/Rulesets/RulesetStore.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index d4b41dcb38..ab169c741d 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -29,6 +29,10 @@ namespace osu.Game.Rulesets // We cannot read assemblies from cwd, so should check loaded assemblies instead. loadFromAppDomain(); loadFromDisk(); + + // the event handler contains code for resolving dependency on the game assembly for rulesets located outside the base game directory. + // It needs to be attached to the assembly lookup event before the actual call to loadUserRulesets() else rulesets located out of the base game directory will fail + // to load as unable to locate the game core assembly. AppDomain.CurrentDomain.AssemblyResolve += resolveRulesetDependencyAssembly; loadUserRulesets(); addMissingRulesets(); From 1dcb0f53a233452392efadd949c12342842eb7c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 19 Apr 2020 16:29:32 +0200 Subject: [PATCH 1137/2376] Fix whitespace formatting --- osu.Game/Rulesets/RulesetStore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index ab169c741d..7e165311a3 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -29,7 +29,7 @@ namespace osu.Game.Rulesets // We cannot read assemblies from cwd, so should check loaded assemblies instead. loadFromAppDomain(); loadFromDisk(); - + // the event handler contains code for resolving dependency on the game assembly for rulesets located outside the base game directory. // It needs to be attached to the assembly lookup event before the actual call to loadUserRulesets() else rulesets located out of the base game directory will fail // to load as unable to locate the game core assembly. From b57d709d151d4296e35a03fb79130cf55e4a34d3 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sun, 19 Apr 2020 18:29:06 +0300 Subject: [PATCH 1138/2376] Don't use Parent --- osu.Game/Screens/Select/BeatmapCarousel.cs | 18 +++++++++--------- osu.Game/Screens/Select/SongSelect.cs | 2 ++ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index b5dfcadeaa..cf3a5a7199 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -28,8 +28,8 @@ namespace osu.Game.Screens.Select { public class BeatmapCarousel : CompositeDrawable, IKeyBindingHandler { - private const float bleed_top = FilterControl.HEIGHT; - private const float bleed_bottom = Footer.HEIGHT; + public float BleedTop; + public float BleedBottom; /// /// Triggered when the loaded change and are completely loaded. @@ -373,17 +373,17 @@ namespace osu.Game.Screens.Select /// the beatmap carousel bleeds into the and the /// /// - private float visibleHalfHeight => (DrawHeight + bleed_bottom + bleed_top) / 2; + private float visibleHalfHeight => (DrawHeight + BleedBottom + BleedTop) / 2; /// /// The position of the lower visible bound with respect to the current scroll position. /// - private float visibleBottomBound => scroll.Current + DrawHeight + bleed_bottom; + private float visibleBottomBound => scroll.Current + DrawHeight + BleedBottom; /// /// The position of the upper visible bound with respect to the current scroll position. /// - private float visibleUpperBound => scroll.Current - bleed_top; + private float visibleUpperBound => scroll.Current - BleedTop; public void FlushPendingFilterOperations() { @@ -641,11 +641,11 @@ namespace osu.Game.Screens.Select case DrawableCarouselBeatmap beatmap: { if (beatmap.Item.State.Value == CarouselItemState.Selected) - // scroll position at currentY makes the set panel appear at the very top of the carousel in screen space - // move down by half of parent height (which is the height of the carousel's visible extent, including semi-transparent areas) - // then reapply parent's padding from the top by adding it + // scroll position at currentY makes the set panel appear at the very top of the carousel's screen space + // move down by half of visible height (height of the carousel's visible extent, including semi-transparent areas) + // then reapply the top semi-transparent area (because carousel's screen space starts below it) // and finally add half of the panel's own height to achieve vertical centering of the panel itself - scrollTarget = currentY - Parent.DrawHeight / 2 + Parent.Padding.Top + beatmap.DrawHeight / 2; + scrollTarget = currentY - visibleHalfHeight + BleedTop + beatmap.DrawHeight / 2; void performMove(float y, float? startY = null) { diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 5bc2e1aa56..bd6204d8cd 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -153,6 +153,8 @@ namespace osu.Game.Screens.Select Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, RelativeSizeAxes = Axes.Both, + BleedTop = FilterControl.HEIGHT, + BleedBottom = Footer.HEIGHT, SelectionChanged = updateSelectedBeatmap, BeatmapSetsChanged = carouselBeatmapsLoaded, GetRecommendedBeatmap = recommender.GetRecommendedBeatmap, From f3fee734417154e36ef9594a67e3060983455d91 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Apr 2020 09:35:00 +0900 Subject: [PATCH 1139/2376] Fix DatabasedKeyBindingContainer not using defaults for non-databased ruleset --- .../Visual/Gameplay/TestSceneKeyBindings.cs | 99 +++++++++++++++++++ .../Bindings/DatabasedKeyBindingContainer.cs | 10 +- 2 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneKeyBindings.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyBindings.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyBindings.cs new file mode 100644 index 0000000000..45d9819c0e --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyBindings.cs @@ -0,0 +1,99 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using NUnit.Framework; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Bindings; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Input.Bindings; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.UI; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.Gameplay +{ + [HeadlessTest] + public class TestSceneKeyBindings : OsuManualInputManagerTestScene + { + private readonly ActionReceiver receiver; + + public TestSceneKeyBindings() + { + Add(new TestKeyBindingContainer + { + Child = receiver = new ActionReceiver() + }); + } + + [Test] + public void TestDefaultsWhenNotDatabased() + { + AddStep("fire key", () => + { + InputManager.PressKey(Key.A); + InputManager.ReleaseKey(Key.A); + }); + + AddAssert("received key", () => receiver.ReceivedAction); + } + + private class TestRuleset : Ruleset + { + public override IEnumerable GetModsFor(ModType type) => + throw new System.NotImplementedException(); + + public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => + throw new System.NotImplementedException(); + + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => + throw new System.NotImplementedException(); + + public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => + throw new System.NotImplementedException(); + + public override IEnumerable GetDefaultKeyBindings(int variant = 0) + { + return new[] + { + new KeyBinding(InputKey.A, TestAction.Down), + }; + } + + public override string Description => "test"; + public override string ShortName => "test"; + } + + private enum TestAction + { + Down, + } + + private class TestKeyBindingContainer : DatabasedKeyBindingContainer + { + public TestKeyBindingContainer() + : base(new TestRuleset().RulesetInfo, 0) + { + } + } + + private class ActionReceiver : CompositeDrawable, IKeyBindingHandler + { + public bool ReceivedAction; + + public bool OnPressed(TestAction action) + { + ReceivedAction = action == TestAction.Down; + return true; + } + + public void OnReleased(TestAction action) + { + throw new System.NotImplementedException(); + } + } + } +} diff --git a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs index e83d899469..94edc33099 100644 --- a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs +++ b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs @@ -62,6 +62,14 @@ namespace osu.Game.Input.Bindings store.KeyBindingChanged -= ReloadMappings; } - protected override void ReloadMappings() => KeyBindings = store.Query(ruleset?.ID, variant).ToList(); + protected override void ReloadMappings() + { + if (ruleset != null && !ruleset.ID.HasValue) + // if the provided ruleset is not stored to the database, we have no way to retrieve custom bindings. + // fallback to defaults instead. + KeyBindings = DefaultKeyBindings; + else + KeyBindings = store.Query(ruleset?.ID, variant).ToList(); + } } } From 2444dd42d0a931a9b6288813956f435b1bc5d4f2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Apr 2020 09:57:46 +0900 Subject: [PATCH 1140/2376] Remove not-implemented-exception --- osu.Game.Tests/Visual/Gameplay/TestSceneKeyBindings.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyBindings.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyBindings.cs index 45d9819c0e..db65e91d17 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyBindings.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyBindings.cs @@ -92,7 +92,6 @@ namespace osu.Game.Tests.Visual.Gameplay public void OnReleased(TestAction action) { - throw new System.NotImplementedException(); } } } From 28318a0140a4e3854d81896e7a762a451f8d7637 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Apr 2020 10:59:08 +0900 Subject: [PATCH 1141/2376] Add mention of notelock in xmldoc (potentially easier to find class) --- osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs index cd9838e7bf..176402c831 100644 --- a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs @@ -11,7 +11,7 @@ using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Osu.UI { /// - /// Ensures that s are hit in-order. + /// Ensures that s are hit in-order. Affectionately known as "note lock". /// If a is hit out of order: /// /// The hit is blocked if it occurred earlier than the previous 's start time. From e1acfd26a6849be6cee96edc66ae58c17e9a1fac Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Apr 2020 10:59:44 +0900 Subject: [PATCH 1142/2376] Simplify return logic --- osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs index 176402c831..1f027e9726 100644 --- a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs @@ -51,10 +51,7 @@ namespace osu.Game.Rulesets.Osu.UI // 1. The last blocking hitobject has been judged. // 2. The current time is after the last hitobject's start time. // Hits at exactly the same time as the blocking hitobject are allowed for maps that contain simultaneous hitobjects (e.g. /b/372245). - if (blockingObject.Judged || time >= blockingObject.HitObject.StartTime) - return true; - - return false; + return blockingObject.Judged || time >= blockingObject.HitObject.StartTime; } /// From 8c85602ad013001281d4148b6109d606d6885c8c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Apr 2020 11:00:42 +0900 Subject: [PATCH 1143/2376] Use foreach for conformity --- osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs index 1f027e9726..53dd1127d6 100644 --- a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs @@ -93,12 +93,12 @@ namespace osu.Game.Rulesets.Osu.UI yield return obj; - for (int i = 0; i < obj.NestedHitObjects.Count; i++) + foreach (var nestedObj in obj.NestedHitObjects) { - if (obj.NestedHitObjects[i].HitObject.StartTime >= targetTime) + if (nestedObj.HitObject.StartTime >= targetTime) break; - yield return obj.NestedHitObjects[i]; + yield return nestedObj; } } } From 6f233917b18dc05bb5f7d620cfc8fc512a6df9f7 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 20 Apr 2020 06:40:51 +0300 Subject: [PATCH 1144/2376] Centralize updating HasCompleted bindable logic --- osu.Game/Rulesets/Scoring/JudgementProcessor.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs index d878ef0a5c..8aef615b5f 100644 --- a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs +++ b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs @@ -58,8 +58,7 @@ namespace osu.Game.Rulesets.Scoring NewJudgement?.Invoke(result); - if (JudgedHits == MaxHits) - hasCompleted.Value = true; + updateHasCompleted(); } /// @@ -70,8 +69,7 @@ namespace osu.Game.Rulesets.Scoring { JudgedHits--; - if (JudgedHits < MaxHits) - hasCompleted.Value = false; + updateHasCompleted(); RevertResultInternal(result); } @@ -135,5 +133,7 @@ namespace osu.Game.Rulesets.Scoring ApplyResult(result); } } + + private void updateHasCompleted() => hasCompleted.Value = JudgedHits == MaxHits; } } From e12e3391fb803f9bdf75ebb51abaf275427c1d07 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 20 Apr 2020 06:42:33 +0300 Subject: [PATCH 1145/2376] Base wait steps duration on the delay used for results display With `* 2` for safety of not potentially going to the next step and the delegate not executed yet. --- .../Visual/Gameplay/TestSceneCompletionCancellation.cs | 6 +++++- osu.Game/Screens/Play/Player.cs | 7 ++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs index 54e1ff5345..aef173c36a 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs @@ -24,6 +24,9 @@ namespace osu.Game.Tests.Visual.Gameplay [Resolved] private AudioManager audio { get; set; } + private int resultsDisplayWaitCount => + (int)((Screens.Play.Player.RESULTS_DISPLAY_DELAY / TimePerAction) * 2); + protected override bool AllowFail => false; public TestSceneCompletionCancellation() @@ -83,7 +86,8 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("rewind to cancel", () => track.Seek(4000)); AddUntilStep("completion cleared by processor", () => !Player.ScoreProcessor.HasCompleted.Value); - AddWaitStep("wait", 5); + // wait to ensure there was no attempt of pushing the results screen. + AddWaitStep("wait", resultsDisplayWaitCount); } protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index c6c83e5379..2f3807753a 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -37,6 +37,11 @@ namespace osu.Game.Screens.Play [Cached] public class Player : ScreenWithBeatmapBackground { + /// + /// The delay upon completion of the beatmap before displaying the results screen. + /// + public const double RESULTS_DISPLAY_DELAY = 1000.0; + public override bool AllowBackButton => false; // handled by HoldForMenuButton protected override UserActivity InitialActivity => new UserActivity.SoloGame(Beatmap.Value.BeatmapInfo, Ruleset.Value); @@ -436,7 +441,7 @@ namespace osu.Game.Screens.Play if (!showResults) return; - using (BeginDelayedSequence(1000)) + using (BeginDelayedSequence(RESULTS_DISPLAY_DELAY)) scheduleGotoRanking(); } From 2c012b9af1106bbc154b25453a8cd71c3eb5001b Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 20 Apr 2020 06:43:18 +0300 Subject: [PATCH 1146/2376] Use AddUntilStep whenever possible Avoid redundant usage --- .../Visual/Gameplay/TestSceneCompletionCancellation.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs index aef173c36a..a3fb17942f 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs @@ -60,9 +60,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("seek to completion again", () => track.Seek(5000)); AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value); - AddWaitStep("wait", 5); - - AddAssert("attempted to push ranking", () => ((FakeRankingPushPlayer)Player).GotoRankingInvoked); + AddUntilStep("attempted to push ranking", () => ((FakeRankingPushPlayer)Player).GotoRankingInvoked); } /// From 355e682e24557b3b87d791943c0e2523e3331de6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 20 Apr 2020 13:23:27 +0900 Subject: [PATCH 1147/2376] Fix typo in exception --- osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs index 53dd1127d6..8e4f81347d 100644 --- a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs @@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Osu.UI return; if (!IsHittable(hitObject, hitObject.HitObject.StartTime + hitObject.Result.TimeOffset)) - throw new InvalidOperationException($"A {hitObject} was hit before it become hittable!"); + throw new InvalidOperationException($"A {hitObject} was hit before it became hittable!"); foreach (var obj in enumerateHitObjectsUpTo(hitObject.HitObject.StartTime)) { From ee1ccb8bcb14c637e6912c9f278b10b7573348af Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Apr 2020 14:03:55 +0900 Subject: [PATCH 1148/2376] Fix in a slightly different and hopefully more understandable way --- osu.Game/Screens/Select/SongSelect.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 9d3dc58a26..478c46fb36 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -426,7 +426,7 @@ namespace osu.Game.Screens.Select } /// - /// selection has been changed as the result of a user interaction. + /// Selection has been changed as the result of a user interaction. /// private void performUpdateSelected() { @@ -435,7 +435,7 @@ namespace osu.Game.Screens.Select selectionChangedDebounce?.Cancel(); - if (beatmap == null) + if (beatmapNoDebounce == null) run(); else selectionChangedDebounce = Scheduler.AddDelayed(run, 200); @@ -448,11 +448,11 @@ namespace osu.Game.Screens.Select { Mods.Value = Array.Empty(); - // the ruleset transfer may cause a deselection of the current beatmap (due to incompatibility). - // this can happen via Carousel.FlushPendingFilterOperations(). - // to ensure a good state, re-transfer no-debounce values. - performUpdateSelected(); - return; + // transferRulesetValue() may trigger a refilter. If the current selection does not match the new ruleset, we want to switch away from it. + // The default logic on WorkingBeatmap change is to switch to a matching ruleset (see workingBeatmapChanged()), but we don't want that here. + // We perform an early selection attempt and clear out the beatmap selection to avoid a second ruleset change (revert). + if (beatmap != null && !Carousel.SelectBeatmap(beatmap, false)) + beatmap = null; } // We may be arriving here due to another component changing the bindable Beatmap. @@ -716,7 +716,7 @@ namespace osu.Game.Screens.Select if (decoupledRuleset.Value?.Equals(Ruleset.Value) == true) return false; - Logger.Log($"decoupled ruleset transferred (\"{decoupledRuleset.Value}\" -> \"{Ruleset.Value}\""); + Logger.Log($"decoupled ruleset transferred (\"{decoupledRuleset.Value}\" -> \"{Ruleset.Value}\")"); rulesetNoDebounce = decoupledRuleset.Value = Ruleset.Value; // if we have a pending filter operation, we want to run it now. From b881293b98410e47dfde82f3f330f8aeda7d2c96 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 20 Apr 2020 14:08:23 +0900 Subject: [PATCH 1149/2376] Allow 10k to be played on a single stage --- .../Beatmaps/ManiaBeatmapConverter.cs | 2 +- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 67 +++++++++++++------ 2 files changed, 48 insertions(+), 21 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index d904474815..189dd17934 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps { TargetColumns = (int)Math.Max(1, roundedCircleSize); - if (TargetColumns >= 10) + if (TargetColumns > 10) { TargetColumns /= 2; Dual = true; diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 2bd88fee90..e8698ef01c 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -250,7 +250,7 @@ namespace osu.Game.Rulesets.Mania { get { - for (int i = 1; i <= 9; i++) + for (int i = 1; i <= 10; i++) yield return (int)PlayfieldType.Single + i; for (int i = 2; i <= 18; i += 2) yield return (int)PlayfieldType.Dual + i; @@ -262,26 +262,53 @@ namespace osu.Game.Rulesets.Mania switch (getPlayfieldType(variant)) { case PlayfieldType.Single: - return new VariantMappingGenerator + switch (variant) { - LeftKeys = new[] - { - InputKey.A, - InputKey.S, - InputKey.D, - InputKey.F - }, - RightKeys = new[] - { - InputKey.J, - InputKey.K, - InputKey.L, - InputKey.Semicolon - }, - SpecialKey = InputKey.Space, - SpecialAction = ManiaAction.Special1, - NormalActionStart = ManiaAction.Key1, - }.GenerateKeyBindingsFor(variant, out _); + case 10: + // 10K is special because it extents one key towards the centre of the keyboard (V/N), rather than towards the edges of the keyboard. + return new VariantMappingGenerator + { + LeftKeys = new[] + { + InputKey.A, + InputKey.S, + InputKey.D, + InputKey.F, + InputKey.V + }, + RightKeys = new[] + { + InputKey.N, + InputKey.J, + InputKey.K, + InputKey.L, + InputKey.Semicolon, + }, + NormalActionStart = ManiaAction.Key1, + }.GenerateKeyBindingsFor(variant, out _); + + default: + return new VariantMappingGenerator + { + LeftKeys = new[] + { + InputKey.A, + InputKey.S, + InputKey.D, + InputKey.F + }, + RightKeys = new[] + { + InputKey.J, + InputKey.K, + InputKey.L, + InputKey.Semicolon + }, + SpecialKey = InputKey.Space, + SpecialAction = ManiaAction.Special1, + NormalActionStart = ManiaAction.Key1, + }.GenerateKeyBindingsFor(variant, out _); + } case PlayfieldType.Dual: int keys = getDualStageKeyCount(variant); From 5b4f69bb8cabf89c41aff3b16c3c081975474236 Mon Sep 17 00:00:00 2001 From: Tim Oliver Date: Mon, 20 Apr 2020 13:32:51 +0800 Subject: [PATCH 1150/2376] Moved flying hit objects to separate files --- .../Objects/Drawables/DrawableCentreHit.cs | 17 -------------- .../Drawables/DrawableFlyingCentreHit.cs | 23 +++++++++++++++++++ .../Objects/Drawables/DrawableFlyingRimHit.cs | 23 +++++++++++++++++++ .../Objects/Drawables/DrawableRimHit.cs | 14 ----------- 4 files changed, 46 insertions(+), 31 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingCentreHit.cs create mode 100644 osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingRimHit.cs diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs index b6f6f04821..4979135f50 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs @@ -2,10 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; -using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; namespace osu.Game.Rulesets.Taiko.Objects.Drawables @@ -26,18 +23,4 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables MainPiece.AccentColour = colours.PinkDarker; } } - - public class DrawableFlyingCentreHit : DrawableCentreHit - { - protected override void CheckForResult(bool userTriggered, double timeOffset) - { - ApplyResult(r => r.Type = HitResult.Good); - } - - public DrawableFlyingCentreHit(double time, bool isStrong = false) - : base(new IgnoreHit { StartTime = time, IsStrong = isStrong }) - { - HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - } - } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingCentreHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingCentreHit.cs new file mode 100644 index 0000000000..826a4467f8 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingCentreHit.cs @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Taiko.Objects.Drawables +{ + public class DrawableFlyingCentreHit : DrawableCentreHit + { + protected override void CheckForResult(bool userTriggered, double timeOffset) + { + ApplyResult(r => r.Type = HitResult.Good); + } + + public DrawableFlyingCentreHit(double time, bool isStrong = false) + : base(new IgnoreHit { StartTime = time, IsStrong = isStrong }) + { + HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingRimHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingRimHit.cs new file mode 100644 index 0000000000..4a6fed8302 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingRimHit.cs @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Taiko.Objects.Drawables +{ + public class DrawableFlyingRimHit : DrawableRimHit + { + protected override void CheckForResult(bool userTriggered, double timeOffset) + { + ApplyResult(r => r.Type = HitResult.Good); + } + + public DrawableFlyingRimHit(double time, bool isStrong = false) + : base(new IgnoreHit { StartTime = time, IsStrong = isStrong }) + { + HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs index e5cfc0562b..c10c195019 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs @@ -26,18 +26,4 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables MainPiece.AccentColour = colours.BlueDarker; } } - - public class DrawableFlyingRimHit : DrawableRimHit - { - protected override void CheckForResult(bool userTriggered, double timeOffset) - { - ApplyResult(r => r.Type = HitResult.Good); - } - - public DrawableFlyingRimHit(double time, bool isStrong = false) - : base(new IgnoreHit { StartTime = time, IsStrong = isStrong }) - { - HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - } - } } From 5d96d672268cbdc0b7cb20d4a9adc47a4acd451c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 20 Apr 2020 14:40:37 +0900 Subject: [PATCH 1151/2376] Add special key definition just for sanity --- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index e8698ef01c..2147776d03 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -284,6 +284,8 @@ namespace osu.Game.Rulesets.Mania InputKey.L, InputKey.Semicolon, }, + SpecialKey = InputKey.Space, + SpecialAction = ManiaAction.Special1, NormalActionStart = ManiaAction.Key1, }.GenerateKeyBindingsFor(variant, out _); From 5464746d3d899a136bfa392789d351559d6f5752 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Apr 2020 15:25:58 +0900 Subject: [PATCH 1152/2376] Switch to using CompositeDrawable --- .../Dashboard/Friends/FriendDisplay.cs | 23 ++++++++----------- osu.Game/Overlays/OverlayView.cs | 14 ++--------- 2 files changed, 12 insertions(+), 25 deletions(-) diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs index 9764f82199..7c4a0a4164 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs @@ -34,16 +34,17 @@ namespace osu.Game.Overlays.Dashboard.Friends private Drawable currentContent; - private readonly FriendOnlineStreamControl onlineStreamControl; - private readonly Box background; - private readonly Box controlBackground; - private readonly UserListToolbar userListToolbar; - private readonly Container itemsPlaceholder; - private readonly LoadingLayer loading; + private FriendOnlineStreamControl onlineStreamControl; + private Box background; + private Box controlBackground; + private UserListToolbar userListToolbar; + private Container itemsPlaceholder; + private LoadingLayer loading; - public FriendDisplay() + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) { - AddRange(new Drawable[] + InternalChildren = new Drawable[] { new Container { @@ -121,12 +122,8 @@ namespace osu.Game.Overlays.Dashboard.Friends } } } - }); - } + }; - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) - { background.Colour = colourProvider.Background4; controlBackground.Colour = colourProvider.Background5; } diff --git a/osu.Game/Overlays/OverlayView.cs b/osu.Game/Overlays/OverlayView.cs index e3a07fc2de..724658f22f 100644 --- a/osu.Game/Overlays/OverlayView.cs +++ b/osu.Game/Overlays/OverlayView.cs @@ -9,29 +9,19 @@ using osu.Game.Online.API; namespace osu.Game.Overlays { /// - /// Drawable which used to represent online content in . + /// A subview containing online content, to be displayed inside a . /// /// Response type - public abstract class OverlayView : Container, IOnlineComponent + public abstract class OverlayView : CompositeDrawable, IOnlineComponent where T : class { [Resolved] protected IAPIProvider API { get; private set; } - protected override Container Content => content; - - private readonly FillFlowContainer content; - protected OverlayView() { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - - AddInternal(content = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - }); } protected override void LoadComplete() From 99e13b8ed9c124484b4410eb9b03da4f2be03997 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Apr 2020 15:32:50 +0900 Subject: [PATCH 1153/2376] Add better xml documentation and extract fetch method --- osu.Game/Overlays/OverlayView.cs | 33 ++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/OverlayView.cs b/osu.Game/Overlays/OverlayView.cs index 724658f22f..3e2c54c726 100644 --- a/osu.Game/Overlays/OverlayView.cs +++ b/osu.Game/Overlays/OverlayView.cs @@ -11,13 +11,18 @@ namespace osu.Game.Overlays /// /// A subview containing online content, to be displayed inside a . /// - /// Response type + /// + /// Automatically performs a data fetch on load. + /// + /// The type of the API response. public abstract class OverlayView : CompositeDrawable, IOnlineComponent where T : class { [Resolved] protected IAPIProvider API { get; private set; } + private APIRequest request; + protected OverlayView() { RelativeSizeAxes = Axes.X; @@ -30,20 +35,36 @@ namespace osu.Game.Overlays API.Register(this); } - private APIRequest request; - + /// + /// Create the API request for fetching data. + /// protected abstract APIRequest CreateRequest(); + /// + /// Fired when results arrive from the main API request. + /// + /// protected abstract void OnSuccess(T response); + /// + /// Force a re-request for data from the API. + /// + protected void PerformFetch() + { + request?.Cancel(); + + request = CreateRequest(); + request.Success += response => Schedule(() => OnSuccess(response)); + + API.Queue(request); + } + public virtual void APIStateChanged(IAPIProvider api, APIState state) { switch (state) { case APIState.Online: - request = CreateRequest(); - request.Success += response => Schedule(() => OnSuccess(response)); - api.Queue(request); + PerformFetch(); break; } } From 6b89c638c9aa2db2744a4d65bad92774e1e49a3a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Apr 2020 15:34:48 +0900 Subject: [PATCH 1154/2376] Move load to bdl --- osu.Game/Overlays/DashboardOverlay.cs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/osu.Game/Overlays/DashboardOverlay.cs b/osu.Game/Overlays/DashboardOverlay.cs index 86c0f3bd83..a72c3f4fa5 100644 --- a/osu.Game/Overlays/DashboardOverlay.cs +++ b/osu.Game/Overlays/DashboardOverlay.cs @@ -19,14 +19,19 @@ namespace osu.Game.Overlays { private CancellationTokenSource cancellationToken; - private readonly Box background; - private readonly Container content; - private readonly DashboardOverlayHeader header; - private readonly LoadingLayer loading; - private readonly OverlayScrollContainer scrollFlow; + private Box background; + private Container content; + private DashboardOverlayHeader header; + private LoadingLayer loading; + private OverlayScrollContainer scrollFlow; public DashboardOverlay() : base(OverlayColourScheme.Purple) + { + } + + [BackgroundDependencyLoader] + private void load() { Children = new Drawable[] { @@ -61,17 +66,14 @@ namespace osu.Game.Overlays }, loading = new LoadingLayer(content), }; - } - [BackgroundDependencyLoader] - private void load() - { background.Colour = ColourProvider.Background5; } protected override void LoadComplete() { base.LoadComplete(); + header.Current.BindValueChanged(onTabChanged); } From e61a90d4695ca1ca8ee6aa479b541d29e6cf08f2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Apr 2020 15:50:48 +0900 Subject: [PATCH 1155/2376] Throw instead of returning zero --- osu.Game/Utils/OrderAttribute.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Utils/OrderAttribute.cs b/osu.Game/Utils/OrderAttribute.cs index 4959caa726..aded7f9814 100644 --- a/osu.Game/Utils/OrderAttribute.cs +++ b/osu.Game/Utils/OrderAttribute.cs @@ -29,7 +29,7 @@ namespace osu.Game.Utils if (type.GetField(i.ToString()).GetCustomAttributes(typeof(OrderAttribute), false).FirstOrDefault() is OrderAttribute attr) return attr.Order; - return 0; + throw new ArgumentException($"Not all values of {nameof(T)} have {nameof(OrderAttribute)} specified."); }); } } From 801f02a3d7bb28b842a7dfd7e1d39b404b7d9690 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Apr 2020 17:48:02 +0900 Subject: [PATCH 1156/2376] Fix inline executions of APIRequest.Perform not getting result populated early enough --- osu.Game/Online/API/APIRequest.cs | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs index 47600e4f68..0bba04cac3 100644 --- a/osu.Game/Online/API/APIRequest.cs +++ b/osu.Game/Online/API/APIRequest.cs @@ -18,24 +18,32 @@ namespace osu.Game.Online.API public T Result { get; private set; } - protected APIRequest() - { - base.Success += () => TriggerSuccess(((OsuJsonWebRequest)WebRequest)?.ResponseObject); - } - /// /// Invoked on successful completion of an API request. /// This will be scheduled to the API's internal scheduler (run on update thread automatically). /// public new event APISuccessHandler Success; + protected override void PostProcess() + { + base.PostProcess(); + Result = ((OsuJsonWebRequest)WebRequest)?.ResponseObject; + } + internal void TriggerSuccess(T result) { if (Result != null) throw new InvalidOperationException("Attempted to trigger success more than once"); Result = result; - Success?.Invoke(result); + + TriggerSuccess(); + } + + internal override void TriggerSuccess() + { + base.TriggerSuccess(); + Success?.Invoke(Result); } } @@ -99,6 +107,8 @@ namespace osu.Game.Online.API if (checkAndScheduleFailure()) return; + PostProcess(); + API.Schedule(delegate { if (cancelled) return; @@ -107,7 +117,14 @@ namespace osu.Game.Online.API }); } - internal void TriggerSuccess() + /// + /// Perform any post-processing actions after a successful request. + /// + protected virtual void PostProcess() + { + } + + internal virtual void TriggerSuccess() { Success?.Invoke(); } From 3f3ff5fdb1b145f7ac885a691f7f0fbe93d1b99c Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 20 Apr 2020 09:24:40 +0000 Subject: [PATCH 1157/2376] Bump Humanizer from 2.7.9 to 2.8.2 Bumps [Humanizer](https://github.com/Humanizr/Humanizer) from 2.7.9 to 2.8.2. - [Release notes](https://github.com/Humanizr/Humanizer/releases) - [Changelog](https://github.com/Humanizr/Humanizer/blob/master/release_notes.md) - [Commits](https://github.com/Humanizr/Humanizer/compare/v2.7.9...v2.8.2) Signed-off-by: dependabot-preview[bot] --- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 5facb04117..35ee0864e1 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -19,7 +19,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index dda1ee5c42..0200fca9a3 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -76,7 +76,7 @@ - + From b3d4b4a3f42696861d8c17cd395423e4a2656f74 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Apr 2020 18:25:39 +0900 Subject: [PATCH 1158/2376] Add back missing fill flow --- .../Dashboard/Friends/FriendDisplay.cs | 124 +++++++++--------- 1 file changed, 65 insertions(+), 59 deletions(-) diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs index 7c4a0a4164..79fda99c73 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs @@ -44,78 +44,84 @@ namespace osu.Game.Overlays.Dashboard.Friends [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) { - InternalChildren = new Drawable[] + InternalChild = new FillFlowContainer { - new Container + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Children = new Drawable[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new Drawable[] + new Container { - controlBackground = new Box + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both - }, - new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding + controlBackground = new Box { - Top = 20, - Horizontal = 45 + RelativeSizeAxes = Axes.Both }, - Child = onlineStreamControl = new FriendOnlineStreamControl(), - } - } - }, - new Container - { - Name = "User List", - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new Drawable[] - { - background = new Box - { - RelativeSizeAxes = Axes.Both - }, - new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Margin = new MarginPadding { Bottom = 20 }, - Children = new Drawable[] + new Container { - new Container + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding - { - Horizontal = 40, - Vertical = 20 - }, - Child = userListToolbar = new UserListToolbar - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - } + Top = 20, + Horizontal = 45 }, - new Container + Child = onlineStreamControl = new FriendOnlineStreamControl(), + } + } + }, + new Container + { + Name = "User List", + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Margin = new MarginPadding { Bottom = 20 }, + Children = new Drawable[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new Drawable[] + new Container { - itemsPlaceholder = new Container + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Horizontal = 50 } + Horizontal = 40, + Vertical = 20 }, - loading = new LoadingLayer(itemsPlaceholder) + Child = userListToolbar = new UserListToolbar + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + } + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + itemsPlaceholder = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Horizontal = 50 } + }, + loading = new LoadingLayer(itemsPlaceholder) + } } } } From 8ebc2ae03dc6080028f5406d361f42f8d68855cc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Apr 2020 20:48:35 +0900 Subject: [PATCH 1159/2376] Never run subtree masking --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index b14927bcd5..e847dcec40 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -398,7 +398,7 @@ namespace osu.Game.Rulesets.Objects.Drawables } } - public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) => AllJudged && base.UpdateSubTreeMasking(source, maskingBounds); + public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) => false; protected override void UpdateAfterChildren() { From a541f9268205f76c1eb4321faefeb1721d4f0f8f Mon Sep 17 00:00:00 2001 From: Lucas A Date: Mon, 20 Apr 2020 13:56:15 +0200 Subject: [PATCH 1160/2376] Resolve ruleset dependencies on game core / framework assemblies by checking already loaded assemblies in AppDomain. --- osu.Game/Rulesets/RulesetStore.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index 7e165311a3..543134cfb4 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -62,10 +62,13 @@ namespace osu.Game.Rulesets var asm = new AssemblyName(args.Name); // the requesting assembly may be located out of the executable's base directory, thus requiring manual resolving of its dependencies. - // this assumes the only explicit dependency of the ruleset is the game core assembly. - // the ruleset dependency on the game core assembly requires manual resolving, transitive dependencies should be resolved automatically - if (asm.Name.Equals(typeof(OsuGame).Assembly.GetName().Name, StringComparison.Ordinal)) - return Assembly.GetExecutingAssembly(); + // this attempts resolving the ruleset dependencies on game core and framework assemblies by returning assemblies with the same assembly name + // already loaded in the AppDomain. + foreach (var curAsm in AppDomain.CurrentDomain.GetAssemblies()) + { + if (asm.Name.Equals(curAsm.GetName().Name, StringComparison.Ordinal)) + return curAsm; + } return loadedAssemblies.Keys.FirstOrDefault(a => a.FullName == asm.FullName); } From 4e271ff46fcae5132bcc7ef235117518cd8f1268 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 20 Apr 2020 21:28:36 +0900 Subject: [PATCH 1161/2376] Add support for 10K mod + 20K dual stages --- .../DualStageVariantGenerator.cs | 64 ++++++++ osu.Game.Rulesets.Mania/ManiaInputManager.cs | 6 + osu.Game.Rulesets.Mania/ManiaRuleset.cs | 152 +----------------- osu.Game.Rulesets.Mania/Mods/ManiaModKey10.cs | 13 ++ .../SingleStageVariantGenerator.cs | 41 +++++ .../VariantMappingGenerator.cs | 61 +++++++ 6 files changed, 189 insertions(+), 148 deletions(-) create mode 100644 osu.Game.Rulesets.Mania/DualStageVariantGenerator.cs create mode 100644 osu.Game.Rulesets.Mania/Mods/ManiaModKey10.cs create mode 100644 osu.Game.Rulesets.Mania/SingleStageVariantGenerator.cs create mode 100644 osu.Game.Rulesets.Mania/VariantMappingGenerator.cs diff --git a/osu.Game.Rulesets.Mania/DualStageVariantGenerator.cs b/osu.Game.Rulesets.Mania/DualStageVariantGenerator.cs new file mode 100644 index 0000000000..8d39e08b26 --- /dev/null +++ b/osu.Game.Rulesets.Mania/DualStageVariantGenerator.cs @@ -0,0 +1,64 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Input.Bindings; + +namespace osu.Game.Rulesets.Mania +{ + public class DualStageVariantGenerator + { + private readonly int singleStageVariant; + private readonly InputKey[] stage1LeftKeys; + private readonly InputKey[] stage1RightKeys; + private readonly InputKey[] stage2LeftKeys; + private readonly InputKey[] stage2RightKeys; + + public DualStageVariantGenerator(int singleStageVariant) + { + this.singleStageVariant = singleStageVariant; + + // 10K is special because it expands towards the centre of the keyboard (VM/BN), rather than towards the edges of the keyboard. + if (singleStageVariant == 10) + { + stage1LeftKeys = new[] { InputKey.Q, InputKey.W, InputKey.E, InputKey.R, InputKey.V }; + stage1RightKeys = new[] { InputKey.M, InputKey.I, InputKey.O, InputKey.P, InputKey.BracketLeft }; + + stage2LeftKeys = new[] { InputKey.S, InputKey.D, InputKey.F, InputKey.G, InputKey.B }; + stage2RightKeys = new[] { InputKey.N, InputKey.J, InputKey.K, InputKey.L, InputKey.Semicolon }; + } + else + { + stage1LeftKeys = new[] { InputKey.Q, InputKey.W, InputKey.E, InputKey.R }; + stage1RightKeys = new[] { InputKey.I, InputKey.O, InputKey.P, InputKey.BracketLeft }; + + stage2LeftKeys = new[] { InputKey.S, InputKey.D, InputKey.F, InputKey.G }; + stage2RightKeys = new[] { InputKey.J, InputKey.K, InputKey.L, InputKey.Semicolon }; + } + } + + public IEnumerable GenerateMappings() + { + var stage1Bindings = new VariantMappingGenerator + { + LeftKeys = stage1LeftKeys, + RightKeys = stage1RightKeys, + SpecialKey = InputKey.V, + SpecialAction = ManiaAction.Special1, + NormalActionStart = ManiaAction.Key1 + }.GenerateKeyBindingsFor(singleStageVariant, out var nextNormal); + + var stage2Bindings = new VariantMappingGenerator + { + LeftKeys = stage2LeftKeys, + RightKeys = stage2RightKeys, + SpecialKey = InputKey.B, + SpecialAction = ManiaAction.Special2, + NormalActionStart = nextNormal + }.GenerateKeyBindingsFor(singleStageVariant, out _); + + return stage1Bindings.Concat(stage2Bindings); + } + } +} diff --git a/osu.Game.Rulesets.Mania/ManiaInputManager.cs b/osu.Game.Rulesets.Mania/ManiaInputManager.cs index 292990fd7e..186fc4b15d 100644 --- a/osu.Game.Rulesets.Mania/ManiaInputManager.cs +++ b/osu.Game.Rulesets.Mania/ManiaInputManager.cs @@ -78,5 +78,11 @@ namespace osu.Game.Rulesets.Mania [Description("Key 18")] Key18, + + [Description("Key 19")] + Key19, + + [Description("Key 20")] + Key20, } } diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 2147776d03..21315e4bfb 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -202,6 +202,7 @@ namespace osu.Game.Rulesets.Mania new ManiaModKey7(), new ManiaModKey8(), new ManiaModKey9(), + new ManiaModKey10(), new ManiaModKey1(), new ManiaModKey2(), new ManiaModKey3()), @@ -252,7 +253,7 @@ namespace osu.Game.Rulesets.Mania { for (int i = 1; i <= 10; i++) yield return (int)PlayfieldType.Single + i; - for (int i = 2; i <= 18; i += 2) + for (int i = 2; i <= 20; i += 2) yield return (int)PlayfieldType.Dual + i; } } @@ -262,102 +263,10 @@ namespace osu.Game.Rulesets.Mania switch (getPlayfieldType(variant)) { case PlayfieldType.Single: - switch (variant) - { - case 10: - // 10K is special because it extents one key towards the centre of the keyboard (V/N), rather than towards the edges of the keyboard. - return new VariantMappingGenerator - { - LeftKeys = new[] - { - InputKey.A, - InputKey.S, - InputKey.D, - InputKey.F, - InputKey.V - }, - RightKeys = new[] - { - InputKey.N, - InputKey.J, - InputKey.K, - InputKey.L, - InputKey.Semicolon, - }, - SpecialKey = InputKey.Space, - SpecialAction = ManiaAction.Special1, - NormalActionStart = ManiaAction.Key1, - }.GenerateKeyBindingsFor(variant, out _); - - default: - return new VariantMappingGenerator - { - LeftKeys = new[] - { - InputKey.A, - InputKey.S, - InputKey.D, - InputKey.F - }, - RightKeys = new[] - { - InputKey.J, - InputKey.K, - InputKey.L, - InputKey.Semicolon - }, - SpecialKey = InputKey.Space, - SpecialAction = ManiaAction.Special1, - NormalActionStart = ManiaAction.Key1, - }.GenerateKeyBindingsFor(variant, out _); - } + return new SingleStageVariantGenerator(variant).GenerateMappings(); case PlayfieldType.Dual: - int keys = getDualStageKeyCount(variant); - - var stage1Bindings = new VariantMappingGenerator - { - LeftKeys = new[] - { - InputKey.Q, - InputKey.W, - InputKey.E, - InputKey.R, - }, - RightKeys = new[] - { - InputKey.X, - InputKey.C, - InputKey.V, - InputKey.B - }, - SpecialKey = InputKey.S, - SpecialAction = ManiaAction.Special1, - NormalActionStart = ManiaAction.Key1 - }.GenerateKeyBindingsFor(keys, out var nextNormal); - - var stage2Bindings = new VariantMappingGenerator - { - LeftKeys = new[] - { - InputKey.Number7, - InputKey.Number8, - InputKey.Number9, - InputKey.Number0 - }, - RightKeys = new[] - { - InputKey.K, - InputKey.L, - InputKey.Semicolon, - InputKey.Quote - }, - SpecialKey = InputKey.I, - SpecialAction = ManiaAction.Special2, - NormalActionStart = nextNormal - }.GenerateKeyBindingsFor(keys, out _); - - return stage1Bindings.Concat(stage2Bindings); + return new DualStageVariantGenerator(getDualStageKeyCount(variant)).GenerateMappings(); } return Array.Empty(); @@ -393,59 +302,6 @@ namespace osu.Game.Rulesets.Mania { return (PlayfieldType)Enum.GetValues(typeof(PlayfieldType)).Cast().OrderByDescending(i => i).First(v => variant >= v); } - - private class VariantMappingGenerator - { - /// - /// All the s available to the left hand. - /// - public InputKey[] LeftKeys; - - /// - /// All the s available to the right hand. - /// - public InputKey[] RightKeys; - - /// - /// The for the special key. - /// - public InputKey SpecialKey; - - /// - /// The at which the normal columns should begin. - /// - public ManiaAction NormalActionStart; - - /// - /// The for the special column. - /// - public ManiaAction SpecialAction; - - /// - /// Generates a list of s for a specific number of columns. - /// - /// The number of columns that need to be bound. - /// The next to use for normal columns. - /// The keybindings. - public IEnumerable GenerateKeyBindingsFor(int columns, out ManiaAction nextNormalAction) - { - ManiaAction currentNormalAction = NormalActionStart; - - var bindings = new List(); - - for (int i = LeftKeys.Length - columns / 2; i < LeftKeys.Length; i++) - bindings.Add(new KeyBinding(LeftKeys[i], currentNormalAction++)); - - if (columns % 2 == 1) - bindings.Add(new KeyBinding(SpecialKey, SpecialAction)); - - for (int i = 0; i < columns / 2; i++) - bindings.Add(new KeyBinding(RightKeys[i], currentNormalAction++)); - - nextNormalAction = currentNormalAction; - return bindings; - } - } } public enum PlayfieldType diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModKey10.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModKey10.cs new file mode 100644 index 0000000000..684370fc3d --- /dev/null +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey10.cs @@ -0,0 +1,13 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Rulesets.Mania.Mods +{ + public class ManiaModKey10 : ManiaKeyMod + { + public override int KeyCount => 10; + public override string Name => "Ten Keys"; + public override string Acronym => "10K"; + public override string Description => @"Play with ten keys."; + } +} diff --git a/osu.Game.Rulesets.Mania/SingleStageVariantGenerator.cs b/osu.Game.Rulesets.Mania/SingleStageVariantGenerator.cs new file mode 100644 index 0000000000..2069329d9a --- /dev/null +++ b/osu.Game.Rulesets.Mania/SingleStageVariantGenerator.cs @@ -0,0 +1,41 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Framework.Input.Bindings; + +namespace osu.Game.Rulesets.Mania +{ + public class SingleStageVariantGenerator + { + private readonly int variant; + private readonly InputKey[] leftKeys; + private readonly InputKey[] rightKeys; + + public SingleStageVariantGenerator(int variant) + { + this.variant = variant; + + // 10K is special because it expands towards the centre of the keyboard (V/N), rather than towards the edges of the keyboard. + if (variant == 10) + { + leftKeys = new[] { InputKey.A, InputKey.S, InputKey.D, InputKey.F, InputKey.V }; + rightKeys = new[] { InputKey.N, InputKey.J, InputKey.K, InputKey.L, InputKey.Semicolon }; + } + else + { + leftKeys = new[] { InputKey.A, InputKey.S, InputKey.D, InputKey.F }; + rightKeys = new[] { InputKey.J, InputKey.K, InputKey.L, InputKey.Semicolon }; + } + } + + public IEnumerable GenerateMappings() => new VariantMappingGenerator + { + LeftKeys = leftKeys, + RightKeys = rightKeys, + SpecialKey = InputKey.Space, + SpecialAction = ManiaAction.Special1, + NormalActionStart = ManiaAction.Key1, + }.GenerateKeyBindingsFor(variant, out _); + } +} diff --git a/osu.Game.Rulesets.Mania/VariantMappingGenerator.cs b/osu.Game.Rulesets.Mania/VariantMappingGenerator.cs new file mode 100644 index 0000000000..878d1088a6 --- /dev/null +++ b/osu.Game.Rulesets.Mania/VariantMappingGenerator.cs @@ -0,0 +1,61 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Framework.Input.Bindings; + +namespace osu.Game.Rulesets.Mania +{ + public class VariantMappingGenerator + { + /// + /// All the s available to the left hand. + /// + public InputKey[] LeftKeys; + + /// + /// All the s available to the right hand. + /// + public InputKey[] RightKeys; + + /// + /// The for the special key. + /// + public InputKey SpecialKey; + + /// + /// The at which the normal columns should begin. + /// + public ManiaAction NormalActionStart; + + /// + /// The for the special column. + /// + public ManiaAction SpecialAction; + + /// + /// Generates a list of s for a specific number of columns. + /// + /// The number of columns that need to be bound. + /// The next to use for normal columns. + /// The keybindings. + public IEnumerable GenerateKeyBindingsFor(int columns, out ManiaAction nextNormalAction) + { + ManiaAction currentNormalAction = NormalActionStart; + + var bindings = new List(); + + for (int i = LeftKeys.Length - columns / 2; i < LeftKeys.Length; i++) + bindings.Add(new KeyBinding(LeftKeys[i], currentNormalAction++)); + + if (columns % 2 == 1) + bindings.Add(new KeyBinding(SpecialKey, SpecialAction)); + + for (int i = 0; i < columns / 2; i++) + bindings.Add(new KeyBinding(RightKeys[i], currentNormalAction++)); + + nextNormalAction = currentNormalAction; + return bindings; + } + } +} From 9b6e26583bdb69c01219ab2b9ceb8d554883d1f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 20 Apr 2020 21:42:43 +0200 Subject: [PATCH 1162/2376] Add xmldocs --- osu.Game/Screens/Select/BeatmapCarousel.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index cf3a5a7199..e21faf321e 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -28,7 +28,14 @@ namespace osu.Game.Screens.Select { public class BeatmapCarousel : CompositeDrawable, IKeyBindingHandler { + /// + /// Height of the area above the carousel that should be treated as visible due to transparency of elements in front of it. + /// public float BleedTop; + + /// + /// Height of the area below the carousel that should be treated as visible due to transparency of elements in front of it. + /// public float BleedBottom; /// From e3cd3cf1da7e8e9458697caccf6bb30c008d5791 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 20 Apr 2020 21:43:07 +0200 Subject: [PATCH 1163/2376] Convert to auto-properties --- osu.Game/Screens/Select/BeatmapCarousel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index e21faf321e..1bbd7c1270 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -31,12 +31,12 @@ namespace osu.Game.Screens.Select /// /// Height of the area above the carousel that should be treated as visible due to transparency of elements in front of it. /// - public float BleedTop; + public float BleedTop { get; set; } /// /// Height of the area below the carousel that should be treated as visible due to transparency of elements in front of it. /// - public float BleedBottom; + public float BleedBottom { get; set; } /// /// Triggered when the loaded change and are completely loaded. From 4c689c6ad2585087d8495514f755e4c25579b9d3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Apr 2020 10:56:04 +0900 Subject: [PATCH 1164/2376] Add constant for max stage keys --- .../Beatmaps/ManiaBeatmapConverter.cs | 2 +- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index 189dd17934..4187e39b43 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps { TargetColumns = (int)Math.Max(1, roundedCircleSize); - if (TargetColumns > 10) + if (TargetColumns > ManiaRuleset.MAX_STAGE_KEYS) { TargetColumns /= 2; Dual = true; diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 21315e4bfb..a37aaa8cc4 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -35,6 +35,11 @@ namespace osu.Game.Rulesets.Mania { public class ManiaRuleset : Ruleset, ILegacyRuleset { + /// + /// The maximum number of supported keys in a single stage. + /// + public const int MAX_STAGE_KEYS = 10; + public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => new DrawableManiaRuleset(this, beatmap, mods); public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor(); @@ -251,9 +256,9 @@ namespace osu.Game.Rulesets.Mania { get { - for (int i = 1; i <= 10; i++) + for (int i = 1; i <= MAX_STAGE_KEYS; i++) yield return (int)PlayfieldType.Single + i; - for (int i = 2; i <= 20; i += 2) + for (int i = 2; i <= MAX_STAGE_KEYS * 2; i += 2) yield return (int)PlayfieldType.Dual + i; } } From a91c63819b0f781f1fdba608aa398a6612bb2e61 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Apr 2020 11:51:20 +0900 Subject: [PATCH 1165/2376] Refactor updateCompletionState implementation for legibility and code share --- osu.Game/Screens/Play/Player.cs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 2f3807753a..ece4c6307e 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -423,8 +423,6 @@ namespace osu.Game.Screens.Play if (!this.IsCurrentScreen()) return; - // cancel push delegate in case judges reverted - // after delegate may have already been scheduled. if (!completionState.NewValue) { completionProgressDelegate?.Cancel(); @@ -433,8 +431,11 @@ namespace osu.Game.Screens.Play return; } + if (completionProgressDelegate != null) + throw new InvalidOperationException($"{nameof(updateCompletionState)} was fired more than once"); + // Only show the completion screen if the player hasn't failed - if (HealthProcessor.HasFailed || completionProgressDelegate != null) + if (HealthProcessor.HasFailed) return; ValidForResume = false; @@ -442,7 +443,7 @@ namespace osu.Game.Screens.Play if (!showResults) return; using (BeginDelayedSequence(RESULTS_DISPLAY_DELAY)) - scheduleGotoRanking(); + completionProgressDelegate = Schedule(GotoRanking); } protected virtual ScoreInfo CreateScore() @@ -694,12 +695,6 @@ namespace osu.Game.Screens.Play storyboardReplacesBackground.Value = false; } - private void scheduleGotoRanking() - { - completionProgressDelegate?.Cancel(); - completionProgressDelegate = Schedule(GotoRanking); - } - #endregion } } From 9373520bca1ebc0f38d16bca5163eb9ed2e28a5c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 21 Apr 2020 05:59:37 +0300 Subject: [PATCH 1166/2376] Add constant for special colour of catcher on default skin --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index f37dae29dd..97d0fb0ada 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -25,6 +25,14 @@ namespace osu.Game.Rulesets.Catch.UI { public static readonly Color4 DEFAULT_HYPER_DASH_COLOUR = Color4.Red; + /// + /// The default colour used directly for . + /// + /// + /// This colour is only used when no skin overrides . + /// + public static readonly Color4 DEFAULT_CATCHER_HYPER_DASH_COLOUR = Color4.OrangeRed; + /// /// Whether we are hyper-dashing or not. /// @@ -285,7 +293,13 @@ namespace osu.Game.Rulesets.Catch.UI if (hyperDashing) { - this.FadeColour(hyperDashColour == DefaultHyperDashColour ? Color4.OrangeRed : hyperDashColour, hyper_dash_transition_length, Easing.OutQuint); + // special behaviour for catcher colour if no skin overrides. + var catcherColour = + hyperDashColour == DEFAULT_HYPER_DASH_COLOUR + ? DEFAULT_CATCHER_HYPER_DASH_COLOUR + : hyperDashColour; + + this.FadeColour(catcherColour, hyper_dash_transition_length, Easing.OutQuint); this.FadeTo(0.2f, hyper_dash_transition_length, Easing.OutQuint); } else From 282d1001093ac757eac1e65933577d1ac035e9d7 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 21 Apr 2020 06:09:57 +0300 Subject: [PATCH 1167/2376] Fix XMLDoc references --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 97d0fb0ada..056e838419 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Catch.UI public static readonly Color4 DEFAULT_HYPER_DASH_COLOUR = Color4.Red; /// - /// The default colour used directly for . + /// The default colour used directly for this 's . /// /// /// This colour is only used when no skin overrides . From 3b0099c687edce710dfcac9ac2d8d0e1a67153f4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Apr 2020 12:26:43 +0900 Subject: [PATCH 1168/2376] Refactor tests --- .../TestSceneCompletionCancellation.cs | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs index a3fb17942f..512584bd42 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs @@ -46,46 +46,54 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestCancelCompletionOnRewind() { - cancelCompletionSteps(); + complete(); + cancel(); - AddAssert("no attempt to push ranking", () => !((FakeRankingPushPlayer)Player).GotoRankingInvoked); + checkNoRanking(); } [Test] public void TestReCompleteAfterCancellation() { - cancelCompletionSteps(); - - // Attempt completing again. - AddStep("seek to completion again", () => track.Seek(5000)); - AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value); + complete(); + cancel(); + complete(); AddUntilStep("attempted to push ranking", () => ((FakeRankingPushPlayer)Player).GotoRankingInvoked); } /// - /// Tests whether can still pause after cancelling completion - /// by reverting back to true. + /// Tests whether can still pause after cancelling completion by reverting back to true. /// [Test] public void TestCanPauseAfterCancellation() { - cancelCompletionSteps(); + complete(); + cancel(); AddStep("pause", () => Player.Pause()); AddAssert("paused successfully", () => Player.GameplayClockContainer.IsPaused.Value); + + checkNoRanking(); } - private void cancelCompletionSteps() + private void complete() { AddStep("seek to completion", () => track.Seek(5000)); AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value); + } + private void cancel() + { AddStep("rewind to cancel", () => track.Seek(4000)); AddUntilStep("completion cleared by processor", () => !Player.ScoreProcessor.HasCompleted.Value); + } + private void checkNoRanking() + { // wait to ensure there was no attempt of pushing the results screen. AddWaitStep("wait", resultsDisplayWaitCount); + AddAssert("no attempt to push ranking", () => !((FakeRankingPushPlayer)Player).GotoRankingInvoked); } protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) From 9252b7876b6221c5a9cc3128de91796278a4d2dd Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 21 Apr 2020 13:58:23 +0900 Subject: [PATCH 1169/2376] Don't serialise AllControlPoints --- osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs index d33a922a32..af6ca24165 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs @@ -56,6 +56,7 @@ namespace osu.Game.Beatmaps.ControlPoints /// /// All control points, of all types. /// + [JsonIgnore] public IEnumerable AllControlPoints => Groups.SelectMany(g => g.ControlPoints).ToArray(); /// From 72fb34f82cf1ae65ad7a96ed98a40a12aae7b26b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 21 Apr 2020 14:19:05 +0900 Subject: [PATCH 1170/2376] Fix overriding control points incorrectly --- .../Formats/LegacyBeatmapDecoderTest.cs | 5 ++++ .../Beatmaps/Formats/LegacyBeatmapDecoder.cs | 30 +++++++++---------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index 33f484a9aa..acb30a6277 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -241,6 +241,11 @@ namespace osu.Game.Tests.Beatmaps.Formats { var controlPoints = decoder.Decode(stream).ControlPointInfo; + Assert.That(controlPoints.TimingPoints.Count, Is.EqualTo(4)); + Assert.That(controlPoints.DifficultyPoints.Count, Is.EqualTo(3)); + Assert.That(controlPoints.EffectPoints.Count, Is.EqualTo(3)); + Assert.That(controlPoints.SamplePoints.Count, Is.EqualTo(3)); + Assert.That(controlPoints.DifficultyPointAt(500).SpeedMultiplier, Is.EqualTo(1.5).Within(0.1)); Assert.That(controlPoints.DifficultyPointAt(1500).SpeedMultiplier, Is.EqualTo(1.5).Within(0.1)); Assert.That(controlPoints.DifficultyPointAt(2500).SpeedMultiplier, Is.EqualTo(0.75).Within(0.1)); diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 33bb9774df..388abf4648 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -386,17 +386,10 @@ namespace osu.Game.Beatmaps.Formats SampleVolume = sampleVolume, CustomSampleBank = customSampleBank, }, timingChange); - - // To handle the scenario where a non-timing line shares the same time value as a subsequent timing line but - // appears earlier in the file, we buffer non-timing control points and rewrite them *after* control points from the timing line - // with the same time value (allowing them to overwrite as necessary). - // - // The expected outcome is that we prefer the non-timing line's adjustments over the timing line's adjustments when time is equal. - if (timingChange) - flushPendingPoints(); } private readonly List pendingControlPoints = new List(); + private readonly HashSet pendingControlPointTypes = new HashSet(); private double pendingControlPointsTime; private void addControlPoint(double time, ControlPoint point, bool timingChange) @@ -405,21 +398,28 @@ namespace osu.Game.Beatmaps.Formats flushPendingPoints(); if (timingChange) - { - beatmap.ControlPointInfo.Add(time, point); - return; - } + pendingControlPoints.Insert(0, point); + else + pendingControlPoints.Add(point); - pendingControlPoints.Add(point); pendingControlPointsTime = time; } private void flushPendingPoints() { - foreach (var p in pendingControlPoints) - beatmap.ControlPointInfo.Add(pendingControlPointsTime, p); + // Changes from non-timing-points are added to the end of the list (see addControlPoint()) and should override any changes from timing-points (added to the start of the list). + for (int i = pendingControlPoints.Count - 1; i >= 0; i--) + { + var type = pendingControlPoints[i].GetType(); + if (pendingControlPointTypes.Contains(type)) + continue; + + pendingControlPointTypes.Add(type); + beatmap.ControlPointInfo.Add(pendingControlPointsTime, pendingControlPoints[i]); + } pendingControlPoints.Clear(); + pendingControlPointTypes.Clear(); } private void handleHitObject(string line) From 89320b510c10a2aad660ab9c3938565828520fde Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Apr 2020 15:13:19 +0900 Subject: [PATCH 1171/2376] Apply class renaming --- .../Online/TestSceneBeatmapListingOverlay.cs | 2 +- ...> TestSceneBeatmapListingSearchControl.cs} | 26 ++++++++--------- ... TestSceneBeatmapListingSortTabControl.cs} | 5 ++-- ...dler.cs => BeatmapListingFilterControl.cs} | 28 +++++++++---------- ...tion.cs => BeatmapListingSearchControl.cs} | 4 +-- osu.Game/Overlays/BeatmapListingOverlay.cs | 2 +- 6 files changed, 33 insertions(+), 34 deletions(-) rename osu.Game.Tests/Visual/UserInterface/{TestSceneBeatmapListingSearchSection.cs => TestSceneBeatmapListingSearchControl.cs} (76%) rename osu.Game.Tests/Visual/UserInterface/{TestSceneBeatmapListingSort.cs => TestSceneBeatmapListingSortTabControl.cs} (91%) rename osu.Game/Overlays/BeatmapListing/{BeatmapListingSearchHandler.cs => BeatmapListingFilterControl.cs} (85%) rename osu.Game/Overlays/BeatmapListing/{BeatmapListingSearchSection.cs => BeatmapListingSearchControl.cs} (97%) diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs index f80687e142..64d1a9ddcd 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs @@ -14,7 +14,7 @@ namespace osu.Game.Tests.Visual.Online public override IReadOnlyList RequiredTypes => new[] { typeof(BeatmapListingOverlay), - typeof(BeatmapListingSearchHandler) + typeof(BeatmapListingFilterControl) }; protected override bool UseOnlineAPI => true; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchSection.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs similarity index 76% rename from osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchSection.cs rename to osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs index 69e3fbd75f..d6ede950df 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchSection.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs @@ -15,19 +15,19 @@ using osuTK; namespace osu.Game.Tests.Visual.UserInterface { - public class TestSceneBeatmapListingSearchSection : OsuTestScene + public class TestSceneBeatmapListingSearchControl : OsuTestScene { public override IReadOnlyList RequiredTypes => new[] { - typeof(BeatmapListingSearchSection), + typeof(BeatmapListingSearchControl), }; [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); - private readonly BeatmapListingSearchSection section; + private readonly BeatmapListingSearchControl control; - public TestSceneBeatmapListingSearchSection() + public TestSceneBeatmapListingSearchControl() { OsuSpriteText query; OsuSpriteText ruleset; @@ -35,7 +35,7 @@ namespace osu.Game.Tests.Visual.UserInterface OsuSpriteText genre; OsuSpriteText language; - Add(section = new BeatmapListingSearchSection + Add(control = new BeatmapListingSearchControl { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -56,19 +56,19 @@ namespace osu.Game.Tests.Visual.UserInterface } }); - section.Query.BindValueChanged(q => query.Text = $"Query: {q.NewValue}", true); - section.Ruleset.BindValueChanged(r => ruleset.Text = $"Ruleset: {r.NewValue}", true); - section.Category.BindValueChanged(c => category.Text = $"Category: {c.NewValue}", true); - section.Genre.BindValueChanged(g => genre.Text = $"Genre: {g.NewValue}", true); - section.Language.BindValueChanged(l => language.Text = $"Language: {l.NewValue}", true); + control.Query.BindValueChanged(q => query.Text = $"Query: {q.NewValue}", true); + control.Ruleset.BindValueChanged(r => ruleset.Text = $"Ruleset: {r.NewValue}", true); + control.Category.BindValueChanged(c => category.Text = $"Category: {c.NewValue}", true); + control.Genre.BindValueChanged(g => genre.Text = $"Genre: {g.NewValue}", true); + control.Language.BindValueChanged(l => language.Text = $"Language: {l.NewValue}", true); } [Test] public void TestCovers() { - AddStep("Set beatmap", () => section.BeatmapSet = beatmap_set); - AddStep("Set beatmap (no cover)", () => section.BeatmapSet = no_cover_beatmap_set); - AddStep("Set null beatmap", () => section.BeatmapSet = null); + AddStep("Set beatmap", () => control.BeatmapSet = beatmap_set); + AddStep("Set beatmap (no cover)", () => control.BeatmapSet = no_cover_beatmap_set); + AddStep("Set null beatmap", () => control.BeatmapSet = null); } private static readonly BeatmapSetInfo beatmap_set = new BeatmapSetInfo diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSort.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSortTabControl.cs similarity index 91% rename from osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSort.cs rename to osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSortTabControl.cs index a5fa085abf..f643d4e3fe 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSort.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSortTabControl.cs @@ -13,18 +13,17 @@ using osuTK; namespace osu.Game.Tests.Visual.UserInterface { - public class TestSceneBeatmapListingSort : OsuTestScene + public class TestSceneBeatmapListingSortTabControl : OsuTestScene { public override IReadOnlyList RequiredTypes => new[] { - typeof(BeatmapListingSortTabControl), typeof(OverlaySortTabControl<>), }; [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); - public TestSceneBeatmapListingSort() + public TestSceneBeatmapListingSortTabControl() { BeatmapListingSortTabControl control; OsuSpriteText current; diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchHandler.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs similarity index 85% rename from osu.Game/Overlays/BeatmapListing/BeatmapListingSearchHandler.cs rename to osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs index ce3d37fb98..8817031bce 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchHandler.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs @@ -21,7 +21,7 @@ using osuTK.Graphics; namespace osu.Game.Overlays.BeatmapListing { - public class BeatmapListingSearchHandler : CompositeDrawable + public class BeatmapListingFilterControl : CompositeDrawable { public Action> SearchFinished; public Action SearchStarted; @@ -32,13 +32,13 @@ namespace osu.Game.Overlays.BeatmapListing [Resolved] private RulesetStore rulesets { get; set; } - private readonly BeatmapListingSearchSection searchSection; + private readonly BeatmapListingSearchControl searchControl; private readonly BeatmapListingSortTabControl sortControl; private readonly Box sortControlBackground; private SearchBeatmapSetsRequest getSetsRequest; - public BeatmapListingSearchHandler() + public BeatmapListingFilterControl() { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; @@ -62,7 +62,7 @@ namespace osu.Game.Overlays.BeatmapListing Radius = 3, Offset = new Vector2(0f, 1f), }, - Child = searchSection = new BeatmapListingSearchSection(), + Child = searchControl = new BeatmapListingSearchControl(), }, new Container { @@ -99,17 +99,17 @@ namespace osu.Game.Overlays.BeatmapListing var sortCriteria = sortControl.Current; var sortDirection = sortControl.SortDirection; - searchSection.Query.BindValueChanged(query => + searchControl.Query.BindValueChanged(query => { sortCriteria.Value = string.IsNullOrEmpty(query.NewValue) ? DirectSortCriteria.Ranked : DirectSortCriteria.Relevance; sortDirection.Value = SortDirection.Descending; queueUpdateSearch(true); }); - searchSection.Ruleset.BindValueChanged(_ => queueUpdateSearch()); - searchSection.Category.BindValueChanged(_ => queueUpdateSearch()); - searchSection.Genre.BindValueChanged(_ => queueUpdateSearch()); - searchSection.Language.BindValueChanged(_ => queueUpdateSearch()); + searchControl.Ruleset.BindValueChanged(_ => queueUpdateSearch()); + searchControl.Category.BindValueChanged(_ => queueUpdateSearch()); + searchControl.Genre.BindValueChanged(_ => queueUpdateSearch()); + searchControl.Language.BindValueChanged(_ => queueUpdateSearch()); sortCriteria.BindValueChanged(_ => queueUpdateSearch()); sortDirection.BindValueChanged(_ => queueUpdateSearch()); @@ -129,13 +129,13 @@ namespace osu.Game.Overlays.BeatmapListing private void updateSearch() { - getSetsRequest = new SearchBeatmapSetsRequest(searchSection.Query.Value, searchSection.Ruleset.Value) + getSetsRequest = new SearchBeatmapSetsRequest(searchControl.Query.Value, searchControl.Ruleset.Value) { - SearchCategory = searchSection.Category.Value, + SearchCategory = searchControl.Category.Value, SortCriteria = sortControl.Current.Value, SortDirection = sortControl.SortDirection.Value, - Genre = searchSection.Genre.Value, - Language = searchSection.Language.Value + Genre = searchControl.Genre.Value, + Language = searchControl.Language.Value }; getSetsRequest.Success += response => Schedule(() => onSearchFinished(response)); @@ -147,7 +147,7 @@ namespace osu.Game.Overlays.BeatmapListing { var beatmaps = response.BeatmapSets.Select(r => r.ToBeatmapSet(rulesets)).ToList(); - searchSection.BeatmapSet = response.Total == 0 ? null : beatmaps.First(); + searchControl.BeatmapSet = response.Total == 0 ? null : beatmaps.First(); SearchFinished?.Invoke(beatmaps); } diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchSection.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs similarity index 97% rename from osu.Game/Overlays/BeatmapListing/BeatmapListingSearchSection.cs rename to osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs index 3f9cc211df..9ae2696a22 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchSection.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs @@ -17,7 +17,7 @@ using osu.Game.Rulesets; namespace osu.Game.Overlays.BeatmapListing { - public class BeatmapListingSearchSection : CompositeDrawable + public class BeatmapListingSearchControl : CompositeDrawable { public Bindable Query => textBox.Current; @@ -53,7 +53,7 @@ namespace osu.Game.Overlays.BeatmapListing private readonly Box background; private readonly UpdateableBeatmapSetCover beatmapCover; - public BeatmapListingSearchSection() + public BeatmapListingSearchControl() { AutoSizeAxes = Axes.Y; RelativeSizeAxes = Axes.X; diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index 31dd692528..e16924464d 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -57,7 +57,7 @@ namespace osu.Game.Overlays Children = new Drawable[] { new BeatmapListingHeader(), - new BeatmapListingSearchHandler + new BeatmapListingFilterControl { SearchStarted = onSearchStarted, SearchFinished = onSearchFinished, From 5e3fad86cffb380f93eb78d1e541bb050b94094c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Apr 2020 15:28:25 +0900 Subject: [PATCH 1172/2376] Fix relax replays playing back incorrectly --- osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs | 13 +++++++++++-- osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs | 18 ++++++++++++++++-- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs b/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs index 1ef235f764..16414261a5 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs @@ -9,17 +9,26 @@ using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; +using osu.Game.Screens.Play; using osuTK; namespace osu.Game.Rulesets.Catch.Mods { - public class CatchModRelax : ModRelax, IApplicableToDrawableRuleset + public class CatchModRelax : ModRelax, IApplicableToDrawableRuleset, IApplicableToPlayer { public override string Description => @"Use the mouse to control the catcher."; + private DrawableRuleset drawableRuleset; + public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { - drawableRuleset.Cursor.Add(new MouseInputHelper((CatchPlayfield)drawableRuleset.Playfield)); + this.drawableRuleset = drawableRuleset; + } + + public void ApplyToPlayer(Player player) + { + if (!drawableRuleset.HasReplayLoaded.Value) + drawableRuleset.Cursor.Add(new MouseInputHelper((CatchPlayfield)drawableRuleset.Playfield)); } private class MouseInputHelper : Drawable, IKeyBindingHandler, IRequireHighFrequencyMousePosition diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs index 9b0759d9d2..9a7b967117 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs @@ -11,11 +11,12 @@ using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.UI; +using osu.Game.Screens.Play; using static osu.Game.Input.Handlers.ReplayInputHandler; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset + public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset, IApplicableToPlayer { public override string Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things."; public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).ToArray(); @@ -33,15 +34,28 @@ namespace osu.Game.Rulesets.Osu.Mods private ReplayState state; private double lastStateChangeTime; + private bool hasReplay; + public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { // grab the input manager for future use. osuInputManager = (OsuInputManager)drawableRuleset.KeyBindingInputManager; - osuInputManager.AllowUserPresses = false; + } + + public void ApplyToPlayer(Player player) + { + if (osuInputManager.ReplayInputHandler != null) + { + hasReplay = true; + return; + } } public void Update(Playfield playfield) { + if (hasReplay) + return; + bool requiresHold = false; bool requiresHit = false; From c2ed6491a9953dce878acf3d36c2c4b9d35b0716 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Apr 2020 15:37:50 +0900 Subject: [PATCH 1173/2376] Move and shorten enum names --- .../TestSceneBeatmapSearchFilter.cs | 5 +- .../API/Requests/SearchBeatmapSetsRequest.cs | 96 ++----------------- .../BeatmapListingSearchControl.cs | 21 ++-- .../Overlays/BeatmapListing/SearchCategory.cs | 26 +++++ .../Overlays/BeatmapListing/SearchGenre.cs | 25 +++++ .../Overlays/BeatmapListing/SearchLanguage.cs | 47 +++++++++ 6 files changed, 119 insertions(+), 101 deletions(-) create mode 100644 osu.Game/Overlays/BeatmapListing/SearchCategory.cs create mode 100644 osu.Game/Overlays/BeatmapListing/SearchGenre.cs create mode 100644 osu.Game/Overlays/BeatmapListing/SearchLanguage.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapSearchFilter.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapSearchFilter.cs index fac58a6754..283fe03af3 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapSearchFilter.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapSearchFilter.cs @@ -8,7 +8,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.Containers; -using osu.Game.Online.API.Requests; using osu.Game.Overlays; using osu.Game.Overlays.BeatmapListing; using osuTK; @@ -41,8 +40,8 @@ namespace osu.Game.Tests.Visual.UserInterface Children = new Drawable[] { new BeatmapSearchRulesetFilterRow(), - new BeatmapSearchFilterRow("Categories"), - new BeatmapSearchFilterRow("Header Name") + new BeatmapSearchFilterRow("Categories"), + new BeatmapSearchFilterRow("Header Name") } }); } diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs index 1206563b18..8345be5f82 100644 --- a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs +++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs @@ -1,26 +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.ComponentModel; using osu.Framework.IO.Network; using osu.Game.Overlays; +using osu.Game.Overlays.BeatmapListing; using osu.Game.Overlays.Direct; using osu.Game.Rulesets; -using osu.Game.Utils; namespace osu.Game.Online.API.Requests { public class SearchBeatmapSetsRequest : APIRequest { - public BeatmapSearchCategory SearchCategory { get; set; } + public SearchCategory SearchCategory { get; set; } public DirectSortCriteria SortCriteria { get; set; } public SortDirection SortDirection { get; set; } - public BeatmapSearchGenre Genre { get; set; } + public SearchGenre Genre { get; set; } - public BeatmapSearchLanguage Language { get; set; } + public SearchLanguage Language { get; set; } private readonly string query; private readonly RulesetInfo ruleset; @@ -32,11 +31,11 @@ namespace osu.Game.Online.API.Requests this.query = string.IsNullOrEmpty(query) ? string.Empty : System.Uri.EscapeDataString(query); this.ruleset = ruleset; - SearchCategory = BeatmapSearchCategory.Any; + SearchCategory = SearchCategory.Any; SortCriteria = DirectSortCriteria.Ranked; SortDirection = SortDirection.Descending; - Genre = BeatmapSearchGenre.Any; - Language = BeatmapSearchLanguage.Any; + Genre = SearchGenre.Any; + Language = SearchLanguage.Any; } protected override WebRequest CreateWebRequest() @@ -49,10 +48,10 @@ namespace osu.Game.Online.API.Requests req.AddParameter("s", SearchCategory.ToString().ToLowerInvariant()); - if (Genre != BeatmapSearchGenre.Any) + if (Genre != SearchGenre.Any) req.AddParameter("g", ((int)Genre).ToString()); - if (Language != BeatmapSearchLanguage.Any) + if (Language != SearchLanguage.Any) req.AddParameter("l", ((int)Language).ToString()); req.AddParameter("sort", $"{SortCriteria.ToString().ToLowerInvariant()}_{directionString}"); @@ -62,81 +61,4 @@ namespace osu.Game.Online.API.Requests protected override string Target => @"beatmapsets/search"; } - - public enum BeatmapSearchCategory - { - Any, - - [Description("Has Leaderboard")] - Leaderboard, - Ranked, - Qualified, - Loved, - Favourites, - - [Description("Pending & WIP")] - Pending, - Graveyard, - - [Description("My Maps")] - Mine, - } - - public enum BeatmapSearchGenre - { - Any = 0, - Unspecified = 1, - - [Description("Video Game")] - VideoGame = 2, - Anime = 3, - Rock = 4, - Pop = 5, - Other = 6, - Novelty = 7, - - [Description("Hip Hop")] - HipHop = 9, - Electronic = 10 - } - - [HasOrderedElements] - public enum BeatmapSearchLanguage - { - [Order(0)] - Any, - - [Order(11)] - Other, - - [Order(1)] - English, - - [Order(6)] - Japanese, - - [Order(2)] - Chinese, - - [Order(10)] - Instrumental, - - [Order(7)] - Korean, - - [Order(3)] - French, - - [Order(4)] - German, - - [Order(9)] - Swedish, - - [Order(8)] - Spanish, - - [Order(5)] - Italian - } } diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs index 9ae2696a22..2ecdb18667 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs @@ -5,7 +5,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Game.Online.API.Requests; using osuTK; using osu.Framework.Bindables; using osu.Game.Beatmaps.Drawables; @@ -23,11 +22,11 @@ namespace osu.Game.Overlays.BeatmapListing public Bindable Ruleset => modeFilter.Current; - public Bindable Category => categoryFilter.Current; + public Bindable Category => categoryFilter.Current; - public Bindable Genre => genreFilter.Current; + public Bindable Genre => genreFilter.Current; - public Bindable Language => languageFilter.Current; + public Bindable Language => languageFilter.Current; public BeatmapSetInfo BeatmapSet { @@ -46,9 +45,9 @@ namespace osu.Game.Overlays.BeatmapListing private readonly BeatmapSearchTextBox textBox; private readonly BeatmapSearchRulesetFilterRow modeFilter; - private readonly BeatmapSearchFilterRow categoryFilter; - private readonly BeatmapSearchFilterRow genreFilter; - private readonly BeatmapSearchFilterRow languageFilter; + private readonly BeatmapSearchFilterRow categoryFilter; + private readonly BeatmapSearchFilterRow genreFilter; + private readonly BeatmapSearchFilterRow languageFilter; private readonly Box background; private readonly UpdateableBeatmapSetCover beatmapCover; @@ -103,9 +102,9 @@ namespace osu.Game.Overlays.BeatmapListing Children = new Drawable[] { modeFilter = new BeatmapSearchRulesetFilterRow(), - categoryFilter = new BeatmapSearchFilterRow(@"Categories"), - genreFilter = new BeatmapSearchFilterRow(@"Genre"), - languageFilter = new BeatmapSearchFilterRow(@"Language"), + categoryFilter = new BeatmapSearchFilterRow(@"Categories"), + genreFilter = new BeatmapSearchFilterRow(@"Genre"), + languageFilter = new BeatmapSearchFilterRow(@"Language"), } } } @@ -113,7 +112,7 @@ namespace osu.Game.Overlays.BeatmapListing } }); - categoryFilter.Current.Value = BeatmapSearchCategory.Leaderboard; + categoryFilter.Current.Value = SearchCategory.Leaderboard; } [BackgroundDependencyLoader] diff --git a/osu.Game/Overlays/BeatmapListing/SearchCategory.cs b/osu.Game/Overlays/BeatmapListing/SearchCategory.cs new file mode 100644 index 0000000000..84859bf5b5 --- /dev/null +++ b/osu.Game/Overlays/BeatmapListing/SearchCategory.cs @@ -0,0 +1,26 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.ComponentModel; + +namespace osu.Game.Overlays.BeatmapListing +{ + public enum SearchCategory + { + Any, + + [Description("Has Leaderboard")] + Leaderboard, + Ranked, + Qualified, + Loved, + Favourites, + + [Description("Pending & WIP")] + Pending, + Graveyard, + + [Description("My Maps")] + Mine, + } +} diff --git a/osu.Game/Overlays/BeatmapListing/SearchGenre.cs b/osu.Game/Overlays/BeatmapListing/SearchGenre.cs new file mode 100644 index 0000000000..b12bba6249 --- /dev/null +++ b/osu.Game/Overlays/BeatmapListing/SearchGenre.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.ComponentModel; + +namespace osu.Game.Overlays.BeatmapListing +{ + public enum SearchGenre + { + Any = 0, + Unspecified = 1, + + [Description("Video Game")] + VideoGame = 2, + Anime = 3, + Rock = 4, + Pop = 5, + Other = 6, + Novelty = 7, + + [Description("Hip Hop")] + HipHop = 9, + Electronic = 10 + } +} diff --git a/osu.Game/Overlays/BeatmapListing/SearchLanguage.cs b/osu.Game/Overlays/BeatmapListing/SearchLanguage.cs new file mode 100644 index 0000000000..dac7e4f1a2 --- /dev/null +++ b/osu.Game/Overlays/BeatmapListing/SearchLanguage.cs @@ -0,0 +1,47 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Utils; + +namespace osu.Game.Overlays.BeatmapListing +{ + [HasOrderedElements] + public enum SearchLanguage + { + [Order(0)] + Any, + + [Order(11)] + Other, + + [Order(1)] + English, + + [Order(6)] + Japanese, + + [Order(2)] + Chinese, + + [Order(10)] + Instrumental, + + [Order(7)] + Korean, + + [Order(3)] + French, + + [Order(4)] + German, + + [Order(9)] + Swedish, + + [Order(8)] + Spanish, + + [Order(5)] + Italian + } +} From eeb76120106b08a108f8036c95391a77fc5f968f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Apr 2020 15:40:08 +0900 Subject: [PATCH 1174/2376] Update DirectOverlay implementation --- osu.Game/Overlays/Direct/FilterControl.cs | 6 +++--- osu.Game/Overlays/DirectOverlay.cs | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game/Overlays/Direct/FilterControl.cs b/osu.Game/Overlays/Direct/FilterControl.cs index e5b2b5cc34..4ab5544550 100644 --- a/osu.Game/Overlays/Direct/FilterControl.cs +++ b/osu.Game/Overlays/Direct/FilterControl.cs @@ -6,20 +6,20 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Game.Graphics; -using osu.Game.Online.API.Requests; +using osu.Game.Overlays.BeatmapListing; using osu.Game.Overlays.SearchableList; using osu.Game.Rulesets; using osuTK.Graphics; namespace osu.Game.Overlays.Direct { - public class FilterControl : SearchableListFilterControl + public class FilterControl : SearchableListFilterControl { private DirectRulesetSelector rulesetSelector; protected override Color4 BackgroundColour => Color4Extensions.FromHex(@"384552"); protected override DirectSortCriteria DefaultTab => DirectSortCriteria.Ranked; - protected override BeatmapSearchCategory DefaultCategory => BeatmapSearchCategory.Leaderboard; + protected override SearchCategory DefaultCategory => SearchCategory.Leaderboard; protected override Drawable CreateSupplementaryControls() => rulesetSelector = new DirectRulesetSelector(); diff --git a/osu.Game/Overlays/DirectOverlay.cs b/osu.Game/Overlays/DirectOverlay.cs index 3eb88be690..5ed39af0dc 100644 --- a/osu.Game/Overlays/DirectOverlay.cs +++ b/osu.Game/Overlays/DirectOverlay.cs @@ -16,6 +16,7 @@ using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Online.API.Requests; +using osu.Game.Overlays.BeatmapListing; using osu.Game.Overlays.Direct; using osu.Game.Overlays.SearchableList; using osu.Game.Rulesets; @@ -24,7 +25,7 @@ using osuTK.Graphics; namespace osu.Game.Overlays { - public class DirectOverlay : SearchableListOverlay + public class DirectOverlay : SearchableListOverlay { private const float panel_padding = 10f; @@ -40,7 +41,7 @@ namespace osu.Game.Overlays protected override Color4 TrianglesColourDark => Color4Extensions.FromHex(@"3f5265"); protected override SearchableListHeader CreateHeader() => new Header(); - protected override SearchableListFilterControl CreateFilterControl() => new FilterControl(); + protected override SearchableListFilterControl CreateFilterControl() => new FilterControl(); private IEnumerable beatmapSets; From 1f0b7465e2410c8c69d16f31becd52e076602901 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Apr 2020 16:06:40 +0900 Subject: [PATCH 1175/2376] Add back missing line --- osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs index 9a7b967117..7b1941b7f9 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs @@ -49,6 +49,8 @@ namespace osu.Game.Rulesets.Osu.Mods hasReplay = true; return; } + + osuInputManager.AllowUserPresses = false; } public void Update(Playfield playfield) From 594cef14738f99d3f0fa3466756bcf151c3df4ac Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Apr 2020 15:47:43 +0900 Subject: [PATCH 1176/2376] Fix BeatmapListingOverlay not taking focus --- .../BeatmapListing/BeatmapListingFilterControl.cs | 2 ++ .../BeatmapListing/BeatmapListingSearchControl.cs | 2 ++ osu.Game/Overlays/BeatmapListingOverlay.cs | 12 +++++++++++- 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs index 8817031bce..8c50409783 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs @@ -159,5 +159,7 @@ namespace osu.Game.Overlays.BeatmapListing base.Dispose(isDisposing); } + + public void TakeFocus() => searchControl.TakeFocus(); } } diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs index 2ecdb18667..29c4fe0d2e 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs @@ -121,6 +121,8 @@ namespace osu.Game.Overlays.BeatmapListing background.Colour = colourProvider.Dark6; } + public void TakeFocus() => textBox.TakeFocus(); + private class BeatmapSearchTextBox : SearchTextBox { protected override Color4 SelectionColour => Color4.Gray; diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index e16924464d..000ca6b91c 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; +using osu.Framework.Input.Events; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Graphics.Containers; @@ -35,6 +36,8 @@ namespace osu.Game.Overlays { } + private BeatmapListingFilterControl filterControl; + [BackgroundDependencyLoader] private void load() { @@ -57,7 +60,7 @@ namespace osu.Game.Overlays Children = new Drawable[] { new BeatmapListingHeader(), - new BeatmapListingFilterControl + filterControl = new BeatmapListingFilterControl { SearchStarted = onSearchStarted, SearchFinished = onSearchFinished, @@ -88,6 +91,13 @@ namespace osu.Game.Overlays }; } + protected override void OnFocus(FocusEvent e) + { + base.OnFocus(e); + + filterControl.TakeFocus(); + } + private CancellationTokenSource cancellationToken; private void onSearchStarted() From 1cec0575b78203dbc257b4d72851012d37eeac91 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Apr 2020 16:00:00 +0900 Subject: [PATCH 1177/2376] Remove unused classes and replace overlay in game --- .../Visual/Online/TestSceneDirectOverlay.cs | 215 ------------- osu.Game.Tests/Visual/TestSceneOsuGame.cs | 7 + .../API/Requests/SearchBeatmapSetsRequest.cs | 5 +- osu.Game/OsuGame.cs | 8 +- .../BeatmapListingFilterControl.cs | 3 +- .../BeatmapListingSortTabControl.cs | 13 +- .../Overlays/BeatmapListing/SortCriteria.cs | 17 + .../Overlays/Direct/DirectRulesetSelector.cs | 93 ------ osu.Game/Overlays/Direct/FilterControl.cs | 47 --- osu.Game/Overlays/Direct/Header.cs | 43 --- osu.Game/Overlays/DirectOverlay.cs | 299 ------------------ osu.Game/Overlays/SocialOverlay.cs | 6 - osu.Game/Overlays/SortDirection.cs | 11 + osu.Game/Overlays/Toolbar/Toolbar.cs | 2 +- ...tton.cs => ToolbarBeatmapListingButton.cs} | 8 +- osu.Game/Screens/Menu/ButtonSystem.cs | 4 +- osu.Game/Screens/Menu/MainMenu.cs | 4 +- 17 files changed, 57 insertions(+), 728 deletions(-) delete mode 100644 osu.Game.Tests/Visual/Online/TestSceneDirectOverlay.cs create mode 100644 osu.Game/Overlays/BeatmapListing/SortCriteria.cs delete mode 100644 osu.Game/Overlays/Direct/DirectRulesetSelector.cs delete mode 100644 osu.Game/Overlays/Direct/FilterControl.cs delete mode 100644 osu.Game/Overlays/Direct/Header.cs delete mode 100644 osu.Game/Overlays/DirectOverlay.cs create mode 100644 osu.Game/Overlays/SortDirection.cs rename osu.Game/Overlays/Toolbar/{ToolbarDirectButton.cs => ToolbarBeatmapListingButton.cs} (63%) diff --git a/osu.Game.Tests/Visual/Online/TestSceneDirectOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneDirectOverlay.cs deleted file mode 100644 index d9873ea243..0000000000 --- a/osu.Game.Tests/Visual/Online/TestSceneDirectOverlay.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.Collections.Generic; -using NUnit.Framework; -using osu.Game.Beatmaps; -using osu.Game.Overlays; - -namespace osu.Game.Tests.Visual.Online -{ - [TestFixture] - public class TestSceneDirectOverlay : OsuTestScene - { - private DirectOverlay direct; - - protected override bool UseOnlineAPI => true; - - protected override void LoadComplete() - { - base.LoadComplete(); - - Add(direct = new DirectOverlay()); - newBeatmaps(); - - AddStep(@"toggle", direct.ToggleVisibility); - AddStep(@"result counts", () => direct.ResultAmounts = new DirectOverlay.ResultCounts(1, 4, 13)); - AddStep(@"trigger disabled", () => Ruleset.Disabled = !Ruleset.Disabled); - } - - private void newBeatmaps() - { - direct.BeatmapSets = new[] - { - new BeatmapSetInfo - { - OnlineBeatmapSetID = 578332, - Metadata = new BeatmapMetadata - { - Title = @"OrVid", - Artist = @"An", - AuthorString = @"RLC", - Source = @"", - Tags = @"acuticnotes an-fillnote revid tear tearvid encrpted encryption axi axivid quad her hervid recoll", - }, - OnlineInfo = new BeatmapSetOnlineInfo - { - Covers = new BeatmapSetOnlineCovers - { - Card = @"https://assets.ppy.sh/beatmaps/578332/covers/card.jpg?1494591390", - Cover = @"https://assets.ppy.sh/beatmaps/578332/covers/cover.jpg?1494591390", - }, - Preview = @"https://b.ppy.sh/preview/578332.mp3", - PlayCount = 97, - FavouriteCount = 72, - }, - Beatmaps = new List - { - new BeatmapInfo - { - Ruleset = Ruleset.Value, - StarDifficulty = 5.35f, - Metadata = new BeatmapMetadata(), - }, - }, - }, - new BeatmapSetInfo - { - OnlineBeatmapSetID = 599627, - Metadata = new BeatmapMetadata - { - Title = @"tiny lamp", - Artist = @"fhana", - AuthorString = @"Sotarks", - Source = @"ぎんぎつね", - Tags = @"lantis junichi sato yuxuki waga kevin mitsunaga towana gingitsune opening op full ver version kalibe collab collaboration", - }, - OnlineInfo = new BeatmapSetOnlineInfo - { - Covers = new BeatmapSetOnlineCovers - { - Card = @"https://assets.ppy.sh/beatmaps/599627/covers/card.jpg?1494539318", - Cover = @"https://assets.ppy.sh/beatmaps/599627/covers/cover.jpg?1494539318", - }, - Preview = @"https//b.ppy.sh/preview/599627.mp3", - PlayCount = 3082, - FavouriteCount = 14, - }, - Beatmaps = new List - { - new BeatmapInfo - { - Ruleset = Ruleset.Value, - StarDifficulty = 5.81f, - Metadata = new BeatmapMetadata(), - }, - }, - }, - new BeatmapSetInfo - { - OnlineBeatmapSetID = 513268, - Metadata = new BeatmapMetadata - { - Title = @"At Gwanghwamun", - Artist = @"KYUHYUN", - AuthorString = @"Cerulean Veyron", - Source = @"", - Tags = @"soul ballad kh super junior sj suju 슈퍼주니어 kt뮤직 sm엔터테인먼트 s.m.entertainment kt music 1st mini album ep", - }, - OnlineInfo = new BeatmapSetOnlineInfo - { - Covers = new BeatmapSetOnlineCovers - { - Card = @"https://assets.ppy.sh/beatmaps/513268/covers/card.jpg?1494502863", - Cover = @"https://assets.ppy.sh/beatmaps/513268/covers/cover.jpg?1494502863", - }, - Preview = @"https//b.ppy.sh/preview/513268.mp3", - PlayCount = 2762, - FavouriteCount = 15, - }, - Beatmaps = new List - { - new BeatmapInfo - { - Ruleset = Ruleset.Value, - StarDifficulty = 0.9f, - Metadata = new BeatmapMetadata(), - }, - new BeatmapInfo - { - Ruleset = Ruleset.Value, - StarDifficulty = 1.1f, - }, - new BeatmapInfo - { - Ruleset = Ruleset.Value, - StarDifficulty = 2.02f, - }, - new BeatmapInfo - { - Ruleset = Ruleset.Value, - StarDifficulty = 3.49f, - }, - }, - }, - new BeatmapSetInfo - { - OnlineBeatmapSetID = 586841, - Metadata = new BeatmapMetadata - { - Title = @"RHAPSODY OF BLUE SKY", - Artist = @"fhana", - AuthorString = @"[Kamiya]", - Source = @"小林さんちのメイドラゴン", - Tags = @"kobayashi san chi no maidragon aozora no opening anime maid dragon oblivion karen dynamix imoutosan pata-mon gxytcgxytc", - }, - OnlineInfo = new BeatmapSetOnlineInfo - { - Covers = new BeatmapSetOnlineCovers - { - Card = @"https://assets.ppy.sh/beatmaps/586841/covers/card.jpg?1494052741", - Cover = @"https://assets.ppy.sh/beatmaps/586841/covers/cover.jpg?1494052741", - }, - Preview = @"https//b.ppy.sh/preview/586841.mp3", - PlayCount = 62317, - FavouriteCount = 161, - }, - Beatmaps = new List - { - new BeatmapInfo - { - Ruleset = Ruleset.Value, - StarDifficulty = 1.26f, - Metadata = new BeatmapMetadata(), - }, - new BeatmapInfo - { - Ruleset = Ruleset.Value, - StarDifficulty = 2.01f, - }, - new BeatmapInfo - { - Ruleset = Ruleset.Value, - StarDifficulty = 2.87f, - }, - new BeatmapInfo - { - Ruleset = Ruleset.Value, - StarDifficulty = 3.76f, - }, - new BeatmapInfo - { - Ruleset = Ruleset.Value, - StarDifficulty = 3.93f, - }, - new BeatmapInfo - { - Ruleset = Ruleset.Value, - StarDifficulty = 4.37f, - }, - new BeatmapInfo - { - Ruleset = Ruleset.Value, - StarDifficulty = 5.13f, - }, - new BeatmapInfo - { - Ruleset = Ruleset.Value, - StarDifficulty = 5.42f, - }, - }, - }, - }; - } - } -} diff --git a/osu.Game.Tests/Visual/TestSceneOsuGame.cs b/osu.Game.Tests/Visual/TestSceneOsuGame.cs index 8793d880e3..d68217dcfd 100644 --- a/osu.Game.Tests/Visual/TestSceneOsuGame.cs +++ b/osu.Game.Tests/Visual/TestSceneOsuGame.cs @@ -47,8 +47,15 @@ namespace osu.Game.Tests.Visual typeof(IdleTracker), typeof(OnScreenDisplay), typeof(NotificationOverlay), +<<<<<<< HEAD typeof(DirectOverlay), typeof(DashboardOverlay), +||||||| parent of 96a3a08a9... Remove unused classes and replace overlay in game + typeof(DirectOverlay), + typeof(SocialOverlay), +======= + typeof(SocialOverlay), +>>>>>>> 96a3a08a9... Remove unused classes and replace overlay in game typeof(ChannelManager), typeof(ChatOverlay), typeof(SettingsOverlay), diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs index 8345be5f82..047496b473 100644 --- a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs +++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs @@ -4,7 +4,6 @@ using osu.Framework.IO.Network; using osu.Game.Overlays; using osu.Game.Overlays.BeatmapListing; -using osu.Game.Overlays.Direct; using osu.Game.Rulesets; namespace osu.Game.Online.API.Requests @@ -13,7 +12,7 @@ namespace osu.Game.Online.API.Requests { public SearchCategory SearchCategory { get; set; } - public DirectSortCriteria SortCriteria { get; set; } + public SortCriteria SortCriteria { get; set; } public SortDirection SortDirection { get; set; } @@ -32,7 +31,7 @@ namespace osu.Game.Online.API.Requests this.ruleset = ruleset; SearchCategory = SearchCategory.Any; - SortCriteria = DirectSortCriteria.Ranked; + SortCriteria = SortCriteria.Ranked; SortDirection = SortDirection.Descending; Genre = SearchGenre.Any; Language = SearchLanguage.Any; diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index c861b84835..f5f7d0cef4 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -65,7 +65,7 @@ namespace osu.Game private NowPlayingOverlay nowPlaying; - private DirectOverlay direct; + private BeatmapListingOverlay beatmapListing; private DashboardOverlay dashboard; @@ -610,7 +610,7 @@ namespace osu.Game loadComponentSingleFile(screenshotManager, Add); //overlay elements - loadComponentSingleFile(direct = new DirectOverlay(), overlayContent.Add, true); + loadComponentSingleFile(beatmapListing = new BeatmapListingOverlay(), overlayContent.Add, true); loadComponentSingleFile(dashboard = new DashboardOverlay(), overlayContent.Add, true); var rankingsOverlay = loadComponentSingleFile(new RankingsOverlay(), overlayContent.Add, true); loadComponentSingleFile(channelManager = new ChannelManager(), AddInternal, true); @@ -670,7 +670,7 @@ namespace osu.Game } // ensure only one of these overlays are open at once. - var singleDisplayOverlays = new OverlayContainer[] { chatOverlay, dashboard, direct, changelogOverlay, rankingsOverlay }; + var singleDisplayOverlays = new OverlayContainer[] { chatOverlay, dashboard, beatmapListing, changelogOverlay, rankingsOverlay }; foreach (var overlay in singleDisplayOverlays) { @@ -865,7 +865,7 @@ namespace osu.Game return true; case GlobalAction.ToggleDirect: - direct.ToggleVisibility(); + beatmapListing.ToggleVisibility(); return true; case GlobalAction.ToggleGameplayMouseButtons: diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs index 8c50409783..4dd60c7113 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs @@ -14,7 +14,6 @@ using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Online.API.Requests; -using osu.Game.Overlays.Direct; using osu.Game.Rulesets; using osuTK; using osuTK.Graphics; @@ -101,7 +100,7 @@ namespace osu.Game.Overlays.BeatmapListing searchControl.Query.BindValueChanged(query => { - sortCriteria.Value = string.IsNullOrEmpty(query.NewValue) ? DirectSortCriteria.Ranked : DirectSortCriteria.Relevance; + sortCriteria.Value = string.IsNullOrEmpty(query.NewValue) ? SortCriteria.Ranked : SortCriteria.Relevance; sortDirection.Value = SortDirection.Descending; queueUpdateSearch(true); }); diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingSortTabControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingSortTabControl.cs index 27c43b092a..4c77a736ac 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingSortTabControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingSortTabControl.cs @@ -8,17 +8,16 @@ using osu.Framework.Graphics; using osuTK.Graphics; using osuTK; using osu.Framework.Input.Events; -using osu.Game.Overlays.Direct; namespace osu.Game.Overlays.BeatmapListing { - public class BeatmapListingSortTabControl : OverlaySortTabControl + public class BeatmapListingSortTabControl : OverlaySortTabControl { public readonly Bindable SortDirection = new Bindable(Overlays.SortDirection.Descending); public BeatmapListingSortTabControl() { - Current.Value = DirectSortCriteria.Ranked; + Current.Value = SortCriteria.Ranked; } protected override SortTabControl CreateControl() => new BeatmapSortTabControl @@ -30,7 +29,7 @@ namespace osu.Game.Overlays.BeatmapListing { public readonly Bindable SortDirection = new Bindable(); - protected override TabItem CreateTabItem(DirectSortCriteria value) => new BeatmapSortTabItem(value) + protected override TabItem CreateTabItem(SortCriteria value) => new BeatmapSortTabItem(value) { SortDirection = { BindTarget = SortDirection } }; @@ -40,12 +39,12 @@ namespace osu.Game.Overlays.BeatmapListing { public readonly Bindable SortDirection = new Bindable(); - public BeatmapSortTabItem(DirectSortCriteria value) + public BeatmapSortTabItem(SortCriteria value) : base(value) { } - protected override TabButton CreateTabButton(DirectSortCriteria value) => new BeatmapTabButton(value) + protected override TabButton CreateTabButton(SortCriteria value) => new BeatmapTabButton(value) { Active = { BindTarget = Active }, SortDirection = { BindTarget = SortDirection } @@ -67,7 +66,7 @@ namespace osu.Game.Overlays.BeatmapListing private readonly SpriteIcon icon; - public BeatmapTabButton(DirectSortCriteria value) + public BeatmapTabButton(SortCriteria value) : base(value) { Add(icon = new SpriteIcon diff --git a/osu.Game/Overlays/BeatmapListing/SortCriteria.cs b/osu.Game/Overlays/BeatmapListing/SortCriteria.cs new file mode 100644 index 0000000000..e409cbdda7 --- /dev/null +++ b/osu.Game/Overlays/BeatmapListing/SortCriteria.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. + +namespace osu.Game.Overlays.BeatmapListing +{ + public enum SortCriteria + { + Title, + Artist, + Difficulty, + Ranked, + Rating, + Plays, + Favourites, + Relevance + } +} diff --git a/osu.Game/Overlays/Direct/DirectRulesetSelector.cs b/osu.Game/Overlays/Direct/DirectRulesetSelector.cs deleted file mode 100644 index 106aaa616b..0000000000 --- a/osu.Game/Overlays/Direct/DirectRulesetSelector.cs +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.UserInterface; -using osu.Framework.Input.Events; -using osu.Game.Graphics.Containers; -using osu.Game.Graphics.UserInterface; -using osu.Game.Rulesets; -using osuTK; -using osuTK.Graphics; - -namespace osu.Game.Overlays.Direct -{ - public class DirectRulesetSelector : RulesetSelector - { - public override bool HandleNonPositionalInput => !Current.Disabled && base.HandleNonPositionalInput; - - public override bool HandlePositionalInput => !Current.Disabled && base.HandlePositionalInput; - - public override bool PropagatePositionalInputSubTree => !Current.Disabled && base.PropagatePositionalInputSubTree; - - public DirectRulesetSelector() - { - TabContainer.Masking = false; - TabContainer.Spacing = new Vector2(10, 0); - AutoSizeAxes = Axes.Both; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - Current.BindDisabledChanged(value => SelectedTab.FadeColour(value ? Color4.DarkGray : Color4.White, 200, Easing.OutQuint), true); - } - - protected override TabItem CreateTabItem(RulesetInfo value) => new DirectRulesetTabItem(value); - - protected override TabFillFlowContainer CreateTabFlow() => new TabFillFlowContainer - { - Direction = FillDirection.Horizontal, - AutoSizeAxes = Axes.Both, - }; - - private class DirectRulesetTabItem : TabItem - { - private readonly ConstrainedIconContainer iconContainer; - - public DirectRulesetTabItem(RulesetInfo value) - : base(value) - { - AutoSizeAxes = Axes.Both; - - Children = new Drawable[] - { - iconContainer = new ConstrainedIconContainer - { - Icon = value.CreateInstance().CreateIcon(), - Size = new Vector2(32), - }, - new HoverClickSounds() - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - updateState(); - } - - protected override bool OnHover(HoverEvent e) - { - base.OnHover(e); - updateState(); - return true; - } - - protected override void OnHoverLost(HoverLostEvent e) - { - base.OnHoverLost(e); - updateState(); - } - - protected override void OnActivated() => updateState(); - - protected override void OnDeactivated() => updateState(); - - private void updateState() => iconContainer.FadeColour(IsHovered || Active.Value ? Color4.White : Color4.Gray, 120, Easing.InQuad); - } - } -} diff --git a/osu.Game/Overlays/Direct/FilterControl.cs b/osu.Game/Overlays/Direct/FilterControl.cs deleted file mode 100644 index 4ab5544550..0000000000 --- a/osu.Game/Overlays/Direct/FilterControl.cs +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Graphics; -using osu.Game.Graphics; -using osu.Game.Overlays.BeatmapListing; -using osu.Game.Overlays.SearchableList; -using osu.Game.Rulesets; -using osuTK.Graphics; - -namespace osu.Game.Overlays.Direct -{ - public class FilterControl : SearchableListFilterControl - { - private DirectRulesetSelector rulesetSelector; - - protected override Color4 BackgroundColour => Color4Extensions.FromHex(@"384552"); - protected override DirectSortCriteria DefaultTab => DirectSortCriteria.Ranked; - protected override SearchCategory DefaultCategory => SearchCategory.Leaderboard; - - protected override Drawable CreateSupplementaryControls() => rulesetSelector = new DirectRulesetSelector(); - - public Bindable Ruleset => rulesetSelector.Current; - - [BackgroundDependencyLoader(true)] - private void load(OsuColour colours, Bindable ruleset) - { - DisplayStyleControl.Dropdown.AccentColour = colours.BlueDark; - rulesetSelector.Current.BindTo(ruleset); - } - } - - public enum DirectSortCriteria - { - Title, - Artist, - Difficulty, - Ranked, - Rating, - Plays, - Favourites, - Relevance, - } -} diff --git a/osu.Game/Overlays/Direct/Header.cs b/osu.Game/Overlays/Direct/Header.cs deleted file mode 100644 index 5b3e394a18..0000000000 --- a/osu.Game/Overlays/Direct/Header.cs +++ /dev/null @@ -1,43 +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.ComponentModel; -using osu.Framework.Extensions.Color4Extensions; -using osuTK.Graphics; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Sprites; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osu.Game.Overlays.SearchableList; - -namespace osu.Game.Overlays.Direct -{ - public class Header : SearchableListHeader - { - protected override Color4 BackgroundColour => Color4Extensions.FromHex(@"252f3a"); - - protected override DirectTab DefaultTab => DirectTab.Search; - protected override Drawable CreateHeaderText() => new OsuSpriteText { Text = @"osu!direct", Font = OsuFont.GetFont(size: 25) }; - protected override IconUsage Icon => OsuIcon.ChevronDownCircle; - - public Header() - { - Tabs.Current.Value = DirectTab.NewestMaps; - Tabs.Current.TriggerChange(); - } - } - - public enum DirectTab - { - Search, - - [Description("Newest Maps")] - NewestMaps = DirectSortCriteria.Ranked, - - [Description("Top Rated")] - TopRated = DirectSortCriteria.Rating, - - [Description("Most Played")] - MostPlayed = DirectSortCriteria.Plays, - } -} diff --git a/osu.Game/Overlays/DirectOverlay.cs b/osu.Game/Overlays/DirectOverlay.cs deleted file mode 100644 index 5ed39af0dc..0000000000 --- a/osu.Game/Overlays/DirectOverlay.cs +++ /dev/null @@ -1,299 +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 System.Linq; -using System.Threading.Tasks; -using Humanizer; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Threading; -using osu.Game.Audio; -using osu.Game.Beatmaps; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osu.Game.Online.API.Requests; -using osu.Game.Overlays.BeatmapListing; -using osu.Game.Overlays.Direct; -using osu.Game.Overlays.SearchableList; -using osu.Game.Rulesets; -using osuTK; -using osuTK.Graphics; - -namespace osu.Game.Overlays -{ - public class DirectOverlay : SearchableListOverlay - { - private const float panel_padding = 10f; - - [Resolved] - private RulesetStore rulesets { get; set; } - - private readonly FillFlowContainer resultCountsContainer; - private readonly OsuSpriteText resultCountsText; - private FillFlowContainer panels; - - protected override Color4 BackgroundColour => Color4Extensions.FromHex(@"485e74"); - protected override Color4 TrianglesColourLight => Color4Extensions.FromHex(@"465b71"); - protected override Color4 TrianglesColourDark => Color4Extensions.FromHex(@"3f5265"); - - protected override SearchableListHeader CreateHeader() => new Header(); - protected override SearchableListFilterControl CreateFilterControl() => new FilterControl(); - - private IEnumerable beatmapSets; - - public IEnumerable BeatmapSets - { - get => beatmapSets; - set - { - if (ReferenceEquals(beatmapSets, value)) return; - - beatmapSets = value?.ToList(); - - if (beatmapSets == null) return; - - var artists = new List(); - var songs = new List(); - var tags = new List(); - - foreach (var s in beatmapSets) - { - artists.Add(s.Metadata.Artist); - songs.Add(s.Metadata.Title); - tags.AddRange(s.Metadata.Tags.Split(' ')); - } - - ResultAmounts = new ResultCounts(distinctCount(artists), distinctCount(songs), distinctCount(tags)); - } - } - - private ResultCounts resultAmounts; - - public ResultCounts ResultAmounts - { - get => resultAmounts; - set - { - if (value == ResultAmounts) return; - - resultAmounts = value; - - updateResultCounts(); - } - } - - public DirectOverlay() - : base(OverlayColourScheme.Blue) - { - ScrollFlow.Children = new Drawable[] - { - resultCountsContainer = new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Margin = new MarginPadding { Top = 5 }, - Children = new Drawable[] - { - new OsuSpriteText - { - Text = "Found ", - Font = OsuFont.GetFont(size: 15) - }, - resultCountsText = new OsuSpriteText - { - Font = OsuFont.GetFont(size: 15, weight: FontWeight.Bold) - }, - } - }, - }; - - Filter.Search.Current.ValueChanged += text => - { - if (!string.IsNullOrEmpty(text.NewValue)) - { - Header.Tabs.Current.Value = DirectTab.Search; - - if (Filter.Tabs.Current.Value == DirectSortCriteria.Ranked) - Filter.Tabs.Current.Value = DirectSortCriteria.Relevance; - } - else - { - Header.Tabs.Current.Value = DirectTab.NewestMaps; - - if (Filter.Tabs.Current.Value == DirectSortCriteria.Relevance) - Filter.Tabs.Current.Value = DirectSortCriteria.Ranked; - } - }; - ((FilterControl)Filter).Ruleset.ValueChanged += _ => queueUpdateSearch(); - Filter.DisplayStyleControl.DisplayStyle.ValueChanged += style => recreatePanels(style.NewValue); - Filter.DisplayStyleControl.Dropdown.Current.ValueChanged += _ => queueUpdateSearch(); - - Header.Tabs.Current.ValueChanged += tab => - { - if (tab.NewValue != DirectTab.Search) - { - currentQuery.Value = string.Empty; - Filter.Tabs.Current.Value = (DirectSortCriteria)Header.Tabs.Current.Value; - queueUpdateSearch(); - } - }; - - currentQuery.ValueChanged += text => queueUpdateSearch(!string.IsNullOrEmpty(text.NewValue)); - - currentQuery.BindTo(Filter.Search.Current); - - Filter.Tabs.Current.ValueChanged += tab => - { - if (Header.Tabs.Current.Value != DirectTab.Search && tab.NewValue != (DirectSortCriteria)Header.Tabs.Current.Value) - Header.Tabs.Current.Value = DirectTab.Search; - - queueUpdateSearch(); - }; - - updateResultCounts(); - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - resultCountsContainer.Colour = colours.Yellow; - } - - private void updateResultCounts() - { - resultCountsContainer.FadeTo(ResultAmounts == null ? 0f : 1f, 200, Easing.OutQuint); - if (ResultAmounts == null) return; - - resultCountsText.Text = "Artist".ToQuantity(ResultAmounts.Artists) + ", " + - "Song".ToQuantity(ResultAmounts.Songs) + ", " + - "Tag".ToQuantity(ResultAmounts.Tags); - } - - private void recreatePanels(PanelDisplayStyle displayStyle) - { - if (panels != null) - { - panels.FadeOut(200); - panels.Expire(); - panels = null; - } - - if (BeatmapSets == null) return; - - var newPanels = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Spacing = new Vector2(panel_padding), - Margin = new MarginPadding { Top = 10 }, - ChildrenEnumerable = BeatmapSets.Select(b => - { - switch (displayStyle) - { - case PanelDisplayStyle.Grid: - return new DirectGridPanel(b) - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - }; - - default: - return new DirectListPanel(b); - } - }) - }; - - LoadComponentAsync(newPanels, p => - { - if (panels != null) ScrollFlow.Remove(panels); - ScrollFlow.Add(panels = newPanels); - }); - } - - protected override void PopIn() - { - base.PopIn(); - - // Queries are allowed to be run only on the first pop-in - if (getSetsRequest == null) - queueUpdateSearch(); - } - - private SearchBeatmapSetsRequest getSetsRequest; - - private readonly Bindable currentQuery = new Bindable(string.Empty); - - private ScheduledDelegate queryChangedDebounce; - - [Resolved] - private PreviewTrackManager previewTrackManager { get; set; } - - private void queueUpdateSearch(bool queryTextChanged = false) - { - BeatmapSets = null; - ResultAmounts = null; - - getSetsRequest?.Cancel(); - - queryChangedDebounce?.Cancel(); - queryChangedDebounce = Scheduler.AddDelayed(updateSearch, queryTextChanged ? 500 : 100); - } - - private void updateSearch() - { - if (!IsLoaded) - return; - - if (State.Value == Visibility.Hidden) - return; - - if (API == null) - return; - - previewTrackManager.StopAnyPlaying(this); - - getSetsRequest = new SearchBeatmapSetsRequest(currentQuery.Value, ((FilterControl)Filter).Ruleset.Value) - { - SearchCategory = Filter.DisplayStyleControl.Dropdown.Current.Value, - SortCriteria = Filter.Tabs.Current.Value - }; - - getSetsRequest.Success += response => - { - Task.Run(() => - { - var sets = response.BeatmapSets.Select(r => r.ToBeatmapSet(rulesets)).ToList(); - - // may not need scheduling; loads async internally. - Schedule(() => - { - BeatmapSets = sets; - recreatePanels(Filter.DisplayStyleControl.DisplayStyle.Value); - }); - }); - }; - - API.Queue(getSetsRequest); - } - - private int distinctCount(List list) => list.Distinct().ToArray().Length; - - public class ResultCounts - { - public readonly int Artists; - public readonly int Songs; - public readonly int Tags; - - public ResultCounts(int artists, int songs, int tags) - { - Artists = artists; - Songs = songs; - Tags = tags; - } - } - } -} diff --git a/osu.Game/Overlays/SocialOverlay.cs b/osu.Game/Overlays/SocialOverlay.cs index 02f7c9b0d3..9548573b4f 100644 --- a/osu.Game/Overlays/SocialOverlay.cs +++ b/osu.Game/Overlays/SocialOverlay.cs @@ -239,10 +239,4 @@ namespace osu.Game.Overlays } } } - - public enum SortDirection - { - Ascending, - Descending - } } diff --git a/osu.Game/Overlays/SortDirection.cs b/osu.Game/Overlays/SortDirection.cs new file mode 100644 index 0000000000..3af9614972 --- /dev/null +++ b/osu.Game/Overlays/SortDirection.cs @@ -0,0 +1,11 @@ +// 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.Overlays +{ + public enum SortDirection + { + Ascending, + Descending + } +} diff --git a/osu.Game/Overlays/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs index 897587d198..227347112c 100644 --- a/osu.Game/Overlays/Toolbar/Toolbar.cs +++ b/osu.Game/Overlays/Toolbar/Toolbar.cs @@ -71,7 +71,7 @@ namespace osu.Game.Overlays.Toolbar { new ToolbarChangelogButton(), new ToolbarRankingsButton(), - new ToolbarDirectButton(), + new ToolbarBeatmapListingButton(), new ToolbarChatButton(), new ToolbarSocialButton(), new ToolbarMusicButton(), diff --git a/osu.Game/Overlays/Toolbar/ToolbarDirectButton.cs b/osu.Game/Overlays/Toolbar/ToolbarBeatmapListingButton.cs similarity index 63% rename from osu.Game/Overlays/Toolbar/ToolbarDirectButton.cs rename to osu.Game/Overlays/Toolbar/ToolbarBeatmapListingButton.cs index 1d07a3ae70..eecb368ee9 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarDirectButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarBeatmapListingButton.cs @@ -6,17 +6,17 @@ using osu.Game.Graphics; namespace osu.Game.Overlays.Toolbar { - public class ToolbarDirectButton : ToolbarOverlayToggleButton + public class ToolbarBeatmapListingButton : ToolbarOverlayToggleButton { - public ToolbarDirectButton() + public ToolbarBeatmapListingButton() { SetIcon(OsuIcon.ChevronDownCircle); } [BackgroundDependencyLoader(true)] - private void load(DirectOverlay direct) + private void load(BeatmapListingOverlay beatmapListing) { - StateContainer = direct; + StateContainer = beatmapListing; } } } diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index fe538728e3..30e5e9702e 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -39,7 +39,7 @@ namespace osu.Game.Screens.Menu public Action OnEdit; public Action OnExit; - public Action OnDirect; + public Action OnBeatmapListing; public Action OnSolo; public Action OnSettings; public Action OnMulti; @@ -130,7 +130,7 @@ namespace osu.Game.Screens.Menu buttonsTopLevel.Add(new Button(@"play", @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), () => State = ButtonSystemState.Play, WEDGE_WIDTH, Key.P)); buttonsTopLevel.Add(new Button(@"osu!editor", @"button-generic-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), () => OnEdit?.Invoke(), 0, Key.E)); - buttonsTopLevel.Add(new Button(@"osu!direct", @"button-direct-select", OsuIcon.ChevronDownCircle, new Color4(165, 204, 0, 255), () => OnDirect?.Invoke(), 0, Key.D)); + buttonsTopLevel.Add(new Button(@"osu!direct", @"button-direct-select", OsuIcon.ChevronDownCircle, new Color4(165, 204, 0, 255), () => OnBeatmapListing?.Invoke(), 0, Key.D)); if (host.CanExit) buttonsTopLevel.Add(new Button(@"exit", string.Empty, OsuIcon.CrossCircle, new Color4(238, 51, 153, 255), () => OnExit?.Invoke(), 0, Key.Q)); diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 174eadfe26..0589e4d12b 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -72,7 +72,7 @@ namespace osu.Game.Screens.Menu private SongTicker songTicker; [BackgroundDependencyLoader(true)] - private void load(DirectOverlay direct, SettingsOverlay settings, RankingsOverlay rankings, OsuConfigManager config, SessionStatics statics) + private void load(BeatmapListingOverlay beatmapListing, SettingsOverlay settings, RankingsOverlay rankings, OsuConfigManager config, SessionStatics statics) { holdDelay = config.GetBindable(OsuSetting.UIHoldActivationDelay); loginDisplayed = statics.GetBindable(Static.LoginOverlayDisplayed); @@ -133,7 +133,7 @@ namespace osu.Game.Screens.Menu }; buttons.OnSettings = () => settings?.ToggleVisibility(); - buttons.OnDirect = () => direct?.ToggleVisibility(); + buttons.OnBeatmapListing = () => beatmapListing?.ToggleVisibility(); buttons.OnChart = () => rankings?.ShowSpotlights(); LoadComponentAsync(background = new BackgroundScreenDefault()); From 9b9b710ded76bfc99d326f94c9fd6a40c69f9412 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Apr 2020 16:03:18 +0900 Subject: [PATCH 1178/2376] Move and rename remaining direct classes --- .../Online/TestSceneDirectDownloadButton.cs | 6 ++--- .../Visual/Online/TestSceneDirectPanel.cs | 18 +++++++-------- osu.Game.Tests/Visual/TestSceneOsuGame.cs | 9 +------- .../BeatmapDownloadTrackingComposite.cs | 2 +- .../Panels/BeatmapPanel.cs} | 8 +++---- .../Panels/BeatmapPanelDownloadButton.cs} | 6 ++--- .../Panels/BeatmapPanelGrid.cs} | 16 +++++++------- .../Panels/BeatmapPanelList.cs} | 22 +++++++++---------- .../Panels}/DownloadProgressBar.cs | 2 +- .../Panels}/IconPill.cs | 2 +- .../Panels}/PlayButton.cs | 2 +- osu.Game/Overlays/BeatmapListingOverlay.cs | 6 ++--- .../Buttons/HeaderDownloadButton.cs | 2 +- .../BeatmapSet/Buttons/PreviewButton.cs | 2 +- osu.Game/Overlays/BeatmapSet/Header.cs | 4 ++-- .../Beatmaps/PaginatedBeatmapContainer.cs | 4 ++-- .../Overlays/Rankings/SpotlightsLayout.cs | 4 ++-- .../Screens/Multi/DrawableRoomPlaylistItem.cs | 4 ++-- 18 files changed, 56 insertions(+), 63 deletions(-) rename osu.Game/Overlays/{Direct => }/BeatmapDownloadTrackingComposite.cs (94%) rename osu.Game/Overlays/{Direct/DirectPanel.cs => BeatmapListing/Panels/BeatmapPanel.cs} (96%) rename osu.Game/Overlays/{Direct/PanelDownloadButton.cs => BeatmapListing/Panels/BeatmapPanelDownloadButton.cs} (93%) rename osu.Game/Overlays/{Direct/DirectGridPanel.cs => BeatmapListing/Panels/BeatmapPanelGrid.cs} (97%) rename osu.Game/Overlays/{Direct/DirectListPanel.cs => BeatmapListing/Panels/BeatmapPanelList.cs} (97%) rename osu.Game/Overlays/{Direct => BeatmapListing/Panels}/DownloadProgressBar.cs (97%) rename osu.Game/Overlays/{Direct => BeatmapListing/Panels}/IconPill.cs (96%) rename osu.Game/Overlays/{Direct => BeatmapListing/Panels}/PlayButton.cs (98%) diff --git a/osu.Game.Tests/Visual/Online/TestSceneDirectDownloadButton.cs b/osu.Game.Tests/Visual/Online/TestSceneDirectDownloadButton.cs index f612992bf6..9fe873cb6a 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneDirectDownloadButton.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneDirectDownloadButton.cs @@ -9,7 +9,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Online; -using osu.Game.Overlays.Direct; +using osu.Game.Overlays.BeatmapListing.Panels; using osu.Game.Rulesets.Osu; using osu.Game.Tests.Resources; using osuTK; @@ -20,7 +20,7 @@ namespace osu.Game.Tests.Visual.Online { public override IReadOnlyList RequiredTypes => new[] { - typeof(PanelDownloadButton) + typeof(BeatmapPanelDownloadButton) }; private TestDownloadButton downloadButton; @@ -143,7 +143,7 @@ namespace osu.Game.Tests.Visual.Online return beatmap; } - private class TestDownloadButton : PanelDownloadButton + private class TestDownloadButton : BeatmapPanelDownloadButton { public new bool DownloadEnabled => base.DownloadEnabled; diff --git a/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs index cb08cded37..5809f93d90 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs @@ -8,7 +8,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Audio; using osu.Game.Beatmaps; -using osu.Game.Overlays.Direct; +using osu.Game.Overlays.BeatmapListing.Panels; using osu.Game.Rulesets; using osu.Game.Users; using osuTK; @@ -20,8 +20,8 @@ namespace osu.Game.Tests.Visual.Online { public override IReadOnlyList RequiredTypes => new[] { - typeof(DirectGridPanel), - typeof(DirectListPanel), + typeof(BeatmapPanelGrid), + typeof(BeatmapPanelList), typeof(IconPill) }; @@ -126,12 +126,12 @@ namespace osu.Game.Tests.Visual.Online Spacing = new Vector2(5, 20), Children = new Drawable[] { - new DirectGridPanel(normal), - new DirectGridPanel(undownloadable), - new DirectGridPanel(manyDifficulties), - new DirectListPanel(normal), - new DirectListPanel(undownloadable), - new DirectListPanel(manyDifficulties), + new BeatmapPanelGrid(normal), + new BeatmapPanelGrid(undownloadable), + new BeatmapPanelGrid(manyDifficulties), + new BeatmapPanelList(normal), + new BeatmapPanelList(undownloadable), + new BeatmapPanelList(manyDifficulties), }, }, }; diff --git a/osu.Game.Tests/Visual/TestSceneOsuGame.cs b/osu.Game.Tests/Visual/TestSceneOsuGame.cs index d68217dcfd..2eaac2a45f 100644 --- a/osu.Game.Tests/Visual/TestSceneOsuGame.cs +++ b/osu.Game.Tests/Visual/TestSceneOsuGame.cs @@ -47,15 +47,8 @@ namespace osu.Game.Tests.Visual typeof(IdleTracker), typeof(OnScreenDisplay), typeof(NotificationOverlay), -<<<<<<< HEAD - typeof(DirectOverlay), + typeof(BeatmapListingOverlay), typeof(DashboardOverlay), -||||||| parent of 96a3a08a9... Remove unused classes and replace overlay in game - typeof(DirectOverlay), - typeof(SocialOverlay), -======= - typeof(SocialOverlay), ->>>>>>> 96a3a08a9... Remove unused classes and replace overlay in game typeof(ChannelManager), typeof(ChatOverlay), typeof(SettingsOverlay), diff --git a/osu.Game/Overlays/Direct/BeatmapDownloadTrackingComposite.cs b/osu.Game/Overlays/BeatmapDownloadTrackingComposite.cs similarity index 94% rename from osu.Game/Overlays/Direct/BeatmapDownloadTrackingComposite.cs rename to osu.Game/Overlays/BeatmapDownloadTrackingComposite.cs index fd04a1541e..f6b5b181c3 100644 --- a/osu.Game/Overlays/Direct/BeatmapDownloadTrackingComposite.cs +++ b/osu.Game/Overlays/BeatmapDownloadTrackingComposite.cs @@ -5,7 +5,7 @@ using osu.Framework.Bindables; using osu.Game.Beatmaps; using osu.Game.Online; -namespace osu.Game.Overlays.Direct +namespace osu.Game.Overlays { public abstract class BeatmapDownloadTrackingComposite : DownloadTrackingComposite { diff --git a/osu.Game/Overlays/Direct/DirectPanel.cs b/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanel.cs similarity index 96% rename from osu.Game/Overlays/Direct/DirectPanel.cs rename to osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanel.cs index 4ad8e95512..f260bf1573 100644 --- a/osu.Game/Overlays/Direct/DirectPanel.cs +++ b/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanel.cs @@ -26,9 +26,9 @@ using osu.Game.Graphics.UserInterface; using osuTK; using osuTK.Graphics; -namespace osu.Game.Overlays.Direct +namespace osu.Game.Overlays.BeatmapListing.Panels { - public abstract class DirectPanel : OsuClickableContainer, IHasContextMenu + public abstract class BeatmapPanel : OsuClickableContainer, IHasContextMenu { public readonly BeatmapSetInfo SetInfo; @@ -49,7 +49,7 @@ namespace osu.Game.Overlays.Direct protected Action ViewBeatmap; - protected DirectPanel(BeatmapSetInfo setInfo) + protected BeatmapPanel(BeatmapSetInfo setInfo) { Debug.Assert(setInfo.OnlineBeatmapSetID != null); @@ -148,7 +148,7 @@ namespace osu.Game.Overlays.Direct if (SetInfo.Beatmaps.Count > maximum_difficulty_icons) { foreach (var ruleset in SetInfo.Beatmaps.Select(b => b.Ruleset).Distinct()) - icons.Add(new GroupedDifficultyIcon(SetInfo.Beatmaps.FindAll(b => b.Ruleset.Equals(ruleset)), ruleset, this is DirectListPanel ? Color4.White : colours.Gray5)); + icons.Add(new GroupedDifficultyIcon(SetInfo.Beatmaps.FindAll(b => b.Ruleset.Equals(ruleset)), ruleset, this is BeatmapPanelList ? Color4.White : colours.Gray5)); } else { diff --git a/osu.Game/Overlays/Direct/PanelDownloadButton.cs b/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelDownloadButton.cs similarity index 93% rename from osu.Game/Overlays/Direct/PanelDownloadButton.cs rename to osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelDownloadButton.cs index 387ced6acb..589f2d5072 100644 --- a/osu.Game/Overlays/Direct/PanelDownloadButton.cs +++ b/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelDownloadButton.cs @@ -11,9 +11,9 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online; -namespace osu.Game.Overlays.Direct +namespace osu.Game.Overlays.BeatmapListing.Panels { - public class PanelDownloadButton : BeatmapDownloadTrackingComposite + public class BeatmapPanelDownloadButton : BeatmapDownloadTrackingComposite { protected bool DownloadEnabled => button.Enabled.Value; @@ -26,7 +26,7 @@ namespace osu.Game.Overlays.Direct private readonly DownloadButton button; private Bindable noVideoSetting; - public PanelDownloadButton(BeatmapSetInfo beatmapSet) + public BeatmapPanelDownloadButton(BeatmapSetInfo beatmapSet) : base(beatmapSet) { InternalChild = shakeContainer = new ShakeContainer diff --git a/osu.Game/Overlays/Direct/DirectGridPanel.cs b/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelGrid.cs similarity index 97% rename from osu.Game/Overlays/Direct/DirectGridPanel.cs rename to osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelGrid.cs index 2528ccec41..caa7eb6441 100644 --- a/osu.Game/Overlays/Direct/DirectGridPanel.cs +++ b/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelGrid.cs @@ -1,25 +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 osuTK; -using osuTK.Graphics; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; +using osu.Game.Graphics; using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osuTK; +using osuTK.Graphics; -namespace osu.Game.Overlays.Direct +namespace osu.Game.Overlays.BeatmapListing.Panels { - public class DirectGridPanel : DirectPanel + public class BeatmapPanelGrid : BeatmapPanel { private const float horizontal_padding = 10; private const float vertical_padding = 5; @@ -31,7 +31,7 @@ namespace osu.Game.Overlays.Direct protected override PlayButton PlayButton => playButton; protected override Box PreviewBar => progressBar; - public DirectGridPanel(BeatmapSetInfo beatmap) + public BeatmapPanelGrid(BeatmapSetInfo beatmap) : base(beatmap) { Width = 380; @@ -156,7 +156,7 @@ namespace osu.Game.Overlays.Direct }, }, }, - new PanelDownloadButton(SetInfo) + new BeatmapPanelDownloadButton(SetInfo) { Size = new Vector2(50, 30), Margin = new MarginPadding(horizontal_padding), diff --git a/osu.Game/Overlays/Direct/DirectListPanel.cs b/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelList.cs similarity index 97% rename from osu.Game/Overlays/Direct/DirectListPanel.cs rename to osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelList.cs index b64142dfe7..3245ddea99 100644 --- a/osu.Game/Overlays/Direct/DirectListPanel.cs +++ b/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelList.cs @@ -1,25 +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 osuTK; -using osuTK.Graphics; +using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Colour; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osu.Framework.Allocation; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; +using osu.Game.Graphics; using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osuTK; +using osuTK.Graphics; -namespace osu.Game.Overlays.Direct +namespace osu.Game.Overlays.BeatmapListing.Panels { - public class DirectListPanel : DirectPanel + public class BeatmapPanelList : BeatmapPanel { private const float transition_duration = 120; private const float horizontal_padding = 10; @@ -27,7 +27,7 @@ namespace osu.Game.Overlays.Direct private const float height = 70; private FillFlowContainer statusContainer; - protected PanelDownloadButton DownloadButton; + protected BeatmapPanelDownloadButton DownloadButton; private PlayButton playButton; private Box progressBar; @@ -36,7 +36,7 @@ namespace osu.Game.Overlays.Direct protected override PlayButton PlayButton => playButton; protected override Box PreviewBar => progressBar; - public DirectListPanel(BeatmapSetInfo beatmap) + public BeatmapPanelList(BeatmapSetInfo beatmap) : base(beatmap) { RelativeSizeAxes = Axes.X; @@ -151,7 +151,7 @@ namespace osu.Game.Overlays.Direct Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, AutoSizeAxes = Axes.Both, - Child = DownloadButton = new PanelDownloadButton(SetInfo) + Child = DownloadButton = new BeatmapPanelDownloadButton(SetInfo) { Size = new Vector2(height - vertical_padding * 3), Margin = new MarginPadding { Left = vertical_padding * 2, Right = vertical_padding }, diff --git a/osu.Game/Overlays/Direct/DownloadProgressBar.cs b/osu.Game/Overlays/BeatmapListing/Panels/DownloadProgressBar.cs similarity index 97% rename from osu.Game/Overlays/Direct/DownloadProgressBar.cs rename to osu.Game/Overlays/BeatmapListing/Panels/DownloadProgressBar.cs index 9a8644efd2..93cf8799b5 100644 --- a/osu.Game/Overlays/Direct/DownloadProgressBar.cs +++ b/osu.Game/Overlays/BeatmapListing/Panels/DownloadProgressBar.cs @@ -10,7 +10,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Online; using osuTK.Graphics; -namespace osu.Game.Overlays.Direct +namespace osu.Game.Overlays.BeatmapListing.Panels { public class DownloadProgressBar : BeatmapDownloadTrackingComposite { diff --git a/osu.Game/Overlays/Direct/IconPill.cs b/osu.Game/Overlays/BeatmapListing/Panels/IconPill.cs similarity index 96% rename from osu.Game/Overlays/Direct/IconPill.cs rename to osu.Game/Overlays/BeatmapListing/Panels/IconPill.cs index d63bb2a292..1cb6c84f13 100644 --- a/osu.Game/Overlays/Direct/IconPill.cs +++ b/osu.Game/Overlays/BeatmapListing/Panels/IconPill.cs @@ -8,7 +8,7 @@ using osu.Framework.Graphics.Sprites; using osuTK; using osuTK.Graphics; -namespace osu.Game.Overlays.Direct +namespace osu.Game.Overlays.BeatmapListing.Panels { public class IconPill : CircularContainer { diff --git a/osu.Game/Overlays/Direct/PlayButton.cs b/osu.Game/Overlays/BeatmapListing/Panels/PlayButton.cs similarity index 98% rename from osu.Game/Overlays/Direct/PlayButton.cs rename to osu.Game/Overlays/BeatmapListing/Panels/PlayButton.cs index d9f335b6a7..e95fdeecf4 100644 --- a/osu.Game/Overlays/Direct/PlayButton.cs +++ b/osu.Game/Overlays/BeatmapListing/Panels/PlayButton.cs @@ -14,7 +14,7 @@ using osu.Game.Graphics.UserInterface; using osuTK; using osuTK.Graphics; -namespace osu.Game.Overlays.Direct +namespace osu.Game.Overlays.BeatmapListing.Panels { public class PlayButton : Container { diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index 000ca6b91c..a024e2c74e 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -17,7 +17,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.BeatmapListing; -using osu.Game.Overlays.Direct; +using osu.Game.Overlays.BeatmapListing.Panels; using osuTK; namespace osu.Game.Overlays @@ -118,14 +118,14 @@ namespace osu.Game.Overlays return; } - var newPanels = new FillFlowContainer + var newPanels = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Spacing = new Vector2(10), Alpha = 0, Margin = new MarginPadding { Vertical = 15 }, - ChildrenEnumerable = beatmaps.Select(b => new DirectGridPanel(b) + ChildrenEnumerable = beatmaps.Select(b => new BeatmapPanelGrid(b) { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, diff --git a/osu.Game/Overlays/BeatmapSet/Buttons/HeaderDownloadButton.cs b/osu.Game/Overlays/BeatmapSet/Buttons/HeaderDownloadButton.cs index e64256b850..56c0052bfe 100644 --- a/osu.Game/Overlays/BeatmapSet/Buttons/HeaderDownloadButton.cs +++ b/osu.Game/Overlays/BeatmapSet/Buttons/HeaderDownloadButton.cs @@ -13,7 +13,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Online; using osu.Game.Online.API; -using osu.Game.Overlays.Direct; +using osu.Game.Overlays.BeatmapListing.Panels; using osu.Game.Users; using osuTK; using osuTK.Graphics; diff --git a/osu.Game/Overlays/BeatmapSet/Buttons/PreviewButton.cs b/osu.Game/Overlays/BeatmapSet/Buttons/PreviewButton.cs index 7eae05e4a9..6accce7d77 100644 --- a/osu.Game/Overlays/BeatmapSet/Buttons/PreviewButton.cs +++ b/osu.Game/Overlays/BeatmapSet/Buttons/PreviewButton.cs @@ -11,7 +11,7 @@ using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Containers; -using osu.Game.Overlays.Direct; +using osu.Game.Overlays.BeatmapListing.Panels; using osuTK; namespace osu.Game.Overlays.BeatmapSet.Buttons diff --git a/osu.Game/Overlays/BeatmapSet/Header.cs b/osu.Game/Overlays/BeatmapSet/Header.cs index 11dc424183..17fa689cd2 100644 --- a/osu.Game/Overlays/BeatmapSet/Header.cs +++ b/osu.Game/Overlays/BeatmapSet/Header.cs @@ -15,8 +15,8 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Online; +using osu.Game.Overlays.BeatmapListing.Panels; using osu.Game.Overlays.BeatmapSet.Buttons; -using osu.Game.Overlays.Direct; using osu.Game.Rulesets; using osuTK; using osuTK.Graphics; @@ -274,7 +274,7 @@ namespace osu.Game.Overlays.BeatmapSet { case DownloadState.LocallyAvailable: // temporary for UX until new design is implemented. - downloadButtonsContainer.Child = new PanelDownloadButton(BeatmapSet.Value) + downloadButtonsContainer.Child = new BeatmapPanelDownloadButton(BeatmapSet.Value) { Width = 50, RelativeSizeAxes = Axes.Y, diff --git a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs index fcd12e2b54..5f70dc4d75 100644 --- a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs @@ -7,7 +7,7 @@ using osu.Framework.Graphics; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; -using osu.Game.Overlays.Direct; +using osu.Game.Overlays.BeatmapListing.Panels; using osu.Game.Users; using osuTK; @@ -33,7 +33,7 @@ namespace osu.Game.Overlays.Profile.Sections.Beatmaps protected override Drawable CreateDrawableItem(APIBeatmapSet model) => !model.OnlineBeatmapSetID.HasValue ? null - : new DirectGridPanel(model.ToBeatmapSet(Rulesets)) + : new BeatmapPanelGrid(model.ToBeatmapSet(Rulesets)) { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, diff --git a/osu.Game/Overlays/Rankings/SpotlightsLayout.cs b/osu.Game/Overlays/Rankings/SpotlightsLayout.cs index 6f06eecd6e..895fa94af5 100644 --- a/osu.Game/Overlays/Rankings/SpotlightsLayout.cs +++ b/osu.Game/Overlays/Rankings/SpotlightsLayout.cs @@ -12,10 +12,10 @@ using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Overlays.Rankings.Tables; using System.Linq; -using osu.Game.Overlays.Direct; using System.Threading; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays.BeatmapListing.Panels; namespace osu.Game.Overlays.Rankings { @@ -140,7 +140,7 @@ namespace osu.Game.Overlays.Rankings AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, Spacing = new Vector2(10), - Children = response.BeatmapSets.Select(b => new DirectGridPanel(b.ToBeatmapSet(rulesets)) + Children = response.BeatmapSets.Select(b => new BeatmapPanelGrid(b.ToBeatmapSet(rulesets)) { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, diff --git a/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs index d7dcca9809..c024304856 100644 --- a/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs @@ -21,7 +21,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Online; using osu.Game.Online.Chat; using osu.Game.Online.Multiplayer; -using osu.Game.Overlays.Direct; +using osu.Game.Overlays.BeatmapListing.Panels; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Screens.Play.HUD; @@ -210,7 +210,7 @@ namespace osu.Game.Screens.Multi return true; } - private class PlaylistDownloadButton : PanelDownloadButton + private class PlaylistDownloadButton : BeatmapPanelDownloadButton { public PlaylistDownloadButton(BeatmapSetInfo beatmapSet) : base(beatmapSet) From b8a1831d98feb8fc6752a2755b66df55de66c832 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 21 Apr 2020 17:14:04 +0900 Subject: [PATCH 1179/2376] Read line widths from skin --- osu.Game/Skinning/LegacySkin.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 91f970d19f..003fa24d5b 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -249,6 +249,14 @@ namespace osu.Game.Skinning case LegacyManiaSkinConfigurationLookups.RightStageImage: return SkinUtils.As(getManiaImage(existing, "StageRight")); + + case LegacyManiaSkinConfigurationLookups.LeftLineWidth: + Debug.Assert(maniaLookup.TargetColumn != null); + return SkinUtils.As(new Bindable(existing.ColumnLineWidth[maniaLookup.TargetColumn.Value])); + + case LegacyManiaSkinConfigurationLookups.RightLineWidth: + Debug.Assert(maniaLookup.TargetColumn != null); + return SkinUtils.As(new Bindable(existing.ColumnLineWidth[maniaLookup.TargetColumn.Value + 1])); } return null; From 0a2b585c65ca963a1fac0eb917328f329b722d54 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 21 Apr 2020 17:14:49 +0900 Subject: [PATCH 1180/2376] Apply missing scale --- osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs index 6504321bb2..1a097405ac 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs @@ -67,6 +67,7 @@ namespace osu.Game.Rulesets.Mania.Skinning { RelativeSizeAxes = Axes.Y, Width = leftLineWidth, + Scale = new Vector2(0.740f, 1), Colour = lineColour, Alpha = hasLeftLine ? 1 : 0 }, @@ -76,6 +77,7 @@ namespace osu.Game.Rulesets.Mania.Skinning Origin = Anchor.TopRight, RelativeSizeAxes = Axes.Y, Width = rightLineWidth, + Scale = new Vector2(0.740f, 1), Colour = lineColour, Alpha = hasRightLine ? 1 : 0 }, From a41ac50e2f052854032131e62d12407e6fd3242d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 21 Apr 2020 17:15:06 +0900 Subject: [PATCH 1181/2376] Line widths should not receive scale factor --- osu.Game/Skinning/LegacyManiaSkinDecoder.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs index 2db902c182..a988bd589f 100644 --- a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs +++ b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs @@ -74,7 +74,7 @@ namespace osu.Game.Skinning switch (pair.Key) { case "ColumnLineWidth": - parseArrayValue(pair.Value, currentConfig.ColumnLineWidth); + parseArrayValue(pair.Value, currentConfig.ColumnLineWidth, false); break; case "ColumnSpacing": @@ -124,7 +124,7 @@ namespace osu.Game.Skinning pendingLines.Clear(); } - private void parseArrayValue(string value, float[] output) + private void parseArrayValue(string value, float[] output, bool applyScaleFactor = true) { string[] values = value.Split(','); @@ -133,7 +133,7 @@ namespace osu.Game.Skinning if (i >= output.Length) break; - output[i] = float.Parse(values[i], CultureInfo.InvariantCulture) * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR; + output[i] = float.Parse(values[i], CultureInfo.InvariantCulture) * (applyScaleFactor ? LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR : 1); } } } From 4642a6093c2b6e5fa559059567e77863125e2c2a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 21 Apr 2020 17:15:13 +0900 Subject: [PATCH 1182/2376] Add test --- .../Resources/special-skin/mania-key1@2x.png | Bin 0 -> 12914 bytes .../Resources/special-skin/skin.ini | 6 ++++++ .../Skinning/TestSceneColumnBackground.cs | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania-key1@2x.png create mode 100644 osu.Game.Rulesets.Mania.Tests/Resources/special-skin/skin.ini diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania-key1@2x.png b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania-key1@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..aa681f6f223ea44319b3a383935237d1a2e4b345 GIT binary patch literal 12914 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 literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/skin.ini b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/skin.ini new file mode 100644 index 0000000000..56564776b3 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/skin.ini @@ -0,0 +1,6 @@ +[General] +Version: 2.4 + +[Mania] +Keys: 4 +ColumnLineWidth: 3,1,3,1,1 \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnBackground.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnBackground.cs index d6bacbe59e..bde323f187 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnBackground.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneColumnBackground.cs @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning { RelativeSizeAxes = Axes.Both, Width = 0.5f, - Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground, 0), _ => new DefaultColumnBackground()) + Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground, 1), _ => new DefaultColumnBackground()) { RelativeSizeAxes = Axes.Both } From a82efa626e9d95f98ccad220f14a3ab4fdb76690 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 21 Apr 2020 11:36:09 +0300 Subject: [PATCH 1183/2376] Add XMLDoc for default hyper-dash colour constant --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 056e838419..6f5e8be92c 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -23,6 +23,9 @@ namespace osu.Game.Rulesets.Catch.UI { public class Catcher : SkinReloadableDrawable, IKeyBindingHandler { + /// + /// The default colour used for all hyper-dashing components. (catcher drawables and fruit) + /// public static readonly Color4 DEFAULT_HYPER_DASH_COLOUR = Color4.Red; /// From ee62739b08e09feeda55b78d5a49dd68a857de0e Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 21 Apr 2020 11:41:53 +0300 Subject: [PATCH 1184/2376] Simplify process of adding catcher trails --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 29 +++++++---------------- osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 4 +--- 2 files changed, 10 insertions(+), 23 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 6f5e8be92c..92f2977c40 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Catch.UI public Container ExplodingFruitTarget; - private Container additiveTarget; + private readonly Container additiveTarget; private Container dashTrails; private Container hyperDashTrails; private Container endGlowSprites; @@ -116,8 +116,10 @@ namespace osu.Game.Rulesets.Catch.UI private int hyperDashDirection; private float hyperDashTargetPosition; - public Catcher(BeatmapDifficulty difficulty = null) + public Catcher(BeatmapDifficulty difficulty = null, Container additiveTarget = null) { + this.additiveTarget = additiveTarget; + RelativePositionAxes = Axes.X; X = 0.5f; @@ -155,27 +157,14 @@ namespace osu.Game.Rulesets.Catch.UI } }; - updateCatcher(); - } - - /// - /// Sets container target to provide catcher additive trails content in. - /// - /// The container to add catcher trails in. - public void SetAdditiveTarget(Container target) - { - if (additiveTarget == target) - return; - - additiveTarget?.RemoveRange(new[] { dashTrails, hyperDashTrails, endGlowSprites }); - - additiveTarget = target; additiveTarget?.AddRange(new[] { - dashTrails ??= new Container { RelativeSizeAxes = Axes.Both, Colour = Color4.White }, - hyperDashTrails ??= new Container { RelativeSizeAxes = Axes.Both, Colour = hyperDashColour }, - endGlowSprites ??= new Container { RelativeSizeAxes = Axes.Both, Colour = hyperDashEndGlowColour }, + dashTrails = new Container { RelativeSizeAxes = Axes.Both, Colour = Color4.White }, + hyperDashTrails = new Container { RelativeSizeAxes = Axes.Both, Colour = hyperDashColour }, + endGlowSprites = new Container { RelativeSizeAxes = Axes.Both, Colour = hyperDashEndGlowColour } }); + + updateCatcher(); } /// diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 641b81599e..1dd94afa9e 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -33,9 +33,7 @@ namespace osu.Game.Rulesets.Catch.UI { RelativeSizeAxes = Axes.X; Height = CATCHER_SIZE; - - Child = MovableCatcher = new Catcher(difficulty); - MovableCatcher.SetAdditiveTarget(this); + Child = MovableCatcher = new Catcher(difficulty, this); } public static float GetCatcherSize(BeatmapDifficulty difficulty) From c8c2b51108a1a0b80c80d5b846c2925231e80e3a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 21 Apr 2020 11:44:10 +0300 Subject: [PATCH 1185/2376] Remove redundant property set Co-Authored-By: Dean Herbert --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 92f2977c40..7ecb245617 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -159,7 +159,7 @@ namespace osu.Game.Rulesets.Catch.UI additiveTarget?.AddRange(new[] { - dashTrails = new Container { RelativeSizeAxes = Axes.Both, Colour = Color4.White }, + dashTrails = new Container { RelativeSizeAxes = Axes.Both }, hyperDashTrails = new Container { RelativeSizeAxes = Axes.Both, Colour = hyperDashColour }, endGlowSprites = new Container { RelativeSizeAxes = Axes.Both, Colour = hyperDashEndGlowColour } }); From d1c701a9972a42af10021bccb5d528ddb51f71c9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Apr 2020 18:34:56 +0900 Subject: [PATCH 1186/2376] Rename existing test to something more relevant --- .../{TestSceneTaikoPlayfield.cs => TestSceneHits.cs} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename osu.Game.Rulesets.Taiko.Tests/{TestSceneTaikoPlayfield.cs => TestSceneHits.cs} (99%) diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayfield.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs similarity index 99% rename from osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayfield.cs rename to osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs index 0d9e813c60..c2ca578dfa 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs @@ -24,7 +24,7 @@ using osuTK; namespace osu.Game.Rulesets.Taiko.Tests { [TestFixture] - public class TestSceneTaikoPlayfield : OsuTestScene + public class TestSceneHits : OsuTestScene { private const double default_duration = 1000; private const float scroll_time = 1000; From e74f9024836b56b4485d724d867fa1f71ea0a5b6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Apr 2020 18:40:18 +0900 Subject: [PATCH 1187/2376] Add playfield test scene --- .../Skinning/TestSceneTaikoPlayfield.cs | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs new file mode 100644 index 0000000000..e255baf459 --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs @@ -0,0 +1,32 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Taiko.UI; +using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Taiko.Tests.Skinning +{ + public class TestSceneTaikoPlayfield : TaikoSkinnableTestScene + { + [Cached(typeof(IScrollingInfo))] + private ScrollingTestContainer.TestScrollingInfo info = new ScrollingTestContainer.TestScrollingInfo + { + Direction = { Value = ScrollingDirection.Left }, + TimeRange = { Value = 5000 }, + }; + + public TestSceneTaikoPlayfield() + { + AddStep("Load playfield", () => SetContents(() => new TaikoPlayfield(new ControlPointInfo()) + { + Height = 0.4f, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + })); + } + } +} From bfc17bf4c09ac2493748c3751f3921b374746086 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Apr 2020 19:00:34 +0900 Subject: [PATCH 1188/2376] Add taiko hit target skinning --- .../metrics-skin/approachcircle@2x.png | Bin 0 -> 13816 bytes .../metrics-skin/taikobigcircle@2x.png | Bin 0 -> 12145 bytes .../Resources/old-skin/approachcircle.png | Bin 0 -> 10333 bytes .../Resources/special-skin/approachcircle.png | Bin 0 -> 4504 bytes .../special-skin/taikobigcircle@2x.png | Bin 0 -> 12374 bytes .../Skinning/TestSceneTaikoPlayfield.cs | 10 +++++ .../Skinning/LegacyHitTarget.cs | 41 ++++++++++++++++++ .../Skinning/TaikoLegacySkinTransformer.cs | 6 +++ .../TaikoSkinComponents.cs | 3 +- osu.Game.Rulesets.Taiko/UI/HitTarget.cs | 2 + osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 7 +-- 11 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/approachcircle@2x.png create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taikobigcircle@2x.png create mode 100755 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/approachcircle.png create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/approachcircle.png create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/taikobigcircle@2x.png create mode 100644 osu.Game.Rulesets.Taiko/Skinning/LegacyHitTarget.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/approachcircle@2x.png b/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/approachcircle@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..72ef665478b758c364197aa79f238230e229b3c4 GIT binary patch literal 13816 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 X;|*6ltxe$d|7PiG>1$T1+eQB$HM;D+ literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taikobigcircle@2x.png b/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taikobigcircle@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..440e5b55e52c8629f69272993a75b1a56002fb60 GIT binary patch literal 12145 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 literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/approachcircle.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/approachcircle.png new file mode 100755 index 0000000000000000000000000000000000000000..5aba6887564782a216c030b6a0143830612193ee GIT binary patch literal 10333 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 H<7ECnYE>!V literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/approachcircle.png b/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/approachcircle.png new file mode 100644 index 0000000000000000000000000000000000000000..56d6d34c1abf6ab153c9cef62eecff3016d21ed9 GIT binary patch literal 4504 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 Qd;A||`ImC#G8P~IA7%MrKL7v# literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs index e255baf459..730eed0e0f 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs @@ -1,9 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Taiko.Skinning; using osu.Game.Rulesets.Taiko.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Tests.Visual; @@ -12,6 +16,12 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning { public class TestSceneTaikoPlayfield : TaikoSkinnableTestScene { + public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] + { + typeof(HitTarget), + typeof(LegacyHitTarget), + }).ToList(); + [Cached(typeof(IScrollingInfo))] private ScrollingTestContainer.TestScrollingInfo info = new ScrollingTestContainer.TestScrollingInfo { diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyHitTarget.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyHitTarget.cs new file mode 100644 index 0000000000..51aea9b9ab --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyHitTarget.cs @@ -0,0 +1,41 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Skinning; +using osuTK; + +namespace osu.Game.Rulesets.Taiko.Skinning +{ + public class LegacyHitTarget : CompositeDrawable + { + [BackgroundDependencyLoader] + private void load(ISkinSource skin) + { + RelativeSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + new Sprite + { + Texture = skin.GetTexture("approachcircle"), + Scale = new Vector2(0.73f), + Alpha = 0.7f, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + new Sprite + { + Texture = skin.GetTexture("taikobigcircle"), + Scale = new Vector2(0.7f), + Alpha = 0.5f, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + }; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs index 3af7df07c4..6b59718173 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs @@ -49,6 +49,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning case TaikoSkinComponents.DrumRollTick: return this.GetAnimation("sliderscorepoint", false, false); + + case TaikoSkinComponents.HitTarget: + if (GetTexture("taikobigcircle") != null) + return new LegacyHitTarget(); + + return null; } return source.GetDrawableComponent(component); diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs index 156ea71c16..775eeb4e38 100644 --- a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs +++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs @@ -10,6 +10,7 @@ namespace osu.Game.Rulesets.Taiko RimHit, DrumRollBody, DrumRollTick, - Swell + Swell, + HitTarget } } diff --git a/osu.Game.Rulesets.Taiko/UI/HitTarget.cs b/osu.Game.Rulesets.Taiko/UI/HitTarget.cs index 2bb208bd1d..88886508af 100644 --- a/osu.Game.Rulesets.Taiko/UI/HitTarget.cs +++ b/osu.Game.Rulesets.Taiko/UI/HitTarget.cs @@ -22,6 +22,8 @@ namespace osu.Game.Rulesets.Taiko.UI public HitTarget() { + RelativeSizeAxes = Axes.Both; + Children = new Drawable[] { new Box diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index bde9085c23..375d9995c0 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -17,6 +17,7 @@ using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.Taiko.Judgements; using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Skinning; using osuTK; using osuTK.Graphics; @@ -42,7 +43,7 @@ namespace osu.Game.Rulesets.Taiko.UI private readonly Container hitExplosionContainer; private readonly Container kiaiExplosionContainer; private readonly JudgementContainer judgementContainer; - internal readonly HitTarget HitTarget; + internal readonly Drawable HitTarget; private readonly ProxyContainer topLevelHitContainer; private readonly ProxyContainer barlineContainer; @@ -90,7 +91,7 @@ namespace osu.Game.Rulesets.Taiko.UI RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Left = HIT_TARGET_OFFSET }, Masking = true, - Children = new Drawable[] + Children = new[] { hitExplosionContainer = new Container { @@ -98,7 +99,7 @@ namespace osu.Game.Rulesets.Taiko.UI FillMode = FillMode.Fit, Blending = BlendingParameters.Additive, }, - HitTarget = new HitTarget + HitTarget = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.HitTarget), _ => new HitTarget()) { Anchor = Anchor.CentreLeft, Origin = Anchor.Centre, From ed9663985b439dba6b5c51281ba40cd6cb2e1c07 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Apr 2020 20:55:33 +0900 Subject: [PATCH 1189/2376] Rename panels --- .../Visual/Online/TestSceneDirectPanel.cs | 16 ++++++++-------- .../BeatmapListing/Panels/BeatmapPanel.cs | 2 +- .../{BeatmapPanelGrid.cs => GridBeatmapPanel.cs} | 4 ++-- .../{BeatmapPanelList.cs => ListBeatmapPanel.cs} | 4 ++-- osu.Game/Overlays/BeatmapListingOverlay.cs | 2 +- .../Beatmaps/PaginatedBeatmapContainer.cs | 2 +- osu.Game/Overlays/Rankings/SpotlightsLayout.cs | 2 +- 7 files changed, 16 insertions(+), 16 deletions(-) rename osu.Game/Overlays/BeatmapListing/Panels/{BeatmapPanelGrid.cs => GridBeatmapPanel.cs} (98%) rename osu.Game/Overlays/BeatmapListing/Panels/{BeatmapPanelList.cs => ListBeatmapPanel.cs} (99%) diff --git a/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs index 5809f93d90..d6ed654bac 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs @@ -20,8 +20,8 @@ namespace osu.Game.Tests.Visual.Online { public override IReadOnlyList RequiredTypes => new[] { - typeof(BeatmapPanelGrid), - typeof(BeatmapPanelList), + typeof(GridBeatmapPanel), + typeof(ListBeatmapPanel), typeof(IconPill) }; @@ -126,12 +126,12 @@ namespace osu.Game.Tests.Visual.Online Spacing = new Vector2(5, 20), Children = new Drawable[] { - new BeatmapPanelGrid(normal), - new BeatmapPanelGrid(undownloadable), - new BeatmapPanelGrid(manyDifficulties), - new BeatmapPanelList(normal), - new BeatmapPanelList(undownloadable), - new BeatmapPanelList(manyDifficulties), + new GridBeatmapPanel(normal), + new GridBeatmapPanel(undownloadable), + new GridBeatmapPanel(manyDifficulties), + new ListBeatmapPanel(normal), + new ListBeatmapPanel(undownloadable), + new ListBeatmapPanel(manyDifficulties), }, }, }; diff --git a/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanel.cs b/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanel.cs index f260bf1573..88c15776cd 100644 --- a/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanel.cs +++ b/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanel.cs @@ -148,7 +148,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels if (SetInfo.Beatmaps.Count > maximum_difficulty_icons) { foreach (var ruleset in SetInfo.Beatmaps.Select(b => b.Ruleset).Distinct()) - icons.Add(new GroupedDifficultyIcon(SetInfo.Beatmaps.FindAll(b => b.Ruleset.Equals(ruleset)), ruleset, this is BeatmapPanelList ? Color4.White : colours.Gray5)); + icons.Add(new GroupedDifficultyIcon(SetInfo.Beatmaps.FindAll(b => b.Ruleset.Equals(ruleset)), ruleset, this is ListBeatmapPanel ? Color4.White : colours.Gray5)); } else { diff --git a/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelGrid.cs b/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs similarity index 98% rename from osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelGrid.cs rename to osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs index caa7eb6441..84d35da096 100644 --- a/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelGrid.cs +++ b/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs @@ -19,7 +19,7 @@ using osuTK.Graphics; namespace osu.Game.Overlays.BeatmapListing.Panels { - public class BeatmapPanelGrid : BeatmapPanel + public class GridBeatmapPanel : BeatmapPanel { private const float horizontal_padding = 10; private const float vertical_padding = 5; @@ -31,7 +31,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels protected override PlayButton PlayButton => playButton; protected override Box PreviewBar => progressBar; - public BeatmapPanelGrid(BeatmapSetInfo beatmap) + public GridBeatmapPanel(BeatmapSetInfo beatmap) : base(beatmap) { Width = 380; diff --git a/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelList.cs b/osu.Game/Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs similarity index 99% rename from osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelList.cs rename to osu.Game/Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs index 3245ddea99..433ea37f06 100644 --- a/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelList.cs +++ b/osu.Game/Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs @@ -19,7 +19,7 @@ using osuTK.Graphics; namespace osu.Game.Overlays.BeatmapListing.Panels { - public class BeatmapPanelList : BeatmapPanel + public class ListBeatmapPanel : BeatmapPanel { private const float transition_duration = 120; private const float horizontal_padding = 10; @@ -36,7 +36,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels protected override PlayButton PlayButton => playButton; protected override Box PreviewBar => progressBar; - public BeatmapPanelList(BeatmapSetInfo beatmap) + public ListBeatmapPanel(BeatmapSetInfo beatmap) : base(beatmap) { RelativeSizeAxes = Axes.X; diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index a024e2c74e..f680f7c67b 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -125,7 +125,7 @@ namespace osu.Game.Overlays Spacing = new Vector2(10), Alpha = 0, Margin = new MarginPadding { Vertical = 15 }, - ChildrenEnumerable = beatmaps.Select(b => new BeatmapPanelGrid(b) + ChildrenEnumerable = beatmaps.Select(b => new GridBeatmapPanel(b) { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, diff --git a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs index 5f70dc4d75..191f3c908a 100644 --- a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs @@ -33,7 +33,7 @@ namespace osu.Game.Overlays.Profile.Sections.Beatmaps protected override Drawable CreateDrawableItem(APIBeatmapSet model) => !model.OnlineBeatmapSetID.HasValue ? null - : new BeatmapPanelGrid(model.ToBeatmapSet(Rulesets)) + : new GridBeatmapPanel(model.ToBeatmapSet(Rulesets)) { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, diff --git a/osu.Game/Overlays/Rankings/SpotlightsLayout.cs b/osu.Game/Overlays/Rankings/SpotlightsLayout.cs index 895fa94af5..917509e842 100644 --- a/osu.Game/Overlays/Rankings/SpotlightsLayout.cs +++ b/osu.Game/Overlays/Rankings/SpotlightsLayout.cs @@ -140,7 +140,7 @@ namespace osu.Game.Overlays.Rankings AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, Spacing = new Vector2(10), - Children = response.BeatmapSets.Select(b => new BeatmapPanelGrid(b.ToBeatmapSet(rulesets)) + Children = response.BeatmapSets.Select(b => new GridBeatmapPanel(b.ToBeatmapSet(rulesets)) { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, From 6193b80589790cc4f90a6141cd34fa6900b2723f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Apr 2020 21:20:57 +0900 Subject: [PATCH 1190/2376] 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 d2bdbc8b61..25942863c5 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 35ee0864e1..9c17c453a6 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -23,7 +23,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 0200fca9a3..07ea4b9c2a 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From e9a2e92adf3573c27096b796d99db094f2fd6f5d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 21 Apr 2020 14:43:22 +0900 Subject: [PATCH 1191/2376] Fix incorrect beatmap comments --- osu.Game.Tests/Resources/hitobject-combo-offset.osu | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Resources/hitobject-combo-offset.osu b/osu.Game.Tests/Resources/hitobject-combo-offset.osu index c1f0dab8e9..d39a3e8548 100644 --- a/osu.Game.Tests/Resources/hitobject-combo-offset.osu +++ b/osu.Game.Tests/Resources/hitobject-combo-offset.osu @@ -5,27 +5,27 @@ osu file format v14 255,193,1000,49,0,0:0:0:0: // Combo index = 4 -// Slider with new combo followed by circle with no new combo +// Spinner with new combo followed by circle with no new combo 256,192,2000,12,0,2000,0:0:0:0: 255,193,3000,1,0,0:0:0:0: // Combo index = 5 -// Slider without new combo followed by circle with no new combo +// Spinner without new combo followed by circle with no new combo 256,192,4000,8,0,5000,0:0:0:0: 255,193,6000,1,0,0:0:0:0: // Combo index = 5 -// Slider without new combo followed by circle with new combo +// Spinner without new combo followed by circle with new combo 256,192,7000,8,0,8000,0:0:0:0: 255,193,9000,5,0,0:0:0:0: // Combo index = 6 -// Slider with new combo and offset (1) followed by circle with new combo and offset (3) +// Spinner with new combo and offset (1) followed by circle with new combo and offset (3) 256,192,10000,28,0,11000,0:0:0:0: 255,193,12000,53,0,0:0:0:0: // Combo index = 11 -// Slider with new combo and offset (2) followed by slider with no new combo followed by circle with no new combo +// Spinner with new combo and offset (2) followed by slider with no new combo followed by circle with no new combo 256,192,13000,44,0,14000,0:0:0:0: 256,192,15000,8,0,16000,0:0:0:0: 255,193,17000,1,0,0:0:0:0: From 9713d903884469c247397ae3f7bd223fd0df21e9 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 21 Apr 2020 14:47:12 +0900 Subject: [PATCH 1192/2376] Always apply beatmap converter/processor --- .../Formats/LegacyBeatmapEncoderTest.cs | 61 +++++++++++++++++-- 1 file changed, 55 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs index f2b3a16f68..62cf2cec43 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs @@ -1,14 +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 NUnit.Framework; +using osu.Framework.Audio.Track; +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.Catch; +using osu.Game.Rulesets.Mania; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Taiko; using osu.Game.Tests.Resources; namespace osu.Game.Tests.Beatmaps.Formats @@ -29,26 +36,68 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.That(encoded.Serialize(), Is.EqualTo(decoded.Serialize())); } - private Beatmap decode(string filename, out Beatmap encoded) + private IBeatmap decode(string filename, out IBeatmap encoded) { - using (var stream = TestResources.OpenResource(filename)) + using (var stream = TestResources.GetStore().GetStream(filename)) using (var sr = new LineBufferedReader(stream)) { - var legacyDecoded = new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(sr); + var legacyDecoded = convert(new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(sr)); using (var ms = new MemoryStream()) using (var sw = new StreamWriter(ms)) - using (var sr2 = new LineBufferedReader(ms)) + using (var sr2 = new LineBufferedReader(ms, true)) { new LegacyBeatmapEncoder(legacyDecoded).Encode(sw); - sw.Flush(); + sw.Flush(); ms.Position = 0; - encoded = new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(sr2); + encoded = convert(new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(sr2)); + return legacyDecoded; } } } + + private IBeatmap convert(IBeatmap beatmap) + { + switch (beatmap.BeatmapInfo.RulesetID) + { + case 0: + beatmap.BeatmapInfo.Ruleset = new OsuRuleset().RulesetInfo; + break; + + case 1: + beatmap.BeatmapInfo.Ruleset = new TaikoRuleset().RulesetInfo; + break; + + case 2: + beatmap.BeatmapInfo.Ruleset = new CatchRuleset().RulesetInfo; + break; + + case 3: + beatmap.BeatmapInfo.Ruleset = new ManiaRuleset().RulesetInfo; + break; + } + + return new TestWorkingBeatmap(beatmap).GetPlayableBeatmap(beatmap.BeatmapInfo.Ruleset); + } + + private class TestWorkingBeatmap : WorkingBeatmap + { + private readonly IBeatmap beatmap; + + public TestWorkingBeatmap(IBeatmap beatmap) + : base(beatmap.BeatmapInfo, null) + { + this.beatmap = beatmap; + } + + protected override IBeatmap GetBeatmap() => beatmap; + + protected override Texture GetBackground() => throw new NotImplementedException(); + + protected override Track GetTrack() => throw new NotImplementedException(); + } } } From 8ea76244a26699bc98a27926315292ee44a83897 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 21 Apr 2020 14:48:32 +0900 Subject: [PATCH 1193/2376] Fix only single beatmap being tested --- osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs index 62cf2cec43..01edafcf31 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs @@ -23,14 +23,12 @@ namespace osu.Game.Tests.Beatmaps.Formats [TestFixture] public class LegacyBeatmapEncoderTest { - private const string normal = "Soleily - Renatus (Gamu) [Insane].osu"; - private static IEnumerable allBeatmaps => TestResources.GetStore().GetAvailableResources().Where(res => res.EndsWith(".osu")); [TestCaseSource(nameof(allBeatmaps))] - public void TestDecodeEncodedBeatmap(string name) + public void TestBeatmap(string name) { - var decoded = decode(normal, out var encoded); + var decoded = decode(name, out var encoded); Assert.That(decoded.HitObjects.Count, Is.EqualTo(encoded.HitObjects.Count)); Assert.That(encoded.Serialize(), Is.EqualTo(decoded.Serialize())); From 1e7e7417ed77dcec18944565b826c28919490a94 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 21 Apr 2020 14:49:31 +0900 Subject: [PATCH 1194/2376] Fix testing relying on control point order --- .../Beatmaps/Formats/LegacyBeatmapEncoderTest.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs index 01edafcf31..bcc873b0b7 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; @@ -9,6 +10,7 @@ using NUnit.Framework; using osu.Framework.Audio.Track; using osu.Framework.Graphics.Textures; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Formats; using osu.Game.IO; using osu.Game.IO.Serialization; @@ -30,10 +32,22 @@ namespace osu.Game.Tests.Beatmaps.Formats { var decoded = decode(name, out var encoded); - Assert.That(decoded.HitObjects.Count, Is.EqualTo(encoded.HitObjects.Count)); + sort(decoded); + sort(encoded); + Assert.That(encoded.Serialize(), Is.EqualTo(decoded.Serialize())); } + private void sort(IBeatmap beatmap) + { + // Sort control points to ensure a sane ordering, as they may be parsed in different orders. This works because each group contains only uniquely-typed control points. + foreach (var g in beatmap.ControlPointInfo.Groups) + { + ArrayList.Adapter((IList)g.ControlPoints).Sort( + Comparer.Create((c1, c2) => string.Compare(c1.GetType().ToString(), c2.GetType().ToString(), StringComparison.Ordinal))); + } + } + private IBeatmap decode(string filename, out IBeatmap encoded) { using (var stream = TestResources.GetStore().GetStream(filename)) From 21949ac499ab3ece38ddfb799a4f704a0a589c4b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 21 Apr 2020 14:49:43 +0900 Subject: [PATCH 1195/2376] Add osu! test beatmap --- .../Resources/sample-beatmap-osu.osu | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 osu.Game.Tests/Resources/sample-beatmap-osu.osu diff --git a/osu.Game.Tests/Resources/sample-beatmap-osu.osu b/osu.Game.Tests/Resources/sample-beatmap-osu.osu new file mode 100644 index 0000000000..27c96077e6 --- /dev/null +++ b/osu.Game.Tests/Resources/sample-beatmap-osu.osu @@ -0,0 +1,32 @@ +osu file format v14 + +[General] +SampleSet: Normal +StackLeniency: 0.7 +Mode: 0 + +[Difficulty] +HPDrainRate:3 +CircleSize:5 +OverallDifficulty:8 +ApproachRate:8 +SliderMultiplier:3.59999990463257 +SliderTickRate:2 + +[TimingPoints] +24,352.941176470588,4,1,1,100,1,0 +6376,-50,4,1,1,100,0,0 + +[HitObjects] +98,69,24,1,0,0:0:0:0: +419,72,200,1,2,0:0:0:0: +81,314,376,1,6,0:0:0:0: +423,321,553,1,12,0:0:0:0: +86,192,729,2,0,P|459:193|460:193,1,359.999990463257 +86,192,1259,2,0,P|246:82|453:203,1,449.999988079071 +86,192,1876,2,0,B|256:30|257:313|464:177,1,359.999990463257 +86,55,2406,2,12,B|447:51|447:51|452:348|452:348|78:344,1,989.999973773957,14|2,0:0|0:0,0:0:0:0: +256,192,3553,12,0,4259,0:0:0:0: +67,57,4435,5,0,0:0:0:0: +440,52,4612,5,0,0:0:0:0: +86,181,4788,6,0,L|492:183,1,359.999990463257 \ No newline at end of file From a702a521f8c4af9fe4d645fcbd472466b6ba6ff5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 21 Apr 2020 14:52:58 +0900 Subject: [PATCH 1196/2376] Fix not being able to serialise converted beatmaps --- .../Converters/TypedListConverter.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/osu.Game/IO/Serialization/Converters/TypedListConverter.cs b/osu.Game/IO/Serialization/Converters/TypedListConverter.cs index 6d244bff60..837650eb0a 100644 --- a/osu.Game/IO/Serialization/Converters/TypedListConverter.cs +++ b/osu.Game/IO/Serialization/Converters/TypedListConverter.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections; using System.Collections.Generic; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -43,13 +44,13 @@ namespace osu.Game.IO.Serialization.Converters var list = new List(); var obj = JObject.Load(reader); - var lookupTable = serializer.Deserialize>(obj["lookup_table"].CreateReader()); + var lookupTable = serializer.Deserialize>(obj["$lookup_table"].CreateReader()); - foreach (var tok in obj["items"]) + foreach (var tok in obj["$items"]) { var itemReader = tok.CreateReader(); - var typeName = lookupTable[(int)tok["type"]]; + var typeName = lookupTable[(int)tok["$type"]]; var instance = (T)Activator.CreateInstance(Type.GetType(typeName)); serializer.Populate(itemReader, instance); @@ -61,7 +62,7 @@ namespace osu.Game.IO.Serialization.Converters public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { - var list = (List)value; + var list = (IList)value; var lookupTable = new List(); var objects = new List(); @@ -84,16 +85,16 @@ namespace osu.Game.IO.Serialization.Converters } var itemObject = JObject.FromObject(item, serializer); - itemObject.AddFirst(new JProperty("type", typeId)); + itemObject.AddFirst(new JProperty("$type", typeId)); objects.Add(itemObject); } writer.WriteStartObject(); - writer.WritePropertyName("lookup_table"); + writer.WritePropertyName("$lookup_table"); serializer.Serialize(writer, lookupTable); - writer.WritePropertyName("items"); + writer.WritePropertyName("$items"); serializer.Serialize(writer, objects); writer.WriteEndObject(); From 3093c3e1857c8d5bc7fce5f5b8fa21b3bfeead05 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 21 Apr 2020 14:55:17 +0900 Subject: [PATCH 1197/2376] Fix custom sample set not being written correctly --- .../Beatmaps/Formats/LegacyBeatmapEncoder.cs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index fe63eec3f9..7721d50227 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Text; @@ -10,6 +11,7 @@ using osu.Game.Audio; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Legacy; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Objects.Types; namespace osu.Game.Beatmaps.Formats @@ -159,7 +161,7 @@ namespace osu.Game.Beatmaps.Formats beatLength = -100 / difficultyPoint.SpeedMultiplier; // Apply the control point to a hit sample to uncover legacy properties (e.g. suffix) - HitSampleInfo tempHitSample = samplePoint.ApplyTo(new HitSampleInfo()); + HitSampleInfo tempHitSample = samplePoint.ApplyTo(new ConvertHitObjectParser.LegacyHitSampleInfo()); // Convert effect flags to the legacy format LegacyEffectFlags effectFlags = LegacyEffectFlags.None; @@ -172,7 +174,7 @@ namespace osu.Game.Beatmaps.Formats writer.Write(FormattableString.Invariant($"{beatLength},")); writer.Write(FormattableString.Invariant($"{(int)beatmap.ControlPointInfo.TimingPointAt(group.Time).TimeSignature},")); writer.Write(FormattableString.Invariant($"{(int)toLegacySampleBank(tempHitSample.Bank)},")); - writer.Write(FormattableString.Invariant($"{toLegacyCustomSampleBank(tempHitSample.Suffix)},")); + writer.Write(FormattableString.Invariant($"{toLegacyCustomSampleBank(tempHitSample)},")); writer.Write(FormattableString.Invariant($"{tempHitSample.Volume},")); writer.Write(FormattableString.Invariant($"{(timingPoint != null ? '1' : '0')},")); writer.Write(FormattableString.Invariant($"{(int)effectFlags}")); @@ -326,7 +328,7 @@ namespace osu.Game.Beatmaps.Formats if (!banksOnly) { - string customSampleBank = toLegacyCustomSampleBank(samples.FirstOrDefault(s => !string.IsNullOrEmpty(s.Name))?.Suffix); + string customSampleBank = toLegacyCustomSampleBank(samples.FirstOrDefault(s => !string.IsNullOrEmpty(s.Name))); string sampleFilename = samples.FirstOrDefault(s => string.IsNullOrEmpty(s.Name))?.LookupNames.First() ?? string.Empty; int volume = samples.FirstOrDefault()?.Volume ?? 100; @@ -382,6 +384,15 @@ namespace osu.Game.Beatmaps.Formats } } - private string toLegacyCustomSampleBank(string sampleSuffix) => string.IsNullOrEmpty(sampleSuffix) ? "0" : sampleSuffix; + private string toLegacyCustomSampleBank(HitSampleInfo hitSampleInfo) + { + if (hitSampleInfo == null) + return "0"; + + if (hitSampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacy) + return legacy.CustomSampleBank.ToString(CultureInfo.InvariantCulture); + + return "0"; + } } } From d8d85e5b08980c80a9f5f0a917dafe16a1ce71a1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 21 Apr 2020 15:04:04 +0900 Subject: [PATCH 1198/2376] Don't output certain properties if they don't exist --- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 7721d50227..3ba68c6086 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -60,7 +60,7 @@ namespace osu.Game.Beatmaps.Formats { writer.WriteLine("[General]"); - writer.WriteLine(FormattableString.Invariant($"AudioFilename: {Path.GetFileName(beatmap.Metadata.AudioFile)}")); + if (beatmap.Metadata.AudioFile != null) writer.WriteLine(FormattableString.Invariant($"AudioFilename: {Path.GetFileName(beatmap.Metadata.AudioFile)}")); writer.WriteLine(FormattableString.Invariant($"AudioLeadIn: {beatmap.BeatmapInfo.AudioLeadIn}")); writer.WriteLine(FormattableString.Invariant($"PreviewTime: {beatmap.Metadata.PreviewTime}")); // Todo: Not all countdown types are supported by lazer yet @@ -105,15 +105,15 @@ namespace osu.Game.Beatmaps.Formats writer.WriteLine("[Metadata]"); writer.WriteLine(FormattableString.Invariant($"Title: {beatmap.Metadata.Title}")); - writer.WriteLine(FormattableString.Invariant($"TitleUnicode: {beatmap.Metadata.TitleUnicode}")); + if (beatmap.Metadata.TitleUnicode != null) writer.WriteLine(FormattableString.Invariant($"TitleUnicode: {beatmap.Metadata.TitleUnicode}")); writer.WriteLine(FormattableString.Invariant($"Artist: {beatmap.Metadata.Artist}")); - writer.WriteLine(FormattableString.Invariant($"ArtistUnicode: {beatmap.Metadata.ArtistUnicode}")); + if (beatmap.Metadata.ArtistUnicode != null) writer.WriteLine(FormattableString.Invariant($"ArtistUnicode: {beatmap.Metadata.ArtistUnicode}")); writer.WriteLine(FormattableString.Invariant($"Creator: {beatmap.Metadata.AuthorString}")); writer.WriteLine(FormattableString.Invariant($"Version: {beatmap.BeatmapInfo.Version}")); - writer.WriteLine(FormattableString.Invariant($"Source: {beatmap.Metadata.Source}")); - writer.WriteLine(FormattableString.Invariant($"Tags: {beatmap.Metadata.Tags}")); - writer.WriteLine(FormattableString.Invariant($"BeatmapID: {beatmap.BeatmapInfo.OnlineBeatmapID ?? 0}")); - writer.WriteLine(FormattableString.Invariant($"BeatmapSetID: {beatmap.BeatmapInfo.BeatmapSet?.OnlineBeatmapSetID ?? -1}")); + if (beatmap.Metadata.Source != null) writer.WriteLine(FormattableString.Invariant($"Source: {beatmap.Metadata.Source}")); + if (beatmap.Metadata.Tags != null) writer.WriteLine(FormattableString.Invariant($"Tags: {beatmap.Metadata.Tags}")); + if (beatmap.BeatmapInfo.OnlineBeatmapID != null) writer.WriteLine(FormattableString.Invariant($"BeatmapID: {beatmap.BeatmapInfo.OnlineBeatmapID}")); + if (beatmap.BeatmapInfo.BeatmapSet?.OnlineBeatmapSetID != null) writer.WriteLine(FormattableString.Invariant($"BeatmapSetID: {beatmap.BeatmapInfo.BeatmapSet.OnlineBeatmapSetID}")); } private void handleDifficulty(TextWriter writer) From 1421e876b11479cdb4d7d313dc84124c9f3c254d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 21 Apr 2020 15:04:43 +0900 Subject: [PATCH 1199/2376] Remove implicit new combo from spinners --- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 3ba68c6086..f1f0a0a5de 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -242,7 +242,7 @@ namespace osu.Game.Beatmaps.Formats break; case IHasEndTime _: - type |= LegacyHitObjectType.Spinner | LegacyHitObjectType.NewCombo; + type |= LegacyHitObjectType.Spinner; break; default: From 516e6a4bb10586bfa3505bf776f7ab263d6a8ddd Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 21 Apr 2020 15:05:24 +0900 Subject: [PATCH 1200/2376] Fix overlapping control points not written correctly --- .../Beatmaps/Formats/LegacyBeatmapEncoder.cs | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index f1f0a0a5de..44ccbb350d 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -50,7 +50,7 @@ namespace osu.Game.Beatmaps.Formats handleEvents(writer); writer.WriteLine(); - handleTimingPoints(writer); + handleControlPoints(writer); writer.WriteLine(); handleHitObjects(writer); @@ -139,7 +139,7 @@ namespace osu.Game.Beatmaps.Formats writer.WriteLine(FormattableString.Invariant($"{(int)LegacyEventType.Break},{b.StartTime},{b.EndTime}")); } - private void handleTimingPoints(TextWriter writer) + private void handleControlPoints(TextWriter writer) { if (beatmap.ControlPointInfo.Groups.Count == 0) return; @@ -148,17 +148,27 @@ namespace osu.Game.Beatmaps.Formats foreach (var group in beatmap.ControlPointInfo.Groups) { - var timingPoint = group.ControlPoints.OfType().FirstOrDefault(); - var difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(group.Time); - var samplePoint = beatmap.ControlPointInfo.SamplePointAt(group.Time); - var effectPoint = beatmap.ControlPointInfo.EffectPointAt(group.Time); + var groupTimingPoint = group.ControlPoints.OfType().FirstOrDefault(); - // Convert beat length the legacy format - double beatLength; - if (timingPoint != null) - beatLength = timingPoint.BeatLength; - else - beatLength = -100 / difficultyPoint.SpeedMultiplier; + // If the group contains a timing control point, it needs to be output separately. + if (groupTimingPoint != null) + { + writer.Write(FormattableString.Invariant($"{groupTimingPoint.Time},")); + writer.Write(FormattableString.Invariant($"{groupTimingPoint.BeatLength},")); + outputControlPointEffectsAt(groupTimingPoint.Time, true); + } + + // Output any remaining effects as secondary non-timing control point. + var difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(group.Time); + writer.Write(FormattableString.Invariant($"{group.Time},")); + writer.Write(FormattableString.Invariant($"{-100 / difficultyPoint.SpeedMultiplier},")); + outputControlPointEffectsAt(group.Time, false); + } + + void outputControlPointEffectsAt(double time, bool isTimingPoint) + { + var samplePoint = beatmap.ControlPointInfo.SamplePointAt(time); + var effectPoint = beatmap.ControlPointInfo.EffectPointAt(time); // Apply the control point to a hit sample to uncover legacy properties (e.g. suffix) HitSampleInfo tempHitSample = samplePoint.ApplyTo(new ConvertHitObjectParser.LegacyHitSampleInfo()); @@ -170,13 +180,11 @@ namespace osu.Game.Beatmaps.Formats if (effectPoint.OmitFirstBarLine) effectFlags |= LegacyEffectFlags.OmitFirstBarLine; - writer.Write(FormattableString.Invariant($"{group.Time},")); - writer.Write(FormattableString.Invariant($"{beatLength},")); - writer.Write(FormattableString.Invariant($"{(int)beatmap.ControlPointInfo.TimingPointAt(group.Time).TimeSignature},")); + writer.Write(FormattableString.Invariant($"{(int)beatmap.ControlPointInfo.TimingPointAt(time).TimeSignature},")); writer.Write(FormattableString.Invariant($"{(int)toLegacySampleBank(tempHitSample.Bank)},")); writer.Write(FormattableString.Invariant($"{toLegacyCustomSampleBank(tempHitSample)},")); writer.Write(FormattableString.Invariant($"{tempHitSample.Volume},")); - writer.Write(FormattableString.Invariant($"{(timingPoint != null ? '1' : '0')},")); + writer.Write(FormattableString.Invariant($"{(isTimingPoint ? '1' : '0')},")); writer.Write(FormattableString.Invariant($"{(int)effectFlags}")); writer.WriteLine(); } From d27ca725f946d7a85da2c37bbb9ee53bd5efb95e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 22 Apr 2020 08:51:21 +0900 Subject: [PATCH 1201/2376] Use IEnumerable instead --- osu.Game/IO/Serialization/Converters/TypedListConverter.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/IO/Serialization/Converters/TypedListConverter.cs b/osu.Game/IO/Serialization/Converters/TypedListConverter.cs index 837650eb0a..64f1ebeb1a 100644 --- a/osu.Game/IO/Serialization/Converters/TypedListConverter.cs +++ b/osu.Game/IO/Serialization/Converters/TypedListConverter.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections; using System.Collections.Generic; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -62,7 +61,7 @@ namespace osu.Game.IO.Serialization.Converters public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { - var list = (IList)value; + var list = (IEnumerable)value; var lookupTable = new List(); var objects = new List(); From ee278a2e1bd3e460a7092e2348c84f0d305f8cb3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 21 Apr 2020 15:55:36 +0900 Subject: [PATCH 1202/2376] Add taiko/catch/mania sample beatmaps --- .../Resources/sample-beatmap-catch.osu | 30 +++++++++++++ .../Resources/sample-beatmap-mania.osu | 39 +++++++++++++++++ .../Resources/sample-beatmap-taiko.osu | 42 +++++++++++++++++++ 3 files changed, 111 insertions(+) create mode 100644 osu.Game.Tests/Resources/sample-beatmap-catch.osu create mode 100644 osu.Game.Tests/Resources/sample-beatmap-mania.osu create mode 100644 osu.Game.Tests/Resources/sample-beatmap-taiko.osu diff --git a/osu.Game.Tests/Resources/sample-beatmap-catch.osu b/osu.Game.Tests/Resources/sample-beatmap-catch.osu new file mode 100644 index 0000000000..09ef762e3e --- /dev/null +++ b/osu.Game.Tests/Resources/sample-beatmap-catch.osu @@ -0,0 +1,30 @@ +osu file format v14 + +[General] +SampleSet: Normal +StackLeniency: 0.7 +Mode: 2 + +[Difficulty] +HPDrainRate:3 +CircleSize:5 +OverallDifficulty:8 +ApproachRate:8 +SliderMultiplier:3.59999990463257 +SliderTickRate:2 + +[TimingPoints] +24,352.941176470588,4,1,1,100,1,0 +6376,-50,4,1,1,100,0,0 + +[HitObjects] +32,183,24,5,0,0:0:0:0: +106,123,200,1,10,0:0:0:0: +199,108,376,1,2,0:0:0:0: +305,105,553,5,4,0:0:0:0: +386,112,729,1,14,0:0:0:0: +486,197,906,5,12,0:0:0:0: +14,199,1082,2,0,L|473:198,1,449.999988079071 +14,199,1700,6,6,P|248:33|490:222,1,629.9999833107,0|8,0:0|0:0,0:0:0:0: +10,190,2494,2,8,B|252:29|254:335|468:167,1,449.999988079071,10|12,0:0|0:0,0:0:0:0: +256,192,3112,12,0,3906,0:0:0:0: \ No newline at end of file diff --git a/osu.Game.Tests/Resources/sample-beatmap-mania.osu b/osu.Game.Tests/Resources/sample-beatmap-mania.osu new file mode 100644 index 0000000000..04d6a31ab6 --- /dev/null +++ b/osu.Game.Tests/Resources/sample-beatmap-mania.osu @@ -0,0 +1,39 @@ +osu file format v14 + +[General] +SampleSet: Normal +StackLeniency: 0.7 +Mode: 3 + +[Difficulty] +HPDrainRate:3 +CircleSize:5 +OverallDifficulty:8 +ApproachRate:8 +SliderMultiplier:3.59999990463257 +SliderTickRate:2 + +[TimingPoints] +24,352.941176470588,4,1,1,100,1,0 +6376,-50,4,1,1,100,0,0 + +[HitObjects] +51,192,24,1,0,0:0:0:0: +153,192,200,1,0,0:0:0:0: +358,192,376,1,0,0:0:0:0: +460,192,553,1,0,0:0:0:0: +460,192,729,128,0,1435:0:0:0:0: +358,192,906,128,0,1612:0:0:0:0: +256,192,1082,128,0,1788:0:0:0:0: +153,192,1259,128,0,1965:0:0:0:0: +51,192,1435,128,0,2141:0:0:0:0: +51,192,2318,1,12,0:0:0:0: +153,192,2318,1,4,0:0:0:0: +256,192,2318,1,6,0:0:0:0: +358,192,2318,1,14,0:0:0:0: +460,192,2318,1,0,0:0:0:0: +51,192,2494,128,0,2582:0:0:0:0: +153,192,2494,128,14,2582:0:0:0:0: +256,192,2494,128,6,2582:0:0:0:0: +358,192,2494,128,4,2582:0:0:0:0: +460,192,2494,128,12,2582:0:0:0:0: \ No newline at end of file diff --git a/osu.Game.Tests/Resources/sample-beatmap-taiko.osu b/osu.Game.Tests/Resources/sample-beatmap-taiko.osu new file mode 100644 index 0000000000..94b4288336 --- /dev/null +++ b/osu.Game.Tests/Resources/sample-beatmap-taiko.osu @@ -0,0 +1,42 @@ +osu file format v14 + +[General] +SampleSet: Normal +StackLeniency: 0.7 +Mode: 1 + +[Difficulty] +HPDrainRate:3 +CircleSize:5 +OverallDifficulty:8 +ApproachRate:8 +SliderMultiplier:3.59999990463257 +SliderTickRate:2 + +[TimingPoints] +24,352.941176470588,4,1,1,100,1,0 +6376,-50,4,1,1,100,0,0 + +[HitObjects] +231,129,24,1,0,0:0:0:0: +231,129,200,1,0,0:0:0:0: +231,129,376,1,0,0:0:0:0: +231,129,553,1,0,0:0:0:0: +231,129,729,1,0,0:0:0:0: +373,132,906,1,4,0:0:0:0: +373,132,1082,1,4,0:0:0:0: +373,132,1259,1,4,0:0:0:0: +373,132,1435,1,4,0:0:0:0: +231,129,1788,1,8,0:0:0:0: +231,129,1964,1,8,0:0:0:0: +231,129,2140,1,8,0:0:0:0: +231,129,2317,1,8,0:0:0:0: +231,129,2493,1,8,0:0:0:0: +373,132,2670,1,12,0:0:0:0: +373,132,2846,1,12,0:0:0:0: +373,132,3023,1,12,0:0:0:0: +373,132,3199,1,12,0:0:0:0: +51,189,3553,2,0,L|150:188,1,89.9999976158143 +52,191,3906,2,0,L|512:189,1,449.999988079071 +26,196,4612,2,4,L|501:195,1,449.999988079071 +17,242,5318,2,10,P|250:69|495:243,1,629.9999833107,0|8,0:0|0:0,0:0:0:0: \ No newline at end of file From ea0ebc8527035e4884acebd77e5cad73de3200e3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 21 Apr 2020 16:04:58 +0900 Subject: [PATCH 1203/2376] Implement beatmap encoding for all legacy rulesets --- .../Beatmaps/Formats/LegacyBeatmapEncoder.cs | 53 ++++++++++++------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 44ccbb350d..af0adb65a5 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -13,6 +13,7 @@ using osu.Game.Beatmaps.Legacy; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Objects.Types; +using osuTK; namespace osu.Game.Beatmaps.Formats { @@ -197,32 +198,40 @@ namespace osu.Game.Beatmaps.Formats writer.WriteLine("[HitObjects]"); - // TODO: implement other legacy rulesets + foreach (var h in beatmap.HitObjects) + handleHitObject(writer, h); + } + + private void handleHitObject(TextWriter writer, HitObject hitObject) + { + Vector2 position = new Vector2(256, 192); + switch (beatmap.BeatmapInfo.RulesetID) { case 0: - foreach (var h in beatmap.HitObjects) - handleOsuHitObject(writer, h); + position = ((IHasPosition)hitObject).Position; + break; + + case 2: + position.X = ((IHasXPosition)hitObject).X * 512; + break; + + case 3: + int totalColumns = (int)Math.Max(1, beatmap.BeatmapInfo.BaseDifficulty.CircleSize); + position.X = (int)Math.Ceiling(((IHasXPosition)hitObject).X * (512f / totalColumns)); break; } - } - private void handleOsuHitObject(TextWriter writer, HitObject hitObject) - { - var positionData = (IHasPosition)hitObject; - - writer.Write(FormattableString.Invariant($"{positionData.X},")); - writer.Write(FormattableString.Invariant($"{positionData.Y},")); + writer.Write(FormattableString.Invariant($"{position.X},")); + writer.Write(FormattableString.Invariant($"{position.Y},")); writer.Write(FormattableString.Invariant($"{hitObject.StartTime},")); writer.Write(FormattableString.Invariant($"{(int)getObjectType(hitObject)},")); - writer.Write(hitObject is IHasCurve - ? FormattableString.Invariant($"0,") - : FormattableString.Invariant($"{(int)toLegacyHitSoundType(hitObject.Samples)},")); + writer.Write(FormattableString.Invariant($"{(int)toLegacyHitSoundType(hitObject.Samples)},")); if (hitObject is IHasCurve curveData) { - addCurveData(writer, curveData, positionData); + addCurveData(writer, curveData, position); writer.Write(getSampleBank(hitObject.Samples, zeroBanks: true)); } else @@ -237,11 +246,15 @@ namespace osu.Game.Beatmaps.Formats private static LegacyHitObjectType getObjectType(HitObject hitObject) { - var comboData = (IHasCombo)hitObject; + LegacyHitObjectType type = 0; - var type = (LegacyHitObjectType)(comboData.ComboOffset << 4); + if (hitObject is IHasCombo combo) + { + type = (LegacyHitObjectType)(combo.ComboOffset << 4); - if (comboData.NewCombo) type |= LegacyHitObjectType.NewCombo; + if (combo.NewCombo) + type |= LegacyHitObjectType.NewCombo; + } switch (hitObject) { @@ -261,7 +274,7 @@ namespace osu.Game.Beatmaps.Formats return type; } - private void addCurveData(TextWriter writer, IHasCurve curveData, IHasPosition positionData) + private void addCurveData(TextWriter writer, IHasCurve curveData, Vector2 position) { PathType? lastType = null; @@ -297,13 +310,13 @@ namespace osu.Game.Beatmaps.Formats else { // New segment with the same type - duplicate the control point - writer.Write(FormattableString.Invariant($"{positionData.X + point.Position.Value.X}:{positionData.Y + point.Position.Value.Y}|")); + writer.Write(FormattableString.Invariant($"{position.X + point.Position.Value.X}:{position.Y + point.Position.Value.Y}|")); } } if (i != 0) { - writer.Write(FormattableString.Invariant($"{positionData.X + point.Position.Value.X}:{positionData.Y + point.Position.Value.Y}")); + writer.Write(FormattableString.Invariant($"{position.X + point.Position.Value.X}:{position.Y + point.Position.Value.Y}")); writer.Write(i != curveData.Path.ControlPoints.Count - 1 ? "|" : ","); } } From d957614fc96fe142dccaeec1030de08e3f4c7453 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 21 Apr 2020 16:33:19 +0900 Subject: [PATCH 1204/2376] Cleanup handling of mania samples --- .../Beatmaps/ManiaBeatmapConverter.cs | 21 +-------- .../Legacy/DistanceObjectPatternGenerator.cs | 10 ++-- .../Legacy/EndTimeObjectPatternGenerator.cs | 15 ++---- .../Objects/Drawables/DrawableHoldNote.cs | 5 ++ osu.Game.Rulesets.Mania/Objects/HoldNote.cs | 46 +++++++++++++++---- 5 files changed, 52 insertions(+), 45 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index 4187e39b43..b803caa1b7 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -238,9 +238,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps { StartTime = HitObject.StartTime, Duration = endTimeData.Duration, - Column = column, - Head = { Samples = sampleInfoListAt(HitObject.StartTime) }, - Tail = { Samples = sampleInfoListAt(endTimeData.EndTime) }, + Samples = HitObject.Samples, + NodeSamples = (HitObject as IHasRepeats)?.NodeSamples }); } else if (HitObject is IHasXPosition) @@ -255,22 +254,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps return pattern; } - - /// - /// Retrieves the sample info list at a point in time. - /// - /// The time to retrieve the sample info list from. - /// - private IList sampleInfoListAt(double time) - { - if (!(HitObject is IHasCurve curveData)) - return HitObject.Samples; - - double segmentTime = (curveData.EndTime - HitObject.StartTime) / curveData.SpanCount(); - - int index = (int)(segmentTime == 0 ? 0 : (time - HitObject.StartTime) / segmentTime); - return curveData.NodeSamples[index]; - } } } } diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs index 315ef96e49..d8d5b67c0e 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs @@ -505,16 +505,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy } else { - var holdNote = new HoldNote + newObject = new HoldNote { StartTime = startTime, - Column = column, Duration = endTime - startTime, - Head = { Samples = sampleInfoListAt(startTime) }, - Tail = { Samples = sampleInfoListAt(endTime) } + Column = column, + Samples = HitObject.Samples, + NodeSamples = (HitObject as IHasRepeats)?.NodeSamples }; - - newObject = holdNote; } pattern.Add(newObject); diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs index b3be08e1f7..907bed0d65 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs @@ -64,21 +64,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy if (holdNote) { - var hold = new HoldNote + newObject = new HoldNote { StartTime = HitObject.StartTime, + Duration = endTime - HitObject.StartTime, Column = column, - Duration = endTime - HitObject.StartTime + Samples = HitObject.Samples, + NodeSamples = (HitObject as IHasRepeats)?.NodeSamples }; - - if (hold.Head.Samples == null) - hold.Head.Samples = new List(); - - hold.Head.Samples.Add(new HitSampleInfo { Name = HitSampleInfo.HIT_NORMAL }); - - hold.Tail.Samples = HitObject.Samples; - - newObject = hold; } else { diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index a9ef661aaa..bc3a136a6a 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -127,6 +127,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables bodyPiece.Anchor = bodyPiece.Origin = e.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft; } + public override void PlaySamples() + { + // The hold note does not play samples itself. + } + protected override void Update() { base.Update(); diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs index 049bf55f90..04f8fd8c99 100644 --- a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/HoldNote.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.Collections.Generic; +using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Judgements; @@ -28,7 +30,9 @@ namespace osu.Game.Rulesets.Mania.Objects set { duration = value; - Tail.StartTime = EndTime; + + if (Tail != null) + Tail.StartTime = EndTime; } } @@ -38,8 +42,12 @@ namespace osu.Game.Rulesets.Mania.Objects set { base.StartTime = value; - Head.StartTime = value; - Tail.StartTime = EndTime; + + if (Head != null) + Head.StartTime = value; + + if (Tail != null) + Tail.StartTime = EndTime; } } @@ -49,20 +57,26 @@ namespace osu.Game.Rulesets.Mania.Objects set { base.Column = value; - Head.Column = value; - Tail.Column = value; + + if (Head != null) + Head.Column = value; + + if (Tail != null) + Tail.Column = value; } } + public List> NodeSamples { get; set; } + /// /// The head note of the hold. /// - public readonly Note Head = new Note(); + public Note Head { get; private set; } /// /// The tail note of the hold. /// - public readonly TailNote Tail = new TailNote(); + public TailNote Tail { get; private set; } /// /// The time between ticks of this hold. @@ -83,8 +97,19 @@ namespace osu.Game.Rulesets.Mania.Objects createTicks(); - AddNested(Head); - AddNested(Tail); + AddNested(Head = new Note + { + StartTime = StartTime, + Column = Column, + Samples = getNodeSamples(0), + }); + + AddNested(Tail = new TailNote + { + StartTime = EndTime, + Column = Column, + Samples = getNodeSamples(1), + }); } private void createTicks() @@ -105,5 +130,8 @@ namespace osu.Game.Rulesets.Mania.Objects public override Judgement CreateJudgement() => new IgnoreJudgement(); protected override HitWindows CreateHitWindows() => HitWindows.Empty; + + private IList getNodeSamples(int nodeIndex) => + nodeIndex < NodeSamples?.Count ? NodeSamples[nodeIndex] : Samples; } } From cc0c82aaebac82c2d3a7eb1844eac44dab47da31 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 21 Apr 2020 16:44:26 +0900 Subject: [PATCH 1205/2376] Implement IHasXPosition on ManiaHitObject --- .../Beatmaps/ManiaBeatmapConverter.cs | 3 +-- osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs | 9 ++++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index b803caa1b7..cb583ac1a9 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -13,7 +13,6 @@ using osu.Game.Rulesets.Mania.Beatmaps.Patterns; using osu.Game.Rulesets.Mania.MathUtils; using osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy; using osuTK; -using osu.Game.Audio; namespace osu.Game.Rulesets.Mania.Beatmaps { @@ -67,7 +66,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps } } - public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition || h is ManiaHitObject); + public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition); protected override Beatmap ConvertBeatmap(IBeatmap original) { diff --git a/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs index 995e1516cb..27bf50493d 100644 --- a/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs +++ b/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs @@ -5,11 +5,12 @@ using osu.Framework.Bindables; using osu.Game.Rulesets.Mania.Objects.Types; using osu.Game.Rulesets.Mania.Scoring; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Objects { - public abstract class ManiaHitObject : HitObject, IHasColumn + public abstract class ManiaHitObject : HitObject, IHasColumn, IHasXPosition { public readonly Bindable ColumnBindable = new Bindable(); @@ -20,5 +21,11 @@ namespace osu.Game.Rulesets.Mania.Objects } protected override HitWindows CreateHitWindows() => new ManiaHitWindows(); + + #region LegacyBeatmapEncoder + + float IHasXPosition.X => Column; + + #endregion } } From d8fdd73e170e8117961a3da8d1ef31e85738c9c2 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 21 Apr 2020 16:45:01 +0900 Subject: [PATCH 1206/2376] Implement IHasCurve on DrumRoll --- .../Beatmaps/TaikoBeatmapConverter.cs | 6 ++-- osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs | 29 ++++++++++++++++++- .../Beatmaps/Formats/LegacyBeatmapEncoder.cs | 8 +++-- 3 files changed, 37 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index 695ada3a00..caf645d5a2 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps /// osu! is generally slower than taiko, so a factor is added to increase /// speed. This must be used everywhere slider length or beat length is used. /// - private const float legacy_velocity_multiplier = 1.4f; + public const float LEGACY_VELOCITY_MULTIPLIER = 1.4f; /// /// Because swells are easier in taiko than spinners are in osu!, @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps // Rewrite the beatmap info to add the slider velocity multiplier original.BeatmapInfo = original.BeatmapInfo.Clone(); original.BeatmapInfo.BaseDifficulty = original.BeatmapInfo.BaseDifficulty.Clone(); - original.BeatmapInfo.BaseDifficulty.SliderMultiplier *= legacy_velocity_multiplier; + original.BeatmapInfo.BaseDifficulty.SliderMultiplier *= LEGACY_VELOCITY_MULTIPLIER; Beatmap converted = base.ConvertBeatmap(original); @@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps double speedAdjustedBeatLength = timingPoint.BeatLength / speedAdjustment; // The true distance, accounting for any repeats. This ends up being the drum roll distance later - double distance = distanceData.Distance * spans * legacy_velocity_multiplier; + double distance = distanceData.Distance * spans * LEGACY_VELOCITY_MULTIPLIER; // The velocity of the taiko hit object - calculated as the velocity of a drum roll double taikoVelocity = taiko_base_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier / speedAdjustedBeatLength; diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs index aacd78f176..ad0cd2f2a8 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs @@ -3,15 +3,20 @@ using osu.Game.Rulesets.Objects.Types; using System; +using System.Collections.Generic; +using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko.Beatmaps; using osu.Game.Rulesets.Taiko.Judgements; +using osuTK; namespace osu.Game.Rulesets.Taiko.Objects { - public class DrumRoll : TaikoHitObject, IHasEndTime + public class DrumRoll : TaikoHitObject, IHasEndTime, IHasCurve { /// /// Drum roll distance that results in a duration of 1 speed-adjusted beat length. @@ -26,6 +31,11 @@ namespace osu.Game.Rulesets.Taiko.Objects public double Duration { get; set; } + /// + /// Velocity of this . + /// + public double Velocity { get; private set; } + /// /// Numer of ticks per beat length. /// @@ -54,6 +64,10 @@ namespace osu.Game.Rulesets.Taiko.Objects base.ApplyDefaultsToSelf(controlPointInfo, difficulty); TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); + DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(StartTime); + + double scoringDistance = base_distance * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier; + Velocity = scoringDistance / timingPoint.BeatLength; tickSpacing = timingPoint.BeatLength / TickRate; overallDifficulty = difficulty.OverallDifficulty; @@ -93,5 +107,18 @@ namespace osu.Game.Rulesets.Taiko.Objects public override Judgement CreateJudgement() => new TaikoDrumRollJudgement(); protected override HitWindows CreateHitWindows() => HitWindows.Empty; + + #region LegacyBeatmapEncoder + + double IHasDistance.Distance => Duration * Velocity; + + int IHasRepeats.RepeatCount { get => 0; set { } } + + List> IHasRepeats.NodeSamples => new List>(); + + SliderPath IHasCurve.Path + => new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(1) }, ((IHasDistance)this).Distance / TaikoBeatmapConverter.LEGACY_VELOCITY_MULTIPLIER); + + #endregion } } diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index af0adb65a5..4b760f1983 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -125,7 +125,12 @@ namespace osu.Game.Beatmaps.Formats 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($"SliderMultiplier: {beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier}")); + + // Taiko adjusts the slider multiplier (see: TaikoBeatmapConverter.LEGACY_VELOCITY_MULTIPLIER) + writer.WriteLine(beatmap.BeatmapInfo.RulesetID == 1 + ? FormattableString.Invariant($"SliderMultiplier: {beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier / 1.4f}") + : FormattableString.Invariant($"SliderMultiplier: {beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier}")); + writer.WriteLine(FormattableString.Invariant($"SliderTickRate: {beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate}")); } @@ -226,7 +231,6 @@ namespace osu.Game.Beatmaps.Formats writer.Write(FormattableString.Invariant($"{position.Y},")); writer.Write(FormattableString.Invariant($"{hitObject.StartTime},")); writer.Write(FormattableString.Invariant($"{(int)getObjectType(hitObject)},")); - writer.Write(FormattableString.Invariant($"{(int)toLegacyHitSoundType(hitObject.Samples)},")); if (hitObject is IHasCurve curveData) From 1f962f5c563d80f40208aa8f5e8fa0d1b54389d9 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 21 Apr 2020 17:20:37 +0900 Subject: [PATCH 1207/2376] Reword comment --- osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index bc3a136a6a..770dd73e7a 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -129,7 +129,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables public override void PlaySamples() { - // The hold note does not play samples itself. + // Samples are played by the head/tail notes. } protected override void Update() From 6da0872ae55d9b07d2d1fe55a4db02455b18a9a6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 21 Apr 2020 17:29:23 +0900 Subject: [PATCH 1208/2376] Use the last node sample for the tail note --- osu.Game.Rulesets.Mania/Objects/HoldNote.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs index 04f8fd8c99..eea2c31260 100644 --- a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs @@ -108,7 +108,7 @@ namespace osu.Game.Rulesets.Mania.Objects { StartTime = EndTime, Column = Column, - Samples = getNodeSamples(1), + Samples = getNodeSamples((NodeSamples?.Count - 1) ?? 1), }); } From ba12e23d9eed0b48effa40f7734c1485adfa91cd Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 21 Apr 2020 17:31:49 +0900 Subject: [PATCH 1209/2376] Fix inspection --- osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs index ad0cd2f2a8..dc2f277e58 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs @@ -16,7 +16,7 @@ using osuTK; namespace osu.Game.Rulesets.Taiko.Objects { - public class DrumRoll : TaikoHitObject, IHasEndTime, IHasCurve + public class DrumRoll : TaikoHitObject, IHasCurve { /// /// Drum roll distance that results in a duration of 1 speed-adjusted beat length. From 0c74f1aaa91c70e7984bbe9c93b68509d6b09d38 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Apr 2020 09:08:33 +0900 Subject: [PATCH 1210/2376] Fix now playing output showing empty brackets when no difficulty specified --- osu.Game/Beatmaps/BeatmapInfo.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 68d113ce40..90c100db05 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -149,7 +149,12 @@ namespace osu.Game.Beatmaps } } - public override string ToString() => $"{Metadata} [{Version}]".Trim(); + public override string ToString() + { + string version = string.IsNullOrEmpty(Version) ? string.Empty : $"[{Version}]"; + + return $"{Metadata} {version}".Trim(); + } public bool Equals(BeatmapInfo other) { From 360c9f8e387bffbb2d4d19d07465d412d0d94e75 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Apr 2020 09:19:34 +0900 Subject: [PATCH 1211/2376] Add test coverage and handle null creator --- .../Beatmaps/ToStringFormattingTest.cs | 61 +++++++++++++++++++ osu.Game/Beatmaps/BeatmapMetadata.cs | 6 +- 2 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Tests/Beatmaps/ToStringFormattingTest.cs diff --git a/osu.Game.Tests/Beatmaps/ToStringFormattingTest.cs b/osu.Game.Tests/Beatmaps/ToStringFormattingTest.cs new file mode 100644 index 0000000000..c477bbd9cf --- /dev/null +++ b/osu.Game.Tests/Beatmaps/ToStringFormattingTest.cs @@ -0,0 +1,61 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Users; + +namespace osu.Game.Tests.Beatmaps +{ + [TestFixture] + public class ToStringFormattingTest + { + [Test] + public void TestArtistTitle() + { + var beatmap = new BeatmapInfo + { + Metadata = new BeatmapMetadata + { + Artist = "artist", + Title = "title" + } + }; + + Assert.That(beatmap.ToString(), Is.EqualTo("artist - title")); + } + + [Test] + public void TestArtistTitleCreator() + { + var beatmap = new BeatmapInfo + { + Metadata = new BeatmapMetadata + { + Artist = "artist", + Title = "title", + Author = new User { Username = "creator" } + } + }; + + Assert.That(beatmap.ToString(), Is.EqualTo("artist - title (creator)")); + } + + [Test] + public void TestArtistTitleCreatorDifficulty() + { + var beatmap = new BeatmapInfo + { + Metadata = new BeatmapMetadata + { + Artist = "artist", + Title = "title", + Author = new User { Username = "creator" } + }, + Version = "difficulty" + }; + + Assert.That(beatmap.ToString(), Is.EqualTo("artist - title (creator) [difficulty]")); + } + } +} diff --git a/osu.Game/Beatmaps/BeatmapMetadata.cs b/osu.Game/Beatmaps/BeatmapMetadata.cs index 001f319307..775d78f1fb 100644 --- a/osu.Game/Beatmaps/BeatmapMetadata.cs +++ b/osu.Game/Beatmaps/BeatmapMetadata.cs @@ -53,7 +53,11 @@ namespace osu.Game.Beatmaps public string AudioFile { get; set; } public string BackgroundFile { get; set; } - public override string ToString() => $"{Artist} - {Title} ({Author})"; + public override string ToString() + { + string author = Author == null ? string.Empty : $"({Author})"; + return $"{Artist} - {Title} {author}".Trim(); + } [JsonIgnore] public string[] SearchableTerms => new[] From 95de2c6f7f9d82a6c8c72e94280d8b7257766ad7 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 22 Apr 2020 05:04:07 +0300 Subject: [PATCH 1212/2376] Mark Catcher.additiveTarget to never be null And provide empty containers instead. --- osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs | 3 ++- .../Difficulty/CatchDifficultyCalculator.cs | 3 ++- osu.Game.Rulesets.Catch/UI/Catcher.cs | 3 ++- osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs index acc5f4e428..3a3e664690 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; namespace osu.Game.Rulesets.Catch.Tests { @@ -23,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.Tests [BackgroundDependencyLoader] private void load() { - SetContents(() => new Catcher + SetContents(() => new Catcher(new Container()) { RelativePositionAxes = Axes.None, Anchor = Anchor.Centre, diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 4d9dbbbc5f..fbdf437b7b 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Difficulty.Preprocessing; using osu.Game.Rulesets.Catch.Difficulty.Skills; @@ -71,7 +72,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty protected override Skill[] CreateSkills(IBeatmap beatmap) { - using (var catcher = new Catcher(beatmap.BeatmapInfo.BaseDifficulty)) + using (var catcher = new Catcher(new Container(), beatmap.BeatmapInfo.BaseDifficulty)) halfCatcherWidth = catcher.CatchWidth * 0.5f; return new Skill[] diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 7ecb245617..029de2cac0 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; @@ -116,7 +117,7 @@ namespace osu.Game.Rulesets.Catch.UI private int hyperDashDirection; private float hyperDashTargetPosition; - public Catcher(BeatmapDifficulty difficulty = null, Container additiveTarget = null) + public Catcher([NotNull] Container additiveTarget, BeatmapDifficulty difficulty = null) { this.additiveTarget = additiveTarget; diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 1dd94afa9e..37d177b936 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Catch.UI { RelativeSizeAxes = Axes.X; Height = CATCHER_SIZE; - Child = MovableCatcher = new Catcher(difficulty, this); + Child = MovableCatcher = new Catcher(this, difficulty); } public static float GetCatcherSize(BeatmapDifficulty difficulty) From 9ab0f6d8bc5a160f9859305a4a70e3d889f644ec Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 22 Apr 2020 05:12:29 +0300 Subject: [PATCH 1213/2376] Separate trail-related logic to its own container --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 120 ++++----------- .../UI/CatcherTrailDisplay.cs | 137 ++++++++++++++++++ 2 files changed, 168 insertions(+), 89 deletions(-) create mode 100644 osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 029de2cac0..e30988d8f7 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -8,7 +8,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; using osu.Framework.Utils; using osu.Game.Beatmaps; @@ -37,6 +36,11 @@ namespace osu.Game.Rulesets.Catch.UI /// public static readonly Color4 DEFAULT_CATCHER_HYPER_DASH_COLOUR = Color4.OrangeRed; + /// + /// The duration between transitioning to hyper-dash state. + /// + public const double HYPER_DASH_TRANSITION_DURATION = 180; + /// /// Whether we are hyper-dashing or not. /// @@ -49,10 +53,10 @@ namespace osu.Game.Rulesets.Catch.UI public Container ExplodingFruitTarget; - private readonly Container additiveTarget; - private Container dashTrails; - private Container hyperDashTrails; - private Container endGlowSprites; + [NotNull] + private readonly Container trailsTarget; + + private CatcherTrailDisplay trails; public CatcherAnimationState CurrentState { get; private set; } @@ -66,33 +70,23 @@ namespace osu.Game.Rulesets.Catch.UI /// internal float CatchWidth => CatcherArea.CATCHER_SIZE * Math.Abs(Scale.X) * allowed_catch_range; - protected bool Dashing + /// + /// The drawable catcher for . + /// + internal Drawable CurrentDrawableCatcher => currentCatcher.Drawable; + + private bool dashing; + + public bool Dashing { get => dashing; - set + protected set { if (value == dashing) return; dashing = value; - Trail |= dashing; - } - } - - /// - /// Activate or deactivate the trail. Will be automatically deactivated when conditions to keep the trail displayed are no longer met. - /// - protected bool Trail - { - get => trail; - set - { - if (value == trail || additiveTarget == null) return; - - trail = value; - - if (Trail) - beginTrail(); + trails.DisplayTrail |= dashing; } } @@ -109,17 +103,13 @@ namespace osu.Game.Rulesets.Catch.UI private int currentDirection; - private bool dashing; - - private bool trail; - private double hyperDashModifier = 1; private int hyperDashDirection; private float hyperDashTargetPosition; - public Catcher([NotNull] Container additiveTarget, BeatmapDifficulty difficulty = null) + public Catcher([NotNull] Container trailsTarget, BeatmapDifficulty difficulty = null) { - this.additiveTarget = additiveTarget; + this.trailsTarget = trailsTarget; RelativePositionAxes = Axes.X; X = 0.5f; @@ -158,12 +148,7 @@ namespace osu.Game.Rulesets.Catch.UI } }; - additiveTarget?.AddRange(new[] - { - dashTrails = new Container { RelativeSizeAxes = Axes.Both }, - hyperDashTrails = new Container { RelativeSizeAxes = Axes.Both, Colour = hyperDashColour }, - endGlowSprites = new Container { RelativeSizeAxes = Axes.Both, Colour = hyperDashEndGlowColour } - }); + trailsTarget.Add(trails = new CatcherTrailDisplay(this)); updateCatcher(); } @@ -257,7 +242,7 @@ namespace osu.Game.Rulesets.Catch.UI if (wasHyperDashing) { updateCatcherColour(false); - Trail &= Dashing; + trails.DisplayTrail &= Dashing; } } else @@ -269,21 +254,15 @@ namespace osu.Game.Rulesets.Catch.UI if (!wasHyperDashing) { updateCatcherColour(true); - Trail = true; - var hyperDashEndGlow = createAdditiveSprite(endGlowSprites); - hyperDashEndGlow.MoveToOffset(new Vector2(0, -10), 1200, Easing.In); - hyperDashEndGlow.ScaleTo(hyperDashEndGlow.Scale * 0.95f).ScaleTo(hyperDashEndGlow.Scale * 1.2f, 1200, Easing.In); - hyperDashEndGlow.FadeOut(1200); - hyperDashEndGlow.Expire(true); + trails.DisplayTrail = true; + trails.DisplayEndGlow(); } } } private void updateCatcherColour(bool hyperDashing) { - const float hyper_dash_transition_length = 180; - if (hyperDashing) { // special behaviour for catcher colour if no skin overrides. @@ -292,17 +271,17 @@ namespace osu.Game.Rulesets.Catch.UI ? DEFAULT_CATCHER_HYPER_DASH_COLOUR : hyperDashColour; - this.FadeColour(catcherColour, hyper_dash_transition_length, Easing.OutQuint); - this.FadeTo(0.2f, hyper_dash_transition_length, Easing.OutQuint); + this.FadeColour(catcherColour, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint); + this.FadeTo(0.2f, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint); } else { - this.FadeColour(Color4.White, hyper_dash_transition_length, Easing.OutQuint); - this.FadeTo(1f, hyper_dash_transition_length, Easing.OutQuint); + this.FadeColour(Color4.White, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint); + this.FadeTo(1f, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint); } - hyperDashTrails?.FadeColour(hyperDashColour, hyper_dash_transition_length, Easing.OutQuint); - endGlowSprites?.FadeColour(hyperDashEndGlowColour, hyper_dash_transition_length, Easing.OutQuint); + trails.HyperDashTrailsColour = hyperDashColour; + trails.EndGlowSpritesColour = hyperDashEndGlowColour; } public bool OnPressed(CatchAction action) @@ -453,22 +432,6 @@ namespace osu.Game.Rulesets.Catch.UI (currentCatcher.Drawable as IFramedAnimation)?.GotoFrame(0); } - private void beginTrail() - { - if (!dashing && !HyperDashing) - { - Trail = false; - return; - } - - var additive = createAdditiveSprite(HyperDashing ? hyperDashTrails : dashTrails); - - additive.FadeTo(0.4f).FadeOut(800, Easing.OutQuint); - additive.Expire(true); - - Scheduler.AddDelayed(beginTrail, HyperDashing ? 25 : 50); - } - private void updateState(CatcherAnimationState state) { if (CurrentState == state) @@ -478,27 +441,6 @@ namespace osu.Game.Rulesets.Catch.UI updateCatcher(); } - private CatcherTrailSprite createAdditiveSprite(Container target) - { - if (target == null) - return null; - - var tex = (currentCatcher.Drawable as TextureAnimation)?.CurrentFrame ?? ((Sprite)currentCatcher.Drawable).Texture; - - var sprite = new CatcherTrailSprite(tex) - { - Anchor = Anchor, - Scale = Scale, - Blending = BlendingParameters.Additive, - RelativePositionAxes = RelativePositionAxes, - Position = Position - }; - - target.Add(sprite); - - return sprite; - } - private void removeFromPlateWithTransform(DrawableHitObject fruit, Action action) { if (ExplodingFruitTarget != null) diff --git a/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs b/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs new file mode 100644 index 0000000000..afbfac9a51 --- /dev/null +++ b/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs @@ -0,0 +1,137 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Animations; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Catch.UI +{ + /// + /// Represents a component responsible for displaying + /// the appropriate catcher trails when requested to. + /// + public class CatcherTrailDisplay : CompositeDrawable + { + private readonly Catcher catcher; + + private readonly Container dashTrails; + private readonly Container hyperDashTrails; + private readonly Container endGlowSprites; + + private Color4 hyperDashTrailsColour; + + public Color4 HyperDashTrailsColour + { + get => hyperDashTrailsColour; + set + { + if (hyperDashTrailsColour == value) + return; + + hyperDashTrailsColour = value; + hyperDashTrails.FadeColour(hyperDashTrailsColour, Catcher.HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint); + } + } + + private Color4 endGlowSpritesColour; + + public Color4 EndGlowSpritesColour + { + get => endGlowSpritesColour; + set + { + if (endGlowSpritesColour == value) + return; + + endGlowSpritesColour = value; + endGlowSprites.FadeColour(endGlowSpritesColour, Catcher.HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint); + } + } + + private bool trail; + + /// + /// Whether to start displaying trails following the catcher. + /// + public bool DisplayTrail + { + get => trail; + set + { + if (trail == value) + return; + + trail = value; + + if (trail) + displayTrail(); + } + } + + public CatcherTrailDisplay(Catcher catcher) + { + this.catcher = catcher ?? throw new ArgumentNullException(nameof(catcher)); + + RelativeSizeAxes = Axes.Both; + + InternalChildren = new[] + { + dashTrails = new Container { RelativeSizeAxes = Axes.Both }, + hyperDashTrails = new Container { RelativeSizeAxes = Axes.Both, Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR }, + endGlowSprites = new Container { RelativeSizeAxes = Axes.Both, Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR }, + }; + } + + /// + /// Displays a single end-glow catcher sprite. + /// + public void DisplayEndGlow() + { + var endGlow = createTrailSprite(endGlowSprites); + + endGlow.MoveToOffset(new Vector2(0, -10), 1200, Easing.In); + endGlow.ScaleTo(endGlow.Scale * 0.95f).ScaleTo(endGlow.Scale * 1.2f, 1200, Easing.In); + endGlow.FadeOut(1200); + endGlow.Expire(true); + } + + private void displayTrail() + { + if (!catcher.Dashing && !catcher.HyperDashing) + { + DisplayTrail = false; + return; + } + + var sprite = createTrailSprite(catcher.HyperDashing ? hyperDashTrails : dashTrails); + + sprite.FadeTo(0.4f).FadeOut(800, Easing.OutQuint); + sprite.Expire(true); + + Scheduler.AddDelayed(displayTrail, catcher.HyperDashing ? 25 : 50); + } + + private CatcherTrailSprite createTrailSprite(Container target) + { + var texture = (catcher.CurrentDrawableCatcher as TextureAnimation)?.CurrentFrame ?? ((Sprite)catcher.CurrentDrawableCatcher).Texture; + + var sprite = new CatcherTrailSprite(texture) + { + Anchor = catcher.Anchor, + Scale = catcher.Scale, + Blending = BlendingParameters.Additive, + RelativePositionAxes = catcher.RelativePositionAxes, + Position = catcher.Position + }; + + target.Add(sprite); + + return sprite; + } + } +} From 5d3475a5ed32b45c6b3fca5fdd823fab0e2141f9 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 22 Apr 2020 05:12:51 +0300 Subject: [PATCH 1214/2376] Retrieve CatcherTrailDisplay for asserting colours set correctly --- .../TestSceneHyperDashColouring.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs index 347b71f3ff..0c8ade9018 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs @@ -113,6 +113,7 @@ namespace osu.Game.Rulesets.Catch.Tests private void checkHyperDashCatcherColour(ISkin skin, Color4 expectedCatcherColour, Color4? expectedEndGlowColour = null) { CatcherArea catcherArea = null; + CatcherTrailDisplay trails = null; AddStep("create hyper-dashing catcher", () => { @@ -123,6 +124,7 @@ namespace osu.Game.Rulesets.Catch.Tests Scale = new Vector2(4f), }, skin); + trails = catcherArea.OfType().Single(); catcherArea.MovableCatcher.SetHyperDashState(2); catcherArea.MovableCatcher.FinishTransforms(); }); @@ -132,8 +134,8 @@ namespace osu.Game.Rulesets.Catch.Tests ? catcherArea.MovableCatcher.Colour == Catcher.DEFAULT_CATCHER_HYPER_DASH_COLOUR : catcherArea.MovableCatcher.Colour == expectedCatcherColour); - AddAssert("catcher trails colours are correct", () => catcherArea.OfType>().Any(c => c.Colour == expectedCatcherColour)); - AddAssert("catcher end-glow colours are correct", () => catcherArea.OfType>().Any(c => c.Colour == (expectedEndGlowColour ?? expectedCatcherColour))); + AddAssert("catcher trails colours are correct", () => trails.HyperDashTrailsColour == expectedCatcherColour); + AddAssert("catcher end-glow colours are correct", () => trails.EndGlowSpritesColour == (expectedEndGlowColour ?? expectedCatcherColour)); AddStep("finish hyper-dashing", () => { From 2d4077e71313c8fdb2c03bddd65db9b0a9555ae5 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 22 Apr 2020 05:25:40 +0300 Subject: [PATCH 1215/2376] Reword special default hyper-dash colour constant a bit --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index e30988d8f7..08e438db56 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -29,7 +29,8 @@ namespace osu.Game.Rulesets.Catch.UI public static readonly Color4 DEFAULT_HYPER_DASH_COLOUR = Color4.Red; /// - /// The default colour used directly for this 's . + /// The default hyper-dash colour used directly for this + /// 's . /// /// /// This colour is only used when no skin overrides . From f841eb7e0607b19feb84b0cc896b027390128663 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 22 Apr 2020 07:27:15 +0300 Subject: [PATCH 1216/2376] Replace constructing a whole Catcher with static calculation methods --- .../Difficulty/CatchDifficultyCalculator.cs | 3 +- osu.Game.Rulesets.Catch/UI/Catcher.cs | 36 +++++++++++++++---- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 4d9dbbbc5f..d99325ff87 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -71,8 +71,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty protected override Skill[] CreateSkills(IBeatmap beatmap) { - using (var catcher = new Catcher(beatmap.BeatmapInfo.BaseDifficulty)) - halfCatcherWidth = catcher.CatchWidth * 0.5f; + halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) * 0.5f; return new Skill[] { diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 920d804e72..ee806b7b99 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -44,11 +44,6 @@ namespace osu.Game.Rulesets.Catch.UI /// private const float allowed_catch_range = 0.8f; - /// - /// Width of the area that can be used to attempt catches during gameplay. - /// - internal float CatchWidth => CatcherArea.CATCHER_SIZE * Math.Abs(Scale.X) * allowed_catch_range; - protected bool Dashing { get => dashing; @@ -79,6 +74,11 @@ namespace osu.Game.Rulesets.Catch.UI } } + /// + /// Width of the area that can be used to attempt catches during gameplay. + /// + private readonly float catchWidth; + private Container caughtFruit; private CatcherSprite catcherIdle; @@ -106,7 +106,9 @@ namespace osu.Game.Rulesets.Catch.UI Size = new Vector2(CatcherArea.CATCHER_SIZE); if (difficulty != null) - Scale = new Vector2(1.0f - 0.7f * (difficulty.CircleSize - 5) / 5); + Scale = CalculateScale(difficulty); + + catchWidth = CalculateCatchWidth(Scale); } [BackgroundDependencyLoader] @@ -139,6 +141,26 @@ namespace osu.Game.Rulesets.Catch.UI updateCatcher(); } + /// + /// Calculates the scale of the catcher based off the provided beatmap difficulty. + /// + internal static Vector2 CalculateScale(BeatmapDifficulty difficulty) + => new Vector2(1.0f - 0.7f * (difficulty.CircleSize - 5) / 5); + + /// + /// Calculates the width of the area used for attempting catches in gameplay. + /// + internal static float CalculateCatchWidth(Vector2 scale) + => CatcherArea.CATCHER_SIZE * Math.Abs(scale.X) * allowed_catch_range; + + /// + /// Calculates the width of the area used for attempting catches in gameplay. + /// + /// + /// + internal static float CalculateCatchWidth(BeatmapDifficulty difficulty) + => CalculateCatchWidth(CalculateScale(difficulty)); + /// /// Add a caught fruit to the catcher's stack. /// @@ -177,7 +199,7 @@ namespace osu.Game.Rulesets.Catch.UI /// Whether the catch is possible. public bool AttemptCatch(CatchHitObject fruit) { - var halfCatchWidth = CatchWidth * 0.5f; + var halfCatchWidth = catchWidth * 0.5f; // this stuff wil disappear once we move fruit to non-relative coordinate space in the future. var catchObjectPosition = fruit.X * CatchPlayfield.BASE_WIDTH; From fccb30e031a40e08533e5ce2f862cbe81d6fc504 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 22 Apr 2020 07:36:59 +0300 Subject: [PATCH 1217/2376] Adjust documents *whoops* --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index ee806b7b99..4dace76008 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -150,14 +150,14 @@ namespace osu.Game.Rulesets.Catch.UI /// /// Calculates the width of the area used for attempting catches in gameplay. /// + /// The scale of the catcher. internal static float CalculateCatchWidth(Vector2 scale) => CatcherArea.CATCHER_SIZE * Math.Abs(scale.X) * allowed_catch_range; /// /// Calculates the width of the area used for attempting catches in gameplay. /// - /// - /// + /// The beatmap difficulty. internal static float CalculateCatchWidth(BeatmapDifficulty difficulty) => CalculateCatchWidth(CalculateScale(difficulty)); From 883788dd5aec1b228328812fb4ea14043c299118 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 22 Apr 2020 07:37:49 +0300 Subject: [PATCH 1218/2376] Privatize externally-unused methods --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 4dace76008..daf9456919 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -106,7 +106,7 @@ namespace osu.Game.Rulesets.Catch.UI Size = new Vector2(CatcherArea.CATCHER_SIZE); if (difficulty != null) - Scale = CalculateScale(difficulty); + Scale = calculateScale(difficulty); catchWidth = CalculateCatchWidth(Scale); } @@ -144,7 +144,7 @@ namespace osu.Game.Rulesets.Catch.UI /// /// Calculates the scale of the catcher based off the provided beatmap difficulty. /// - internal static Vector2 CalculateScale(BeatmapDifficulty difficulty) + private static Vector2 calculateScale(BeatmapDifficulty difficulty) => new Vector2(1.0f - 0.7f * (difficulty.CircleSize - 5) / 5); /// @@ -159,7 +159,7 @@ namespace osu.Game.Rulesets.Catch.UI /// /// The beatmap difficulty. internal static float CalculateCatchWidth(BeatmapDifficulty difficulty) - => CalculateCatchWidth(CalculateScale(difficulty)); + => CalculateCatchWidth(calculateScale(difficulty)); /// /// Add a caught fruit to the catcher's stack. From 58af75ad576396258285b2722dfb07d1176f7889 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 22 Apr 2020 13:45:12 +0900 Subject: [PATCH 1219/2376] Add back missing line --- osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index cb583ac1a9..1c8116754f 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -237,6 +237,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps { StartTime = HitObject.StartTime, Duration = endTimeData.Duration, + Column = column, Samples = HitObject.Samples, NodeSamples = (HitObject as IHasRepeats)?.NodeSamples }); From 7cdc9a599c6ffb39c7635e63cbf8fa57bbcb2684 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 22 Apr 2020 16:27:07 +0900 Subject: [PATCH 1220/2376] Fix mania holds written as spinners --- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 4b760f1983..fc853a7a68 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -248,7 +248,7 @@ namespace osu.Game.Beatmaps.Formats writer.WriteLine(); } - private static LegacyHitObjectType getObjectType(HitObject hitObject) + private LegacyHitObjectType getObjectType(HitObject hitObject) { LegacyHitObjectType type = 0; @@ -267,7 +267,10 @@ namespace osu.Game.Beatmaps.Formats break; case IHasEndTime _: - type |= LegacyHitObjectType.Spinner; + if (beatmap.BeatmapInfo.RulesetID == 3) + type |= LegacyHitObjectType.Hold; + else + type |= LegacyHitObjectType.Spinner; break; default: From 3b805daa0b1fedac05e46e26fa6e77ba9cc1b4dc Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 22 Apr 2020 16:40:07 +0900 Subject: [PATCH 1221/2376] Fix hold note end time being written incorrectly --- .../Beatmaps/Formats/LegacyBeatmapEncoder.cs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index fc853a7a68..b5a8f1604c 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -240,8 +240,9 @@ namespace osu.Game.Beatmaps.Formats } else { - if (hitObject is IHasEndTime endTimeData) - writer.Write(FormattableString.Invariant($"{endTimeData.EndTime},")); + if (hitObject is IHasEndTime _) + addEndTimeData(writer, hitObject); + writer.Write(getSampleBank(hitObject.Samples)); } @@ -344,6 +345,20 @@ namespace osu.Game.Beatmaps.Formats } } + private void addEndTimeData(TextWriter writer, HitObject hitObject) + { + var endTimeData = (IHasEndTime)hitObject; + var type = getObjectType(hitObject); + + char suffix = ','; + + // Holds write the end time as if it's part of sample data. + if (type == LegacyHitObjectType.Hold) + suffix = ':'; + + writer.Write(FormattableString.Invariant($"{endTimeData.EndTime}{suffix}")); + } + private string getSampleBank(IList samples, bool banksOnly = false, bool zeroBanks = false) { LegacySampleBank normalBank = toLegacySampleBank(samples.SingleOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL)?.Bank); From 730b5ea1a986a6772520d8babd185ede3543c2ab Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 22 Apr 2020 10:40:21 +0300 Subject: [PATCH 1222/2376] Make the Catcher.Colour assertion read better --- .../TestSceneHyperDashColouring.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs index 0c8ade9018..1e1746efb3 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs @@ -130,9 +130,16 @@ namespace osu.Game.Rulesets.Catch.Tests }); AddAssert("catcher colour is correct", () => - expectedCatcherColour == Catcher.DEFAULT_HYPER_DASH_COLOUR - ? catcherArea.MovableCatcher.Colour == Catcher.DEFAULT_CATCHER_HYPER_DASH_COLOUR - : catcherArea.MovableCatcher.Colour == expectedCatcherColour); + { + var expected = expectedCatcherColour; + + if (expected == Catcher.DEFAULT_HYPER_DASH_COLOUR) + // The expected colour for Catcher.Colour is another colour + // for the default skin, assert with that instead. + expected = Catcher.DEFAULT_CATCHER_HYPER_DASH_COLOUR; + + return catcherArea.MovableCatcher.Colour == expected; + }); AddAssert("catcher trails colours are correct", () => trails.HyperDashTrailsColour == expectedCatcherColour); AddAssert("catcher end-glow colours are correct", () => trails.EndGlowSpritesColour == (expectedEndGlowColour ?? expectedCatcherColour)); From bffe6742e0cde53ce1ce4c6f76546e79eca41abd Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 22 Apr 2020 10:43:49 +0300 Subject: [PATCH 1223/2376] Replace finishing catcher transforms with until-true step --- osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs index 1e1746efb3..589bafe400 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs @@ -126,10 +126,9 @@ namespace osu.Game.Rulesets.Catch.Tests trails = catcherArea.OfType().Single(); catcherArea.MovableCatcher.SetHyperDashState(2); - catcherArea.MovableCatcher.FinishTransforms(); }); - AddAssert("catcher colour is correct", () => + AddUntilStep("catcher colour is correct", () => { var expected = expectedCatcherColour; From 44405d4771b76649e64fd7bd81e13cef17977b71 Mon Sep 17 00:00:00 2001 From: Tim Oliver Date: Wed, 22 Apr 2020 15:50:23 +0800 Subject: [PATCH 1224/2376] Moved result to load complete for flying hits --- .../Objects/Drawables/DrawableFlyingCentreHit.cs | 3 ++- .../Objects/Drawables/DrawableFlyingRimHit.cs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingCentreHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingCentreHit.cs index 826a4467f8..f70d940bd2 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingCentreHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingCentreHit.cs @@ -9,8 +9,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { public class DrawableFlyingCentreHit : DrawableCentreHit { - protected override void CheckForResult(bool userTriggered, double timeOffset) + protected override void LoadComplete() { + base.LoadComplete(); ApplyResult(r => r.Type = HitResult.Good); } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingRimHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingRimHit.cs index 4a6fed8302..9005dac653 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingRimHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingRimHit.cs @@ -9,8 +9,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { public class DrawableFlyingRimHit : DrawableRimHit { - protected override void CheckForResult(bool userTriggered, double timeOffset) + protected override void LoadComplete() { + base.LoadComplete(); ApplyResult(r => r.Type = HitResult.Good); } From 9c22d2f1dd167cd6b28ec2bdd7a3949219773209 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 22 Apr 2020 17:41:24 +0900 Subject: [PATCH 1225/2376] Use platform bindings for editor actions --- osu.Game/Screens/Edit/Editor.cs | 49 +++++++++++++++++---------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 9a1f450dc6..5665f4b25d 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -22,6 +22,7 @@ using osu.Game.Screens.Edit.Design; using osuTK.Input; using System.Collections.Generic; using osu.Framework; +using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Logging; using osu.Game.Beatmaps; @@ -37,7 +38,7 @@ using osu.Game.Users; namespace osu.Game.Screens.Edit { [Cached(typeof(IBeatSnapProvider))] - public class Editor : ScreenWithBeatmapBackground, IKeyBindingHandler, IBeatSnapProvider + public class Editor : ScreenWithBeatmapBackground, IKeyBindingHandler, IKeyBindingHandler, IBeatSnapProvider { public override float BackgroundParallaxAmount => 0.1f; @@ -230,6 +231,30 @@ namespace osu.Game.Screens.Edit clock.ProcessFrame(); } + public bool OnPressed(PlatformAction action) + { + switch (action.ActionType) + { + case PlatformActionType.Undo: + undo(); + return true; + + case PlatformActionType.Redo: + redo(); + return true; + + case PlatformActionType.Save: + saveBeatmap(); + return true; + } + + return false; + } + + public void OnReleased(PlatformAction action) + { + } + protected override bool OnKeyDown(KeyDownEvent e) { switch (e.Key) @@ -241,28 +266,6 @@ namespace osu.Game.Screens.Edit case Key.Right: seek(e, 1); return true; - - case Key.S: - if (e.ControlPressed) - { - saveBeatmap(); - return true; - } - - break; - - case Key.Z: - if (e.ControlPressed) - { - if (e.ShiftPressed) - redo(); - else - undo(); - - return true; - } - - break; } return base.OnKeyDown(e); From 8b0274fedd51ff2e897930786b4c04187f541daa Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 22 Apr 2020 17:55:50 +0900 Subject: [PATCH 1226/2376] Remove obsolete methods --- .../Objects/Drawables/DrawableHitObject.cs | 26 ------------------- 1 file changed, 26 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index e847dcec40..1316ac1156 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reflection; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -180,11 +179,6 @@ namespace osu.Game.Rulesets.Objects.Drawables private void apply(HitObject hitObject) { -#pragma warning disable 618 // can be removed 20200417 - if (GetType().GetMethod(nameof(AddNested), BindingFlags.NonPublic | BindingFlags.Instance)?.DeclaringType != typeof(DrawableHitObject)) - return; -#pragma warning restore 618 - if (nestedHitObjects.IsValueCreated) { nestedHitObjects.Value.Clear(); @@ -194,8 +188,6 @@ namespace osu.Game.Rulesets.Objects.Drawables foreach (var h in hitObject.NestedHitObjects) { var drawableNested = CreateNestedHitObject(h) ?? throw new InvalidOperationException($"{nameof(CreateNestedHitObject)} returned null for {h.GetType().ReadableName()}."); - - addNested(drawableNested); AddNestedHitObject(drawableNested); } } @@ -208,13 +200,6 @@ namespace osu.Game.Rulesets.Objects.Drawables { } - /// - /// Adds a nested . This should not be used except for legacy nested usages. - /// - /// - [Obsolete("Use AddNestedHitObject() / ClearNestedHitObjects() / CreateNestedHitObject() instead.")] // can be removed 20200417 - protected virtual void AddNested(DrawableHitObject h) => addNested(h); - /// /// Invoked by the base to remove all previously-added nested s. /// @@ -229,17 +214,6 @@ namespace osu.Game.Rulesets.Objects.Drawables /// The drawable representation for . protected virtual DrawableHitObject CreateNestedHitObject(HitObject hitObject) => null; - private void addNested(DrawableHitObject hitObject) - { - // Todo: Exists for legacy purposes, can be removed 20200417 - - hitObject.OnNewResult += (d, r) => OnNewResult?.Invoke(d, r); - hitObject.OnRevertResult += (d, r) => OnRevertResult?.Invoke(d, r); - hitObject.ApplyCustomUpdateState += (d, j) => ApplyCustomUpdateState?.Invoke(d, j); - - nestedHitObjects.Value.Add(hitObject); - } - #region State / Transform Management /// From e1142b424d2f140dbb61521920c33336d5f0992e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 22 Apr 2020 18:14:21 +0900 Subject: [PATCH 1227/2376] Fix test failures --- .../Editor/TestSceneEditorChangeStates.cs | 41 ++++--------------- osu.Game/Screens/Edit/Editor.cs | 12 +++--- 2 files changed, 15 insertions(+), 38 deletions(-) diff --git a/osu.Game.Tests/Visual/Editor/TestSceneEditorChangeStates.cs b/osu.Game.Tests/Visual/Editor/TestSceneEditorChangeStates.cs index dd1b6cf6aa..efc2a6f552 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneEditorChangeStates.cs +++ b/osu.Game.Tests/Visual/Editor/TestSceneEditorChangeStates.cs @@ -10,24 +10,22 @@ using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Compose.Components.Timeline; -using osuTK.Input; namespace osu.Game.Tests.Visual.Editor { public class TestSceneEditorChangeStates : ScreenTestScene { private EditorBeatmap editorBeatmap; + private TestEditor editor; public override void SetUpSteps() { base.SetUpSteps(); - Screens.Edit.Editor editor = null; - AddStep("load editor", () => { Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); - LoadScreen(editor = new Screens.Edit.Editor()); + LoadScreen(editor = new TestEditor()); }); AddUntilStep("wait for editor to load", () => editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true @@ -160,36 +158,15 @@ namespace osu.Game.Tests.Visual.Editor AddAssert("no hitobject added", () => addedObject == null); } - private void addUndoSteps() + private void addUndoSteps() => AddStep("undo", () => editor.Undo()); + + private void addRedoSteps() => AddStep("redo", () => editor.Redo()); + + private class TestEditor : Screens.Edit.Editor { - AddStep("press undo", () => - { - InputManager.PressKey(Key.LControl); - InputManager.PressKey(Key.Z); - }); + public new void Undo() => base.Undo(); - AddStep("release keys", () => - { - InputManager.ReleaseKey(Key.LControl); - InputManager.ReleaseKey(Key.Z); - }); - } - - private void addRedoSteps() - { - AddStep("press redo", () => - { - InputManager.PressKey(Key.LControl); - InputManager.PressKey(Key.LShift); - InputManager.PressKey(Key.Z); - }); - - AddStep("release keys", () => - { - InputManager.ReleaseKey(Key.LControl); - InputManager.ReleaseKey(Key.LShift); - InputManager.ReleaseKey(Key.Z); - }); + public new void Redo() => base.Redo(); } } } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 5665f4b25d..54e4af94a4 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -158,8 +158,8 @@ namespace osu.Game.Screens.Edit { Items = new[] { - undoMenuItem = new EditorMenuItem("Undo", MenuItemType.Standard, undo), - redoMenuItem = new EditorMenuItem("Redo", MenuItemType.Standard, redo) + undoMenuItem = new EditorMenuItem("Undo", MenuItemType.Standard, Undo), + redoMenuItem = new EditorMenuItem("Redo", MenuItemType.Standard, Redo) } } } @@ -236,11 +236,11 @@ namespace osu.Game.Screens.Edit switch (action.ActionType) { case PlatformActionType.Undo: - undo(); + Undo(); return true; case PlatformActionType.Redo: - redo(); + Redo(); return true; case PlatformActionType.Save: @@ -329,9 +329,9 @@ namespace osu.Game.Screens.Edit return base.OnExiting(next); } - private void undo() => changeHandler.RestoreState(-1); + protected void Undo() => changeHandler.RestoreState(-1); - private void redo() => changeHandler.RestoreState(1); + protected void Redo() => changeHandler.RestoreState(1); private void resetTrack(bool seekToStart = false) { From 93151f761215c135ae625241532fa6b899e73747 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 22 Apr 2020 18:32:59 +0900 Subject: [PATCH 1228/2376] Add back necessary events + addition to list --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 1316ac1156..0047142cbd 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -188,6 +188,12 @@ namespace osu.Game.Rulesets.Objects.Drawables foreach (var h in hitObject.NestedHitObjects) { var drawableNested = CreateNestedHitObject(h) ?? throw new InvalidOperationException($"{nameof(CreateNestedHitObject)} returned null for {h.GetType().ReadableName()}."); + + drawableNested.OnNewResult += (d, r) => OnNewResult?.Invoke(d, r); + drawableNested.OnRevertResult += (d, r) => OnRevertResult?.Invoke(d, r); + drawableNested.ApplyCustomUpdateState += (d, j) => ApplyCustomUpdateState?.Invoke(d, j); + + nestedHitObjects.Value.Add(drawableNested); AddNestedHitObject(drawableNested); } } From 0a34fddcc3cfda42bc40e393374b7c0decd824d8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 22 Apr 2020 19:38:27 +0900 Subject: [PATCH 1229/2376] Fix TestBeatmap not setting appropriate ruleset ID --- osu.Game/Tests/Beatmaps/TestBeatmap.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Tests/Beatmaps/TestBeatmap.cs b/osu.Game/Tests/Beatmaps/TestBeatmap.cs index 96e3c037a3..a7c84bf692 100644 --- a/osu.Game/Tests/Beatmaps/TestBeatmap.cs +++ b/osu.Game/Tests/Beatmaps/TestBeatmap.cs @@ -23,6 +23,7 @@ namespace osu.Game.Tests.Beatmaps HitObjects = baseBeatmap.HitObjects; BeatmapInfo.Ruleset = ruleset; + BeatmapInfo.RulesetID = ruleset.ID ?? 0; BeatmapInfo.BeatmapSet.Metadata = BeatmapInfo.Metadata; BeatmapInfo.BeatmapSet.Beatmaps = new List { BeatmapInfo }; BeatmapInfo.BeatmapSet.OnlineInfo = new BeatmapSetOnlineInfo From 08982e0e00da8fe144793a8b7fa49713fdf98145 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 22 Apr 2020 19:49:21 +0900 Subject: [PATCH 1230/2376] Ensure editor tests wait for load to complete --- .../Editor/TestSceneEditorChangeStates.cs | 21 +++++++------------ osu.Game/Tests/Visual/EditorTestScene.cs | 10 ++++++++- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/osu.Game.Tests/Visual/Editor/TestSceneEditorChangeStates.cs b/osu.Game.Tests/Visual/Editor/TestSceneEditorChangeStates.cs index dd1b6cf6aa..c68015d1a2 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneEditorChangeStates.cs +++ b/osu.Game.Tests/Visual/Editor/TestSceneEditorChangeStates.cs @@ -4,35 +4,28 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Testing; -using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit; -using osu.Game.Screens.Edit.Compose.Components.Timeline; using osuTK.Input; namespace osu.Game.Tests.Visual.Editor { - public class TestSceneEditorChangeStates : ScreenTestScene + public class TestSceneEditorChangeStates : EditorTestScene { + public TestSceneEditorChangeStates() + : base(new OsuRuleset()) + { + } + private EditorBeatmap editorBeatmap; public override void SetUpSteps() { base.SetUpSteps(); - Screens.Edit.Editor editor = null; - - AddStep("load editor", () => - { - Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); - LoadScreen(editor = new Screens.Edit.Editor()); - }); - - AddUntilStep("wait for editor to load", () => editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true - && editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); - AddStep("get beatmap", () => editorBeatmap = editor.ChildrenOfType().Single()); + AddStep("get beatmap", () => editorBeatmap = Editor.ChildrenOfType().Single()); } [Test] diff --git a/osu.Game/Tests/Visual/EditorTestScene.cs b/osu.Game/Tests/Visual/EditorTestScene.cs index 80bc3bdb87..88e50d4858 100644 --- a/osu.Game/Tests/Visual/EditorTestScene.cs +++ b/osu.Game/Tests/Visual/EditorTestScene.cs @@ -3,9 +3,13 @@ using System; using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Testing; using osu.Game.Rulesets; +using osu.Game.Rulesets.Edit; using osu.Game.Screens.Edit; +using osu.Game.Screens.Edit.Compose.Components.Timeline; namespace osu.Game.Tests.Visual { @@ -13,6 +17,8 @@ namespace osu.Game.Tests.Visual { public override IReadOnlyList RequiredTypes => new[] { typeof(Editor), typeof(EditorScreen) }; + protected Editor Editor { get; private set; } + private readonly Ruleset ruleset; protected EditorTestScene(Ruleset ruleset) @@ -30,7 +36,9 @@ namespace osu.Game.Tests.Visual { base.SetUpSteps(); - AddStep("Load editor", () => LoadScreen(new Editor())); + AddStep("load editor", () => LoadScreen(Editor = new Editor())); + AddUntilStep("wait for editor to load", () => Editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true + && Editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); } } } From 617d27ace9620128db73f1da44278d2111d801a0 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Wed, 22 Apr 2020 15:19:29 +0200 Subject: [PATCH 1231/2376] Restart branch --- .../Skinning/TaikoLegacySkinTransformer.cs | 12 ++- .../TaikoSkinComponents.cs | 1 + .../UI/DefaultTaikoDonTextureAnimation.cs | 60 ++++++++++++++ .../UI/DrawableTaikoCharacter.cs | 80 +++++++++++++++++++ .../UI/TaikoDonAnimationState.cs | 13 +++ osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 39 +++++++-- 6 files changed, 196 insertions(+), 9 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/UI/DefaultTaikoDonTextureAnimation.cs create mode 100644 osu.Game.Rulesets.Taiko/UI/DrawableTaikoCharacter.cs create mode 100644 osu.Game.Rulesets.Taiko/UI/TaikoDonAnimationState.cs diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs index 78eec94590..722dbf2671 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs @@ -7,6 +7,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Textures; using osu.Game.Audio; +using osu.Game.Rulesets.Taiko.UI; using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.Skinning @@ -32,9 +33,16 @@ namespace osu.Game.Rulesets.Taiko.Skinning return new LegacyInputDrum(); return null; - } - return source.GetDrawableComponent(component); + case TaikoSkinComponents.TaikoDon: + if (GetTexture("pippidonclear0") != null) + return new DrawableTaikoCharacter(); + + return null; + + default: + return source.GetDrawableComponent(component); + } } public Texture GetTexture(string componentName) => source.GetTexture(componentName); diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs index 6d4581db80..a68cc54efa 100644 --- a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs +++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs @@ -6,5 +6,6 @@ namespace osu.Game.Rulesets.Taiko public enum TaikoSkinComponents { InputDrum, + TaikoDon } } diff --git a/osu.Game.Rulesets.Taiko/UI/DefaultTaikoDonTextureAnimation.cs b/osu.Game.Rulesets.Taiko/UI/DefaultTaikoDonTextureAnimation.cs new file mode 100644 index 0000000000..1fefed953d --- /dev/null +++ b/osu.Game.Rulesets.Taiko/UI/DefaultTaikoDonTextureAnimation.cs @@ -0,0 +1,60 @@ +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Animations; +using osu.Framework.Graphics.Textures; +using osu.Game.Skinning; + +namespace osu.Game.Rulesets.Taiko.UI +{ + public sealed class DefaultTaikoDonTextureAnimation : TextureAnimation + { + private readonly TaikoDonAnimationState _state; + private int currentFrame; + + public DefaultTaikoDonTextureAnimation(TaikoDonAnimationState state) : base(false) + { + _state = state; + this.Stop(); + + Origin = Anchor.BottomLeft; + Anchor = Anchor.BottomLeft; + } + + [BackgroundDependencyLoader] + private void load(ISkinSource skin) + { + for (int i = 0;; i++) + { + var textureName = $"pippidon{_getStateString(_state)}{i}"; + Texture texture = skin.GetTexture(textureName); + + if (texture == null) + break; + + AddFrame(texture); + } + } + + /// + /// Advances the current frame by one. + /// + public void Move() + { + if (FrameCount <= currentFrame) + currentFrame = 0; + + GotoFrame(currentFrame); + + currentFrame++; + } + + private string _getStateString(TaikoDonAnimationState state) => state switch + { + TaikoDonAnimationState.Clear => "clear", + TaikoDonAnimationState.Fail => "fail", + TaikoDonAnimationState.Idle => "idle", + TaikoDonAnimationState.Kiai => "kiai", + _ => null + }; + } +} diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoCharacter.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoCharacter.cs new file mode 100644 index 0000000000..897670d049 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoCharacter.cs @@ -0,0 +1,80 @@ +using osu.Framework.Allocation; +using osu.Framework.Audio.Track; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Textures; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics.Containers; +using osuTK; + +namespace osu.Game.Rulesets.Taiko.UI +{ + public sealed class DrawableTaikoCharacter : BeatSyncedContainer + { + private static DefaultTaikoDonTextureAnimation idleDrawable, clearDrawable, kiaiDrawable, failDrawable; + + private TaikoDonAnimationState state; + + public DrawableTaikoCharacter() + { + RelativeSizeAxes = Axes.Both; + //Size = new Vector2(1f, 2.5f); + //Origin = Anchor.BottomLeft; + var xd = new Vector2(1); + } + + private DefaultTaikoDonTextureAnimation getStateDrawable() => State switch + { + TaikoDonAnimationState.Idle => idleDrawable, + TaikoDonAnimationState.Clear => clearDrawable, + TaikoDonAnimationState.Kiai => kiaiDrawable, + TaikoDonAnimationState.Fail => failDrawable, + _ => null + }; + + public TaikoDonAnimationState State + { + get => state; + set + { + state = value; + + foreach (var child in InternalChildren) + child.Hide(); + + getStateDrawable().Show(); + } + } + + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + InternalChildren = new[] + { + idleDrawable = new DefaultTaikoDonTextureAnimation(TaikoDonAnimationState.Idle), + clearDrawable = new DefaultTaikoDonTextureAnimation(TaikoDonAnimationState.Clear), + kiaiDrawable = new DefaultTaikoDonTextureAnimation(TaikoDonAnimationState.Kiai), + failDrawable = new DefaultTaikoDonTextureAnimation(TaikoDonAnimationState.Fail), + }; + + // sets the state, to make sure we have the correct sprite loaded and set. + State = TaikoDonAnimationState.Idle; + } + + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) + { + base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); + + getStateDrawable().Move(); + + //var signature = timingPoint.TimeSignature == TimeSignatures.SimpleQuadruple ? 4 : 3; + //var length = timingPoint.BeatLength; + //var rate = 1000d / length; + //adjustableClock.Rate = rate; + // + //// Start animating on the first beat. + //if (beatIndex < 1) + // adjustableClock.Start(); + // Logger.GetLogger(LoggingTarget.Information).Add($"Length = {length}ms | Rate = {rate}x | BPM = {timingPoint.BPM} / {signature}"); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoDonAnimationState.cs b/osu.Game.Rulesets.Taiko/UI/TaikoDonAnimationState.cs new file mode 100644 index 0000000000..773710ee7e --- /dev/null +++ b/osu.Game.Rulesets.Taiko/UI/TaikoDonAnimationState.cs @@ -0,0 +1,13 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Rulesets.Taiko.UI +{ + public enum TaikoDonAnimationState + { + Idle, + Clear, + Kiai, + Fail + } +} diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index bde9085c23..d5cabb8662 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -19,6 +19,9 @@ using osu.Game.Rulesets.Taiko.Judgements; using osu.Game.Rulesets.Taiko.Objects; using osuTK; using osuTK.Graphics; +using osu.Game.Rulesets.Scoring; +using osu.Framework.Bindables; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.UI { @@ -53,6 +56,10 @@ namespace osu.Game.Rulesets.Taiko.UI private readonly Box overlayBackground; private readonly Box background; + private readonly SkinnableDrawable characterDrawable; + + private Bindable frameTime = new Bindable(100); + public TaikoPlayfield(ControlPointInfo controlPoints) { InternalChildren = new Drawable[] @@ -186,6 +193,12 @@ namespace osu.Game.Rulesets.Taiko.UI { Name = "Top level hit objects", RelativeSizeAxes = Axes.Both, + }, + characterDrawable = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.TaikoDon), _ => new Container(), confineMode: ConfineMode.ScaleToFit) + { + Origin = Anchor.BottomLeft, + Anchor = Anchor.TopLeft, + RelativePositionAxes = Axes.None } }; } @@ -254,16 +267,28 @@ namespace osu.Game.Rulesets.Taiko.UI break; } - } - private class ProxyContainer : LifetimeManagementContainer - { - public new MarginPadding Padding + if (characterDrawable.Drawable is DrawableTaikoCharacter character) { - set => base.Padding = value; + if (result.Type == HitResult.Miss && result.Judgement.AffectsCombo) + { + character.State = TaikoDonAnimationState.Fail; + } + else + { + character.State = judgedObject.HitObject.Kiai ? TaikoDonAnimationState.Kiai : TaikoDonAnimationState.Idle; + } } - - public void Add(Drawable proxy) => AddInternal(proxy); } } + + class ProxyContainer : LifetimeManagementContainer + { + public new MarginPadding Padding + { + set => base.Padding = value; + } + + public void Add(Drawable proxy) => AddInternal(proxy); + } } From 26779a57b4b211ab86e9bdd41d194c1d7271e5e5 Mon Sep 17 00:00:00 2001 From: Tim Oliver Date: Wed, 22 Apr 2020 22:49:30 +0800 Subject: [PATCH 1232/2376] Exposed public ability to unproxy content --- .../Objects/Drawables/DrawableTaikoHitObject.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs index 5f892dd2fa..b2f9086184 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs @@ -76,6 +76,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables /// public Drawable CreateProxiedContent() => proxiedContent.CreateProxy(); + public void RemoveProxiedContent() => UnproxyContent(); + public abstract bool OnPressed(TaikoAction action); public virtual void OnReleased(TaikoAction action) From 2600518b1bfdfc39809dc5221a7b2e783e7fd0bb Mon Sep 17 00:00:00 2001 From: Tim Oliver Date: Wed, 22 Apr 2020 22:50:00 +0800 Subject: [PATCH 1233/2376] Moved drumroll container and removed rewound notes --- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 31 ++++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index e947795fe5..70839ce806 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -119,7 +119,14 @@ namespace osu.Game.Rulesets.Taiko.UI RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Left = HIT_TARGET_OFFSET }, Masking = true, - Child = HitObjectContainer + Children = new Drawable[] + { + HitObjectContainer, + drumRollHitContainer = new ScrollingHitObjectContainer + { + Name = "Drumroll hit" + } + } }, kiaiExplosionContainer = new Container { @@ -135,14 +142,6 @@ namespace osu.Game.Rulesets.Taiko.UI RelativeSizeAxes = Axes.Y, Margin = new MarginPadding { Left = HIT_TARGET_OFFSET }, Blending = BlendingParameters.Additive - }, - drumRollHitContainer = new ScrollingHitObjectContainer - { - Name = "Drumroll hit", - RelativeSizeAxes = Axes.Both, - FillMode = FillMode.Stretch, - Margin = new MarginPadding { Left = HIT_TARGET_OFFSET }, - Width = 1.0f } } }, @@ -282,6 +281,20 @@ namespace osu.Game.Rulesets.Taiko.UI } } + protected override void Update() + { + base.Update(); + + if (Time.Elapsed < 0) + { + foreach (DrawableHit taikoHit in drumRollHitContainer.Objects) + { + taikoHit.RemoveProxiedContent(); + drumRollHitContainer.Remove(taikoHit); + } + } + } + private class ProxyContainer : LifetimeManagementContainer { public new MarginPadding Padding From 40f11ed15c944dd0007a5d637a1215c2ac33c902 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 23 Apr 2020 10:37:05 +0900 Subject: [PATCH 1234/2376] Resolve broken test scene --- .../Visual/Editor/TestSceneEditorChangeStates.cs | 7 ++++--- osu.Game/Tests/Visual/EditorTestScene.cs | 4 +++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Editor/TestSceneEditorChangeStates.cs b/osu.Game.Tests/Visual/Editor/TestSceneEditorChangeStates.cs index 7b4747592a..efdcc6f78b 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneEditorChangeStates.cs +++ b/osu.Game.Tests/Visual/Editor/TestSceneEditorChangeStates.cs @@ -19,7 +19,6 @@ namespace osu.Game.Tests.Visual.Editor } private EditorBeatmap editorBeatmap; - private TestEditor editor; public override void SetUpSteps() { @@ -153,9 +152,11 @@ namespace osu.Game.Tests.Visual.Editor AddAssert("no hitobject added", () => addedObject == null); } - private void addUndoSteps() => AddStep("undo", () => editor.Undo()); + private void addUndoSteps() => AddStep("undo", () => ((TestEditor)Editor).Undo()); - private void addRedoSteps() => AddStep("redo", () => editor.Redo()); + private void addRedoSteps() => AddStep("redo", () => ((TestEditor)Editor).Redo()); + + protected override Screens.Edit.Editor CreateEditor() => new TestEditor(); private class TestEditor : Screens.Edit.Editor { diff --git a/osu.Game/Tests/Visual/EditorTestScene.cs b/osu.Game/Tests/Visual/EditorTestScene.cs index 88e50d4858..caf2bc0ff1 100644 --- a/osu.Game/Tests/Visual/EditorTestScene.cs +++ b/osu.Game/Tests/Visual/EditorTestScene.cs @@ -36,9 +36,11 @@ namespace osu.Game.Tests.Visual { base.SetUpSteps(); - AddStep("load editor", () => LoadScreen(Editor = new Editor())); + AddStep("load editor", () => LoadScreen(Editor = CreateEditor())); AddUntilStep("wait for editor to load", () => Editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true && Editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); } + + protected virtual Editor CreateEditor() => new Editor(); } } From 86ef73aa27934b377c5a931c0e66b2f96cc0fc9a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 23 Apr 2020 11:16:59 +0900 Subject: [PATCH 1235/2376] Implement HitObjectContainer.Clear() --- osu.Game/Rulesets/UI/HitObjectContainer.cs | 9 +++++++++ .../Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs | 8 ++++++++ 2 files changed, 17 insertions(+) diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index dea981c3ad..f4f66f1272 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -43,6 +43,15 @@ namespace osu.Game.Rulesets.UI return true; } + public virtual void Clear(bool disposeChildren = true) + { + ClearInternal(disposeChildren); + + foreach (var kvp in startTimeMap) + kvp.Value.bindable.UnbindAll(); + startTimeMap.Clear(); + } + public int IndexOf(DrawableHitObject hitObject) => IndexOfInternal(hitObject); private void onStartTimeChanged(DrawableHitObject hitObject) diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index 108f98d5fc..57f58be55a 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -58,6 +58,14 @@ namespace osu.Game.Rulesets.UI.Scrolling return result; } + public override void Clear(bool disposeChildren = true) + { + base.Clear(disposeChildren); + + initialStateCache.Invalidate(); + hitObjectInitialStateCache.Clear(); + } + private float scrollLength; protected override void Update() From 6df45164face4fb8b31e3cb7a9cfaf9d97003ec7 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 23 Apr 2020 11:17:16 +0900 Subject: [PATCH 1236/2376] Expose direction from scrolling test container --- osu.Game/Tests/Visual/ScrollingTestContainer.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Tests/Visual/ScrollingTestContainer.cs b/osu.Game/Tests/Visual/ScrollingTestContainer.cs index 3b741fcf1d..994f23577d 100644 --- a/osu.Game/Tests/Visual/ScrollingTestContainer.cs +++ b/osu.Game/Tests/Visual/ScrollingTestContainer.cs @@ -30,6 +30,11 @@ namespace osu.Game.Tests.Visual set => scrollingInfo.TimeRange.Value = value; } + public ScrollingDirection Direction + { + set => scrollingInfo.Direction.Value = value; + } + public IScrollingInfo ScrollingInfo => scrollingInfo; [Cached(Type = typeof(IScrollingInfo))] From 6376128bde406d3340feb7f9dd946d8bdae77225 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 23 Apr 2020 05:33:28 +0300 Subject: [PATCH 1237/2376] Remove unnecessary using directive --- osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 57930a21fa..d99325ff87 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Difficulty.Preprocessing; using osu.Game.Rulesets.Catch.Difficulty.Skills; From ca56e6c0d2a1fb09749bd8f4070cfeaed947048c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Apr 2020 12:10:26 +0900 Subject: [PATCH 1238/2376] Rename taiko HitTarget classes to avoid conflict with mania --- .../Skinning/TestSceneTaikoPlayfield.cs | 4 ++-- .../{LegacyHitTarget.cs => TaikoLegacyHitTarget.cs} | 8 +++++--- .../Skinning/TaikoLegacySkinTransformer.cs | 2 +- .../UI/{HitTarget.cs => TaikoHitTarget.cs} | 6 ++---- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) rename osu.Game.Rulesets.Taiko/Skinning/{LegacyHitTarget.cs => TaikoLegacyHitTarget.cs} (80%) rename osu.Game.Rulesets.Taiko/UI/{HitTarget.cs => TaikoHitTarget.cs} (95%) diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs index 730eed0e0f..cdd6f9ab19 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs @@ -18,8 +18,8 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning { public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] { - typeof(HitTarget), - typeof(LegacyHitTarget), + typeof(TaikoHitTarget), + typeof(TaikoLegacyHitTarget), }).ToList(); [Cached(typeof(IScrollingInfo))] diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyHitTarget.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacyHitTarget.cs similarity index 80% rename from osu.Game.Rulesets.Taiko/Skinning/LegacyHitTarget.cs rename to osu.Game.Rulesets.Taiko/Skinning/TaikoLegacyHitTarget.cs index 51aea9b9ab..b80f273d24 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyHitTarget.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacyHitTarget.cs @@ -10,7 +10,7 @@ using osuTK; namespace osu.Game.Rulesets.Taiko.Skinning { - public class LegacyHitTarget : CompositeDrawable + public class TaikoLegacyHitTarget : CompositeDrawable { [BackgroundDependencyLoader] private void load(ISkinSource skin) @@ -22,7 +22,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning new Sprite { Texture = skin.GetTexture("approachcircle"), - Scale = new Vector2(0.73f), + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.73f) * 0.625f, Alpha = 0.7f, Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -30,7 +31,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning new Sprite { Texture = skin.GetTexture("taikobigcircle"), - Scale = new Vector2(0.7f), + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.7f) * 0.625f, Alpha = 0.5f, Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs index 6b59718173..eaa69283e4 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning case TaikoSkinComponents.HitTarget: if (GetTexture("taikobigcircle") != null) - return new LegacyHitTarget(); + return new TaikoLegacyHitTarget(); return null; } diff --git a/osu.Game.Rulesets.Taiko/UI/HitTarget.cs b/osu.Game.Rulesets.Taiko/UI/TaikoHitTarget.cs similarity index 95% rename from osu.Game.Rulesets.Taiko/UI/HitTarget.cs rename to osu.Game.Rulesets.Taiko/UI/TaikoHitTarget.cs index 88886508af..7de1593ab6 100644 --- a/osu.Game.Rulesets.Taiko/UI/HitTarget.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoHitTarget.cs @@ -13,14 +13,14 @@ namespace osu.Game.Rulesets.Taiko.UI /// /// A component that is displayed at the hit position in the taiko playfield. /// - internal class HitTarget : Container + internal class TaikoHitTarget : Container { /// /// Thickness of all drawn line pieces. /// private const float border_thickness = 2.5f; - public HitTarget() + public TaikoHitTarget() { RelativeSizeAxes = Axes.Both; @@ -41,7 +41,6 @@ namespace osu.Game.Rulesets.Taiko.UI Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - FillMode = FillMode.Fit, Scale = new Vector2(TaikoHitObject.DEFAULT_STRONG_SIZE), Masking = true, BorderColour = Color4.White, @@ -63,7 +62,6 @@ namespace osu.Game.Rulesets.Taiko.UI Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - FillMode = FillMode.Fit, Scale = new Vector2(TaikoHitObject.DEFAULT_SIZE), Masking = true, BorderColour = Color4.White, diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 375d9995c0..69c9003434 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -99,7 +99,7 @@ namespace osu.Game.Rulesets.Taiko.UI FillMode = FillMode.Fit, Blending = BlendingParameters.Additive, }, - HitTarget = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.HitTarget), _ => new HitTarget()) + HitTarget = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.HitTarget), _ => new TaikoHitTarget()) { Anchor = Anchor.CentreLeft, Origin = Anchor.Centre, From 8d5732aabd87662ec038358e63b630e23153605b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 23 Apr 2020 12:17:11 +0900 Subject: [PATCH 1239/2376] Make placements happen on mouse down --- .../TestSceneSliderPlacementBlueprint.cs | 17 ++++++++++++++++ .../HitCircles/HitCirclePlacementBlueprint.cs | 20 +++++++++++++------ .../Components/PathControlPointPiece.cs | 4 +++- .../Sliders/SliderPlacementBlueprint.cs | 10 +++++----- 4 files changed, 39 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderPlacementBlueprint.cs index 9fc479953e..fe9973f4d8 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderPlacementBlueprint.cs @@ -259,6 +259,23 @@ namespace osu.Game.Rulesets.Osu.Tests assertControlPointType(2, PathType.PerfectCurve); } + [Test] + public void TestBeginPlacementWithoutReleasingMouse() + { + addMovementStep(new Vector2(200)); + AddStep("press left button", () => InputManager.PressButton(MouseButton.Left)); + + addMovementStep(new Vector2(400, 200)); + AddStep("release left button", () => InputManager.ReleaseButton(MouseButton.Left)); + + addClickStep(MouseButton.Right); + + assertPlaced(true); + assertLength(200); + assertControlPointCount(2); + assertControlPointType(0, PathType.Linear); + } + private void addMovementStep(Vector2 position) => AddStep($"move mouse to {position}", () => InputManager.MoveMouseTo(InputManager.ToScreenSpace(position))); private void addClickStep(MouseButton button) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs index 407f5f540e..2f400160b8 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs @@ -6,6 +6,7 @@ using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; using osu.Game.Rulesets.Osu.Objects; using osuTK; +using osuTK.Input; namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles { @@ -28,16 +29,23 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles circlePiece.UpdateFrom(HitObject); } - protected override bool OnClick(ClickEvent e) + protected override bool OnMouseDown(MouseDownEvent e) { - EndPlacement(true); - return true; + if (e.Button == MouseButton.Left) + { + BeginPlacement(); + return true; + } + + return base.OnMouseDown(e); } - public override void UpdatePosition(Vector2 screenSpacePosition) + protected override void OnMouseUp(MouseUpEvent e) { - BeginPlacement(); - HitObject.Position = ToLocalSpace(screenSpacePosition); + if (e.Button == MouseButton.Left) + EndPlacement(true); } + + public override void UpdatePosition(Vector2 screenSpacePosition) => HitObject.Position = ToLocalSpace(screenSpacePosition); } } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs index fed149b5c5..d0c1eb5317 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs @@ -28,7 +28,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components public Action RequestSelection; public readonly BindableBool IsSelected = new BindableBool(); - public readonly PathControlPoint ControlPoint; private readonly Slider slider; @@ -146,6 +145,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components protected override bool OnDragStart(DragStartEvent e) { + if (RequestSelection == null) + return false; + if (e.Button == MouseButton.Left) { changeHandler?.BeginChange(); diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 9af972dbce..ac30f5a762 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -82,8 +82,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders } } - protected override bool OnClick(ClickEvent e) + protected override bool OnMouseDown(MouseDownEvent e) { + if (e.Button != MouseButton.Left) + return base.OnMouseDown(e); + switch (state) { case PlacementState.Initial: @@ -91,9 +94,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders break; case PlacementState.Body: - if (e.Button != MouseButton.Left) - break; - if (canPlaceNewControlPoint(out var lastPoint)) { // Place a new point by detatching the current cursor. @@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders currentSegmentLength = 1; } - return true; + break; } return true; From 58bf288595a2fc16f3491c614480bd1d997d0afe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Apr 2020 12:17:48 +0900 Subject: [PATCH 1240/2376] Remove DrawableHit's custom sizing logic Turns out this was unnecessary and never actually being used. --- osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index 9333e5f144..fe9a89f2be 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -92,8 +92,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables // The input manager processes all input prior to us updating, so this is the perfect time // for us to remove the extra press blocking, before input is handled in the next frame pressHandledThisFrame = false; - - Size = BaseSize * Parent.RelativeChildSize; } protected override void UpdateStateTransforms(ArmedState state) From c59096a9419c3087d35137350bc480a90972d3c6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 23 Apr 2020 12:05:34 +0900 Subject: [PATCH 1241/2376] Fix note placement --- .../ManiaPlacementBlueprintTestScene.cs | 2 - .../TestSceneNotePlacementBlueprint.cs | 42 +++++++++++++++++++ .../Blueprints/ManiaPlacementBlueprint.cs | 2 +- .../Edit/Blueprints/NotePlacementBlueprint.cs | 9 ++++ 4 files changed, 52 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs index afde1c9521..aac77c9c1c 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs @@ -41,8 +41,6 @@ namespace osu.Game.Rulesets.Mania.Tests AccentColour = Color4.OrangeRed, Clock = new FramedClock(new StopwatchClock()), // No scroll }); - - AddStep("change direction", () => ((ScrollingTestContainer)HitObjectContainer).Flip()); } protected override Container CreateHitObjectContainer() => new ScrollingTestContainer(ScrollingDirection.Down) { RelativeSizeAxes = Axes.Both }; diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneNotePlacementBlueprint.cs index d7b539a2a0..2d97e61aa5 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneNotePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneNotePlacementBlueprint.cs @@ -1,17 +1,59 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; +using NUnit.Framework; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Testing; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Edit.Blueprints; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects.Drawables; +using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.UI; +using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Tests.Visual; +using osuTK; +using osuTK.Input; namespace osu.Game.Rulesets.Mania.Tests { public class TestSceneNotePlacementBlueprint : ManiaPlacementBlueprintTestScene { + [SetUp] + public void Setup() => Schedule(() => + { + this.ChildrenOfType().ForEach(c => c.Clear()); + + ResetPlacement(); + + ((ScrollingTestContainer)HitObjectContainer).Direction = ScrollingDirection.Down; + }); + + [Test] + public void TestPlaceBeforeCurrentTimeDownwards() + { + AddStep("move mouse before current time", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single().ScreenSpaceDrawQuad.BottomLeft - new Vector2(0, 10))); + + AddStep("click", () => InputManager.Click(MouseButton.Left)); + + AddAssert("note start time < 0", () => getNote().StartTime < 0); + } + + [Test] + public void TestPlaceAfterCurrentTimeDownwards() + { + AddStep("move mouse after current time", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single())); + + AddStep("click", () => InputManager.Click(MouseButton.Left)); + + AddAssert("note start time > 0", () => getNote().StartTime > 0); + } + + private Note getNote() => this.ChildrenOfType().FirstOrDefault()?.HitObject; + protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableNote((Note)hitObject); protected override PlacementBlueprint CreateBlueprint() => new NotePlacementBlueprint(); } diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs index 6ddf212266..f228daa5e3 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs @@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints return base.OnMouseDown(e); HitObject.Column = Column.Index; - BeginPlacement(TimeAt(e.ScreenSpaceMousePosition)); + BeginPlacement(TimeAt(e.ScreenSpaceMousePosition), true); return true; } diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs index 32c6a6fd07..fd8ef52cef 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs @@ -4,6 +4,7 @@ using osu.Framework.Graphics; using osu.Game.Rulesets.Mania.Edit.Blueprints.Components; using osu.Game.Rulesets.Mania.Objects; +using osuTK; namespace osu.Game.Rulesets.Mania.Edit.Blueprints { @@ -26,5 +27,13 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints Width = SnappedWidth; Position = SnappedMousePosition; } + + public override void UpdatePosition(Vector2 screenSpacePosition) + { + base.UpdatePosition(screenSpacePosition); + + // Continue updating the position until placement is finished on mouse up. + BeginPlacement(TimeAt(screenSpacePosition), true); + } } } From 37f7e0a7349967077f701cf8c3e263573f2f31e5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Apr 2020 12:33:34 +0900 Subject: [PATCH 1242/2376] Restructure TaikoPlayfield for better skin support --- .../Skinning/TestSceneTaikoPlayfield.cs | 6 +- .../Skinning/TaikoLegacySkinTransformer.cs | 18 ++ .../TaikoSkinComponents.cs | 4 +- osu.Game.Rulesets.Taiko/UI/HitExplosion.cs | 2 +- .../UI/PlayfieldBackground.cs | 61 ++++++ osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 187 ++++++++---------- 6 files changed, 165 insertions(+), 113 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/UI/PlayfieldBackground.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs index cdd6f9ab19..3c7360a6bd 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs @@ -5,7 +5,9 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; +using osu.Framework.Testing; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Taiko.Skinning; using osu.Game.Rulesets.Taiko.UI; @@ -20,6 +22,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning { typeof(TaikoHitTarget), typeof(TaikoLegacyHitTarget), + typeof(PlayfieldBackground), }).ToList(); [Cached(typeof(IScrollingInfo))] @@ -33,10 +36,11 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning { AddStep("Load playfield", () => SetContents(() => new TaikoPlayfield(new ControlPointInfo()) { - Height = 0.4f, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, })); + + AddRepeatStep("change height", () => this.ChildrenOfType().ForEach(p => p.Height = Math.Max(0.2f, (p.Height + 0.2f) % 1f)), 50); } } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs index eaa69283e4..919788f08a 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Textures; using osu.Game.Audio; using osu.Game.Skinning; +using osuTK; namespace osu.Game.Rulesets.Taiko.Skinning { @@ -55,6 +56,23 @@ namespace osu.Game.Rulesets.Taiko.Skinning return new TaikoLegacyHitTarget(); return null; + + case TaikoSkinComponents.PlayfieldBackgroundRight: + if (GetTexture("taiko-bar-right") != null) + return this.GetAnimation("taiko-bar-right", false, false).With(d => + { + d.RelativeSizeAxes = Axes.Both; + d.Size = Vector2.One; + }); + + return null; + + case TaikoSkinComponents.PlayfieldBackgroundLeft: + // This is displayed inside LegacyInputDrum. It is required to be there for layout purposes (can be seen on legacy skins). + if (GetTexture("taiko-bar-right") != null) + return Drawable.Empty(); + + return null; } return source.GetDrawableComponent(component); diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs index 775eeb4e38..60a2be7f99 100644 --- a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs +++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs @@ -11,6 +11,8 @@ namespace osu.Game.Rulesets.Taiko DrumRollBody, DrumRollTick, Swell, - HitTarget + HitTarget, + PlayfieldBackgroundLeft, + PlayfieldBackgroundRight } } diff --git a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs index 404960c26f..d4118d38b6 100644 --- a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Taiko.UI JudgedObject = judgedObject; - Anchor = Anchor.CentreLeft; + Anchor = Anchor.Centre; Origin = Anchor.Centre; RelativeSizeAxes = Axes.Both; diff --git a/osu.Game.Rulesets.Taiko/UI/PlayfieldBackground.cs b/osu.Game.Rulesets.Taiko/UI/PlayfieldBackground.cs new file mode 100644 index 0000000000..39fb6c0476 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/UI/PlayfieldBackground.cs @@ -0,0 +1,61 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Taiko.UI +{ + public class PlayfieldBackground : CompositeDrawable + { + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Name = "Transparent playfield background"; + RelativeSizeAxes = Axes.Both; + Masking = true; + BorderColour = colours.Gray1; + + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Colour = Color4.Black.Opacity(0.2f), + Radius = 5, + }; + + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colours.Gray0, + Alpha = 0.6f + }, + new Container + { + Name = "Border", + RelativeSizeAxes = Axes.Both, + Masking = true, + MaskingSmoothness = 0, + BorderThickness = 2, + AlwaysPresent = true, + Children = new[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true + } + } + } + }; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 69c9003434..4587e896ba 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -6,7 +6,6 @@ using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; @@ -25,162 +24,132 @@ namespace osu.Game.Rulesets.Taiko.UI { public class TaikoPlayfield : ScrollingPlayfield { + private readonly ControlPointInfo controlPoints; + /// /// Default height of a when inside a . /// public const float DEFAULT_HEIGHT = 178; - /// - /// The offset from which the center of the hit target lies at. - /// - public const float HIT_TARGET_OFFSET = 100; - /// /// The size of the left area of the playfield. This area contains the input drum. /// - private const float left_area_size = 240; + private const float left_area_size = 180; - private readonly Container hitExplosionContainer; - private readonly Container kiaiExplosionContainer; - private readonly JudgementContainer judgementContainer; - internal readonly Drawable HitTarget; + private Container hitExplosionContainer; + private Container kiaiExplosionContainer; + private JudgementContainer judgementContainer; + internal Drawable HitTarget; - private readonly ProxyContainer topLevelHitContainer; - private readonly ProxyContainer barlineContainer; + private ProxyContainer topLevelHitContainer; + private ProxyContainer barlineContainer; + private Container rightArea; + private Container leftArea; - private readonly Container overlayBackgroundContainer; - private readonly Container backgroundContainer; - - private readonly Box overlayBackground; - private readonly Box background; + private Container hitTargetOffsetContent; public TaikoPlayfield(ControlPointInfo controlPoints) + { + this.controlPoints = controlPoints; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) { InternalChildren = new Drawable[] { - backgroundContainer = new Container - { - Name = "Transparent playfield background", - RelativeSizeAxes = Axes.Both, - Masking = true, - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Shadow, - Colour = Color4.Black.Opacity(0.2f), - Radius = 5, - }, - Children = new Drawable[] - { - background = new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0.6f - }, - } - }, - new Container + new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.PlayfieldBackgroundRight), _ => new PlayfieldBackground()), + rightArea = new Container { Name = "Right area", RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Left = left_area_size }, + RelativePositionAxes = Axes.Both, + Masking = true, Children = new Drawable[] { new Container { Name = "Masked elements before hit objects", RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Left = HIT_TARGET_OFFSET }, - Masking = true, + FillMode = FillMode.Fit, Children = new[] { hitExplosionContainer = new Container { RelativeSizeAxes = Axes.Both, - FillMode = FillMode.Fit, Blending = BlendingParameters.Additive, }, HitTarget = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.HitTarget), _ => new TaikoHitTarget()) { - Anchor = Anchor.CentreLeft, - Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - FillMode = FillMode.Fit } } }, - barlineContainer = new ProxyContainer + hitTargetOffsetContent = new Container { RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Left = HIT_TARGET_OFFSET } - }, - new Container - { - Name = "Hit objects", - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Left = HIT_TARGET_OFFSET }, - Masking = true, - Child = HitObjectContainer - }, - kiaiExplosionContainer = new Container - { - Name = "Kiai hit explosions", - RelativeSizeAxes = Axes.Both, - FillMode = FillMode.Fit, - Margin = new MarginPadding { Left = HIT_TARGET_OFFSET }, - Blending = BlendingParameters.Additive - }, - judgementContainer = new JudgementContainer - { - Name = "Judgements", - RelativeSizeAxes = Axes.Y, - Margin = new MarginPadding { Left = HIT_TARGET_OFFSET }, - Blending = BlendingParameters.Additive + Children = new Drawable[] + { + barlineContainer = new ProxyContainer + { + RelativeSizeAxes = Axes.Both, + }, + new Container + { + Name = "Hit objects", + RelativeSizeAxes = Axes.Both, + Child = HitObjectContainer + }, + kiaiExplosionContainer = new Container + { + Name = "Kiai hit explosions", + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fit, + Blending = BlendingParameters.Additive + }, + judgementContainer = new JudgementContainer + { + Name = "Judgements", + RelativeSizeAxes = Axes.Y, + Blending = BlendingParameters.Additive + }, + } }, } }, - overlayBackgroundContainer = new Container + leftArea = new Container { Name = "Left overlay", - RelativeSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fit, + BorderColour = colours.Gray0, Size = new Vector2(left_area_size, 1), Children = new Drawable[] { - overlayBackground = new Box + new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.PlayfieldBackgroundLeft), _ => new Container { RelativeSizeAxes = Axes.Both, - }, + Children = new Drawable[] + { + new Box + { + Colour = colours.Gray1, + RelativeSizeAxes = Axes.Both, + }, + new Box + { + Anchor = Anchor.TopRight, + RelativeSizeAxes = Axes.Y, + Width = 10, + Colour = Framework.Graphics.Colour.ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.6f), Color4.Black.Opacity(0)), + }, + } + }), new InputDrum(controlPoints) { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Scale = new Vector2(0.9f), - Margin = new MarginPadding { Right = 20 } + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, }, - new Box - { - Anchor = Anchor.TopRight, - RelativeSizeAxes = Axes.Y, - Width = 10, - Colour = Framework.Graphics.Colour.ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.6f), Color4.Black.Opacity(0)), - }, - } - }, - new Container - { - Name = "Border", - RelativeSizeAxes = Axes.Both, - Masking = true, - MaskingSmoothness = 0, - BorderThickness = 2, - AlwaysPresent = true, - Children = new[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - AlwaysPresent = true - } } }, topLevelHitContainer = new ProxyContainer @@ -191,14 +160,12 @@ namespace osu.Game.Rulesets.Taiko.UI }; } - [BackgroundDependencyLoader] - private void load(OsuColour colours) + protected override void Update() { - overlayBackgroundContainer.BorderColour = colours.Gray0; - overlayBackground.Colour = colours.Gray1; + base.Update(); - backgroundContainer.BorderColour = colours.Gray1; - background.Colour = colours.Gray0; + rightArea.Padding = new MarginPadding { Left = leftArea.DrawWidth }; + hitTargetOffsetContent.Padding = new MarginPadding { Left = HitTarget.DrawWidth / 2 }; } public override void Add(DrawableHitObject h) From 49568a3d562884e8960b8ac5d728f476de872d37 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Apr 2020 12:19:30 +0900 Subject: [PATCH 1243/2376] Adjust input drum to work with new playfield changes --- osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs | 9 +++++++++ osu.Game.Rulesets.Taiko/UI/InputDrum.cs | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs index c61e35692b..276a9d76a8 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs @@ -78,6 +78,15 @@ namespace osu.Game.Rulesets.Taiko.Skinning } } + protected override void Update() + { + base.Update(); + + // Relying on RelativeSizeAxes.Both + FillMode.Fit doesn't work due to the precise pixel layout requirements. + // This is a bit ugly but makes the non-legacy implementations a lot cleaner to implement. + Scale = new Vector2(Parent.DrawHeight / Size.Y); + } + /// /// A half-drum. Contains one centre and one rim hit. /// diff --git a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs index 422ea2f929..38026517d9 100644 --- a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs +++ b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs @@ -31,7 +31,6 @@ namespace osu.Game.Rulesets.Taiko.UI sampleMapping = new DrumSampleMapping(controlPoints); RelativeSizeAxes = Axes.Both; - FillMode = FillMode.Fit; } [BackgroundDependencyLoader] @@ -40,6 +39,8 @@ namespace osu.Game.Rulesets.Taiko.UI Child = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.InputDrum), _ => new Container { RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fit, + Scale = new Vector2(0.9f), Children = new Drawable[] { new TaikoHalfDrum(false) From 2e022fbcb5e47519e57a3c973cbb58295e636788 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Apr 2020 12:47:57 +0900 Subject: [PATCH 1244/2376] Add comment about padding update computation --- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 4587e896ba..4e0ef64ce1 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -164,6 +164,8 @@ namespace osu.Game.Rulesets.Taiko.UI { base.Update(); + // Padding is required to be updated for elements which are based on "absolute" X sized elements. + // This is basically allowing for correct alignment as relative pieces move around them. rightArea.Padding = new MarginPadding { Left = leftArea.DrawWidth }; hitTargetOffsetContent.Padding = new MarginPadding { Left = HitTarget.DrawWidth / 2 }; } From 22d2607ff58eb51bfa38d04e08a097011caabf8e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 23 Apr 2020 12:53:09 +0900 Subject: [PATCH 1245/2376] Only commit if placement is active --- .../Edit/Blueprints/NotePlacementBlueprint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs index fd8ef52cef..888ce695c2 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs @@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints base.UpdatePosition(screenSpacePosition); // Continue updating the position until placement is finished on mouse up. - BeginPlacement(TimeAt(screenSpacePosition), true); + BeginPlacement(TimeAt(screenSpacePosition), PlacementActive); } } } From 4f0b5a34d3a1325b77d9901cfd43d6af55a0e29c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 23 Apr 2020 12:53:23 +0900 Subject: [PATCH 1246/2376] Fix hold note placement body sized incorrectly --- .../Objects/Drawables/DrawableHoldNote.cs | 5 ++++- .../Objects/Drawables/Pieces/DefaultBodyPiece.cs | 1 - 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index a9ef661aaa..d63c0326a7 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -51,7 +51,10 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables AddRangeInternal(new[] { - bodyPiece = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HoldNoteBody, hitObject.Column), _ => new DefaultBodyPiece()) + bodyPiece = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HoldNoteBody, hitObject.Column), _ => new DefaultBodyPiece + { + RelativeSizeAxes = Axes.Both + }) { RelativeSizeAxes = Axes.X }, diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/DefaultBodyPiece.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/DefaultBodyPiece.cs index 0ee0a14df3..bc4a095395 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/DefaultBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/DefaultBodyPiece.cs @@ -34,7 +34,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces public DefaultBodyPiece() { - RelativeSizeAxes = Axes.Both; Blending = BlendingParameters.Additive; AddLayout(subtractionCache); From 61d2580e1c847cde16cfb4bb023dcc978730e636 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Apr 2020 13:07:30 +0900 Subject: [PATCH 1247/2376] Fix gap to left of InputDrum on legacy skins --- .../Skinning/LegacyInputDrum.cs | 47 ++++++++++--------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs index 276a9d76a8..1e8cade01d 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs @@ -20,36 +20,41 @@ namespace osu.Game.Rulesets.Taiko.Skinning { private LegacyHalfDrum left; private LegacyHalfDrum right; + private Container content; public LegacyInputDrum() { - Size = new Vector2(180, 200); + RelativeSizeAxes = Axes.Both; } [BackgroundDependencyLoader] private void load(ISkinSource skin) { - Children = new Drawable[] + Child = content = new Container { - new Sprite + Size = new Vector2(180, 200), + Children = new Drawable[] { - Texture = skin.GetTexture("taiko-bar-left") - }, - left = new LegacyHalfDrum(false) - { - Name = "Left Half", - RelativeSizeAxes = Axes.Both, - RimAction = TaikoAction.LeftRim, - CentreAction = TaikoAction.LeftCentre - }, - right = new LegacyHalfDrum(true) - { - Name = "Right Half", - RelativeSizeAxes = Axes.Both, - Origin = Anchor.TopRight, - Scale = new Vector2(-1, 1), - RimAction = TaikoAction.RightRim, - CentreAction = TaikoAction.RightCentre + new Sprite + { + Texture = skin.GetTexture("taiko-bar-left") + }, + left = new LegacyHalfDrum(false) + { + Name = "Left Half", + RelativeSizeAxes = Axes.Both, + RimAction = TaikoAction.LeftRim, + CentreAction = TaikoAction.LeftCentre + }, + right = new LegacyHalfDrum(true) + { + Name = "Right Half", + RelativeSizeAxes = Axes.Both, + Origin = Anchor.TopRight, + Scale = new Vector2(-1, 1), + RimAction = TaikoAction.RightRim, + CentreAction = TaikoAction.RightCentre + } } }; @@ -84,7 +89,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning // Relying on RelativeSizeAxes.Both + FillMode.Fit doesn't work due to the precise pixel layout requirements. // This is a bit ugly but makes the non-legacy implementations a lot cleaner to implement. - Scale = new Vector2(Parent.DrawHeight / Size.Y); + content.Scale = new Vector2(DrawHeight / content.Size.Y); } /// From 4032d669596a2031eca223c37bc2cddb9a102725 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Apr 2020 13:17:46 +0900 Subject: [PATCH 1248/2376] Apply same legacy scale adjust logic to TaikoLegacyHitTarget --- .../Skinning/TaikoLegacyHitTarget.cs | 52 ++++++++++++------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacyHitTarget.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacyHitTarget.cs index b80f273d24..7c1e65f569 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacyHitTarget.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacyHitTarget.cs @@ -5,6 +5,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; +using osu.Game.Rulesets.Taiko.UI; using osu.Game.Skinning; using osuTK; @@ -12,32 +13,47 @@ namespace osu.Game.Rulesets.Taiko.Skinning { public class TaikoLegacyHitTarget : CompositeDrawable { + private Container content; + [BackgroundDependencyLoader] private void load(ISkinSource skin) { RelativeSizeAxes = Axes.Both; - InternalChildren = new Drawable[] + InternalChild = content = new Container { - new Sprite + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] { - Texture = skin.GetTexture("approachcircle"), - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.73f) * 0.625f, - Alpha = 0.7f, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - new Sprite - { - Texture = skin.GetTexture("taikobigcircle"), - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.7f) * 0.625f, - Alpha = 0.5f, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, + new Sprite + { + Texture = skin.GetTexture("approachcircle"), + Scale = new Vector2(0.73f), + Alpha = 0.7f, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + new Sprite + { + Texture = skin.GetTexture("taikobigcircle"), + Scale = new Vector2(0.7f), + Alpha = 0.5f, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + } }; } + + protected override void Update() + { + base.Update(); + + // Relying on RelativeSizeAxes.Both + FillMode.Fit doesn't work due to the precise pixel layout requirements. + // This is a bit ugly but makes the non-legacy implementations a lot cleaner to implement. + content.Scale = new Vector2(DrawHeight / TaikoPlayfield.DEFAULT_HEIGHT); + } } } From 559487b20583fa087760f0410da44d8d35927892 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Apr 2020 13:23:49 +0900 Subject: [PATCH 1249/2376] Move playfield background implementation to its own file --- .../Skinning/TestSceneTaikoPlayfield.cs | 2 +- .../Skinning/TaikoLegacySkinTransformer.cs | 2 + .../UI/PlayfieldBackgroundLeft.cs | 37 +++++++++++++++++++ ...kground.cs => PlayfieldBackgroundRight.cs} | 2 +- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 25 +------------ 5 files changed, 43 insertions(+), 25 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/UI/PlayfieldBackgroundLeft.cs rename osu.Game.Rulesets.Taiko/UI/{PlayfieldBackground.cs => PlayfieldBackgroundRight.cs} (96%) diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs index 3c7360a6bd..16b3c036a3 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning { typeof(TaikoHitTarget), typeof(TaikoLegacyHitTarget), - typeof(PlayfieldBackground), + typeof(PlayfieldBackgroundRight), }).ToList(); [Cached(typeof(IScrollingInfo))] diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs index 919788f08a..15bbd32eb1 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs @@ -59,11 +59,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning case TaikoSkinComponents.PlayfieldBackgroundRight: if (GetTexture("taiko-bar-right") != null) + { return this.GetAnimation("taiko-bar-right", false, false).With(d => { d.RelativeSizeAxes = Axes.Both; d.Size = Vector2.One; }); + } return null; diff --git a/osu.Game.Rulesets.Taiko/UI/PlayfieldBackgroundLeft.cs b/osu.Game.Rulesets.Taiko/UI/PlayfieldBackgroundLeft.cs new file mode 100644 index 0000000000..2a8890a95d --- /dev/null +++ b/osu.Game.Rulesets.Taiko/UI/PlayfieldBackgroundLeft.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 osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Taiko.UI +{ + internal class PlayfieldBackgroundLeft : CompositeDrawable + { + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + RelativeSizeAxes = Axes.Both; + InternalChildren = new Drawable[] + { + new Box + { + Colour = colours.Gray1, + RelativeSizeAxes = Axes.Both, + }, + new Box + { + Anchor = Anchor.TopRight, + RelativeSizeAxes = Axes.Y, + Width = 10, + Colour = Framework.Graphics.Colour.ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.6f), Color4.Black.Opacity(0)), + }, + }; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/UI/PlayfieldBackground.cs b/osu.Game.Rulesets.Taiko/UI/PlayfieldBackgroundRight.cs similarity index 96% rename from osu.Game.Rulesets.Taiko/UI/PlayfieldBackground.cs rename to osu.Game.Rulesets.Taiko/UI/PlayfieldBackgroundRight.cs index 39fb6c0476..44bfdacf37 100644 --- a/osu.Game.Rulesets.Taiko/UI/PlayfieldBackground.cs +++ b/osu.Game.Rulesets.Taiko/UI/PlayfieldBackgroundRight.cs @@ -12,7 +12,7 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Taiko.UI { - public class PlayfieldBackground : CompositeDrawable + public class PlayfieldBackgroundRight : CompositeDrawable { [BackgroundDependencyLoader] private void load(OsuColour colours) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 4e0ef64ce1..1e8d37e3e5 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -3,10 +3,8 @@ using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; @@ -18,7 +16,6 @@ using osu.Game.Rulesets.Taiko.Judgements; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Skinning; using osuTK; -using osuTK.Graphics; namespace osu.Game.Rulesets.Taiko.UI { @@ -58,7 +55,7 @@ namespace osu.Game.Rulesets.Taiko.UI { InternalChildren = new Drawable[] { - new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.PlayfieldBackgroundRight), _ => new PlayfieldBackground()), + new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.PlayfieldBackgroundRight), _ => new PlayfieldBackgroundRight()), rightArea = new Container { Name = "Right area", @@ -126,25 +123,7 @@ namespace osu.Game.Rulesets.Taiko.UI Size = new Vector2(left_area_size, 1), Children = new Drawable[] { - new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.PlayfieldBackgroundLeft), _ => new Container - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Box - { - Colour = colours.Gray1, - RelativeSizeAxes = Axes.Both, - }, - new Box - { - Anchor = Anchor.TopRight, - RelativeSizeAxes = Axes.Y, - Width = 10, - Colour = Framework.Graphics.Colour.ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.6f), Color4.Black.Opacity(0)), - }, - } - }), + new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.PlayfieldBackgroundLeft), _ => new PlayfieldBackgroundLeft()), new InputDrum(controlPoints) { Anchor = Anchor.CentreLeft, From 12c235027df835b189669ade2313f8128b4d31ea Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 23 Apr 2020 13:28:27 +0900 Subject: [PATCH 1250/2376] Remove stale file --- .../Edit/Masks/ManiaSelectionBlueprint.cs | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 osu.Game.Rulesets.Mania/Edit/Masks/ManiaSelectionBlueprint.cs diff --git a/osu.Game.Rulesets.Mania/Edit/Masks/ManiaSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Masks/ManiaSelectionBlueprint.cs deleted file mode 100644 index 433db79ae0..0000000000 --- a/osu.Game.Rulesets.Mania/Edit/Masks/ManiaSelectionBlueprint.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Graphics; -using osu.Game.Rulesets.Edit; -using osu.Game.Rulesets.Objects.Drawables; - -namespace osu.Game.Rulesets.Mania.Edit.Masks -{ - public abstract class ManiaSelectionBlueprint : OverlaySelectionBlueprint - { - protected ManiaSelectionBlueprint(DrawableHitObject drawableObject) - : base(drawableObject) - { - RelativeSizeAxes = Axes.None; - } - } -} From f804be25d13e785dd7a0152f7b9b7c79d93d3138 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Apr 2020 13:36:33 +0900 Subject: [PATCH 1251/2376] Remove incorrect area sizing (now using fillmode / relative instead) --- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 1e8d37e3e5..8402ebb4c4 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -15,7 +15,6 @@ using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.Taiko.Judgements; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Skinning; -using osuTK; namespace osu.Game.Rulesets.Taiko.UI { @@ -28,11 +27,6 @@ namespace osu.Game.Rulesets.Taiko.UI /// public const float DEFAULT_HEIGHT = 178; - /// - /// The size of the left area of the playfield. This area contains the input drum. - /// - private const float left_area_size = 180; - private Container hitExplosionContainer; private Container kiaiExplosionContainer; private JudgementContainer judgementContainer; @@ -120,7 +114,6 @@ namespace osu.Game.Rulesets.Taiko.UI RelativeSizeAxes = Axes.Both, FillMode = FillMode.Fit, BorderColour = colours.Gray0, - Size = new Vector2(left_area_size, 1), Children = new Drawable[] { new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.PlayfieldBackgroundLeft), _ => new PlayfieldBackgroundLeft()), From b4e1ad81d07d89f4ea9b62dc684c1562629cb472 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Apr 2020 13:48:08 +0900 Subject: [PATCH 1252/2376] Fix alignment of right half of legacy input drum --- osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs index 1e8cade01d..81d645e294 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs @@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning const float ratio = 1.6f; // because the right half is flipped, we need to position using width - position to get the true "topleft" origin position - float negativeScaleAdjust = Width / ratio; + float negativeScaleAdjust = content.Width / ratio; if (skin.GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value >= 2.1m) { From 2a1cc35541f46c8bc36d8447fddce0d7c37589de Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Apr 2020 14:01:50 +0900 Subject: [PATCH 1253/2376] Add test scene --- .../Skinning/TestSceneDrawableBarLine.cs | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs new file mode 100644 index 0000000000..a1a5c8c836 --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs @@ -0,0 +1,90 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Objects.Drawables; +using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Taiko.Tests.Skinning +{ + [TestFixture] + public class TestSceneDrawableBarLine : TaikoSkinnableTestScene + { + public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] + { + typeof(DrawableBarLine), + }).ToList(); + + [Cached(typeof(IScrollingInfo))] + private ScrollingTestContainer.TestScrollingInfo info = new ScrollingTestContainer.TestScrollingInfo + { + Direction = { Value = ScrollingDirection.Left }, + TimeRange = { Value = 5000 }, + }; + + [BackgroundDependencyLoader] + private void load() + { + AddStep("Bar line ", () => SetContents(() => + { + var hoc = new ScrollingHitObjectContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Height = 0.8f + }; + + hoc.Add(new DrawableBarLine(createBarLineAtCurrentTime()) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + + return hoc; + })); + + AddStep("Bar line (major)", () => SetContents(() => + { + var hoc = new ScrollingHitObjectContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Height = 0.8f + }; + + hoc.Add(new DrawableBarLineMajor(createBarLineAtCurrentTime(true)) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + + return hoc; + })); + } + + private BarLine createBarLineAtCurrentTime(bool major = false) + { + var drumroll = new BarLine + { + Major = major, + StartTime = Time.Current + 1000, + }; + + var cpi = new ControlPointInfo(); + cpi.Add(0, new TimingControlPoint { BeatLength = 500 }); + + drumroll.ApplyDefaults(cpi, new BeatmapDifficulty()); + + return drumroll; + } + } +} From 12f156dcecdb34228c7cb4209b09fbfc00d79821 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Apr 2020 14:32:48 +0900 Subject: [PATCH 1254/2376] Add taiko barline skinning support --- .../Objects/Drawables/DrawableBarLine.cs | 13 +++++---- .../Objects/Drawables/DrawableBarLineMajor.cs | 2 +- .../Skinning/LegacyBarLine.cs | 27 +++++++++++++++++++ .../Skinning/TaikoLegacySkinTransformer.cs | 6 +++++ .../TaikoSkinComponents.cs | 3 ++- 5 files changed, 44 insertions(+), 7 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/Skinning/LegacyBarLine.cs diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs index e9caabbcc8..1e08e921a6 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Rulesets.Objects; using osuTK; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { @@ -27,7 +28,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables /// /// The visual line tracker. /// - protected Box Tracker; + protected SkinnableDrawable Line; /// /// The bar line. @@ -45,13 +46,15 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables RelativeSizeAxes = Axes.Y; Width = tracker_width; - AddInternal(Tracker = new Box + AddInternal(Line = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.BarLine), _ => new Box + { + RelativeSizeAxes = Axes.Both, + EdgeSmoothness = new Vector2(0.5f, 0), + }) { Anchor = Anchor.Centre, Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - EdgeSmoothness = new Vector2(0.5f, 0), - Alpha = 0.75f + Alpha = 0.75f, }); } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLineMajor.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLineMajor.cs index 4d3a1a3f8a..62aab3524b 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLineMajor.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLineMajor.cs @@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables } }); - Tracker.Alpha = 1f; + Line.Alpha = 1f; } protected override void LoadComplete() diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyBarLine.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyBarLine.cs new file mode 100644 index 0000000000..7d08a21ab1 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyBarLine.cs @@ -0,0 +1,27 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Game.Skinning; +using osuTK; + +namespace osu.Game.Rulesets.Taiko.Skinning +{ + public class LegacyBarLine : Sprite + { + [BackgroundDependencyLoader] + private void load(ISkinSource skin) + { + Texture = skin.GetTexture("taiko-barline"); + + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + RelativeSizeAxes = Axes.Both; + Size = new Vector2(1, 0.88f); + FillMode = FillMode.Fill; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs index 15bbd32eb1..447d6ae455 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs @@ -75,6 +75,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning return Drawable.Empty(); return null; + + case TaikoSkinComponents.BarLine: + if (GetTexture("taiko-barline") != null) + return new LegacyBarLine(); + + return null; } return source.GetDrawableComponent(component); diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs index 60a2be7f99..a90ce608b2 100644 --- a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs +++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs @@ -13,6 +13,7 @@ namespace osu.Game.Rulesets.Taiko Swell, HitTarget, PlayfieldBackgroundLeft, - PlayfieldBackgroundRight + PlayfieldBackgroundRight, + BarLine } } From 8f31846defad9ea469b4aedd5970355177c20d69 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Apr 2020 14:39:34 +0900 Subject: [PATCH 1255/2376] Add playfield background to test scene to better understand bounds --- .../Skinning/TestSceneDrawableBarLine.cs | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs index a1a5c8c836..e72f42a0b4 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs @@ -7,10 +7,12 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects.Drawables; +using osu.Game.Rulesets.Taiko.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Tests.Visual; @@ -36,11 +38,19 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning { AddStep("Bar line ", () => SetContents(() => { - var hoc = new ScrollingHitObjectContainer + ScrollingHitObjectContainer hoc; + + var cont = new Container { + RelativeSizeAxes = Axes.Both, + Height = 0.8f, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Height = 0.8f + Children = new Drawable[] + { + new TaikoPlayfield(new ControlPointInfo()), + hoc = new ScrollingHitObjectContainer() + } }; hoc.Add(new DrawableBarLine(createBarLineAtCurrentTime()) @@ -49,16 +59,24 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning Origin = Anchor.Centre, }); - return hoc; + return cont; })); AddStep("Bar line (major)", () => SetContents(() => { - var hoc = new ScrollingHitObjectContainer + ScrollingHitObjectContainer hoc; + + var cont = new Container { + RelativeSizeAxes = Axes.Both, + Height = 0.8f, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Height = 0.8f + Children = new Drawable[] + { + new TaikoPlayfield(new ControlPointInfo()), + hoc = new ScrollingHitObjectContainer() + } }; hoc.Add(new DrawableBarLineMajor(createBarLineAtCurrentTime(true)) @@ -67,7 +85,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning Origin = Anchor.Centre, }); - return hoc; + return cont; })); } @@ -76,7 +94,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning var drumroll = new BarLine { Major = major, - StartTime = Time.Current + 1000, + StartTime = Time.Current + 2000, }; var cpi = new ControlPointInfo(); From ab93c819b54f930211c507f28ea23b3ae9ca6cfa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Apr 2020 15:26:03 +0900 Subject: [PATCH 1256/2376] Add metric right background element --- .../metrics-skin/taiko-bar-right@2x.png | Bin 0 -> 7830 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taiko-bar-right@2x.png diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taiko-bar-right@2x.png b/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taiko-bar-right@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..5ca8a40d88a13e447d26cd8c0e777173d8e60445 GIT binary patch literal 7830 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 Of{?|_1M@zM+V&ruzN7m9 literal 0 HcmV?d00001 From 712331a2fe524fb6a3c7a8d77a8095d50d7c83d2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Apr 2020 15:26:20 +0900 Subject: [PATCH 1257/2376] Add metric barline element --- .../Resources/metrics-skin/taiko-barline@2x.png | Bin 0 -> 1551 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taiko-barline@2x.png diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taiko-barline@2x.png b/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taiko-barline@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..3e44f3309585d83ce100c59f08e880582b4feb70 GIT binary patch literal 1551 zcmbVMU5MON6wZ{kw7awwY(eVFC539K`JE)YxozB?o!NE*W;@Idbc)#aZgOWP&LlTY z?sjIkAY}zbL=i1mw)!H1|5kh|XzBV=MQ9PhC&357H&+pT5E1cae!{NQzg`HrKi~b% z`OZD(JX4uFzH4m%7{_tD>~g8f{yR7>cY4QmHoJE9^jGY+Gb*1-IBxvj^viLpuiVFR z+xGjlI;}g8K$ip}^2h=fdqKp|9Cvi87a_NeDOkWwKeYHCKf1vK-?R9Wx+6PL5x4yE za*XGf=W6b9+ciCY>S1uS2N^+tDFVHq6DF`{@qJ#%=IOS?gFb||ExwQ%1a+qZiX_HB z7Y)Hxv`H{wii)AhdE+6V%8Dt;lai7bWCh9w)F;4j@XT85HDI-LY-o$EEWSl)1SP54 z?TX#JNaCiXm?q;;B~=v|LP(ZEih4qr92hZ_aN@>(M12y1lo2hEMQZWP(}9E_%4ox6 z7$z1lsfQv-5#>};ALzLmE?SH`edC@h;SLUPNE3!tGHlc$lq4_npld$CJ(Ye!XoLAm>Fi?4u+JO@3uOj&em@wpRrbW#V!?Ut#8&}JTi7s-n;eFd&ciS@k0B%Kc0J_?4qB( z*TISbbBCY4^~B8d&Np-Cetmq}Ufa6&_8Z*%mSbms;PUc8@%f9RvG&gN?(Np^H=k6l ze|tfB^=o~-@X}2>{%rM=*J_o*wteBD`X%?&B}MrB@+uv7etTPg%f9f%>j%$#ywcvY s=a&z1@BMr=_vr4Y_SNCqmUoYQcxCnFFCUw_oVILxcCPgP%-NMc0h$5s%m4rY literal 0 HcmV?d00001 From e3a3144236fe6e2790dafb5198248392b9227145 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 23 Apr 2020 11:07:55 +0300 Subject: [PATCH 1258/2376] Rename editor tests namespace from "Editor" to "Editing" --- osu.Game.Tests/{Editor => Editing}/EditorChangeHandlerTest.cs | 2 +- .../{Editor => Editing}/LegacyEditorBeatmapPatcherTest.cs | 2 +- .../TestSceneHitObjectComposerDistanceSnapping.cs | 2 +- .../Visual/{Editor => Editing}/TestSceneBeatDivisorControl.cs | 2 +- .../Visual/{Editor => Editing}/TestSceneComposeScreen.cs | 2 +- .../Visual/{Editor => Editing}/TestSceneDistanceSnapGrid.cs | 2 +- .../Visual/{Editor => Editing}/TestSceneEditorChangeStates.cs | 4 ++-- .../{Editor => Editing}/TestSceneEditorComposeRadioButtons.cs | 2 +- .../Visual/{Editor => Editing}/TestSceneEditorMenuBar.cs | 2 +- .../Visual/{Editor => Editing}/TestSceneEditorSeekSnapping.cs | 2 +- .../{Editor => Editing}/TestSceneEditorSummaryTimeline.cs | 2 +- .../Visual/{Editor => Editing}/TestSceneHitObjectComposer.cs | 2 +- .../Visual/{Editor => Editing}/TestScenePlaybackControl.cs | 2 +- .../TestSceneTimelineBlueprintContainer.cs | 2 +- .../{Editor => Editing}/TestSceneTimelineTickDisplay.cs | 2 +- .../Visual/{Editor => Editing}/TestSceneTimingScreen.cs | 2 +- .../Visual/{Editor => Editing}/TestSceneWaveform.cs | 2 +- .../{Editor => Editing}/TestSceneZoomableScrollContainer.cs | 2 +- .../Visual/{Editor => Editing}/TimelineTestScene.cs | 2 +- 19 files changed, 20 insertions(+), 20 deletions(-) rename osu.Game.Tests/{Editor => Editing}/EditorChangeHandlerTest.cs (98%) rename osu.Game.Tests/{Editor => Editing}/LegacyEditorBeatmapPatcherTest.cs (99%) rename osu.Game.Tests/{Editor => Editing}/TestSceneHitObjectComposerDistanceSnapping.cs (99%) rename osu.Game.Tests/Visual/{Editor => Editing}/TestSceneBeatDivisorControl.cs (98%) rename osu.Game.Tests/Visual/{Editor => Editing}/TestSceneComposeScreen.cs (96%) rename osu.Game.Tests/Visual/{Editor => Editing}/TestSceneDistanceSnapGrid.cs (99%) rename osu.Game.Tests/Visual/{Editor => Editing}/TestSceneEditorChangeStates.cs (98%) rename osu.Game.Tests/Visual/{Editor => Editing}/TestSceneEditorComposeRadioButtons.cs (97%) rename osu.Game.Tests/Visual/{Editor => Editing}/TestSceneEditorMenuBar.cs (99%) rename osu.Game.Tests/Visual/{Editor => Editing}/TestSceneEditorSeekSnapping.cs (99%) rename osu.Game.Tests/Visual/{Editor => Editing}/TestSceneEditorSummaryTimeline.cs (95%) rename osu.Game.Tests/Visual/{Editor => Editing}/TestSceneHitObjectComposer.cs (98%) rename osu.Game.Tests/Visual/{Editor => Editing}/TestScenePlaybackControl.cs (96%) rename osu.Game.Tests/Visual/{Editor => Editing}/TestSceneTimelineBlueprintContainer.cs (95%) rename osu.Game.Tests/Visual/{Editor => Editing}/TestSceneTimelineTickDisplay.cs (95%) rename osu.Game.Tests/Visual/{Editor => Editing}/TestSceneTimingScreen.cs (96%) rename osu.Game.Tests/Visual/{Editor => Editing}/TestSceneWaveform.cs (98%) rename osu.Game.Tests/Visual/{Editor => Editing}/TestSceneZoomableScrollContainer.cs (99%) rename osu.Game.Tests/Visual/{Editor => Editing}/TimelineTestScene.cs (99%) diff --git a/osu.Game.Tests/Editor/EditorChangeHandlerTest.cs b/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs similarity index 98% rename from osu.Game.Tests/Editor/EditorChangeHandlerTest.cs rename to osu.Game.Tests/Editing/EditorChangeHandlerTest.cs index 9613f250c4..feda1ae0e9 100644 --- a/osu.Game.Tests/Editor/EditorChangeHandlerTest.cs +++ b/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs @@ -5,7 +5,7 @@ using NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Screens.Edit; -namespace osu.Game.Tests.Editor +namespace osu.Game.Tests.Editing { [TestFixture] public class EditorChangeHandlerTest diff --git a/osu.Game.Tests/Editor/LegacyEditorBeatmapPatcherTest.cs b/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs similarity index 99% rename from osu.Game.Tests/Editor/LegacyEditorBeatmapPatcherTest.cs rename to osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs index c24418d688..a3ab677d96 100644 --- a/osu.Game.Tests/Editor/LegacyEditorBeatmapPatcherTest.cs +++ b/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs @@ -17,7 +17,7 @@ using osu.Game.Screens.Edit; using osuTK; using Decoder = osu.Game.Beatmaps.Formats.Decoder; -namespace osu.Game.Tests.Editor +namespace osu.Game.Tests.Editing { [TestFixture] public class LegacyEditorBeatmapPatcherTest diff --git a/osu.Game.Tests/Editor/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs similarity index 99% rename from osu.Game.Tests/Editor/TestSceneHitObjectComposerDistanceSnapping.cs rename to osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs index 3cb5909ba9..168ec0f09d 100644 --- a/osu.Game.Tests/Editor/TestSceneHitObjectComposerDistanceSnapping.cs +++ b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs @@ -14,7 +14,7 @@ using osu.Game.Rulesets.Osu.Edit; using osu.Game.Screens.Edit; using osu.Game.Tests.Visual; -namespace osu.Game.Tests.Editor +namespace osu.Game.Tests.Editing { [HeadlessTest] public class TestSceneHitObjectComposerDistanceSnapping : EditorClockTestScene diff --git a/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs b/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs similarity index 98% rename from osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs rename to osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs index fd7a5980f3..f6e69fd8bf 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneBeatDivisorControl.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs @@ -14,7 +14,7 @@ using osu.Game.Screens.Edit.Compose.Components; using osuTK; using osuTK.Input; -namespace osu.Game.Tests.Visual.Editor +namespace osu.Game.Tests.Visual.Editing { public class TestSceneBeatDivisorControl : OsuManualInputManagerTestScene { diff --git a/osu.Game.Tests/Visual/Editor/TestSceneComposeScreen.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs similarity index 96% rename from osu.Game.Tests/Visual/Editor/TestSceneComposeScreen.cs rename to osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs index a8830824c0..6f5655006e 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneComposeScreen.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposeScreen.cs @@ -9,7 +9,7 @@ using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Compose; -namespace osu.Game.Tests.Visual.Editor +namespace osu.Game.Tests.Visual.Editing { [TestFixture] public class TestSceneComposeScreen : EditorClockTestScene diff --git a/osu.Game.Tests/Visual/Editor/TestSceneDistanceSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs similarity index 99% rename from osu.Game.Tests/Visual/Editor/TestSceneDistanceSnapGrid.cs rename to osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs index f49256a633..417d16fdb0 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneDistanceSnapGrid.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs @@ -13,7 +13,7 @@ using osu.Game.Screens.Edit.Compose.Components; using osuTK; using osuTK.Graphics; -namespace osu.Game.Tests.Visual.Editor +namespace osu.Game.Tests.Visual.Editing { public class TestSceneDistanceSnapGrid : EditorClockTestScene { diff --git a/osu.Game.Tests/Visual/Editor/TestSceneEditorChangeStates.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorChangeStates.cs similarity index 98% rename from osu.Game.Tests/Visual/Editor/TestSceneEditorChangeStates.cs rename to osu.Game.Tests/Visual/Editing/TestSceneEditorChangeStates.cs index efc2a6f552..d85bf15d52 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneEditorChangeStates.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorChangeStates.cs @@ -11,7 +11,7 @@ using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Compose.Components.Timeline; -namespace osu.Game.Tests.Visual.Editor +namespace osu.Game.Tests.Visual.Editing { public class TestSceneEditorChangeStates : ScreenTestScene { @@ -162,7 +162,7 @@ namespace osu.Game.Tests.Visual.Editor private void addRedoSteps() => AddStep("redo", () => editor.Redo()); - private class TestEditor : Screens.Edit.Editor + private class TestEditor : Editor { public new void Undo() => base.Undo(); diff --git a/osu.Game.Tests/Visual/Editor/TestSceneEditorComposeRadioButtons.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorComposeRadioButtons.cs similarity index 97% rename from osu.Game.Tests/Visual/Editor/TestSceneEditorComposeRadioButtons.cs rename to osu.Game.Tests/Visual/Editing/TestSceneEditorComposeRadioButtons.cs index 1709067d5d..2deeaef1f6 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneEditorComposeRadioButtons.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorComposeRadioButtons.cs @@ -7,7 +7,7 @@ using NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Screens.Edit.Components.RadioButtons; -namespace osu.Game.Tests.Visual.Editor +namespace osu.Game.Tests.Visual.Editing { [TestFixture] public class TestSceneEditorComposeRadioButtons : OsuTestScene diff --git a/osu.Game.Tests/Visual/Editor/TestSceneEditorMenuBar.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorMenuBar.cs similarity index 99% rename from osu.Game.Tests/Visual/Editor/TestSceneEditorMenuBar.cs rename to osu.Game.Tests/Visual/Editing/TestSceneEditorMenuBar.cs index 53c2d62067..2cbdacb61c 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneEditorMenuBar.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorMenuBar.cs @@ -10,7 +10,7 @@ using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.UserInterface; using osu.Game.Screens.Edit.Components.Menus; -namespace osu.Game.Tests.Visual.Editor +namespace osu.Game.Tests.Visual.Editing { [TestFixture] public class TestSceneEditorMenuBar : OsuTestScene diff --git a/osu.Game.Tests/Visual/Editor/TestSceneEditorSeekSnapping.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSeekSnapping.cs similarity index 99% rename from osu.Game.Tests/Visual/Editor/TestSceneEditorSeekSnapping.cs rename to osu.Game.Tests/Visual/Editing/TestSceneEditorSeekSnapping.cs index 3118e0cabe..41d1459103 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneEditorSeekSnapping.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSeekSnapping.cs @@ -13,7 +13,7 @@ using osu.Game.Rulesets.Osu.Objects; using osuTK; using osuTK.Graphics; -namespace osu.Game.Tests.Visual.Editor +namespace osu.Game.Tests.Visual.Editing { [TestFixture] public class TestSceneEditorSeekSnapping : EditorClockTestScene diff --git a/osu.Game.Tests/Visual/Editor/TestSceneEditorSummaryTimeline.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs similarity index 95% rename from osu.Game.Tests/Visual/Editor/TestSceneEditorSummaryTimeline.cs rename to osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs index 2e04eb50ca..c92423545d 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneEditorSummaryTimeline.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs @@ -10,7 +10,7 @@ using osu.Game.Rulesets.Osu; using osu.Game.Screens.Edit.Components.Timelines.Summary; using osuTK; -namespace osu.Game.Tests.Visual.Editor +namespace osu.Game.Tests.Visual.Editing { [TestFixture] public class TestSceneEditorSummaryTimeline : EditorClockTestScene diff --git a/osu.Game.Tests/Visual/Editor/TestSceneHitObjectComposer.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs similarity index 98% rename from osu.Game.Tests/Visual/Editor/TestSceneHitObjectComposer.cs rename to osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs index e41c2427fb..ddaca26220 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneHitObjectComposer.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs @@ -20,7 +20,7 @@ using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Compose.Components; using osuTK; -namespace osu.Game.Tests.Visual.Editor +namespace osu.Game.Tests.Visual.Editing { [TestFixture] public class TestSceneHitObjectComposer : EditorClockTestScene diff --git a/osu.Game.Tests/Visual/Editor/TestScenePlaybackControl.cs b/osu.Game.Tests/Visual/Editing/TestScenePlaybackControl.cs similarity index 96% rename from osu.Game.Tests/Visual/Editor/TestScenePlaybackControl.cs rename to osu.Game.Tests/Visual/Editing/TestScenePlaybackControl.cs index 0d4fe4366d..3af976cae0 100644 --- a/osu.Game.Tests/Visual/Editor/TestScenePlaybackControl.cs +++ b/osu.Game.Tests/Visual/Editing/TestScenePlaybackControl.cs @@ -9,7 +9,7 @@ using osu.Game.Beatmaps; using osu.Game.Screens.Edit.Components; using osuTK; -namespace osu.Game.Tests.Visual.Editor +namespace osu.Game.Tests.Visual.Editing { [TestFixture] public class TestScenePlaybackControl : OsuTestScene diff --git a/osu.Game.Tests/Visual/Editor/TestSceneTimelineBlueprintContainer.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineBlueprintContainer.cs similarity index 95% rename from osu.Game.Tests/Visual/Editor/TestSceneTimelineBlueprintContainer.cs rename to osu.Game.Tests/Visual/Editing/TestSceneTimelineBlueprintContainer.cs index 4d8f877575..5ab2f49b4a 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneTimelineBlueprintContainer.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineBlueprintContainer.cs @@ -7,7 +7,7 @@ using NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Screens.Edit.Compose.Components.Timeline; -namespace osu.Game.Tests.Visual.Editor +namespace osu.Game.Tests.Visual.Editing { [TestFixture] public class TestSceneTimelineBlueprintContainer : TimelineTestScene diff --git a/osu.Game.Tests/Visual/Editor/TestSceneTimelineTickDisplay.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineTickDisplay.cs similarity index 95% rename from osu.Game.Tests/Visual/Editor/TestSceneTimelineTickDisplay.cs rename to osu.Game.Tests/Visual/Editing/TestSceneTimelineTickDisplay.cs index 43a3cd6122..e33040acdc 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneTimelineTickDisplay.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineTickDisplay.cs @@ -8,7 +8,7 @@ using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components.Timeline; using osuTK; -namespace osu.Game.Tests.Visual.Editor +namespace osu.Game.Tests.Visual.Editing { [TestFixture] public class TestSceneTimelineTickDisplay : TimelineTestScene diff --git a/osu.Game.Tests/Visual/Editor/TestSceneTimingScreen.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs similarity index 96% rename from osu.Game.Tests/Visual/Editor/TestSceneTimingScreen.cs rename to osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs index ae09a7fa47..a6dbe9571e 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneTimingScreen.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs @@ -9,7 +9,7 @@ using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Timing; -namespace osu.Game.Tests.Visual.Editor +namespace osu.Game.Tests.Visual.Editing { [TestFixture] public class TestSceneTimingScreen : EditorClockTestScene diff --git a/osu.Game.Tests/Visual/Editor/TestSceneWaveform.cs b/osu.Game.Tests/Visual/Editing/TestSceneWaveform.cs similarity index 98% rename from osu.Game.Tests/Visual/Editor/TestSceneWaveform.cs rename to osu.Game.Tests/Visual/Editing/TestSceneWaveform.cs index e2762f3d5f..0c1296b82c 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneWaveform.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneWaveform.cs @@ -14,7 +14,7 @@ using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Osu; using osuTK.Graphics; -namespace osu.Game.Tests.Visual.Editor +namespace osu.Game.Tests.Visual.Editing { [TestFixture] public class TestSceneWaveform : OsuTestScene diff --git a/osu.Game.Tests/Visual/Editor/TestSceneZoomableScrollContainer.cs b/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs similarity index 99% rename from osu.Game.Tests/Visual/Editor/TestSceneZoomableScrollContainer.cs rename to osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs index 19d19c2759..082268d824 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneZoomableScrollContainer.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs @@ -15,7 +15,7 @@ using osu.Game.Screens.Edit.Compose.Components.Timeline; using osuTK; using osuTK.Graphics; -namespace osu.Game.Tests.Visual.Editor +namespace osu.Game.Tests.Visual.Editing { public class TestSceneZoomableScrollContainer : OsuManualInputManagerTestScene { diff --git a/osu.Game.Tests/Visual/Editor/TimelineTestScene.cs b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs similarity index 99% rename from osu.Game.Tests/Visual/Editor/TimelineTestScene.cs rename to osu.Game.Tests/Visual/Editing/TimelineTestScene.cs index 7081eb3af5..56b2860e96 100644 --- a/osu.Game.Tests/Visual/Editor/TimelineTestScene.cs +++ b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs @@ -18,7 +18,7 @@ using osu.Game.Screens.Edit.Compose.Components.Timeline; using osuTK; using osuTK.Graphics; -namespace osu.Game.Tests.Visual.Editor +namespace osu.Game.Tests.Visual.Editing { public abstract class TimelineTestScene : EditorClockTestScene { From 0a840a26133b76a1bbd80b9d77782843460e28d9 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 23 Apr 2020 17:41:33 +0900 Subject: [PATCH 1259/2376] Fix mania not getting its own selection handler --- osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs index d744036b4c..cea27498c3 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs @@ -30,5 +30,7 @@ namespace osu.Game.Rulesets.Mania.Edit return base.CreateBlueprintFor(hitObject); } + + protected override SelectionHandler CreateSelectionHandler() => new ManiaSelectionHandler(); } } From 4ebb28d3e7fdc0a412643f5e4a3effce353e9ebe Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 23 Apr 2020 17:52:54 +0900 Subject: [PATCH 1260/2376] wip --- .../Edit/Blueprints/HoldNoteSelectionBlueprint.cs | 2 ++ osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs | 4 ++-- .../Screens/Edit/Compose/Components/BlueprintContainer.cs | 7 +++++-- osu.Game/Tests/Beatmaps/TestBeatmap.cs | 1 + 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs index d569d68b59..43d43ef252 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs @@ -76,5 +76,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints } public override Quad SelectionQuad => ScreenSpaceDrawQuad; + + public override Vector2 SelectionPoint => DrawableObject.Head.ScreenSpaceDrawQuad.Centre; } } diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs index 9069a636a8..78f159b733 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Mania.Edit var maniaBlueprint = (ManiaSelectionBlueprint)moveEvent.Blueprint; int lastColumn = maniaBlueprint.DrawableObject.HitObject.Column; - adjustOrigins(maniaBlueprint); + // adjustOrigins(maniaBlueprint); performDragMovement(moveEvent); performColumnMovement(lastColumn, moveEvent); @@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Mania.Edit // When scrolling downwards the anchor position is at the bottom of the screen, however the movement event assumes the anchor is at the top of the screen. // This causes the delta to assume a positive hitobject position, and which can be corrected for by subtracting the parent height. if (scrollingInfo.Direction.Value == ScrollingDirection.Down) - delta -= moveEvent.Blueprint.Parent.DrawHeight; // todo: probably wrong + delta -= moveEvent.Blueprint.Parent.DrawHeight; // todo: definitely wrong foreach (var selectionBlueprint in SelectedBlueprints) { diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index ad16e22e5e..0823be01f8 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -401,15 +401,18 @@ namespace osu.Game.Screens.Edit.Compose.Components HitObject draggedObject = movementBlueprint.HitObject; - // The final movement position, relative to screenSpaceMovementStartPosition + // The final movement position, relative to movementBlueprintOriginalPosition Vector2 movePosition = movementBlueprintOriginalPosition.Value + e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition; - (Vector2 snappedPosition, double snappedTime) = snapProvider.GetSnappedPosition(ToLocalSpace(movePosition), draggedObject.StartTime); + (Vector2 snappedPosition, _) = snapProvider.GetSnappedPosition(ToLocalSpace(movePosition), draggedObject.StartTime); // Move the hitobjects if (!selectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprint, ToScreenSpace(snappedPosition)))) return true; + // Todo: Temp + (_, double snappedTime) = snapProvider.GetSnappedPosition(ToLocalSpace(snappedPosition), draggedObject.StartTime); + // Apply the start time at the newly snapped-to position double offset = snappedTime - draggedObject.StartTime; foreach (HitObject obj in selectionHandler.SelectedHitObjects) diff --git a/osu.Game/Tests/Beatmaps/TestBeatmap.cs b/osu.Game/Tests/Beatmaps/TestBeatmap.cs index 96e3c037a3..a7c84bf692 100644 --- a/osu.Game/Tests/Beatmaps/TestBeatmap.cs +++ b/osu.Game/Tests/Beatmaps/TestBeatmap.cs @@ -23,6 +23,7 @@ namespace osu.Game.Tests.Beatmaps HitObjects = baseBeatmap.HitObjects; BeatmapInfo.Ruleset = ruleset; + BeatmapInfo.RulesetID = ruleset.ID ?? 0; BeatmapInfo.BeatmapSet.Metadata = BeatmapInfo.Metadata; BeatmapInfo.BeatmapSet.Beatmaps = new List { BeatmapInfo }; BeatmapInfo.BeatmapSet.OnlineInfo = new BeatmapSetOnlineInfo From b471a240cc9f1510a945352937ddd6b3834fb8f7 Mon Sep 17 00:00:00 2001 From: Tim Oliver Date: Thu, 23 Apr 2020 16:59:56 +0800 Subject: [PATCH 1261/2376] Fixed merge typo in playfield members --- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index a13a818f68..d44de496f4 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Taiko.UI private readonly Container kiaiExplosionContainer; private readonly JudgementContainer judgementContainer; private readonly ScrollingHitObjectContainer drumRollHitContainer; - internal readonly HitTarget HitTarget; + internal readonly Drawable HitTarget; private readonly ProxyContainer topLevelHitContainer; private readonly ProxyContainer barlineContainer; From a9897ba627311977186f50a9db0b97326e8351b4 Mon Sep 17 00:00:00 2001 From: Tim Oliver Date: Thu, 23 Apr 2020 18:15:12 +0800 Subject: [PATCH 1262/2376] Moved proxy behaviour to drumroll container --- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index d44de496f4..07f3fe4e01 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -195,7 +195,8 @@ namespace osu.Game.Rulesets.Taiko.UI { Name = "Top level hit objects", RelativeSizeAxes = Axes.Both, - } + }, + drumRollHitContainer.CreateProxy() }; } @@ -241,7 +242,7 @@ namespace osu.Game.Rulesets.Taiko.UI drawableHit = new DrawableFlyingCentreHit(Time.Current, isStrong); drumRollHitContainer.Add(drawableHit); - topLevelHitContainer.Add(drawableHit.CreateProxiedContent()); + } internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result) From f1ae8af5818958f67b057151ed3c58726fccfa44 Mon Sep 17 00:00:00 2001 From: Tim Oliver Date: Thu, 23 Apr 2020 18:16:05 +0800 Subject: [PATCH 1263/2376] Removed un-needed using directives --- osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs index d4d073ee94..f767403c65 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs @@ -1,11 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Allocation; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Graphics; -using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; using osu.Game.Skinning; From dded4f8176265a4bc5571856424edd9eef883943 Mon Sep 17 00:00:00 2001 From: Tim Oliver Date: Thu, 23 Apr 2020 18:17:31 +0800 Subject: [PATCH 1264/2376] Fixed syntax warnings in Taiko playfield --- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 07f3fe4e01..f4982fa7e6 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -242,7 +242,6 @@ namespace osu.Game.Rulesets.Taiko.UI drawableHit = new DrawableFlyingCentreHit(Time.Current, isStrong); drumRollHitContainer.Add(drawableHit); - } internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result) @@ -289,8 +288,9 @@ namespace osu.Game.Rulesets.Taiko.UI if (Time.Elapsed < 0) { - foreach (DrawableHit taikoHit in drumRollHitContainer.Objects) + foreach (var o in drumRollHitContainer.Objects) { + var taikoHit = (DrawableHit)o; taikoHit.RemoveProxiedContent(); drumRollHitContainer.Remove(taikoHit); } From 0a0ea39431ebf6432c0aa7eb071591931074a7fc Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 23 Apr 2020 13:24:18 +0300 Subject: [PATCH 1265/2376] Mark the top ruleset creation method as can-be-null --- osu.Game/Tests/Visual/OsuTestScene.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index 25ac768272..83db86c0a0 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Track; @@ -145,6 +146,7 @@ namespace osu.Game.Tests.Visual /// /// When testing against ruleset-specific components, this method must be overriden to their ruleset. /// + [CanBeNull] protected virtual Ruleset CreateRuleset() => null; protected virtual IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset); From c059588a09a761d6c92304d71b77218c836c915f Mon Sep 17 00:00:00 2001 From: Tim Oliver Date: Thu, 23 Apr 2020 18:26:40 +0800 Subject: [PATCH 1266/2376] Removed un-needed unproxy method --- .../Objects/Drawables/DrawableTaikoHitObject.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs index e0ff236297..1be04f1760 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs @@ -76,8 +76,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables /// public Drawable CreateProxiedContent() => proxiedContent.CreateProxy(); - public void RemoveProxiedContent() => UnproxyContent(); - public abstract bool OnPressed(TaikoAction action); public virtual void OnReleased(TaikoAction action) From 1fa3764a1db26d70a124437d3c6ae6e70c780674 Mon Sep 17 00:00:00 2001 From: Tim Oliver Date: Thu, 23 Apr 2020 18:26:53 +0800 Subject: [PATCH 1267/2376] Cleaned up Update method in Taiko Playfield --- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index f4982fa7e6..3d371d5260 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -286,14 +286,12 @@ namespace osu.Game.Rulesets.Taiko.UI { base.Update(); + // When rewinding, make sure to remove any auxilliary hit notes that were + // spawned and played during a drumroll. if (Time.Elapsed < 0) { foreach (var o in drumRollHitContainer.Objects) - { - var taikoHit = (DrawableHit)o; - taikoHit.RemoveProxiedContent(); - drumRollHitContainer.Remove(taikoHit); - } + drumRollHitContainer.Remove(o); } } From 2fa47992dc58f0c8293be22eba14673391181933 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 23 Apr 2020 13:25:06 +0300 Subject: [PATCH 1268/2376] Seal the ruleset creation methods and let abstract method take their place Also makes `CreatePlayerRuleset()` and `CreateRulesetForSkinProvider()` not-null to avoid unwanted behaviour with their derivers --- .../CatchSkinnableTestScene.cs | 2 +- .../Mods/TestSceneCatchModPerfect.cs | 4 +-- .../TestSceneCatchPlayer.cs | 2 +- .../Mods/TestSceneManiaModPerfect.cs | 4 +-- .../Skinning/ManiaSkinnableTestScene.cs | 4 +-- .../TestSceneManiaPlayer.cs | 2 +- .../Mods/TestSceneOsuModDifficultyAdjust.cs | 4 +-- .../Mods/TestSceneOsuModDoubleTime.cs | 4 +-- .../Mods/TestSceneOsuModPerfect.cs | 4 +-- .../OsuSkinnableTestScene.cs | 2 +- .../TestSceneMissHitWindowJudgements.cs | 4 +-- .../TestSceneOsuPlayer.cs | 2 +- .../Mods/TestSceneTaikoModPerfect.cs | 4 +-- .../TaikoSkinnableTestScene.cs | 2 +- .../TestSceneTaikoPlayer.cs | 2 +- .../Visual/Gameplay/TestPlayerTestScene.cs | 2 +- osu.Game/Tests/Visual/PlayerTestScene.cs | 30 ++++++++++++++----- osu.Game/Tests/Visual/SkinnableTestScene.cs | 19 +++++++----- 18 files changed, 58 insertions(+), 39 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/CatchSkinnableTestScene.cs b/osu.Game.Rulesets.Catch.Tests/CatchSkinnableTestScene.cs index c0060af74a..0c46b078b5 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchSkinnableTestScene.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchSkinnableTestScene.cs @@ -16,6 +16,6 @@ namespace osu.Game.Rulesets.Catch.Tests typeof(CatchLegacySkinTransformer), }; - protected override Ruleset CreateRuleset() => new CatchRuleset(); + protected override Ruleset CreateRulesetForSkinProvider() => new CatchRuleset(); } } diff --git a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs index 1e69a3f1b6..3e06e78dba 100644 --- a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs +++ b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs @@ -13,6 +13,8 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods { public class TestSceneCatchModPerfect : ModPerfectTestScene { + protected override Ruleset CreatePlayerRuleset() => new CatchRuleset(); + public TestSceneCatchModPerfect() : base(new CatchModPerfect()) { @@ -50,7 +52,5 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods // We only care about testing misses, hits are tested via JuiceStream [TestCase(true)] public void TestTinyDroplet(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new TinyDroplet { StartTime = 1000 }), shouldMiss); - - protected override Ruleset CreateRuleset() => new CatchRuleset(); } } diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayer.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayer.cs index 722f3b5a3b..e1de461e3b 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayer.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayer.cs @@ -16,6 +16,6 @@ namespace osu.Game.Rulesets.Catch.Tests typeof(CatchRuleset), }; - protected override Ruleset CreateRuleset() => new CatchRuleset(); + protected override Ruleset CreatePlayerRuleset() => new CatchRuleset(); } } diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModPerfect.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModPerfect.cs index 72ef58ec73..2e3b21aed7 100644 --- a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModPerfect.cs +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModPerfect.cs @@ -10,6 +10,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods { public class TestSceneManiaModPerfect : ModPerfectTestScene { + protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset(); + public TestSceneManiaModPerfect() : base(new ManiaModPerfect()) { @@ -22,7 +24,5 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods [TestCase(false)] [TestCase(true)] public void TestHoldNote(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new HoldNote { StartTime = 1000, EndTime = 3000 }), shouldMiss); - - protected override Ruleset CreateRuleset() => new ManiaRuleset(); } } diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs index f41ba4db42..a3c1d518c5 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs @@ -34,6 +34,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning typeof(ManiaSettingsSubsection) }; + protected override Ruleset CreateRulesetForSkinProvider() => new ManiaRuleset(); + protected ManiaSkinnableTestScene() { scrollingInfo.Direction.Value = ScrollingDirection.Down; @@ -58,8 +60,6 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning AddStep("change direction to up", () => scrollingInfo.Direction.Value = ScrollingDirection.Up); } - protected override Ruleset CreateRuleset() => new ManiaRuleset(); - private class TestScrollingInfo : IScrollingInfo { public readonly Bindable Direction = new Bindable(); diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaPlayer.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaPlayer.cs index 11663605e2..f4640fd05b 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaPlayer.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaPlayer.cs @@ -14,6 +14,6 @@ namespace osu.Game.Rulesets.Mania.Tests typeof(ManiaRuleset), }; - protected override Ruleset CreateRuleset() => new ManiaRuleset(); + protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset(); } } diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs index 6c5949ca85..7c396054f1 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs @@ -15,6 +15,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods { public class TestSceneOsuModDifficultyAdjust : ModTestScene { + protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); + [Test] public void TestNoAdjustment() => CreateModTest(new ModTestData { @@ -77,7 +79,5 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods { return Player.ScoreProcessor.JudgedHits >= 2; } - - protected override Ruleset CreateRuleset() => new OsuRuleset(); } } diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDoubleTime.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDoubleTime.cs index c61ef2724b..94ef6140e9 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDoubleTime.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDoubleTime.cs @@ -10,6 +10,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods { public class TestSceneOsuModDoubleTime : ModTestScene { + protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); + [TestCase(0.5)] [TestCase(1.01)] [TestCase(1.5)] @@ -26,7 +28,5 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods Precision.AlmostEquals(Player.GameplayClockContainer.GameplayClock.Rate, mod.SpeedChange.Value) }); } - - protected override Ruleset CreateRuleset() => new OsuRuleset(); } } diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModPerfect.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModPerfect.cs index ddbbf9554c..985baa8cf5 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModPerfect.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModPerfect.cs @@ -13,6 +13,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods { public class TestSceneOsuModPerfect : ModPerfectTestScene { + protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); + public TestSceneOsuModPerfect() : base(new OsuModPerfect()) { @@ -48,7 +50,5 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods CreateHitObjectTest(new HitObjectTestData(spinner), shouldMiss); } - - protected override Ruleset CreateRuleset() => new OsuRuleset(); } } diff --git a/osu.Game.Rulesets.Osu.Tests/OsuSkinnableTestScene.cs b/osu.Game.Rulesets.Osu.Tests/OsuSkinnableTestScene.cs index 1458270193..90ebbd9f04 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuSkinnableTestScene.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuSkinnableTestScene.cs @@ -16,6 +16,6 @@ namespace osu.Game.Rulesets.Osu.Tests typeof(OsuLegacySkinTransformer), }; - protected override Ruleset CreateRuleset() => new OsuRuleset(); + protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset(); } } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs index 13457ccaf9..f3221ffe32 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs @@ -19,6 +19,8 @@ namespace osu.Game.Rulesets.Osu.Tests { public class TestSceneMissHitWindowJudgements : ModTestScene { + protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); + [Test] public void TestMissViaEarlyHit() { @@ -61,8 +63,6 @@ namespace osu.Game.Rulesets.Osu.Tests }); } - protected override Ruleset CreateRuleset() => new OsuRuleset(); - private class TestAutoMod : OsuModAutoplay { public override Score CreateReplayScore(IBeatmap beatmap) => new Score diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuPlayer.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuPlayer.cs index 102f8bf841..4ae19624c0 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuPlayer.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuPlayer.cs @@ -16,6 +16,6 @@ namespace osu.Game.Rulesets.Osu.Tests typeof(OsuRuleset), }; - protected override Ruleset CreateRuleset() => new OsuRuleset(); + protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); } } diff --git a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModPerfect.cs b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModPerfect.cs index a9c962bfa0..a83cc16413 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModPerfect.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModPerfect.cs @@ -12,6 +12,8 @@ namespace osu.Game.Rulesets.Taiko.Tests.Mods { public class TestSceneTaikoModPerfect : ModPerfectTestScene { + protected override Ruleset CreatePlayerRuleset() => new TestTaikoRuleset(); + public TestSceneTaikoModPerfect() : base(new TaikoModPerfect()) { @@ -29,8 +31,6 @@ namespace osu.Game.Rulesets.Taiko.Tests.Mods [TestCase(true)] public void TestSwell(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new Swell { StartTime = 1000, EndTime = 3000 }), shouldMiss); - protected override Ruleset CreateRuleset() => new TestTaikoRuleset(); - private class TestTaikoRuleset : TaikoRuleset { public override HealthProcessor CreateHealthProcessor(double drainStartTime) => new TestTaikoHealthProcessor(); diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoSkinnableTestScene.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoSkinnableTestScene.cs index 98e6c2ec52..6db2a6907f 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoSkinnableTestScene.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoSkinnableTestScene.cs @@ -16,6 +16,6 @@ namespace osu.Game.Rulesets.Taiko.Tests typeof(TaikoLegacySkinTransformer), }; - protected override Ruleset CreateRuleset() => new TaikoRuleset(); + protected override Ruleset CreateRulesetForSkinProvider() => new TaikoRuleset(); } } diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayer.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayer.cs index 4c5ab7eabf..bc6f664942 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayer.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayer.cs @@ -14,6 +14,6 @@ namespace osu.Game.Rulesets.Taiko.Tests typeof(TaikoRuleset) }; - protected override Ruleset CreateRuleset() => new TaikoRuleset(); + protected override Ruleset CreatePlayerRuleset() => new TaikoRuleset(); } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestPlayerTestScene.cs b/osu.Game.Tests/Visual/Gameplay/TestPlayerTestScene.cs index 2130171449..bbf0136b00 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestPlayerTestScene.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestPlayerTestScene.cs @@ -11,6 +11,6 @@ namespace osu.Game.Tests.Visual.Gameplay /// public abstract class TestPlayerTestScene : PlayerTestScene { - protected override Ruleset CreateRuleset() => new OsuRuleset(); + protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); } } diff --git a/osu.Game/Tests/Visual/PlayerTestScene.cs b/osu.Game/Tests/Visual/PlayerTestScene.cs index f5e78fbbd1..53abf83e72 100644 --- a/osu.Game/Tests/Visual/PlayerTestScene.cs +++ b/osu.Game/Tests/Visual/PlayerTestScene.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Testing; @@ -23,6 +24,22 @@ namespace osu.Game.Tests.Visual protected OsuConfigManager LocalConfig; + /// + /// Creates the ruleset for setting up the component. + /// + [NotNull] + protected abstract Ruleset CreatePlayerRuleset(); + + protected sealed override Ruleset CreateRuleset() => CreatePlayerRuleset(); + + [NotNull] + private readonly Ruleset ruleset; + + protected PlayerTestScene() + { + ruleset = CreatePlayerRuleset(); + } + [BackgroundDependencyLoader] private void load() { @@ -46,7 +63,7 @@ namespace osu.Game.Tests.Visual action?.Invoke(); - AddStep(CreateRuleset().RulesetInfo.Name, LoadPlayer); + AddStep(ruleset.Description, LoadPlayer); AddUntilStep("player loaded", () => Player.IsLoaded && Player.Alpha == 1); } @@ -56,28 +73,27 @@ namespace osu.Game.Tests.Visual protected void LoadPlayer() { - var beatmap = CreateBeatmap(Ruleset.Value); + var beatmap = CreateBeatmap(ruleset.RulesetInfo); Beatmap.Value = CreateWorkingBeatmap(beatmap); + Ruleset.Value = ruleset.RulesetInfo; SelectedMods.Value = Array.Empty(); - var rulesetInstance = Ruleset.Value.CreateInstance(); - if (!AllowFail) { - var noFailMod = rulesetInstance.GetAllMods().FirstOrDefault(m => m is ModNoFail); + var noFailMod = ruleset.GetAllMods().FirstOrDefault(m => m is ModNoFail); if (noFailMod != null) SelectedMods.Value = new[] { noFailMod }; } if (Autoplay) { - var mod = rulesetInstance.GetAutoplayMod(); + var mod = ruleset.GetAutoplayMod(); if (mod != null) SelectedMods.Value = SelectedMods.Value.Concat(mod.Yield()).ToArray(); } - Player = CreatePlayer(rulesetInstance); + Player = CreatePlayer(ruleset); LoadScreen(Player); } diff --git a/osu.Game/Tests/Visual/SkinnableTestScene.cs b/osu.Game/Tests/Visual/SkinnableTestScene.cs index d648afd504..98164031b0 100644 --- a/osu.Game/Tests/Visual/SkinnableTestScene.cs +++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Text.RegularExpressions; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Graphics; @@ -13,6 +14,7 @@ using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets; using osu.Game.Skinning; using osuTK; using osuTK.Graphics; @@ -26,16 +28,17 @@ namespace osu.Game.Tests.Visual private Skin specialSkin; private Skin oldSkin; + /// + /// Creates the ruleset for adding the ruleset-specific skin transforming component. + /// + [NotNull] + protected abstract Ruleset CreateRulesetForSkinProvider(); + + protected sealed override Ruleset CreateRuleset() => CreateRulesetForSkinProvider(); + protected SkinnableTestScene() : base(2, 3) { - // avoid running silently incorrectly. - if (CreateRuleset() == null) - { - throw new InvalidOperationException( - $"No ruleset provided, override {nameof(CreateRuleset)} to the ruleset belonging to the skinnable content." - + "This is required to add the legacy skin transformer for the content to behave as expected."); - } } [BackgroundDependencyLoader] @@ -110,7 +113,7 @@ namespace osu.Game.Tests.Visual { new OutlineBox { Alpha = autoSize ? 1 : 0 }, mainProvider.WithChild( - new SkinProvidingContainer(Ruleset.Value.CreateInstance().CreateLegacySkinProvider(mainProvider, beatmap)) + new SkinProvidingContainer(CreateRulesetForSkinProvider().CreateLegacySkinProvider(mainProvider, beatmap)) { Child = created, RelativeSizeAxes = !autoSize ? Axes.Both : Axes.None, From 029d15f2a2f19ed2f88bb61d58af947df3411702 Mon Sep 17 00:00:00 2001 From: Tim Oliver Date: Thu, 23 Apr 2020 20:14:39 +0800 Subject: [PATCH 1269/2376] Fixed syntax warning for playfield children --- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 3d371d5260..66894fe883 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Taiko.UI public TaikoPlayfield(ControlPointInfo controlPoints) { - InternalChildren = new Drawable[] + InternalChildren = new[] { backgroundContainer = new Container { From 085b6ae25f0f6bd4c01b4077090d01be35ef3b67 Mon Sep 17 00:00:00 2001 From: Shivam Date: Thu, 23 Apr 2020 20:24:03 +0200 Subject: [PATCH 1270/2376] Add background video for showcase scene (Tournament Client) --- .../Screens/Showcase/ShowcaseScreen.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs b/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs index d809dfc994..1bf67fe607 100644 --- a/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs +++ b/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Tournament.Components; namespace osu.Game.Tournament.Screens.Showcase { @@ -10,7 +12,14 @@ namespace osu.Game.Tournament.Screens.Showcase [BackgroundDependencyLoader] private void load() { - AddInternal(new TournamentLogo()); + AddRangeInternal(new Drawable[] { + new TournamentLogo(), + new TourneyVideo("showcase") + { + Loop = true, + RelativeSizeAxes = Axes.Both, + } + }); } } } From 28dcfe867c3afc866791d2d43934ae0a7626586d Mon Sep 17 00:00:00 2001 From: Shivam Date: Thu, 23 Apr 2020 21:09:12 +0200 Subject: [PATCH 1271/2376] Add Chroma keying to the background of the showcase video. --- .../Screens/Showcase/ShowcaseScreen.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs b/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs index 1bf67fe607..85cf8d2e1f 100644 --- a/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs +++ b/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs @@ -4,11 +4,14 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Tournament.Components; +using osu.Framework.Graphics.Shapes; +using osuTK.Graphics; namespace osu.Game.Tournament.Screens.Showcase { - public class ShowcaseScreen : BeatmapInfoScreen + public class ShowcaseScreen : BeatmapInfoScreen, IProvideVideo { + private Box chroma; [BackgroundDependencyLoader] private void load() { @@ -18,6 +21,16 @@ namespace osu.Game.Tournament.Screens.Showcase { Loop = true, RelativeSizeAxes = Axes.Both, + }, + chroma = new Box + { + // chroma key area for stable gameplay + Name = "chroma", + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Height = 695, + Width = 1366, + Colour = new Color4(0, 255, 0, 255), } }); } From dba737105ee9d74050d55fdd61e525a4cc388973 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Apr 2020 08:57:01 +0900 Subject: [PATCH 1272/2376] Update test scene for dynamic compilation --- .../Skinning/TestSceneDrawableBarLine.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs index e72f42a0b4..58d69fc32a 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs @@ -12,6 +12,7 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects.Drawables; +using osu.Game.Rulesets.Taiko.Skinning; using osu.Game.Rulesets.Taiko.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Tests.Visual; @@ -24,6 +25,8 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] { typeof(DrawableBarLine), + typeof(LegacyBarLine), + typeof(BarLine), }).ToList(); [Cached(typeof(IScrollingInfo))] From 608596c3b39a83d61fa21d30dd92eab4334bfee5 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Fri, 24 Apr 2020 02:50:10 +0200 Subject: [PATCH 1273/2376] Rename DefaultTaikoDonTextureAnimation to TaikoDonTextureAnimation --- .../UI/DrawableTaikoCharacter.cs | 14 ++++++-------- ...ureAnimation.cs => TaikoDonTextureAnimation.cs} | 11 ++++++----- 2 files changed, 12 insertions(+), 13 deletions(-) rename osu.Game.Rulesets.Taiko/UI/{DefaultTaikoDonTextureAnimation.cs => TaikoDonTextureAnimation.cs} (79%) diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoCharacter.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoCharacter.cs index 897670d049..85f18b3ec7 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoCharacter.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoCharacter.cs @@ -1,16 +1,14 @@ using osu.Framework.Allocation; using osu.Framework.Audio.Track; -using osu.Framework.Graphics; using osu.Framework.Graphics.Textures; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics.Containers; -using osuTK; namespace osu.Game.Rulesets.Taiko.UI { public sealed class DrawableTaikoCharacter : BeatSyncedContainer { - private static DefaultTaikoDonTextureAnimation idleDrawable, clearDrawable, kiaiDrawable, failDrawable; + private static TaikoDonTextureAnimation idleDrawable, clearDrawable, kiaiDrawable, failDrawable; private TaikoDonAnimationState state; @@ -22,7 +20,7 @@ namespace osu.Game.Rulesets.Taiko.UI var xd = new Vector2(1); } - private DefaultTaikoDonTextureAnimation getStateDrawable() => State switch + private TaikoDonTextureAnimation getStateDrawable() => State switch { TaikoDonAnimationState.Idle => idleDrawable, TaikoDonAnimationState.Clear => clearDrawable, @@ -50,10 +48,10 @@ namespace osu.Game.Rulesets.Taiko.UI { InternalChildren = new[] { - idleDrawable = new DefaultTaikoDonTextureAnimation(TaikoDonAnimationState.Idle), - clearDrawable = new DefaultTaikoDonTextureAnimation(TaikoDonAnimationState.Clear), - kiaiDrawable = new DefaultTaikoDonTextureAnimation(TaikoDonAnimationState.Kiai), - failDrawable = new DefaultTaikoDonTextureAnimation(TaikoDonAnimationState.Fail), + idleDrawable = new TaikoDonTextureAnimation(TaikoDonAnimationState.Idle), + clearDrawable = new TaikoDonTextureAnimation(TaikoDonAnimationState.Clear), + kiaiDrawable = new TaikoDonTextureAnimation(TaikoDonAnimationState.Kiai), + failDrawable = new TaikoDonTextureAnimation(TaikoDonAnimationState.Fail), }; // sets the state, to make sure we have the correct sprite loaded and set. diff --git a/osu.Game.Rulesets.Taiko/UI/DefaultTaikoDonTextureAnimation.cs b/osu.Game.Rulesets.Taiko/UI/TaikoDonTextureAnimation.cs similarity index 79% rename from osu.Game.Rulesets.Taiko/UI/DefaultTaikoDonTextureAnimation.cs rename to osu.Game.Rulesets.Taiko/UI/TaikoDonTextureAnimation.cs index 1fefed953d..315cd57f13 100644 --- a/osu.Game.Rulesets.Taiko/UI/DefaultTaikoDonTextureAnimation.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoDonTextureAnimation.cs @@ -6,18 +6,19 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.UI { - public sealed class DefaultTaikoDonTextureAnimation : TextureAnimation + public sealed class TaikoDonTextureAnimation : TextureAnimation { - private readonly TaikoDonAnimationState _state; + private readonly TaikoDonAnimationState state; private int currentFrame; - public DefaultTaikoDonTextureAnimation(TaikoDonAnimationState state) : base(false) + public TaikoDonTextureAnimation(TaikoDonAnimationState state) : base(false) { - _state = state; + this.state = state; this.Stop(); Origin = Anchor.BottomLeft; Anchor = Anchor.BottomLeft; + AutoSizeAxes = Axes.Y; } [BackgroundDependencyLoader] @@ -25,7 +26,7 @@ namespace osu.Game.Rulesets.Taiko.UI { for (int i = 0;; i++) { - var textureName = $"pippidon{_getStateString(_state)}{i}"; + var textureName = $"pippidon{_getStateString(state)}{i}"; Texture texture = skin.GetTexture(textureName); if (texture == null) From bbe831698c5b315a373d16ef2b5996f682607619 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Fri, 24 Apr 2020 02:50:33 +0200 Subject: [PATCH 1274/2376] Remove unused code --- .../UI/DrawableTaikoCharacter.cs | 13 ------------- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 1 - 2 files changed, 14 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoCharacter.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoCharacter.cs index 85f18b3ec7..aace96aa9b 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoCharacter.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoCharacter.cs @@ -15,9 +15,6 @@ namespace osu.Game.Rulesets.Taiko.UI public DrawableTaikoCharacter() { RelativeSizeAxes = Axes.Both; - //Size = new Vector2(1f, 2.5f); - //Origin = Anchor.BottomLeft; - var xd = new Vector2(1); } private TaikoDonTextureAnimation getStateDrawable() => State switch @@ -63,16 +60,6 @@ namespace osu.Game.Rulesets.Taiko.UI base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); getStateDrawable().Move(); - - //var signature = timingPoint.TimeSignature == TimeSignatures.SimpleQuadruple ? 4 : 3; - //var length = timingPoint.BeatLength; - //var rate = 1000d / length; - //adjustableClock.Rate = rate; - // - //// Start animating on the first beat. - //if (beatIndex < 1) - // adjustableClock.Start(); - // Logger.GetLogger(LoggingTarget.Information).Add($"Length = {length}ms | Rate = {rate}x | BPM = {timingPoint.BPM} / {signature}"); } } } diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index f4e24afb32..c86a6f61b2 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -22,7 +22,6 @@ using osuTK; using osuTK.Graphics; using osu.Game.Rulesets.Scoring; using osu.Framework.Bindables; -using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.UI { From 6de08db65316679f99830c6448540aeb5b89ea06 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Fri, 24 Apr 2020 02:50:47 +0200 Subject: [PATCH 1275/2376] Add removed skin component back --- osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs index bd98c2ea87..ff7590c561 100644 --- a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs +++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs @@ -6,6 +6,7 @@ namespace osu.Game.Rulesets.Taiko public enum TaikoSkinComponents { InputDrum, + CentreHit, RimHit, DrumRollBody, DrumRollTick, From 1c13fa6c6199ed5a51d985f140b209fb804ddd4a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Apr 2020 10:27:32 +0900 Subject: [PATCH 1276/2376] Fix editor crashing when entering with no beatmap selected --- .../Edit/Compose/Components/Timeline/Timeline.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index 1cb4f737c1..8e6b3d5424 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -60,9 +60,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline waveform.Waveform = b.NewValue.Waveform; track = b.NewValue.Track; - MaxZoom = getZoomLevelForVisibleMilliseconds(500); - MinZoom = getZoomLevelForVisibleMilliseconds(10000); - Zoom = getZoomLevelForVisibleMilliseconds(2000); + if (track.Length > 0) + { + MaxZoom = getZoomLevelForVisibleMilliseconds(500); + MinZoom = getZoomLevelForVisibleMilliseconds(10000); + Zoom = getZoomLevelForVisibleMilliseconds(2000); + } }, true); } @@ -135,7 +138,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private void scrollToTrackTime() { - if (!track.IsLoaded) + if (!track.IsLoaded || track.Length == 0) return; ScrollTo((float)(adjustableClock.CurrentTime / track.Length) * Content.DrawWidth, false); From ca692f30e66fabf102579803561c96ef2d176d9b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 24 Apr 2020 12:27:56 +0900 Subject: [PATCH 1277/2376] Do full placement on mouse down --- .../Blueprints/HitCircles/HitCirclePlacementBlueprint.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs index 2f400160b8..dad199715e 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs @@ -33,19 +33,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles { if (e.Button == MouseButton.Left) { - BeginPlacement(); + EndPlacement(true); return true; } return base.OnMouseDown(e); } - protected override void OnMouseUp(MouseUpEvent e) - { - if (e.Button == MouseButton.Left) - EndPlacement(true); - } - public override void UpdatePosition(Vector2 screenSpacePosition) => HitObject.Position = ToLocalSpace(screenSpacePosition); } } From 2b0deec491f5260a3c68763cfaccf45384ec32a0 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 24 Apr 2020 13:20:41 +0900 Subject: [PATCH 1278/2376] Finish note placement on mouse down --- .../Edit/Blueprints/HoldNotePlacementBlueprint.cs | 7 +++++++ .../Edit/Blueprints/ManiaPlacementBlueprint.cs | 6 ------ .../Edit/Blueprints/NotePlacementBlueprint.cs | 12 +++++++----- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs index 7bbde400ea..b3dd392202 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Graphics; +using osu.Framework.Input.Events; using osu.Game.Rulesets.Mania.Edit.Blueprints.Components; using osu.Game.Rulesets.Mania.Objects; using osuTK; @@ -46,6 +47,12 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints bodyPiece.Height = (bottomPosition - topPosition).Y; } + protected override void OnMouseUp(MouseUpEvent e) + { + base.OnMouseUp(e); + EndPlacement(true); + } + private double originalStartTime; public override void UpdatePosition(Vector2 screenSpacePosition) diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs index f228daa5e3..400abb6380 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs @@ -54,12 +54,6 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints return true; } - protected override void OnMouseUp(MouseUpEvent e) - { - EndPlacement(true); - base.OnMouseUp(e); - } - public override void UpdatePosition(Vector2 screenSpacePosition) { if (!PlacementActive) diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs index 888ce695c2..2b7b383dbe 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs @@ -2,9 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; +using osu.Framework.Input.Events; using osu.Game.Rulesets.Mania.Edit.Blueprints.Components; using osu.Game.Rulesets.Mania.Objects; -using osuTK; namespace osu.Game.Rulesets.Mania.Edit.Blueprints { @@ -28,12 +28,14 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints Position = SnappedMousePosition; } - public override void UpdatePosition(Vector2 screenSpacePosition) + protected override bool OnMouseDown(MouseDownEvent e) { - base.UpdatePosition(screenSpacePosition); + base.OnMouseDown(e); - // Continue updating the position until placement is finished on mouse up. - BeginPlacement(TimeAt(screenSpacePosition), PlacementActive); + // Place the note immediately. + EndPlacement(true); + + return true; } } } From dbf39be6070c863109150cc8610cc275ee1aca1a Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Fri, 24 Apr 2020 06:59:05 +0200 Subject: [PATCH 1279/2376] Decide on the name "Mascot", add testing, bug fixed, etc. --- .../Skinning/TestSceneDrawableTaikoMascot.cs | 155 ++++++++++++++++++ .../Skinning/TaikoLegacySkinTransformer.cs | 2 +- .../UI/DrawableTaikoCharacter.cs | 65 -------- .../UI/DrawableTaikoMascot.cs | 99 +++++++++++ .../UI/TaikoDonTextureAnimation.cs | 61 ------- ...nState.cs => TaikoMascotAnimationState.cs} | 2 +- .../UI/TaikoMascotTextureAnimation.cs | 88 ++++++++++ osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 16 +- 8 files changed, 353 insertions(+), 135 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs delete mode 100644 osu.Game.Rulesets.Taiko/UI/DrawableTaikoCharacter.cs create mode 100644 osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs delete mode 100644 osu.Game.Rulesets.Taiko/UI/TaikoDonTextureAnimation.cs rename osu.Game.Rulesets.Taiko/UI/{TaikoDonAnimationState.cs => TaikoMascotAnimationState.cs} (86%) create mode 100644 osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs new file mode 100644 index 0000000000..dc89fa3a59 --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs @@ -0,0 +1,155 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko.Judgements; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Objects.Drawables; +using osu.Game.Rulesets.Taiko.UI; +using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Skinning; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Taiko.Tests.Skinning +{ + [TestFixture] + public class TestSceneDrawableTaikoMascot : TaikoSkinnableTestScene + { + public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] + { + typeof(DrawableTaikoMascot), + }).ToList(); + + [Cached(typeof(IScrollingInfo))] + private ScrollingTestContainer.TestScrollingInfo info = new ScrollingTestContainer.TestScrollingInfo + { + Direction = { Value = ScrollingDirection.Left }, + TimeRange = { Value = 5000 }, + }; + + private readonly List mascots = new List(); + private readonly List skinnables = new List(); + private readonly List playfields = new List(); + + [Test] + public void TestStateTextures() + { + AddStep("Create mascot (idle)", () => + { + skinnables.Clear(); + SetContents(() => + { + var skinnable = getMascot(); + skinnables.Add(skinnable); + return skinnable; + }); + }); + + AddUntilStep("Wait for SkinnableDrawable", () => skinnables.Any(d => d.Drawable is DrawableTaikoMascot)); + + AddStep("Collect mascots", () => + { + mascots.Clear(); + + foreach (var skinnable in skinnables) + { + if (skinnable.Drawable is DrawableTaikoMascot mascot) + mascots.Add(mascot); + } + }); + + AddStep("Clear state", () => setState(TaikoMascotAnimationState.Clear)); + + AddStep("Kiai state", () => setState(TaikoMascotAnimationState.Kiai)); + + AddStep("Fail state", () => setState(TaikoMascotAnimationState.Fail)); + } + + private void setState(TaikoMascotAnimationState state) + { + foreach (var mascot in mascots) + { + if (mascot == null) + continue; + + mascot.Dumb = true; + mascot.State = state; + } + } + + private SkinnableDrawable getMascot() => + new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.TaikoDon), _ => new Container(), confineMode: ConfineMode.ScaleToFit) + { + RelativePositionAxes = Axes.Both + }; + + [Test] + public void TestPlayfield() + { + AddStep("Create playfield", () => + { + playfields.Clear(); + SetContents(() => + { + var playfield = new TaikoPlayfield(new ControlPointInfo()) + { + Height = 0.4f, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + }; + + playfields.Add(playfield); + + return playfield; + }); + }); + + AddUntilStep("Wait for SkinnableDrawable", () => playfields.Any(p => p.ChildrenOfType().Any())); + + AddStep("Collect mascots", () => + { + mascots.Clear(); + + foreach (var playfield in playfields) + { + var mascot = playfield.ChildrenOfType().SingleOrDefault(); + + if (mascot != null) + mascots.Add(mascot); + } + }); + + AddStep("Create hit (miss)", () => + { + foreach (var playfield in playfields) + addJudgement(playfield, HitResult.Miss); + }); + + AddAssert("Check if state is fail", () => mascots.Where(d => d != null).All(d => d.PlayfieldState.Value == TaikoMascotAnimationState.Fail)); + + AddStep("Create hit (great)", () => + { + foreach (var playfield in playfields) + addJudgement(playfield, HitResult.Great); + }); + + AddAssert("Check if state is idle", () => mascots.Where(d => d != null).All(d => d.PlayfieldState.Value == TaikoMascotAnimationState.Idle)); + } + + private void addJudgement(TaikoPlayfield playfield, HitResult result) + { + playfield.OnNewResult(new DrawableRimHit(new Hit()), new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = result }); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs index cc79822417..dfc9297a33 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs @@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning case TaikoSkinComponents.TaikoDon: if (GetTexture("pippidonclear0") != null) - return new DrawableTaikoCharacter(); + return new DrawableTaikoMascot(); return null; diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoCharacter.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoCharacter.cs deleted file mode 100644 index aace96aa9b..0000000000 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoCharacter.cs +++ /dev/null @@ -1,65 +0,0 @@ -using osu.Framework.Allocation; -using osu.Framework.Audio.Track; -using osu.Framework.Graphics.Textures; -using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Graphics.Containers; - -namespace osu.Game.Rulesets.Taiko.UI -{ - public sealed class DrawableTaikoCharacter : BeatSyncedContainer - { - private static TaikoDonTextureAnimation idleDrawable, clearDrawable, kiaiDrawable, failDrawable; - - private TaikoDonAnimationState state; - - public DrawableTaikoCharacter() - { - RelativeSizeAxes = Axes.Both; - } - - private TaikoDonTextureAnimation getStateDrawable() => State switch - { - TaikoDonAnimationState.Idle => idleDrawable, - TaikoDonAnimationState.Clear => clearDrawable, - TaikoDonAnimationState.Kiai => kiaiDrawable, - TaikoDonAnimationState.Fail => failDrawable, - _ => null - }; - - public TaikoDonAnimationState State - { - get => state; - set - { - state = value; - - foreach (var child in InternalChildren) - child.Hide(); - - getStateDrawable().Show(); - } - } - - [BackgroundDependencyLoader] - private void load(TextureStore textures) - { - InternalChildren = new[] - { - idleDrawable = new TaikoDonTextureAnimation(TaikoDonAnimationState.Idle), - clearDrawable = new TaikoDonTextureAnimation(TaikoDonAnimationState.Clear), - kiaiDrawable = new TaikoDonTextureAnimation(TaikoDonAnimationState.Kiai), - failDrawable = new TaikoDonTextureAnimation(TaikoDonAnimationState.Fail), - }; - - // sets the state, to make sure we have the correct sprite loaded and set. - State = TaikoDonAnimationState.Idle; - } - - protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) - { - base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); - - getStateDrawable().Move(); - } - } -} diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs new file mode 100644 index 0000000000..fbac1e9d0b --- /dev/null +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs @@ -0,0 +1,99 @@ +using osu.Framework.Allocation; +using osu.Framework.Audio.Track; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Textures; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics.Containers; + +namespace osu.Game.Rulesets.Taiko.UI +{ + public sealed class DrawableTaikoMascot : BeatSyncedContainer + { + private static TaikoMascotTextureAnimation idleDrawable, clearDrawable, kiaiDrawable, failDrawable; + private EffectControlPoint lastEffectControlPoint; + private TaikoMascotAnimationState state; + + public Bindable PlayfieldState; + + /// + /// Determines if there should be no "state logic", intended for testing. + /// + public bool Dumb { get; set; } + + public TaikoMascotAnimationState State + { + get => state; + set + { + state = value; + + foreach (var child in InternalChildren) + child.Hide(); + + var drawable = getStateDrawable(State); + + drawable?.Show(); + } + } + + public DrawableTaikoMascot(TaikoMascotAnimationState startingState = TaikoMascotAnimationState.Idle) + { + RelativeSizeAxes = Axes.Both; + PlayfieldState = new Bindable(); + PlayfieldState.BindValueChanged((b) => + { + if (lastEffectControlPoint != null) + State = getFinalAnimationState(lastEffectControlPoint, b.NewValue); + }); + + State = startingState; + } + + private TaikoMascotTextureAnimation getStateDrawable(TaikoMascotAnimationState state) => state switch + { + TaikoMascotAnimationState.Idle => idleDrawable, + TaikoMascotAnimationState.Clear => clearDrawable, + TaikoMascotAnimationState.Kiai => kiaiDrawable, + TaikoMascotAnimationState.Fail => failDrawable, + _ => null + }; + + private TaikoMascotAnimationState getFinalAnimationState(EffectControlPoint effectPoint, TaikoMascotAnimationState playfieldState) + { + if (playfieldState == TaikoMascotAnimationState.Fail) + return playfieldState; + + return effectPoint.KiaiMode ? TaikoMascotAnimationState.Kiai : TaikoMascotAnimationState.Idle; + } + + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + InternalChildren = new[] + { + idleDrawable = new TaikoMascotTextureAnimation(TaikoMascotAnimationState.Idle), + clearDrawable = new TaikoMascotTextureAnimation(TaikoMascotAnimationState.Clear), + kiaiDrawable = new TaikoMascotTextureAnimation(TaikoMascotAnimationState.Kiai), + failDrawable = new TaikoMascotTextureAnimation(TaikoMascotAnimationState.Fail), + }; + + // making sure we have the correct sprite set + State = state; + } + + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) + { + base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); + + if (!Dumb) + State = getFinalAnimationState(lastEffectControlPoint = effectPoint, PlayfieldState.Value); + + if (State == TaikoMascotAnimationState.Clear) + return; + + var drawable = getStateDrawable(State); + drawable.Move(); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoDonTextureAnimation.cs b/osu.Game.Rulesets.Taiko/UI/TaikoDonTextureAnimation.cs deleted file mode 100644 index 315cd57f13..0000000000 --- a/osu.Game.Rulesets.Taiko/UI/TaikoDonTextureAnimation.cs +++ /dev/null @@ -1,61 +0,0 @@ -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Animations; -using osu.Framework.Graphics.Textures; -using osu.Game.Skinning; - -namespace osu.Game.Rulesets.Taiko.UI -{ - public sealed class TaikoDonTextureAnimation : TextureAnimation - { - private readonly TaikoDonAnimationState state; - private int currentFrame; - - public TaikoDonTextureAnimation(TaikoDonAnimationState state) : base(false) - { - this.state = state; - this.Stop(); - - Origin = Anchor.BottomLeft; - Anchor = Anchor.BottomLeft; - AutoSizeAxes = Axes.Y; - } - - [BackgroundDependencyLoader] - private void load(ISkinSource skin) - { - for (int i = 0;; i++) - { - var textureName = $"pippidon{_getStateString(state)}{i}"; - Texture texture = skin.GetTexture(textureName); - - if (texture == null) - break; - - AddFrame(texture); - } - } - - /// - /// Advances the current frame by one. - /// - public void Move() - { - if (FrameCount <= currentFrame) - currentFrame = 0; - - GotoFrame(currentFrame); - - currentFrame++; - } - - private string _getStateString(TaikoDonAnimationState state) => state switch - { - TaikoDonAnimationState.Clear => "clear", - TaikoDonAnimationState.Fail => "fail", - TaikoDonAnimationState.Idle => "idle", - TaikoDonAnimationState.Kiai => "kiai", - _ => null - }; - } -} diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoDonAnimationState.cs b/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimationState.cs similarity index 86% rename from osu.Game.Rulesets.Taiko/UI/TaikoDonAnimationState.cs rename to osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimationState.cs index 773710ee7e..02bf245b7b 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoDonAnimationState.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimationState.cs @@ -3,7 +3,7 @@ namespace osu.Game.Rulesets.Taiko.UI { - public enum TaikoDonAnimationState + public enum TaikoMascotAnimationState { Idle, Clear, diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs b/osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs new file mode 100644 index 0000000000..19a533156e --- /dev/null +++ b/osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs @@ -0,0 +1,88 @@ +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Animations; +using osu.Framework.Graphics.Textures; +using osu.Game.Skinning; + +namespace osu.Game.Rulesets.Taiko.UI +{ + public sealed class TaikoMascotTextureAnimation : TextureAnimation + { + private const float clear_animation_speed = 1000 / 10F; + private static readonly int[] clear_animation_sequence = new[] { 0, 1, 2, 3, 4, 5, 6, 5, 6, 5, 4, 3, 2, 1, 0 }; + private int currentFrame; + + public TaikoMascotAnimationState State { get; } + + public TaikoMascotTextureAnimation(TaikoMascotAnimationState state) + : base(true) + { + State = state; + + // We're animating on beat if it's not the clear animation + if (state == TaikoMascotAnimationState.Clear) + DefaultFrameLength = clear_animation_speed; + else + this.Stop(); + + Origin = Anchor.BottomLeft; + Anchor = Anchor.BottomLeft; + } + + [BackgroundDependencyLoader] + private void load(ISkinSource skin) + { + if (State == TaikoMascotAnimationState.Clear) + { + foreach (var textureIndex in clear_animation_sequence) + { + var textureName = _getStateTextureName(textureIndex); + Texture texture = skin.GetTexture(textureName); + + if (texture == null) + break; + + AddFrame(texture); + } + } + else + { + for (int i = 0;; i++) + { + var textureName = _getStateTextureName(i); + Texture texture = skin.GetTexture(textureName); + + if (texture == null) + break; + + AddFrame(texture); + } + } + } + + /// Advances the current frame by one. + public void Move() + { + if (FrameCount == 0) // Frames are apparently broken + return; + + if (FrameCount <= currentFrame) + currentFrame = 0; + + GotoFrame(currentFrame); + + currentFrame += 1; + } + + private string _getStateTextureName(int i) => $"pippidon{_getStateString(State)}{i}"; + + private string _getStateString(TaikoMascotAnimationState state) => state switch + { + TaikoMascotAnimationState.Clear => "clear", + TaikoMascotAnimationState.Fail => "fail", + TaikoMascotAnimationState.Idle => "idle", + TaikoMascotAnimationState.Kiai => "kiai", + _ => null + }; + } +} diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index c86a6f61b2..3bf5d084ee 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -268,16 +268,18 @@ namespace osu.Game.Rulesets.Taiko.UI break; } - if (characterDrawable.Drawable is DrawableTaikoCharacter character) + if (characterDrawable.Drawable is DrawableTaikoMascot mascot) { - if (result.Type == HitResult.Miss && result.Judgement.AffectsCombo) + var isFailing = result.Type == HitResult.Miss; + + // Only take combo in consideration when it's not a strong hit (it's always false) + if (!(judgedObject.HitObject is StrongHitObject)) { - character.State = TaikoDonAnimationState.Fail; - } - else - { - character.State = judgedObject.HitObject.Kiai ? TaikoDonAnimationState.Kiai : TaikoDonAnimationState.Idle; + if (isFailing) + isFailing = result.Judgement.AffectsCombo; } + + mascot.PlayfieldState.Value = isFailing ? TaikoMascotAnimationState.Fail : TaikoMascotAnimationState.Idle; } } } From ac44185f091266d551080feb35c8d3c4caf2da00 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Fri, 24 Apr 2020 07:09:20 +0200 Subject: [PATCH 1280/2376] Fix formatting --- .../UI/DrawableTaikoMascot.cs | 19 +++++++++++-------- .../UI/TaikoMascotTextureAnimation.cs | 19 +++++++++++-------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs index fbac1e9d0b..dae0b47900 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Taiko.UI { RelativeSizeAxes = Axes.Both; PlayfieldState = new Bindable(); - PlayfieldState.BindValueChanged((b) => + PlayfieldState.BindValueChanged(b => { if (lastEffectControlPoint != null) State = getFinalAnimationState(lastEffectControlPoint, b.NewValue); @@ -50,14 +50,17 @@ namespace osu.Game.Rulesets.Taiko.UI State = startingState; } - private TaikoMascotTextureAnimation getStateDrawable(TaikoMascotAnimationState state) => state switch + private TaikoMascotTextureAnimation getStateDrawable(TaikoMascotAnimationState state) { - TaikoMascotAnimationState.Idle => idleDrawable, - TaikoMascotAnimationState.Clear => clearDrawable, - TaikoMascotAnimationState.Kiai => kiaiDrawable, - TaikoMascotAnimationState.Fail => failDrawable, - _ => null - }; + return state switch + { + TaikoMascotAnimationState.Idle => idleDrawable, + TaikoMascotAnimationState.Clear => clearDrawable, + TaikoMascotAnimationState.Kiai => kiaiDrawable, + TaikoMascotAnimationState.Fail => failDrawable, + _ => null + }; + } private TaikoMascotAnimationState getFinalAnimationState(EffectControlPoint effectPoint, TaikoMascotAnimationState playfieldState) { diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs b/osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs index 19a533156e..dd4785ea81 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs @@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Taiko.UI public sealed class TaikoMascotTextureAnimation : TextureAnimation { private const float clear_animation_speed = 1000 / 10F; - private static readonly int[] clear_animation_sequence = new[] { 0, 1, 2, 3, 4, 5, 6, 5, 6, 5, 4, 3, 2, 1, 0 }; + private static readonly int[] clear_animation_sequence = { 0, 1, 2, 3, 4, 5, 6, 5, 6, 5, 4, 3, 2, 1, 0 }; private int currentFrame; public TaikoMascotAnimationState State { get; } @@ -76,13 +76,16 @@ namespace osu.Game.Rulesets.Taiko.UI private string _getStateTextureName(int i) => $"pippidon{_getStateString(State)}{i}"; - private string _getStateString(TaikoMascotAnimationState state) => state switch + private string _getStateString(TaikoMascotAnimationState state) { - TaikoMascotAnimationState.Clear => "clear", - TaikoMascotAnimationState.Fail => "fail", - TaikoMascotAnimationState.Idle => "idle", - TaikoMascotAnimationState.Kiai => "kiai", - _ => null - }; + return state switch + { + TaikoMascotAnimationState.Clear => "clear", + TaikoMascotAnimationState.Fail => "fail", + TaikoMascotAnimationState.Idle => "idle", + TaikoMascotAnimationState.Kiai => "kiai", + _ => null + }; + } } } From abb687286be341b609ab92705f012f5c9a6a4496 Mon Sep 17 00:00:00 2001 From: Joehu Date: Thu, 23 Apr 2020 22:34:00 -0700 Subject: [PATCH 1281/2376] Fix score multiplier being cut off in mod select at higher ui scales --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index e9b3598625..b94f5cb570 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -103,7 +103,7 @@ namespace osu.Game.Overlays.Mods { new Dimension(GridSizeMode.Absolute, 90), new Dimension(GridSizeMode.Distributed), - new Dimension(GridSizeMode.Absolute, 70), + new Dimension(GridSizeMode.AutoSize), }, Content = new[] { @@ -197,7 +197,8 @@ namespace osu.Game.Overlays.Mods // Footer new Container { - RelativeSizeAxes = Axes.Both, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, Origin = Anchor.TopCentre, Anchor = Anchor.TopCentre, Children = new Drawable[] @@ -215,7 +216,6 @@ namespace osu.Game.Overlays.Mods AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, Width = content_width, - Direction = FillDirection.Horizontal, Padding = new MarginPadding { Vertical = 15, From 118db03b56ec35eba67b900b930a73d61838e191 Mon Sep 17 00:00:00 2001 From: Joehu Date: Thu, 23 Apr 2020 22:41:38 -0700 Subject: [PATCH 1282/2376] Fix vertical spacing and score multiplier splitting apart Also cleans up margin and its hacks (alignment done with anchor/origin now). --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 73 +++++++++++----------- 1 file changed, 36 insertions(+), 37 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index b94f5cb570..6ab72bc02a 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -57,6 +57,8 @@ namespace osu.Game.Overlays.Mods protected Color4 HighMultiplierColour; private const float content_width = 0.8f; + private const float footer_button_spacing = 20; + private readonly FillFlowContainer footerContainer; private SampleChannel sampleOn, sampleOff; @@ -216,6 +218,7 @@ namespace osu.Game.Overlays.Mods AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, Width = content_width, + Spacing = new Vector2(footer_button_spacing, footer_button_spacing / 2), Padding = new MarginPadding { Vertical = 15, @@ -228,10 +231,8 @@ namespace osu.Game.Overlays.Mods Width = 180, Text = "Deselect All", Action = DeselectAll, - Margin = new MarginPadding - { - Right = 20 - } + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, }, CustomiseButton = new TriangleButton { @@ -239,49 +240,47 @@ namespace osu.Game.Overlays.Mods Text = "Customisation", Action = () => ModSettingsContainer.Alpha = ModSettingsContainer.Alpha == 1 ? 0 : 1, Enabled = { Value = false }, - Margin = new MarginPadding - { - Right = 20 - } + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, }, CloseButton = new TriangleButton { Width = 180, Text = "Close", Action = Hide, - Margin = new MarginPadding - { - Right = 20 - } + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, }, - new OsuSpriteText + new FillFlowContainer { - Text = @"Score Multiplier:", - Font = OsuFont.GetFont(size: 30), - Margin = new MarginPadding + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(footer_button_spacing / 2, 0), + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Children = new Drawable[] { - Top = 5, - Right = 10 - } + new OsuSpriteText + { + Text = @"Score Multiplier:", + Font = OsuFont.GetFont(size: 30), + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + }, + MultiplierLabel = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 30, weight: FontWeight.Bold, fixedWidth: true), + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + }, + UnrankedLabel = new OsuSpriteText + { + Text = @"(Unranked)", + Font = OsuFont.GetFont(size: 30, weight: FontWeight.Bold), + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + }, + }, }, - MultiplierLabel = new OsuSpriteText - { - Font = OsuFont.GetFont(size: 30, weight: FontWeight.Bold), - Margin = new MarginPadding - { - Top = 5 - } - }, - UnrankedLabel = new OsuSpriteText - { - Text = @"(Unranked)", - Font = OsuFont.GetFont(size: 30, weight: FontWeight.Bold), - Margin = new MarginPadding - { - Top = 5, - Left = 10 - } - } } } }, From 0f6ec274f9547881484b73559e6d27a0e709eaad Mon Sep 17 00:00:00 2001 From: Joehu Date: Thu, 23 Apr 2020 22:44:17 -0700 Subject: [PATCH 1283/2376] Add transitions to footer when flowing to another row --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 6ab72bc02a..a7b7e50422 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -219,6 +219,8 @@ namespace osu.Game.Overlays.Mods RelativeSizeAxes = Axes.X, Width = content_width, Spacing = new Vector2(footer_button_spacing, footer_button_spacing / 2), + LayoutDuration = 100, + LayoutEasing = Easing.OutQuint, Padding = new MarginPadding { Vertical = 15, From 84aa37d7c3a9cd1b92c792441b1ea6e217d03d0e Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Fri, 24 Apr 2020 07:57:16 +0200 Subject: [PATCH 1284/2376] Fix all local tests --- osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs | 5 ++++- osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs | 5 ++++- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs index dae0b47900..f05c335456 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs @@ -1,4 +1,7 @@ -using osu.Framework.Allocation; +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs b/osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs index dd4785ea81..4a95717c8c 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs @@ -1,4 +1,7 @@ -using osu.Framework.Allocation; +// 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.Animations; using osu.Framework.Graphics.Textures; diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 3bf5d084ee..6d9d263141 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -284,7 +284,7 @@ namespace osu.Game.Rulesets.Taiko.UI } } - class ProxyContainer : LifetimeManagementContainer + internal class ProxyContainer : LifetimeManagementContainer { public new MarginPadding Padding { From 8ae119ab62e68968f84caff0d101281bee5d88e9 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Fri, 24 Apr 2020 08:05:57 +0200 Subject: [PATCH 1285/2376] Fix last formatting error --- osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs b/osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs index 4a95717c8c..922b2d08b6 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs @@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Taiko.UI } else { - for (int i = 0;; i++) + for (int i = 0; true; i++) { var textureName = _getStateTextureName(i); Texture texture = skin.GetTexture(textureName); From 05b3db0147aadad9a8114d9f74f4cfdfd045aa14 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Apr 2020 16:56:18 +0900 Subject: [PATCH 1286/2376] Remove masking --- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 8402ebb4c4..6f7d4a1854 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -55,7 +55,6 @@ namespace osu.Game.Rulesets.Taiko.UI Name = "Right area", RelativeSizeAxes = Axes.Both, RelativePositionAxes = Axes.Both, - Masking = true, Children = new Drawable[] { new Container From cbcd915ec8489caf449c02f42f3e3abf801a69fc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Apr 2020 18:18:10 +0900 Subject: [PATCH 1287/2376] Fix crash on switching comments page at an inopportune time --- osu.Game/Overlays/Comments/CommentsContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Comments/CommentsContainer.cs b/osu.Game/Overlays/Comments/CommentsContainer.cs index 591a9dc86e..e7bfeaf968 100644 --- a/osu.Game/Overlays/Comments/CommentsContainer.cs +++ b/osu.Game/Overlays/Comments/CommentsContainer.cs @@ -153,7 +153,7 @@ namespace osu.Game.Overlays.Comments request?.Cancel(); loadCancellation?.Cancel(); request = new GetCommentsRequest(id.Value, type.Value, Sort.Value, currentPage++, 0); - request.Success += onSuccess; + request.Success += res => Schedule(() => onSuccess(res)); api.PerformAsync(request); } From 39a593120cbf84493263f6b1ec7cc99a8f7c390c Mon Sep 17 00:00:00 2001 From: Shivam Date: Fri, 24 Apr 2020 17:00:35 +0200 Subject: [PATCH 1288/2376] Fixed CodeInspect errors --- .../Screens/Showcase/ShowcaseScreen.cs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs b/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs index 85cf8d2e1f..e48ec63d01 100644 --- a/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs +++ b/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs @@ -11,26 +11,26 @@ namespace osu.Game.Tournament.Screens.Showcase { public class ShowcaseScreen : BeatmapInfoScreen, IProvideVideo { - private Box chroma; [BackgroundDependencyLoader] private void load() { - AddRangeInternal(new Drawable[] { + AddRangeInternal(new Drawable[] + { new TournamentLogo(), new TourneyVideo("showcase") { Loop = true, RelativeSizeAxes = Axes.Both, }, - chroma = new Box - { - // chroma key area for stable gameplay - Name = "chroma", - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Height = 695, - Width = 1366, - Colour = new Color4(0, 255, 0, 255), + new Box + { + // chroma key area for stable gameplay + Name = "chroma", + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Height = 695, + Width = 1366, + Colour = new Color4(0, 255, 0, 255), } }); } From 2be3a8184d0c2e3726a29683d3c18cd2d96ca9fe Mon Sep 17 00:00:00 2001 From: Tim Oliver Date: Sat, 25 Apr 2020 00:15:37 +0800 Subject: [PATCH 1289/2376] Removed modifications to drum roll object --- .../Objects/Drawables/DrawableDrumRoll.cs | 22 +------------------ 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index e416c23c30..5e731e5ad6 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -36,14 +36,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private Color4 colourIdle; private Color4 colourEngaged; - private bool judgingStarted; - - /// - /// A handler action for when the drumroll has been hit, - /// regardless of any judgement. - /// - public Action OnHit; - public DrawableDrumRoll(DrumRoll drumRoll) : base(drumRoll) { @@ -103,27 +95,15 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.DrumRollBody), _ => new ElongatedCirclePiece()); - public override bool OnPressed(TaikoAction action) - { - if (judgingStarted) - OnHit.Invoke(action, HitObject.IsStrong); - - return false; - } + public override bool OnPressed(TaikoAction action) => false; private void onNewResult(DrawableHitObject obj, JudgementResult result) { if (!(obj is DrawableDrumRollTick)) return; - DrawableDrumRollTick drumRollTick = (DrawableDrumRollTick)obj; - if (result.Type > HitResult.Miss) - { - OnHit.Invoke(drumRollTick.JudgedAction, HitObject.IsStrong); - judgingStarted = true; rollingHits++; - } else rollingHits--; From 477fe72fcf8d4966ea25f7ee476305b0a2019112 Mon Sep 17 00:00:00 2001 From: Tim Oliver Date: Sat, 25 Apr 2020 00:15:59 +0800 Subject: [PATCH 1290/2376] Changed note playback to happen on new result --- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 66894fe883..a03010af00 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -12,6 +12,8 @@ using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.Taiko.Objects.Drawables; @@ -222,24 +224,23 @@ namespace osu.Game.Rulesets.Taiko.UI barlineContainer.Add(barline.CreateProxy()); break; - case DrawableDrumRoll drumRoll: - drumRoll.OnHit += onDrumrollArbitraryHit; - break; - case DrawableTaikoHitObject taikoObject: topLevelHitContainer.Add(taikoObject.CreateProxiedContent()); break; } } - private void onDrumrollArbitraryHit(TaikoAction action, bool isStrong) + private void playDrumrollHit(DrawableDrumRollTick drumrollTick) { - DrawableHit drawableHit; + TaikoAction action = drumrollTick.JudgedAction; + bool isStrong = drumrollTick.HitObject.IsStrong; + double time = drumrollTick.HitObject.GetEndTime(); + DrawableHit drawableHit; if (action == TaikoAction.LeftRim || action == TaikoAction.RightRim) - drawableHit = new DrawableFlyingRimHit(Time.Current, isStrong); + drawableHit = new DrawableFlyingRimHit(time, isStrong); else - drawableHit = new DrawableFlyingCentreHit(Time.Current, isStrong); + drawableHit = new DrawableFlyingCentreHit(time, isStrong); drumRollHitContainer.Add(drawableHit); } @@ -249,6 +250,9 @@ namespace osu.Game.Rulesets.Taiko.UI if (!DisplayJudgements.Value) return; + if ((judgedObject is DrawableDrumRollTick) && result.Type != HitResult.Miss) + playDrumrollHit((DrawableDrumRollTick)judgedObject); + if (!judgedObject.DisplayResult) return; From 364f5bf7885ac853a968b7796cdbd37f4f006b1b Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Fri, 24 Apr 2020 22:57:23 +0200 Subject: [PATCH 1291/2376] Update osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Bartłomiej Dach --- osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs b/osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs index 922b2d08b6..2c04d3e1dc 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs @@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Taiko.UI { public sealed class TaikoMascotTextureAnimation : TextureAnimation { - private const float clear_animation_speed = 1000 / 10F; + private const float clear_animation_speed = 1000 / 10f; private static readonly int[] clear_animation_sequence = { 0, 1, 2, 3, 4, 5, 6, 5, 6, 5, 4, 3, 2, 1, 0 }; private int currentFrame; From 4b60be87b591a2bb6d8b1b5378790e7a0509e478 Mon Sep 17 00:00:00 2001 From: Joehu Date: Fri, 24 Apr 2020 16:34:41 -0700 Subject: [PATCH 1292/2376] Move unranked label under multiplier number to avoid width changes --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 30 +++++++++++++++------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index a7b7e50422..36d21c8b46 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -268,18 +268,30 @@ namespace osu.Game.Overlays.Mods Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, }, - MultiplierLabel = new OsuSpriteText + new FillFlowContainer { - Font = OsuFont.GetFont(size: 30, weight: FontWeight.Bold, fixedWidth: true), - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - }, - UnrankedLabel = new OsuSpriteText - { - Text = @"(Unranked)", - Font = OsuFont.GetFont(size: 30, weight: FontWeight.Bold), + AutoSizeAxes = Axes.Both, Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, + Direction = FillDirection.Vertical, + LayoutDuration = 100, + LayoutEasing = Easing.OutQuint, + Children = new Drawable[] + { + MultiplierLabel = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 25, weight: FontWeight.Bold, fixedWidth: true), + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + }, + UnrankedLabel = new OsuSpriteText + { + Text = @"(Unranked)", + Font = OsuFont.GetFont(size: 15, weight: FontWeight.Bold), + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + }, + } }, }, }, From 3cc0b21eaef64e58730d8a7732230290a1dc10b1 Mon Sep 17 00:00:00 2001 From: Tim Oliver Date: Sat, 25 Apr 2020 13:18:02 +0800 Subject: [PATCH 1293/2376] Added more smart checking to removing rewound drumroll hits --- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index a03010af00..cae6b1c80b 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -295,7 +295,10 @@ namespace osu.Game.Rulesets.Taiko.UI if (Time.Elapsed < 0) { foreach (var o in drumRollHitContainer.Objects) - drumRollHitContainer.Remove(o); + { + if (o.HitObject.StartTime >= Time.Current) + drumRollHitContainer.Remove(o); + } } } From c1c930c472208bd5b0fddde8389863863c06de93 Mon Sep 17 00:00:00 2001 From: Tim Oliver Date: Sat, 25 Apr 2020 13:47:20 +0800 Subject: [PATCH 1294/2376] Fixed linting warnings --- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 2 +- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 203d37006a..d1d2571ec7 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Taiko.UI [BackgroundDependencyLoader] private void load(OsuColour colours) { - InternalChildren = new Drawable[] + InternalChildren = new[] { new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.PlayfieldBackgroundRight), _ => new PlayfieldBackgroundRight()), rightArea = new Container diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index b5a8f1604c..7727f25967 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -240,7 +240,7 @@ namespace osu.Game.Beatmaps.Formats } else { - if (hitObject is IHasEndTime _) + if (hitObject is IHasEndTime) addEndTimeData(writer, hitObject); writer.Write(getSampleBank(hitObject.Samples)); From 37cc1ed5a244cae4d2cddbf2c8dc54e9c2ea5c34 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 25 Apr 2020 09:45:11 +0300 Subject: [PATCH 1295/2376] Fix potential null reference while hiding toolbar --- osu.Game/Overlays/Toolbar/Toolbar.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs index 227347112c..bac73c4379 100644 --- a/osu.Game/Overlays/Toolbar/Toolbar.cs +++ b/osu.Game/Overlays/Toolbar/Toolbar.cs @@ -148,7 +148,7 @@ namespace osu.Game.Overlays.Toolbar protected override void PopOut() { - userButton?.StateContainer.Hide(); + userButton.StateContainer?.Hide(); this.MoveToY(-DrawSize.Y, transition_time, Easing.OutQuint); this.FadeOut(transition_time); From 1953c8fc107d87a9941597ec94235a180f05b339 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 25 Apr 2020 09:53:09 +0300 Subject: [PATCH 1296/2376] Fix ruleset selector not receiving key input on toolbar absence --- osu.Game/Overlays/Toolbar/Toolbar.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Overlays/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs index bac73c4379..301747acbc 100644 --- a/osu.Game/Overlays/Toolbar/Toolbar.cs +++ b/osu.Game/Overlays/Toolbar/Toolbar.cs @@ -33,6 +33,10 @@ namespace osu.Game.Overlays.Toolbar private readonly Bindable overlayActivationMode = new Bindable(OverlayActivation.All); + // Required for toolbar components that need to listen for key input + // to invoke specific actions while toolbar is in hidden state. + public override bool PropagateNonPositionalInputSubTree => true; + public Toolbar() { RelativeSizeAxes = Axes.X; From f0ebbb1807aaf293751bce16c6c08c7bae15ea6d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 25 Apr 2020 09:54:37 +0300 Subject: [PATCH 1297/2376] Rewrite toolbar test scene and add test cases --- .../Visual/Menus/TestSceneToolbar.cs | 62 ++++++++++++++++--- 1 file changed, 52 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs index f24589ed35..8fbbc8ebd8 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs @@ -5,13 +5,17 @@ using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; using osu.Game.Overlays.Toolbar; +using osu.Game.Rulesets; +using osuTK.Input; namespace osu.Game.Tests.Visual.Menus { [TestFixture] - public class TestSceneToolbar : OsuTestScene + public class TestSceneToolbar : OsuManualInputManagerTestScene { public override IReadOnlyList RequiredTypes => new[] { @@ -21,24 +25,62 @@ namespace osu.Game.Tests.Visual.Menus typeof(ToolbarNotificationButton), }; - public TestSceneToolbar() + private Toolbar toolbar; + + [Resolved] + private RulesetStore rulesets { get; set; } + + [SetUp] + public void SetUp() => Schedule(() => + { + Child = toolbar = new Toolbar { State = { Value = Visibility.Visible } }; + }); + + [Test] + public void TestNotificationCounter() { - var toolbar = new Toolbar { State = { Value = Visibility.Visible } }; ToolbarNotificationButton notificationButton = null; - AddStep("create toolbar", () => - { - Add(toolbar); - notificationButton = toolbar.Children.OfType().Last().Children.OfType().First(); - }); - - void setNotifications(int count) => AddStep($"set notification count to {count}", () => notificationButton.NotificationCount.Value = count); + AddStep("retrieve notification button", () => notificationButton = toolbar.ChildrenOfType().Single()); setNotifications(1); setNotifications(2); setNotifications(3); setNotifications(0); setNotifications(144); + + void setNotifications(int count) + => AddStep($"set notification count to {count}", + () => notificationButton.NotificationCount.Value = count); + } + + [TestCase(false)] + [TestCase(true)] + public void TestRulesetSwitchingShortcut(bool toolbarHidden) + { + ToolbarRulesetSelector rulesetSelector = null; + + if (toolbarHidden) + AddStep("hide toolbar", () => toolbar.Hide()); + + AddStep("retrieve ruleset selector", () => rulesetSelector = toolbar.ChildrenOfType().Single()); + + for (int i = 0; i < 4; i++) + { + var expected = rulesets.AvailableRulesets.ElementAt(i); + var numberKey = Key.Number1 + i; + + AddStep($"switch to ruleset {i} via shortcut", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.PressKey(numberKey); + + InputManager.ReleaseKey(Key.ControlLeft); + InputManager.ReleaseKey(numberKey); + }); + + AddUntilStep("ruleset switched", () => rulesetSelector.Current.Value.Equals(expected)); + } } } } From b50e8471d20fd113db31027692d472524b0d02f5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 25 Apr 2020 18:23:09 +0900 Subject: [PATCH 1298/2376] Reword comment --- osu.Game/Overlays/Toolbar/Toolbar.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs index 301747acbc..1b748cb672 100644 --- a/osu.Game/Overlays/Toolbar/Toolbar.cs +++ b/osu.Game/Overlays/Toolbar/Toolbar.cs @@ -33,8 +33,7 @@ namespace osu.Game.Overlays.Toolbar private readonly Bindable overlayActivationMode = new Bindable(OverlayActivation.All); - // Required for toolbar components that need to listen for key input - // to invoke specific actions while toolbar is in hidden state. + // Toolbar components like RulesetSelector should receive keyboard input events even when the toolbar is hidden. public override bool PropagateNonPositionalInputSubTree => true; public Toolbar() From a756486a4d74073f7e131ff47235ba91d892649f Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sat, 25 Apr 2020 20:35:46 +0200 Subject: [PATCH 1299/2376] Make settings section icons actual drawables. --- osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.cs | 8 +++++++- osu.Game/Overlays/KeyBinding/RulesetBindingsSection.cs | 8 +++++++- osu.Game/Overlays/Settings/Sections/AudioSection.cs | 8 ++++++-- osu.Game/Overlays/Settings/Sections/DebugSection.cs | 7 ++++++- osu.Game/Overlays/Settings/Sections/GameplaySection.cs | 7 ++++++- osu.Game/Overlays/Settings/Sections/GeneralSection.cs | 7 ++++++- osu.Game/Overlays/Settings/Sections/GraphicsSection.cs | 7 ++++++- osu.Game/Overlays/Settings/Sections/InputSection.cs | 7 ++++++- osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs | 7 ++++++- osu.Game/Overlays/Settings/Sections/OnlineSection.cs | 7 ++++++- osu.Game/Overlays/Settings/Sections/SkinSection.cs | 6 +++++- osu.Game/Overlays/Settings/SettingsSection.cs | 3 +-- osu.Game/Overlays/Settings/SidebarButton.cs | 7 ++++--- 13 files changed, 72 insertions(+), 17 deletions(-) diff --git a/osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.cs b/osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.cs index 56e93b6a1e..214da69dfb 100644 --- a/osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.cs +++ b/osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.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.Graphics; using osu.Framework.Graphics.Sprites; using osu.Game.Input.Bindings; using osu.Game.Overlays.Settings; @@ -9,7 +10,12 @@ namespace osu.Game.Overlays.KeyBinding { public class GlobalKeyBindingsSection : SettingsSection { - public override IconUsage Icon => FontAwesome.Solid.Globe; + public override Drawable CreateIcon() => new SpriteIcon + { + RelativeSizeAxes = Axes.Both, + Icon = FontAwesome.Solid.Globe + }; + public override string Header => "Global"; public GlobalKeyBindingsSection(GlobalActionContainer manager) diff --git a/osu.Game/Overlays/KeyBinding/RulesetBindingsSection.cs b/osu.Game/Overlays/KeyBinding/RulesetBindingsSection.cs index 1f4042c57c..b2814907eb 100644 --- a/osu.Game/Overlays/KeyBinding/RulesetBindingsSection.cs +++ b/osu.Game/Overlays/KeyBinding/RulesetBindingsSection.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.Graphics; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Overlays.Settings; @@ -10,7 +11,12 @@ namespace osu.Game.Overlays.KeyBinding { public class RulesetBindingsSection : SettingsSection { - public override IconUsage Icon => (ruleset.CreateInstance().CreateIcon() as SpriteIcon)?.Icon ?? OsuIcon.Hot; + public override Drawable CreateIcon() => ruleset?.CreateInstance()?.CreateIcon() ?? new SpriteIcon + { + RelativeSizeAxes = Axes.Both, + Icon = OsuIcon.Hot + }; + public override string Header => ruleset.Name; private readonly RulesetInfo ruleset; diff --git a/osu.Game/Overlays/Settings/Sections/AudioSection.cs b/osu.Game/Overlays/Settings/Sections/AudioSection.cs index b18488b616..5f55b268d0 100644 --- a/osu.Game/Overlays/Settings/Sections/AudioSection.cs +++ b/osu.Game/Overlays/Settings/Sections/AudioSection.cs @@ -13,9 +13,13 @@ namespace osu.Game.Overlays.Settings.Sections { public override string Header => "Audio"; - public override IEnumerable FilterTerms => base.FilterTerms.Concat(new[] { "sound" }); + public override Drawable CreateIcon() => new SpriteIcon + { + RelativeSizeAxes = Axes.Both, + Icon = FontAwesome.Solid.VolumeUp + }; - public override IconUsage Icon => FontAwesome.Solid.VolumeUp; + public override IEnumerable FilterTerms => base.FilterTerms.Concat(new[] { "sound" }); public AudioSection() { diff --git a/osu.Game/Overlays/Settings/Sections/DebugSection.cs b/osu.Game/Overlays/Settings/Sections/DebugSection.cs index f62de0b243..a3ca6cf6de 100644 --- a/osu.Game/Overlays/Settings/Sections/DebugSection.cs +++ b/osu.Game/Overlays/Settings/Sections/DebugSection.cs @@ -10,7 +10,12 @@ namespace osu.Game.Overlays.Settings.Sections public class DebugSection : SettingsSection { public override string Header => "Debug"; - public override IconUsage Icon => FontAwesome.Solid.Bug; + + public override Drawable CreateIcon() => new SpriteIcon + { + RelativeSizeAxes = Axes.Both, + Icon = FontAwesome.Solid.Bug + }; public DebugSection() { diff --git a/osu.Game/Overlays/Settings/Sections/GameplaySection.cs b/osu.Game/Overlays/Settings/Sections/GameplaySection.cs index 97d9d3c697..38f68b97ba 100644 --- a/osu.Game/Overlays/Settings/Sections/GameplaySection.cs +++ b/osu.Game/Overlays/Settings/Sections/GameplaySection.cs @@ -13,7 +13,12 @@ namespace osu.Game.Overlays.Settings.Sections public class GameplaySection : SettingsSection { public override string Header => "Gameplay"; - public override IconUsage Icon => FontAwesome.Regular.Circle; + + public override Drawable CreateIcon() => new SpriteIcon + { + RelativeSizeAxes = Axes.Both, + Icon = FontAwesome.Regular.Circle + }; public GameplaySection() { diff --git a/osu.Game/Overlays/Settings/Sections/GeneralSection.cs b/osu.Game/Overlays/Settings/Sections/GeneralSection.cs index d9947f16cc..bc87281f20 100644 --- a/osu.Game/Overlays/Settings/Sections/GeneralSection.cs +++ b/osu.Game/Overlays/Settings/Sections/GeneralSection.cs @@ -10,7 +10,12 @@ namespace osu.Game.Overlays.Settings.Sections public class GeneralSection : SettingsSection { public override string Header => "General"; - public override IconUsage Icon => FontAwesome.Solid.Cog; + + public override Drawable CreateIcon() => new SpriteIcon + { + RelativeSizeAxes = Axes.Both, + Icon = FontAwesome.Solid.Cog + }; public GeneralSection() { diff --git a/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs b/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs index 89caa3dc8f..3ac6686e36 100644 --- a/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs +++ b/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs @@ -10,7 +10,12 @@ namespace osu.Game.Overlays.Settings.Sections public class GraphicsSection : SettingsSection { public override string Header => "Graphics"; - public override IconUsage Icon => FontAwesome.Solid.Laptop; + + public override Drawable CreateIcon() => new SpriteIcon + { + RelativeSizeAxes = Axes.Both, + Icon = FontAwesome.Solid.Laptop + }; public GraphicsSection() { diff --git a/osu.Game/Overlays/Settings/Sections/InputSection.cs b/osu.Game/Overlays/Settings/Sections/InputSection.cs index 2a348b4e03..2c395d325a 100644 --- a/osu.Game/Overlays/Settings/Sections/InputSection.cs +++ b/osu.Game/Overlays/Settings/Sections/InputSection.cs @@ -10,7 +10,12 @@ namespace osu.Game.Overlays.Settings.Sections public class InputSection : SettingsSection { public override string Header => "Input"; - public override IconUsage Icon => FontAwesome.Regular.Keyboard; + + public override Drawable CreateIcon() => new SpriteIcon + { + RelativeSizeAxes = Axes.Both, + Icon = FontAwesome.Solid.Keyboard + }; public InputSection(KeyBindingPanel keyConfig) { diff --git a/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs b/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs index 0f3acd5b7f..69f00d1549 100644 --- a/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs +++ b/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs @@ -11,7 +11,12 @@ namespace osu.Game.Overlays.Settings.Sections public class MaintenanceSection : SettingsSection { public override string Header => "Maintenance"; - public override IconUsage Icon => FontAwesome.Solid.Wrench; + + public override Drawable CreateIcon() => new SpriteIcon + { + RelativeSizeAxes = Axes.Both, + Icon = FontAwesome.Solid.Wrench + }; public MaintenanceSection() { diff --git a/osu.Game/Overlays/Settings/Sections/OnlineSection.cs b/osu.Game/Overlays/Settings/Sections/OnlineSection.cs index 80295690c0..c22174f050 100644 --- a/osu.Game/Overlays/Settings/Sections/OnlineSection.cs +++ b/osu.Game/Overlays/Settings/Sections/OnlineSection.cs @@ -10,7 +10,12 @@ namespace osu.Game.Overlays.Settings.Sections public class OnlineSection : SettingsSection { public override string Header => "Online"; - public override IconUsage Icon => FontAwesome.Solid.GlobeAsia; + + public override Drawable CreateIcon() => new SpriteIcon + { + RelativeSizeAxes = Axes.Both, + Icon = FontAwesome.Solid.GlobeAsia + }; public OnlineSection() { diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index b229014c84..3edfc4346f 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -19,7 +19,11 @@ namespace osu.Game.Overlays.Settings.Sections public override string Header => "Skin"; - public override IconUsage Icon => FontAwesome.Solid.PaintBrush; + public override Drawable CreateIcon() => new SpriteIcon + { + RelativeSizeAxes = Axes.Both, + Icon = FontAwesome.Solid.PaintBrush + }; private readonly Bindable dropdownBindable = new Bindable { Default = SkinInfo.Default }; private readonly Bindable configBindable = new Bindable(); diff --git a/osu.Game/Overlays/Settings/SettingsSection.cs b/osu.Game/Overlays/Settings/SettingsSection.cs index be3696029e..97e4ba9da7 100644 --- a/osu.Game/Overlays/Settings/SettingsSection.cs +++ b/osu.Game/Overlays/Settings/SettingsSection.cs @@ -11,7 +11,6 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using System.Collections.Generic; using System.Linq; -using osu.Framework.Graphics.Sprites; namespace osu.Game.Overlays.Settings { @@ -20,7 +19,7 @@ namespace osu.Game.Overlays.Settings protected FillFlowContainer FlowContent; protected override Container Content => FlowContent; - public abstract IconUsage Icon { get; } + public abstract Drawable CreateIcon(); public abstract string Header { get; } public IEnumerable FilterableChildren => Children.OfType(); diff --git a/osu.Game/Overlays/Settings/SidebarButton.cs b/osu.Game/Overlays/Settings/SidebarButton.cs index 68836bc6b3..30a53b351d 100644 --- a/osu.Game/Overlays/Settings/SidebarButton.cs +++ b/osu.Game/Overlays/Settings/SidebarButton.cs @@ -11,12 +11,13 @@ using osu.Framework.Graphics.Sprites; 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 { - private readonly SpriteIcon drawableIcon; + private readonly ConstrainedIconContainer iconContainer; private readonly SpriteText headerText; private readonly Box selectionIndicator; private readonly Container text; @@ -30,7 +31,7 @@ namespace osu.Game.Overlays.Settings { section = value; headerText.Text = value.Header; - drawableIcon.Icon = value.Icon; + iconContainer.Icon = value.CreateIcon(); } } @@ -78,7 +79,7 @@ namespace osu.Game.Overlays.Settings Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, }, - drawableIcon = new SpriteIcon + iconContainer = new ConstrainedIconContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, From 801968ed515e32990af009c4bfa2ee4a04c97b5e Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sun, 26 Apr 2020 21:17:40 +0200 Subject: [PATCH 1300/2376] Remove un-needed RelativeSizeAxes specifications. --- osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.cs | 1 - osu.Game/Overlays/KeyBinding/RulesetBindingsSection.cs | 1 - osu.Game/Overlays/Settings/Sections/AudioSection.cs | 1 - osu.Game/Overlays/Settings/Sections/DebugSection.cs | 1 - osu.Game/Overlays/Settings/Sections/GraphicsSection.cs | 1 - osu.Game/Overlays/Settings/Sections/InputSection.cs | 1 - osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs | 1 - osu.Game/Overlays/Settings/Sections/OnlineSection.cs | 1 - osu.Game/Overlays/Settings/Sections/SkinSection.cs | 1 - 9 files changed, 9 deletions(-) diff --git a/osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.cs b/osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.cs index 214da69dfb..5b44c486a3 100644 --- a/osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.cs +++ b/osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.cs @@ -12,7 +12,6 @@ namespace osu.Game.Overlays.KeyBinding { public override Drawable CreateIcon() => new SpriteIcon { - RelativeSizeAxes = Axes.Both, Icon = FontAwesome.Solid.Globe }; diff --git a/osu.Game/Overlays/KeyBinding/RulesetBindingsSection.cs b/osu.Game/Overlays/KeyBinding/RulesetBindingsSection.cs index b2814907eb..332fb6c8fc 100644 --- a/osu.Game/Overlays/KeyBinding/RulesetBindingsSection.cs +++ b/osu.Game/Overlays/KeyBinding/RulesetBindingsSection.cs @@ -13,7 +13,6 @@ namespace osu.Game.Overlays.KeyBinding { public override Drawable CreateIcon() => ruleset?.CreateInstance()?.CreateIcon() ?? new SpriteIcon { - RelativeSizeAxes = Axes.Both, Icon = OsuIcon.Hot }; diff --git a/osu.Game/Overlays/Settings/Sections/AudioSection.cs b/osu.Game/Overlays/Settings/Sections/AudioSection.cs index 5f55b268d0..69538358f1 100644 --- a/osu.Game/Overlays/Settings/Sections/AudioSection.cs +++ b/osu.Game/Overlays/Settings/Sections/AudioSection.cs @@ -15,7 +15,6 @@ namespace osu.Game.Overlays.Settings.Sections public override Drawable CreateIcon() => new SpriteIcon { - RelativeSizeAxes = Axes.Both, Icon = FontAwesome.Solid.VolumeUp }; diff --git a/osu.Game/Overlays/Settings/Sections/DebugSection.cs b/osu.Game/Overlays/Settings/Sections/DebugSection.cs index a3ca6cf6de..44d4088972 100644 --- a/osu.Game/Overlays/Settings/Sections/DebugSection.cs +++ b/osu.Game/Overlays/Settings/Sections/DebugSection.cs @@ -13,7 +13,6 @@ namespace osu.Game.Overlays.Settings.Sections public override Drawable CreateIcon() => new SpriteIcon { - RelativeSizeAxes = Axes.Both, Icon = FontAwesome.Solid.Bug }; diff --git a/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs b/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs index 3ac6686e36..c1b4b0bbcb 100644 --- a/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs +++ b/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs @@ -13,7 +13,6 @@ namespace osu.Game.Overlays.Settings.Sections public override Drawable CreateIcon() => new SpriteIcon { - RelativeSizeAxes = Axes.Both, Icon = FontAwesome.Solid.Laptop }; diff --git a/osu.Game/Overlays/Settings/Sections/InputSection.cs b/osu.Game/Overlays/Settings/Sections/InputSection.cs index 2c395d325a..b43453f53d 100644 --- a/osu.Game/Overlays/Settings/Sections/InputSection.cs +++ b/osu.Game/Overlays/Settings/Sections/InputSection.cs @@ -13,7 +13,6 @@ namespace osu.Game.Overlays.Settings.Sections public override Drawable CreateIcon() => new SpriteIcon { - RelativeSizeAxes = Axes.Both, Icon = FontAwesome.Solid.Keyboard }; diff --git a/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs b/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs index 69f00d1549..73c88b8e71 100644 --- a/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs +++ b/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs @@ -14,7 +14,6 @@ namespace osu.Game.Overlays.Settings.Sections public override Drawable CreateIcon() => new SpriteIcon { - RelativeSizeAxes = Axes.Both, Icon = FontAwesome.Solid.Wrench }; diff --git a/osu.Game/Overlays/Settings/Sections/OnlineSection.cs b/osu.Game/Overlays/Settings/Sections/OnlineSection.cs index c22174f050..150cddb388 100644 --- a/osu.Game/Overlays/Settings/Sections/OnlineSection.cs +++ b/osu.Game/Overlays/Settings/Sections/OnlineSection.cs @@ -13,7 +13,6 @@ namespace osu.Game.Overlays.Settings.Sections public override Drawable CreateIcon() => new SpriteIcon { - RelativeSizeAxes = Axes.Both, Icon = FontAwesome.Solid.GlobeAsia }; diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 3edfc4346f..75c8db1612 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -21,7 +21,6 @@ namespace osu.Game.Overlays.Settings.Sections public override Drawable CreateIcon() => new SpriteIcon { - RelativeSizeAxes = Axes.Both, Icon = FontAwesome.Solid.PaintBrush }; From a436f8e6d42165e5bf392fb2cf2be3a9378e72b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 26 Apr 2020 22:54:01 +0200 Subject: [PATCH 1301/2376] Trim other leftover RelativeSizeAxes --- osu.Game/Overlays/Settings/Sections/GameplaySection.cs | 1 - osu.Game/Overlays/Settings/Sections/GeneralSection.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/GameplaySection.cs b/osu.Game/Overlays/Settings/Sections/GameplaySection.cs index 38f68b97ba..aca507f20a 100644 --- a/osu.Game/Overlays/Settings/Sections/GameplaySection.cs +++ b/osu.Game/Overlays/Settings/Sections/GameplaySection.cs @@ -16,7 +16,6 @@ namespace osu.Game.Overlays.Settings.Sections public override Drawable CreateIcon() => new SpriteIcon { - RelativeSizeAxes = Axes.Both, Icon = FontAwesome.Regular.Circle }; diff --git a/osu.Game/Overlays/Settings/Sections/GeneralSection.cs b/osu.Game/Overlays/Settings/Sections/GeneralSection.cs index bc87281f20..fefc3fe6a7 100644 --- a/osu.Game/Overlays/Settings/Sections/GeneralSection.cs +++ b/osu.Game/Overlays/Settings/Sections/GeneralSection.cs @@ -13,7 +13,6 @@ namespace osu.Game.Overlays.Settings.Sections public override Drawable CreateIcon() => new SpriteIcon { - RelativeSizeAxes = Axes.Both, Icon = FontAwesome.Solid.Cog }; From 3c1730d0caaa933263b950a20ba1aadbde16522a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 26 Apr 2020 23:59:24 +0200 Subject: [PATCH 1302/2376] Expose SongBar's height --- osu.Game.Tournament/Components/SongBar.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tournament/Components/SongBar.cs b/osu.Game.Tournament/Components/SongBar.cs index 8d766ec9ba..e86fd890c1 100644 --- a/osu.Game.Tournament/Components/SongBar.cs +++ b/osu.Game.Tournament/Components/SongBar.cs @@ -22,7 +22,7 @@ namespace osu.Game.Tournament.Components { private BeatmapInfo beatmap; - private const float height = 145; + public const float HEIGHT = 145 / 2f; [Resolved] private IBindable ruleset { get; set; } @@ -157,7 +157,7 @@ namespace osu.Game.Tournament.Components new Container { RelativeSizeAxes = Axes.X, - Height = height / 2, + Height = HEIGHT, Width = 0.5f, Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, @@ -229,7 +229,7 @@ namespace osu.Game.Tournament.Components { RelativeSizeAxes = Axes.X, Width = 0.5f, - Height = height / 2, + Height = HEIGHT, Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, } From b9e0fed4679d5b7c6193cb1c48ef8659eaa59f3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 27 Apr 2020 00:02:58 +0200 Subject: [PATCH 1303/2376] Use SongBar height instead of hard-coded dimensions --- .../Screens/Showcase/ShowcaseScreen.cs | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs b/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs index e48ec63d01..aea531e88d 100644 --- a/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs +++ b/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Tournament.Components; using osu.Framework.Graphics.Shapes; using osuTK.Graphics; @@ -22,15 +23,19 @@ namespace osu.Game.Tournament.Screens.Showcase Loop = true, RelativeSizeAxes = Axes.Both, }, - new Box + new Container { - // chroma key area for stable gameplay - Name = "chroma", - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Height = 695, - Width = 1366, - Colour = new Color4(0, 255, 0, 255), + Padding = new MarginPadding { Bottom = SongBar.HEIGHT }, + RelativeSizeAxes = Axes.Both, + Child = new Box + { + // chroma key area for stable gameplay + Name = "chroma", + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.Both, + Colour = new Color4(0, 255, 0, 255), + } } }); } From 743b4f05b3640e728319a1eed500e73bf760de63 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 27 Apr 2020 08:36:36 +0900 Subject: [PATCH 1304/2376] Rename out of place variable --- .../Skinning/TestSceneDrawableBarLine.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs index 58d69fc32a..cffe47f62a 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs @@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning private BarLine createBarLineAtCurrentTime(bool major = false) { - var drumroll = new BarLine + var barline = new BarLine { Major = major, StartTime = Time.Current + 2000, @@ -103,9 +103,9 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning var cpi = new ControlPointInfo(); cpi.Add(0, new TimingControlPoint { BeatLength = 500 }); - drumroll.ApplyDefaults(cpi, new BeatmapDifficulty()); + barline.ApplyDefaults(cpi, new BeatmapDifficulty()); - return drumroll; + return barline; } } } From 75c588c59d8f8548b8c1b307bc7a5dc3daaf91e0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 27 Apr 2020 08:36:48 +0900 Subject: [PATCH 1305/2376] Remove stray space --- .../Skinning/TestSceneDrawableBarLine.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs index cffe47f62a..70493aa69a 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning [BackgroundDependencyLoader] private void load() { - AddStep("Bar line ", () => SetContents(() => + AddStep("Bar line", () => SetContents(() => { ScrollingHitObjectContainer hoc; From dc6acf6ec9f133225a45391723c7b2b9647688fe Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Mon, 27 Apr 2020 01:40:57 +0200 Subject: [PATCH 1306/2376] Various code changes, fixes --- .../Resources/old-skin/pippidonclear0.png | Bin 0 -> 72589 bytes .../Resources/old-skin/pippidonclear1.png | Bin 0 -> 40613 bytes .../Resources/old-skin/pippidonclear2.png | Bin 0 -> 73308 bytes .../Resources/old-skin/pippidonclear3.png | Bin 0 -> 34541 bytes .../Resources/old-skin/pippidonclear4.png | Bin 0 -> 71177 bytes .../Resources/old-skin/pippidonclear5.png | Bin 0 -> 77056 bytes .../Resources/old-skin/pippidonclear6.png | Bin 0 -> 78392 bytes .../Resources/old-skin/pippidonclear7.png | Bin 0 -> 77056 bytes .../Resources/old-skin/pippidonclear8.png | Bin 0 -> 71177 bytes .../Resources/old-skin/pippidonfail0.png | Bin 0 -> 67970 bytes .../Resources/old-skin/pippidonfail1.png | Bin 0 -> 69118 bytes .../Resources/old-skin/pippidonfail2.png | Bin 0 -> 73351 bytes .../Resources/old-skin/pippidonidle0.png | Bin 0 -> 68649 bytes .../Resources/old-skin/pippidonidle1.png | Bin 0 -> 69329 bytes .../Resources/old-skin/pippidonkiai0.png | Bin 0 -> 76964 bytes .../Resources/old-skin/pippidonkiai1.png | Bin 0 -> 75434 bytes .../Skinning/TestSceneDrawableTaikoMascot.cs | 191 ++++++++++++------ .../UI/DrawableTaikoMascot.cs | 99 ++++----- .../UI/TaikoMascotTextureAnimation.cs | 39 ++-- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 14 +- 20 files changed, 207 insertions(+), 136 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonclear0.png create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonclear1.png create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonclear2.png create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonclear3.png create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonclear4.png create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonclear5.png create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonclear6.png create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonclear7.png create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonclear8.png create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonfail0.png create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonfail1.png create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonfail2.png create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonidle0.png create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonidle1.png create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonkiai0.png create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonkiai1.png diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonclear0.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonclear0.png new file mode 100644 index 0000000000000000000000000000000000000000..a5f4d03e2a61075513c85f24ab39280c31f591d9 GIT binary patch literal 72589 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�F*Q&jpbtTl0ro%YGC`VnPkLo4Qt@L z#vt!}@>>R}YsVov?S!j!{xxkipf|%A_e=R;$v0DVATGDtBRI6FdY{h@Ke=&S!$7q@ z>l*d!Rz-Ch4Az~4F4TyJ&ni^!zhir*p8N1a&n5>74ATW2`Y53fq2lEeVe6JFF^P>Y za?qwxeFYt%Jjr6#{lo@(XwqYv^26u4`1orSUZu19RWFzZKV#K*+VaNT?i0QTJL@#M z>17S+G}Kgz+_S#f&h1Ju#N$Z)&3(oXZ#XlSDLwuXrNo?K9-mfL#%cuw)lq2FL+*DJ z4OyA1O_?7Wb<@9D7uBs(hz3={XxR~Wv!2In99^hg zVDOQTz4;k!*qIYK(YW5}s&h&4DtT!kV)sf@Hn|mVsHs(j>6n;2@7g0$umAFj#^3pf zIyAq)RP{WCm7S^|jk3=8xb#-dW(5VS#SJg%)DC7;6p&}AWt4t;+&#*OXO&#(DgE*a zdsE+(nDdpk=Sribe>4SXXPXODSoJ5wa{S`Jk(9IZkqtK0xb}w2k0`8sg{D0a6Zul6 z&#&fKA!-0MFa*I7j1(ed1}4qb@@v#SRrj5JmM(eh?8C-Pdt=q2-C7u%*Xc0zs+28?rOAZ zTW*D25GcoC_nx75KO3(r`;JmbcuV&yE)bXf!TiYz|E2VDL_5PPtK&7#YCs2as~mcx zGn2nCL;EY0?Qkvc|1W__ZU~tUD|y#x%1C(GC|@FFy%D6a8qX*wtf%|9XdrJ-X6m7C zk0v#{Yv2fW=_`FC?!vQLK-7VcaI6qDzXRGu{*WvMos2^X$FLPc7ZMzzN|8PE*!kya zFS>!x4F2uqo(MPrjhkWHyg5 zl~kcRuYGSN3@qBdj%9(2jCeD=&0UzA_s1y3lft)fK!JDaR7p=xdOpdP)>HMJ=pA69mv()QhE zNR0JX6&>uKxWfvEb0kkz#=$qOrQ69nPg82_B}O@qH=Psr_3)g;wqFr7UT|+zTUi%97uOa7* zRDwG>5X1ia&c9g)PQAuDF7Jrgvqf(`cY_k+y~-#cLQf1Gt-C)zx*QDie&3_>hL2EE za_%Hlotj^ulka{*j)+1m@hkxZWH}Y^^-Y?-D^qL!`PzNEkFZW^{6l; z4N#oTqS)8L3)!ifw0w23%f&|DlWObRNna@$4%{>zPmzugeoXk!m4J^pcJ9C*(16K5p6q0EEt z8a4WneOhN74bo4XvHq zNnmxfBn|Rgw$CcpVP~FFW+V;P#=3IYw2bG8NelG))F*YyX_xBWGw;z4 zZ!grrBJ`s~dAvFLb4_2(x*XSyuUfs2`gA|pdR~!mVoT<1b62Hx`!;Fzq~f_hywUfB znwtFMWQB&YxsMxJ>6)_Q2c=K)ZDzZ${V{URo2ErCJ)oC=nX0*eib_jaAv` z{QOFkn?C2a-MZkCbCi+h_O}Lx>bzbTsKIr)b7WTpuw(lxk6*0J^qi1&CY0_u`DQ&a zsc(Vu_Vrb{kAM~=*#WdFE{Zn|TFoUL;!?t&0@V7G#HZBuB%PC4UuYv=A* z#l**JPhyhdQxOV+4EP@{g0Hjxz6UPC4lrd{P1S21uYL{c>!dEntK%gXt5v(o zt_TtHDYji(wJJduoi|(wNjZMqTJ=uXO@q!<+@P!7XDSwcdGJzgh?(p@a-r#Q=jn&Z zZ>d@9qxU6~s^!!1i*@URm&ut)9xfNcBf9FcXMa>!wJ2*B`N}&tC@^M|LK3zsAoYNG z??J^JOw-21RP9d6(EjvPrKBb*J$=72QuoM}k~Nn?Di92PX*cALu@io=yHH?|F~cO@ zON>LRUTxIy&E{EAYhmHQD!{DtC32=OvMeiuO-G9MdyWJmkb=WH7#-1BL1DcV6izeq z4O=?X|5n=GtK3I9h^*dUlg5ov-=Rm&6XQ#K_0lX|_2>vw-U)d(5BSj79AkupP%&9# zSK_L=yb`s2A=HSx#%s_X>+A7L;}q*;j0r?`nzabDN9(gR?azsdiY`R3bRNX+$yCqd zhimWNX?bso&8+-gItQ*JH(`ew!Zzh-HP(Pgg+(-0)v!9MA6d&huc~!1tCetYk!b;* z?=>nVT9*zuLs28&Dwo;u3WJPU-|OvYDd zbnKmmBwf46gOIS6YTmGu93cT_^y5@=a*|RFNoSF=pl0U0iR1t{`@UN`HPl^^Ea0iBBVx-oJKv7DHodY^0@P+o|dKa4@d zHu=G@_M`or*X-A+^(5`aK}w_tyS!M*C8|_yrt!BttV^D{oWmmHqX)jw&13JA)3Mje zyyr%uF^HO*fj^4B-^-Zopv_gAa}jm{Xwu?*UEZRek_L}1_X}K(gb!~$RY^(I*&Os~ zdy%Hhf8WaUQ4(N+@``UiH8mblx-|UgQ_%QUzgQVV?UR z7}7%FHD0kM-bxcpY&z4ISqpPmT}xyK8R=+l0l|$F7~I(X*vNVe$R}RBG;p}Bw!q^T zKA{`N-+jbq@b&)5=U2zEpHn zGGt4*9giII+|Eu#L$Kdl)fnlli1O%fjLTB>)gtSvbyOAgX;4eSb=xbY z%c)9_DmagGX(KrGpn_A3B_C8)Qj!AG6BU$}qy>Nes^3$TO+y~lm-EIMCOUexd?a{Vc;YcwA!K`+#Z+ulbTu zsy*DV`r$%`QeuXh4&=}bd&Fgrzv>x{eDx+P~BwUN*2!O5>0WsS=$7bkpV3|?4a z%yxTNEMC$mE28EP*~%Egqp!q~3HjnSsqm`N3X7_(!0I(E#g~5ljoTVGQB_N0(i{MG!21AhXS*m<}(guWjT#Pbtn^+V21M!XSN*XTqeUi zbDU}FhV|TBlJ(p?W|_O@G`(4Ry2Dt883v{)(~&AiW{MmcNyZQvxiPfMD1G+lIP;s` zCrSXMrM;;CE&6>mZN+z!Xug!S7*rf3MsMqxPH;*XYC`31CpafG-GR7`4G*XC~zrZzdtz0LQnvgWcvMh+3 zYXgUn_K{$bxJ~5Nf9~S~lzDkYP&!u-;jkIp=dVsE@WQUA%SsM;iV9lSZ8E%B!Y-HAXTLb|6Q* z;fLc5IfPn*C~cyq;zG8lV+a~zwh0bZ%JOeiDUU@axtetb=*?@d*RaPga-S(0a6NTz zGg9kze(OD9K;h$u!ud)p6bXs#2?P3g`?t`s@OvO_{1tRAbUI{^VZxL5@tY`QpS8}s zB<~IDtd9sk7ZI9&*0+29PLQtbd#%RKc-b7gKTtwgx9XsV48Bsk_Wj~MlLIHCgUsSZ zN^#v`VbztE=2^VrsSs}14F`gzkHN#DRpP;%a7U1je;L?~ z$(v|-&LirW)@a2SK&*%Pj{Ly)@;wkb4cAVq$4VXU`5{CN0)iV@tNNI1{I9{0Iz#Dk z_gc^ML{sDfld^Sc57xv9&#U{O(kUlI+x!&#SW0>(yRCgQx&xAVoCGJ8Uy(wF9kd(U~?sJ`dCD-4YmCn)Rq zz2hV~ZxB%Y2nfqzIR-*WJ+@HGkvC&FP&dGG>MY43Rs32Sy z5GcXZ++@m*PL%SrfoSj$wWacmp{VTVz`Nb#1Dmw@yF>$6%l7gkY6}6C3|T*|pzz|& zt5if}J7Sd*JKQ`uXbLJjFA-99)DllU4GXWTliFRNuV#){m8!*T;DrRymt#l0q{n~$ zQt2uGdS4qrIpa{r9cufgi3szq_a%H8 z4ymb-kSIk2S5f8AN=nO2S1J=%tjuy6%S=*MmMJ64zm=uS23(@?Q{OEX5wKdYbL)$= zamROdoGb-TPdVNgJDYP;NQiK_2?PErW_L=qLB_U0%C;ZVCUt&0mX~PlUjYeYd>8NM zv-Wpz&inJc5AYn=-(g=DuR}^Gd(^+(Veox?7q9bOJlfX->`KC8-sc!)1qsrLL5O?~ z_Z6Kn-{(?A^#*Ior?04epW=;pjs!De*7&=>(1Ra5BS*R`cG!!kaa+;aAWmjGd4#B` zdrSr+(In`_wc*DjT%fjQGspt;a=mTXA&5R zNQdH5)j?7E-)UFrfp1y-!P%09!?g9sE`Cw3O?y*Wjy>K)%@cceo(yNGAg&qr6uuau zG>9C694H5CG)JUHQDBp{Bh!)ZB;~LRuF~X5AIh2MHU1W>8W}YPS32EVdaeMg*p5kf zM|tFlq@XTAfCwU<(~0Japa~1BsWUqc(XIE~t`jflT%z?TIWbdjUiGA2p1)8D`)A8( zDwJ2}%MWNz}*QwW^Qs|t~5O;inw0?`U!whOKB)MrZaPz76>HRscdsiB^9B0CSz3Q1TAjp$p zkEiP|BJ9Go3DY*tdz&}B&)WoQmz({(DBokh*Zw?@_H)i3?)z-mmBpK=F-XrY7u!X{ zy*|Sj8_ikw8CBh=(NInPY>e9WKya5lh_Sx)^wIik;m1l#b0yI+pdq1tW*NhUg%6o? z_>K_{XBqRv;X}|wX?VNaf}iN}r1q*~4_Su^xFf*nxZ?nt{1}(v`X+^9SeEhv${biOm$IP)p&+@`c<#R0z?O_?K zEr6VyF*OxY>lH)V2+CsvY_FASNIKiO)l{5ZBn@a=%m>ktZa2n@%pVXj#R!}oo^5eq z-KU7kZPc|z4~;y&mrlO(G9A~nl65}{189$pLFP|hsn6efPt!JT)Peo8%vCw_BWYAK z`@j*N%czX{gPV_QEUs`X5`0b6w#&oL$HA5z!;D#t!W!}ghwJ?A*J;A9FIbhoEHM4u zIU0W7ElN!At$@6*mO)0^?!8}69z$RwEqiux+glCgBpW`kPmWY3vj`tX`)u79-QjgPH8E!H6)6hDVn~v1a5lzTaTXy6+HBXm}GnHTY89`o%*<>Jf^H#eeM7h*7s_(>C0S9zXzgLK3dc)9~p; zsz&$KjorKJ`Z0H_UdyBFNY*Ucp%Itdr*%6Sskwxg7g1BsI6&dUCn$T^ITs0`u0sq_ zhebVLO1Gl=t({8ScbA-Lvn?{rGeJi=;F2QT4K>Z^6(XcD@`LTUEpqBG0wJdlE)h>T z0lTaM18bQ3c${igX`tw8wN2SomeX_!&ddzO9oVC|EP->Rpa+%PU19Up3H{a`xZ@y>HCSVt7%)> zobNF0)1|s{^!EU5Rb?zg>ECp5)IGB*B%TBpn zzpnn$NV@d=CeKih_PTrAMf46H60(<{2Zgp$SoN2zBuW(x>G6-sk^F;2h%a$q9S4tJv*nDct!LVIi5v6=DMcNcO^l!g_6TW`T{Do5Kcw8#0EZ znS4g9;iJa=i5r9)SgKq=qw)nP9n^tc$f#->tgBJb`vpYk#zAMP&ycbu=@di1JCkw^ z!|*+!vLGAGR$Nqy66?v1VNijo`M8Rx*tm^wA9)Iy+4N1@=IDnf$NRjtpMGgKy>`c) zsuo4{v%tONzIs9j!y_8#>|R6k+Cz8f{6|ZUq(Nl05PdT94fSn~$IrDe3-Wb?ClC;m z?Epexhz$`y78bFg^qd*%tes<6BvO%(k?`7)NE$Z+TO8SHmDykFjs5$nGaPfC*V#6Q zg=g%1`=&8mB?u_1tN@~&TBFdWUvcV{Tj>TREPDjJ<;QIY! zT`;J`RCtEk#y!EBn|PAKL_%~-9c3=RJ6Y7%3OtPp8iU zBGfmbawu_z!uXb`NDw>UU6jJH-_K_mIWsrKq$ci9u8qzF!9j@uP*(8=KnV{(z>K$K5X%0760y2Ahf8*xcQK~&Y* z6{x-!kfqjlppJDvqq`#4J2Y=woC31;TdFp)dLP}~|3r;?`XzPjL4WK!L`H?GXP4gk z?#C5|bhlb@(_X}d03cKlHyf@HNQ*dK+7OWs7OS&5l2Xlfu97otfr7*O7}0}yxGfXWF+D2!<_t4;RZ7@tB!iz^f&o!ao`5yiTohF|v6OsnLB5K?okR3$L5R>R; zYch$QFTyK#(39iFs9po>RGEU=;g&rbDt*B&{kf4&+PW7s43srDn~K0q#kg7Abl&7A z>m!5mp&Q0rAq=igk9aeScme}6wC-S}E*LskRSl8(5|DL|vwJCE@h(Hw`&7AFYu(tR zoz8vqMRn;`wNwN{z2hQPDY>3z%>UJtPKttpgS^R_GC{pTI*dXP3FMif{?Ud&AnKaW z1Z3}1X7UuvcU|86)|rX#7?OV3RDgU*8gl&17`|x;k+}kUbsw)Eu3d57o2(&E$eLJY zK2JH(!9lK&EQ?YI24J2ELm}ff;T#Q}=pUkPI8O)3wNT#N1HOarL*d&ZtxVgyuZWsf z2T|jHLs6e#jNZ*sGEHmt*Zoh7v`Df%fIPQd&m5$TMSJwu7Vf2WPiQ!FG*k+17!@P0 zvi753K!KDcb%HqyX72Gd1wuF}se2W@BSgJ@^;aA16Yu*%p*X=xfMTUc@lXPV6!)Sj z!KDOum*QGHI4x4#U5f-O6oNYiibL@Nfue;#Z{BK8=hE}E`Sju(e%!LcY;GctlKU!rJ~X{CAsEvyCHVBC6TcK>Iuta6=z=~_Z) zHpj4)-3Y;ef5&UnpDcK{AR+!Y1^!0e&>qO5rv~yuG~>;IF)aYV_Sk2_O}kgqG4Jyo z&W4_j2)^o!>-+JkA+7axM5c7+T8dX`q`9+)a7u$kK47MqwGhIh*}*6iJ;3_R=#g34 zW5urDA`@rG)wnaCeL5#EXNHBZUgl54aL@Jm%`PV-Lrg-vKQt1>coyfYGaMTKk?Zo4 zK{_0Hh!=6XT{5{g34j#$sV&0~L!mcLSL4@g!CKQ57gv@51Qkx-Uwbe2h{}PJJ2~`< z#K&)MWt8=kmcDQ^q~HfWo}bUx)s>ad4mD1VbJoh!r$(hiz15OrT-EGB@|>uot_iJp zretZlQceM=`_YFHwFlJa7bZc&6B^K^wdDl)8Nm4h>Xh}Tb$t<+<9EXq;+9{u^2D3| zcQtN|1sl-OKbbZ8z5;ohO)ZTAKDFxDxOM)_kBH;2?D?fjj%s8;p;wvhYS1j>;fh<^ zT|Ay4Wd{XdP(_%4Sp2m8wh@y+GnHi9PQpnSEBhKiw#R2_Q)1iK*>2dgbWOn`^5z?7 z;446m&o`=TBh|T3S}2>n)}$JX5M@C;3q>?yN&-{6oy#gE4vE*QeFFbUq@oGQZ*D6~ zL+*)NK{mM7*&w5s@veoD%j_<}z-*v(6l`t7S7by*P3VZ!zIG^C>mW>0(WRk~b?O;(c(Nt%y zThc*fU)WuTRi8nR;zaI{)=Hin|6GWk^-X9Db@Z*^pGOcm!$M|-HHZm zaI0bDcT<_oQ`wn*u_j*XJmELUa!EU>l~Z=;au7?Rd*fRvYq4e@;_Iq_pN*g*Pw(Xx z#bXvn-VGf1cO2CsR;S3a&*w#ETC`>)FtFTP7If+rIpaS4Gi&?=@~ z0!d%$&KWXd@CcW{>O*;^=Lc+0iMf0eZ(aZ#6j@VC$TM0~JA2K;_n^a$7GGXE9PuK4 zLxu$xR`b3$m^enc*R(t`7HIlBi}*nmcQ}QbEGD4q`iN)b!t;2L zA27Wt04zOyQ!ED}1zqC+b$7LTGL)Osaki~Ly)*R+)L-{9JQOKG-D|$+h%#zeP(mCD zm!N}{56(0*!aaFlf|{jbOAx}3LyA_KPyJGb(Pyci6q~AryC)PA)^&*1e+=sMJLX>_ zHPb9U`|cSWYWFzTt`?C3>z3rSd-9N{8wuK+T9E*vAie0wTX?dnwKcN)U6=7J=b~6@ zf|KX|vb0x`GOus+9j*d`OB$;E=l%4_k*#)=_Cf?t!)Tq72`&Ky?3@1f%D8S$VnH8L zMg7aT;S$W6`=h6(kU=X1R$Q_6it` z>sP|o=n|0DVke9bZ6@sZrXzuf=&IVVXgcY0#4r;*rTw7h+=p<{>BUNb4uX8vzSDC{ zI>pIB5n1)t^#+dcCamY{S_W+e;J=Vk`PDFz3h*F_^ z;)Rbt2XaY~j2K?c$J;5ZJ#KR_2D}4W^d);V4;x41d_)ct9jGpOVbMTqw(M_5Mpn$I zFFZ?5+Kuh}6^Aq5Byc%vb&F-Y!G9RRk~zci;B74g;paRDlwozsC?he7#R*Fsn!(EF zu%r)=PY11<4_o!d;X4D24mwxP^7(?;Z^g*0=ASKjg#%cOS=7XR4+pnMgYCC)UG$>c zAdSvc83-c0Td8PJO7dKmvGK89TRxRqYH} zXK~shaeP>}rUBg}$2POO;;tvZ&U~YN3qe~Q=PAO$ML5b0vH!lK1dCEKgC!(JVm33B zp0P9O7t+3CC{xq)dXX1W-qbYy*FB4P67xLJGoNWHiEzQ9d7z&0$8H6)qu}il+T)8I zpNp6`w8EOtVKyv4YYvI`VkS##fjUb9u4?;qnpQqadQt64W|3ENSYZ*+kTaOc^|a59 z)%IaCapGsH_4EGz5=OS;F6>Tqm-B!!qAr@=RqtTun8xm+<)*GWcGK0;{^3^YVuEzc zKkePr&!=kV=Wc}GI@qL+o@MYstPh(Wn}QzoW?E1cz-5SrBn;0Lp*&1G_8oFuMk}f} zvFB`#D>P{OBK58O(v_OV$7{t-6h0^WWUxCQ)Dh6`B=0)KRZ8WPAod6#gy@@KG=r+u zFil2YR-@dcdUjy%ih<^Qp)2Aanc>T9v@U%dbqsQLvw-8P1Gu$-Yr++#0R&N`Pr0fS zxd36`x+3VzD>|jC-mjCgJLU1x2c27M!^BvVDW#n_!c3W}E|8jgc%1!{kUlYTLyv4A z%IE&8gtb5zYve&L z6F}aoq0Ox3(N9A^yy10dH43vD*+UaLXdp>;H1(S$d$0`c~Jbpw|8 zqD%Ji1_l~@hN3E(60WVg6a6*1-SxY9&yQ6S&dHP-gQ*rKRf2bTCAlClaH?60X8Qgk z4*HtgKB)q&f=_(AAF%#W!Q1}Cs$*-P!<+6P_D zHUCe$_q|;L%=!12tCPp`Gu8rHn|~w#BsA-*BN#6Ha^`pq+avGybUD7Y3W6^lfBKa! zx(21B_K0VNUrUyh%?jW2GS&W!X-%iT;m@u=7EyVu($U}3U(8ah&MOk3X6optOfCA{ z`L5WvXMGhKdkTi)IzNkuuPlAL!6~PcdYeQ@5LeBZAbj6#UU<@dX04vxSZBQAI~#d1 zu(F9&*#TR=rSyH8UD2@(OwlR5GaXPAm%>QRO{HV;Vjb=X9he zPiTVil9#*i3x4m%m6`=h?Wl69EEn?$9hP8NomSESqE0k;q%t14U;j~Fs{I>O@Ro>+ zv=Dzq%|CiHY1H)h$Z71GE#3J8!{!qQ6R+bEKmQ9$_D>CAx0nxhOI^VU3e?W7IV8jt zFKW^M~GV>yk`}r9Hl=iHYirp$1Txout zOGke{(SpLuc<8i5+}U@g&#e)|)WqqcvxrE;?|6Pqf2(EorS08bBDDTC zv3{|}Sf|flVS)D-l@atJ&N`VGV?9@?9w~V@&@3v@v?=`2j(1hRvVr=VZIH~u042RP z$*irL`(KgEn;n>B25q55^uScCN<29N)Za+Yu8ggblK7;v4!&nxd7YbEgll{Dw(s8} zI_I9>)&nT@$*NL1Jn3hiyg`Nh2GVnBoUx>rk#X^S7%>-C$gljh+`FjVzX6UBzbKfy zT{}jPlOGF;T6fHk&^p{e54qF}@z*R7?&kA!e`$Sj7PS$b$d;5OwwDSFE$Z{flcv?I z5=sXhgjH#N(KlQMf^e_tuaIWh8)!I({9`YP!PZp=8&L z6l|gCI}Yt3yCx+aF1o^SQD|)>QT^ofry$uh&qXGsr&lm9U}~@!V4-i?*TY1yf0+N4 z7Kg(E<>uOhNrV_cat5c%_wx75N(TR7%(F&p1-#94GwWGo!}RqM{1l{&5Y#hj;%#r5 z<^zgH(H+u(;t5ktqJb6TMk=NgkXZ7_{GOmj={>Se-C5m0D<|S~n_=D@LWr*5Zlv@+ z2gV+`4X?6C`-N6ogCf1;fICTr=jmt;hkL`@^Vm3pU`z!s=Y4loP)9F%TNEp|y7GIi z5P{FTf2y0spT1`!Oc?4?E%a=ht?$5tfJDJ4k@0W@SXdDyPA)4aO9KEFawkj7@gBY+ zJzRm>m;}d&UshC&1ZcA+r5w!(2m}-f1m{%HIbgx}UM1=4U#rv{XMipw11^tmmt&XK zL*6jW5uSh{T}{Vy!(aXBJEnoYUuzhB!ot|RzJ)kktdLJ;A;12J{ZK3)b$k7^=L4O=`9)=-J}E0W4O3nQmuow6R~KZui2n12H)H&dbVsoMhk!^l?5My z6m0FHzWpykM38IhHd{vKQm5vP1Grk(h_tY9t}w{N%YI$W%Mh$>W?R1{>NMS(16pd} z(LHZHn(y(q@MQ6=5}KLkfs7tVxDl|}J!cqRI3|H737^mpElUmmgb2?3G;R7q#^_CJ zfs4qo7(@OoC@83eeW_Xty0J2Gb#T$p;R?9_%89(Hq1{$qx8zRdq3dCI4W|>Ws@4yX z%_(BKjk%SI$JWFUxk+k8VqxNfijAh(^$J01Wn8AExYZcgqFS|a2J2Ph|CyAN$v=L++GACipK!GnAnpe() zKQf|}!(0v$ zXnGOC{5EB>yn6fW+(F7=m<-*Y=kC&yDdNn%ztk*iqHjMVQ|N1v#?VpZm} zmpkJGk!@ZOWAXvJRhxwX+1Ty}n*zz6dqhz#jGy#qH8~%Y7hH5z2fZ(Rc&xXf^JiC-#f)%X7Q_aOC)? z25bL)UxEoO_5M}iv;VD!gL|3Ig=YN+kieO22na^lmln9j8+};NIuvGk zp4g-KiS$&*Ln_nQ8D1UL07cO=H0{3V>+7Qi0BdEsCKkZ7#swbEuR~{dh=iL!3yBL< z?|QgxP-Jn%jG1H({d@RczZot15kDVTo~I$B&zEexCN<}1aA`i2J1s^a3YuA*(nLsq zh!A9XTzTJNo76B9AIyl`{?qnSi#Ms!>2ni}F#3F9yAkg?!C%FOM66X*F=J2%^$Lxx z&q{xN=#F$2L(7tOoqKvD=#4V9S)$f%KIYz>NLUqDwAYMpBlglF>bHL#ef z)`~<&y=`p(<@#Z}+`dxYtlaR+E2Sb$359QYU247aRaTFGql|+mEV7UnY}m8*Cm`7w z)BNs+Lp<yvkJZ5)2-ezY*U07r1=ImQAa8vaO$A2GE*3ZH`BW)Xn`B=>)GTm zP|1@ZS?#X$#bri!;o`8Om)x{q>GDsTOR`1@pZx`uVf&?x+*n6ijuq^pAg;pp zeGk0_-m__iHptOyp|*7vyS%3I|HO!qKUPu*t*JCP4I_>qV8(pkqyJ?Uzwp`&L~@xF z+zVI#-W;T7vR07p_s+irdF5g|*9w22tSlu|nTb@N5p~g+7uA;39eZex{D&%iN0@tw zkaG`*Gov}zHa_jq_KNiuAW!(Ylcw1xqVQYnPkqxYk*uRIpU-Hw2NvFEX9lbLuK zIm+`%Zz;PT7sr7csc7@P3Z{PiYwo)WCpoN@SVIqXX{Hd-;fM3^>6!xYc~kJCa|Gyx zu0<<{?Vq$v!}Cd@nwTFYRuDZN@TH4fK;-9_jJhPjOw!#$_WoaZ9*u|b!-V^t1YBQ$ zQ}BZdYk{X!UzlkKVZ1|&91dH1Dgq%$4(2hywa410xccz0Fy1DWGy< zb+ldceKpOZp-Z73m&odwOwj5Yn!0cO3g5mYr|(PnG)rNl%s>20{7Lou8vcvOKuR2r zyJr-PyldtZG@v{0YROtMkWK3h~$vI59NaxksQ}& zv*;9EZzDhCVhv1c+?kzF*!UA(kHm@o5m^(PN&VTL#N67dq$_`f!xkp|NWGZ?E#NhO z@jsT)`C*&*y#h|0r_W}Yh_V6XGu!6J@taTJPtNL#O>pXCGL-_xExW_he91tmt@svD znn%Z78I^N7&7v526VL{yy%e|5k(Qb)GF1-*k#!>RZ!iT_;|gR@vd z1W3*nfO{++ur{a8DB((8=$;3lhY{;_Spay57CB6_(^%g8-pa!(+3o@qS3zySy*0{4!nak2Oixut-}bAS{nZeNRDbzuankut3X&Xk6X zS$5zAhxPJbV`o~}pZ{lWPvi1&3gbaWcG!;C%ge89QpE>5*R#u^Uz2KTs-19JNO4+M z=~~WAvol7)RR6Qlr@!)rWIJ1A)~<@ww}H={@|t)A5x9jD4lSzvK@)6fzE72Zl>evE zo4UT*ViC~tHfble5X7U;*A3eYnlHZnH&04p$1vvf8@bdQGVH4MfA2;>*fAHy9CQPn z*u=^mOG|Vo!wPN_OH1>(iCH4Y(uzJ~9}l|0(srH(6<%Q(V7T`FGh8f9Nh}I$M*lG# g5&Qql%2@7J%D#)R2V)X#`VaO}QP7aDl{E|dKa5ZyL;wH) literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonclear1.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonclear1.png new file mode 100644 index 0000000000000000000000000000000000000000..b239cc561c5cfccc67f04badfa8ab1b54ef6852f GIT binary patch literal 40613 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�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 literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonclear3.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonclear3.png new file mode 100644 index 0000000000000000000000000000000000000000..31e0b91e9fdcb82988b6b8d7b2505a38fdf70358 GIT binary patch literal 34541 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%0Dr63wml-6F*tONlh7MSGocvsq)+w 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 L&GqVZUE=-^HyYM< literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonclear5.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonclear5.png new file mode 100644 index 0000000000000000000000000000000000000000..417af6c61166c727b9ab50cdd14a498c68744e0d GIT binary patch literal 77056 zcmXV0WmHt(*A|q{fuT!sXrzZ!VCXIh2?+&6V(3OXhgOhg04Zrux*G(B?if|F_Wkor6#`;!4`YlH@DW#?-|7Z9vr)VbmUlAsO1TJIKECw z{h?)xr_8ze+cf<;>BnI5`LgR#ck$wGfeK}_$f#hM01miS;Bw5vV zf9TJ>&u8bVt;k0BR3tnO^k}_~hl1q*mby_tgzh;++Y}sG8+SMqKHtK(qQ2Qb46?np zp zF&Wp`D);sOg%2`)Kpjfn|KFi0PG0dkTI{?)YTSfWTWR=h`r`#T?EVoFL(HQjM1-)e zj5@YB;vw@q*0rW=lHgi!L?9u}oD0GTqa2*{1Elp%Ts2-Yox~Q!yefQF zP6m#B8>4{0A>-+}X$Y~^hvYv4JjE5}UPE?e8~}%*wg|W>04DpDow}7B7bq9FO`k$1 z@jJ{k?O={0XJA7Ke;z@KOU$EI_X&<*-UAuCBdh_OiIf~bTre0MNn^frC)#*_DK1qf z{%M$Cz)CX$vdFr?ieT5NhOO{7Qj?xjzOUA^(9{q760V7S~CYMVp2ZOt1*2(kf) z5xp+eHp8pop@PBZh!SoJ!{#YV_|Yz_K3RF3Eez6ZE`&>(Q>4f$!|$RaMg9hHw7 zIPSdG<@`FK8Jz+U>ro<#%ZV6bwLu@UG71g0reKvQ?E9Y-7%C`O!w>hOW*~Kn5G$pz zO;)k8EFMk;(U9XZXDuc4LN`6x{6bO|88qZhp|c1vY(q_Le9cA(r>H|(Y*wSB>VK?% zK0h!F?-@7sN9-n!>ss1slCa_e_DYLpnIAT|oJ`z}?Tl$$(X_nbrOBTqX1u4qkR?3OL3N6|H zBX7lddx`A%s2N$M%k5!NW3@AyD?6a@JvQ}yGZT+05NblSo$Tq56%0n$@Nfh;wgq6Z3)rDC}z7t>CN2Aeu1Sr~h+%I&7 z0nY*+X4>-i`TwNvOvsV49~&h|S>;-eqg!N(iQpER#fv+bA6Trp8K_KbWoaB7Gw0K& zVTXPRpZ_d0;z!r*)d}aHUzb5_wrd~B57Of5c@W~sq^(_VTt6t*S;4aaJSswO6|jyNb$o2rx3Ked?Ira62x^VJBT3v|gKwb1&O1sew zS4itK z5|@Qcth6_Z+$9)fvg8#3QoB|sG6??c>j%QWqn#KtAo zt>rL$@HfJVCVMI7kU<6jst|!op06KJzo4LCXb^9FIc^m8N>uohw{J)HGLf+XpY1Yw z!ZCp5$>+-nk!4CKU^7h2T+1rdDPeac2*AxvjI)q3zuZWTc;qSkpn%K|3uKj z9FurJHpnQ>1{zow^O3Jnv_2xMt`0`0F$YE5Mng(mNSR1+`HnwfpAbPzorL$A%GTjP zr@!PcV4Ci>UCfD8T}-RrKKz?Ho%8m|hL)YlLM?{}w}3xnwcO^|HOl1kv3l!V*c9h< zhmQsRcVMS|5;~KwJP;1=+5J!U#5DckY4d$Kzb~RQr_7wMi$k1`vFnOSV#PZd2d*oE zTM~Q*DpsC}v(oYJBXcL299OOle{Jw>TGNUg@dSdwKDg_IUxo&;KG_g8G&RT^<^n59 zpJ$TAX-P!)zNEmpifYHEzPepEO_j&-jSJV3v3a1Q(JLB`aZhdrs<{ z_7Z~vH@)swIQA8NFK!g;w1UKZCTrWv&*~rSk=b_yxlUrw{sMm9xcW9I(nWM9 zy3e_uy@&(#Fmv&d-Y%%wo=xTsssfn0837B!xS=6}eh-UGLLCgI4^jYf6QVn6)7(-W z18S>UmRqrsft54CCoyMY8_R2%jDE(#jPM(F4gBRlB!>;QHP=1k^s3Z@8yX(aQtP>} z!8qYW`AC$tSn~@|_xR!L?JY0ZlToU`G9|OeiMTHr~U zdTC?mhs`6S*sYAz4Xu#?M#CxEkT<3V!ogkLRm&s@!bpYVC}hupRGftNAiEigI>>+2 z6|U8@-~n10z5ODQ&{okNHUphk*O8mA==x)sAo=OeuUBJ^{X>WWywCIWyt^lh`1kH2 z@>CCvoj>#R6mz=LJ2JTIaJz9y_paJhzC0S@T_=D>=DVyhOwd^EVKUZjS!um(_a~Yl z**r22{jiv7_3w6heA6tcrzG+~DtgQVArjRWT(!*#J5UCE#Ybf}ji%Osq%ftc&xXoi1{0TAv|o^j0N|ENlWaSV6i2)og2p+ zy8Sq(&Lie`d4G1s6%f0-KfZF z@NmciS9y!#9U`oZdDBhGGeL^k=hhTOD#56nF9qo86dBx^1AN)|-B+##`#WA@ri>z^ zWgl%V;Z3q^-!k4miByPb+4r4`v?JmUwTrm>^Urq{+vM9r(b>8bL0km>Scfst9#B7WgR*CE>@p z&zqb8R7v_ki)kQ)opTi%Yg4%yP^Via5D%f^)9s6xct?eRBRl2HXm-1vk*HfZN?Y~Z zn`(g|-##wijZRbn z_fCZHil1R-sXX2qKn`X}_K}hMcXj!2C}ms?|I$X+|7c~3PV`pN(9p^~ z8OSDPn8Zs8i~R0-5uWz*4Pe7JF#{JsN^TggSb`fC;i>-O_C?g*%u_#ruOOqIg_2m4gP@y7_B7tiT?DgrjdoJd;3QbE0+&%`Hwlvy_xp z7}dCoj+dn4kaKM-=Q3V|e#;F-56Q_XUr2{nI=igy6O*9@#Rb^mN86De5Fs&+U@Bjf z1+LA7{XGibSv84eXQ`n?sS1!`9uJPK?YE0l4b_Se-no&S4mtMJC9`-GElk=F;C1#W z!QrcPhIo19IoLjHk+Wy2e7i59veQPI5~$HN^2gOF&7cPov|}t1q1!H6a4Y_c#b9nG zw7&g4BJQ$*_V>mw(pR4%2)jZ>pNK(k;?m#ZvuL_Z65aSb1}a6GyVm;nx;b4(^+7?r zWu00KlY@syJ(+5}$>j)c1t3#&0xewdI+|Uf28+d4CUam&&mnv>t+->B?3e&YGm<`W zf`jrXTbOv4J{B`T+SX7CJ^d3;!gB610U3EFX?~uxC$XJ22;eV_X)rO7nDP}ib)dHS zsiv^2L?=j>TuYc-$KBOMF)fv^G$IuK2|{4|P{z#HA|mAYu-m&)9Z1lYbI+T1kc zTf5yF|0EDIG-s992}5ZOEC$Y>wZyAI25f${OgCWJb5z3(sWE^6lDb`M6noe-tv}d< zs0|FZj?erZIUzbp!TDGwOZHuh<+z``?{n?yIZuAB0F4O58CUbGw2E~ry8OpTHd!)z zkLG}xDr)j73ivJpW6J?ID(f!_OL0w>WQmVxVgmsBUocw}=NpnwF+^RCVn9`UukvNX z4h{0fyZQG@Ds$hSzWOQ!EY2*iJ-P1t3TEJf=#3S^qKh;hcV0bxzlOadBXtNM_xSfj zP0nzq8L9hOo66T--fApZ8L*A)QMS1Xjl>N#P2=P{YZPYS6BtX|xJlra#UK3cEB~{& z1IpBP^usKXQCSA#YZLC06qD8;!51*uV68I8=bKZ9&XZFPv%2?d0iSrTOr_(cpd3od zQ3@I}b{674h3uv~sT{LmfOT_>$5Mk3_@7tSQ|~dDpC3httS=fdFiS;ETH|VgF*nM! zojO`K;2Xatz8Ve2va8;t!#5dH5i2pWEA;f?}~$stMd+#=IO6m z8JTRPzxUqjULIay{z_aQCrB59S0K7iP=5ZW6_<_N%i)HKf>iUpsv2PhBXba2;3_UvuGgP;;;tN#N38x-L# z9p?jV@ihE;AXL6eh2Vle(~fLV;i}zkicui?yKR;jauThV`C5;rvrU+lvDB!HUm)XB zKF%raRA03JaLBIV;D&S|K_zq&woM2UWR00l2GBE zr$?qIu+KwyRe@NXhCV;sjzWn3ieHcZpjOYMrc&N&CC6!f_qJCyvsi+ST_cmRRZeK$ zH7TZj2nXOX#NlC4ns(As-8{@@GdVbjcV>u6WG~{Vqx#gujg6ih8D(6E|3T^KD6X5Z z$;`A<_uPpm>bL|l+;}b@7{lH^U;WqCbLQF)y%+&(e&AOwg|C0n=3$T-jHqjIm}W$HE9{Yt26a!{WX8PQC3jOha)V5Rl+hn1#I z6x{fFZ3Qh}&NL#Z@-P8~i%m&>uOYU)Ka5G;;r8^c8=yB+DN_IfwJOcik}_5dYF| zY5z^F*#MDcXypZX+qw%nx_IN=?ULDa!YAad7FybHb6@wS8Ijx*RA!j0{bzkH*Wi)Y zRyUu2G$%%mbMm;7<}LIK;i)nRdgels-YouxirUWa2XcM)D5?p-laTC8Rz0XaZmWg^ zXnFDu9RCU@9)tzUYuCh!ew91u(rarpOW393SEJxe=Turn|IG<{Nw~R zlxCT%Z!O}MmwI+TwlY8-BUZwE?>h(9Cy2Wumw7AtJ*^gV7~T)&1tumN0lGazCw>_F zPC1xDTUlN8q#12z+}Y1Y<(WC7kPu&!wAc$lu(KE$&r(({Zdn7efEcM)YwT*9T*<`7 zVH}xkM^AfbLWf}2IJ;AKNd~Ph^p?Y!b{Q_Y2SY_85+b;L4j_ zHsbkx7J}Y=FG=vbbn0#hO{#st0rMq0KcIiZGH-ST%pnT8XY$B`$qKf=2S-#jDE5YC z>q7LQ{zZLE;-^W(AYDv$2fBRhpMQI@Mn>)Q^~CijL?PU8Dz5D_&}aRRV+Ue0{V@{82~d@sG8e zLe?%-P9e_%owfaWd^(@lR({USMSkUW=HID@Cz@opt4F0XJ~nx__U9O?5uXN=#$*K;$>Lh^x3&? zrs$vkYu6%Xj>LKaE)YT2LcaEW(8OTL&ng{72~E+1R$xR+CXK=92gBo~UVB@e-}SFf z$UL)2drP#JMZj*m>}Tisc?cC`k8cDFfXyjb%VIqi#a9KAOL>l}>wT?d;1LiR%Wyg= z77xw8jMK!h;2!*DLU?rpRe!Q0F$+;8o2_X+h@Mgi&4Q>Utfk3;N5Df5xN!Tg`+B%t zUgKl0`g1;<=kEx^&86tChF}8RBy=q;Rid2qjq@gb#jouC+9Z;YMPMEt1?dg82E*at z>EkcUz9MpEum}wh0qd!>WlzwxUa&1HN=IBAY^Z;THBI}Bb?oS)2xM6yb_u7$h(hy6 zsa~LNf=_f`-K%I|w=bKFjznVzU)A%w+2fd}d=TUN-Q28X^c)*AY4d~mhJN# zQnj6HI!}2t*-M4x-xXN)Z}rgBT-K$($c+E)mi>_%G~hu|Q5m}B*VUA6%6F*qiT@ z)E$}-tVw34=XaenFO;JCvHy3~^?~JS*c`M)!uh&15*l~)&}f8^dwrzzC<)F+Oi){NY|)lT@`21N3^Dn$yJJ$O_k3g)m(VR zz3!EiNy(^I3N9Bf`{D?=@+MhP_wD#=n6-UMH! zjGEvq&+V7n&3BwYxkH=(^4fVHAn4vO8AXZi z-v(5;W`r_Io=i9Xs=+;qTy;v6VRf$#%N;q${1M|hWGs)C&S123sAoD@;tcz7l z{}R1&TzFP~ReI}1dh9SWx$%akcTlpxh5|%`ipfcx`-W|{97?f@!@(>zbNqQhcxTnt zT8TIWpXx!uvQ6tN+pXgc2m4x=e&a~i5XXowg}SGI;$Q<~<%VS=#_8YI)d4dpRQDcw z*#Wk@e}l97<7JC@3`?aXK-BuRf`GYDnJ>`<&yxW9H1CgBKFGSbjPz%CX`X*f*Z~hR zN}<3ilr1k)8zqd?9!HPh4snH6Vq}3G z(*4xeWgho;jhcBRX)Z(@=d~lI<(-FwX`?Oci8fNuTFG~6O~*wVa8uAm{+7&nVT8(h zvQnj19P;yB4f&?>0x8Io<$OFU2g8>RwPW6OTFI?{q|~@d+C-L~q5HQ8-pBOaD*LtC z>7}6R)o;-@ksflA)>gTf~3PTEl=TZp9 zS8uR&pIz{F$+(j~VpM8DMY`74koXQdB($|jYYEBd{&&K=NX@m+F5ENnMK8C{*eon< zUGaB&OMHt~1)XVbvZ3*GNuY{7`5dWe{~fU$^*%Iz>Uf1~P{0>C)>o5_NQr>kZDJF3 z!;mG($p^MLo=5Hl4Mbd2Uz1Pf2aTYbnB=c0%YY38Ln=PXtx_n z?e1Lwr0y0l9PKy;8hrq%B7V{GMU^i(97E>vBSN~cj`97_dHu~qTL$8y>e6&?&YE`; zzt#O|Q1sGuB;fbn#B=^6Jkd~One6QK{Kw-3yMUnDhu^#Ws16<<;wq+ZoF41F*tYf9 zM6JM;mKj=2hgJf+g4*YVSy_QftNt}{!9&Uk!LS3v-HG{m3Vm5$Q82zlEl1};S(=cwVyEOqr7j}y}{dn(TMBlLxbZsdx30T&*?1=iw>HR`>* zXPg@(CUk@S@yJgLIJWgkM<;FDVE%m}gf86oPc@s5+LgEZTU47jOp4{aQ3=T612IYZ z9^6A~^l<_D&;3J43xIrrG~S&qY@ol0DMz1L&rkWbN2aoSrg0K_HG#X4?Go9AlNZ#= zU)`@%$8gOnx#u1>fe7x`%lqP*O=4FX)8O^QuBVZ=yHw1qr=LM({$4R$G*WljBT~!< z7**oNgm0p%F3V+O12)((IgKW0w}OrD&9w#6xVWl)_)b}4HJ6>-l!o1jrs6A)P2((z zM_MT#9m9<>(4-xv!NL%BrIvlelA!ONVHktGq@8(EC${&Uwl^dAwlwZxW`n=ERvRjw zFz|l;;rxyF*yttH>+f)^cnm7)Zv7~c$2cSQgHT3z-R%?Am01G{ws)?xo50ZMJcyWU zr2l4Ddbsz+v`7QTDXJF`Iz3~v<`(>X!GoT&l8E-_S!OAT4S+8N4{p( zI$PSYUu6l_#e$cVX=l?$Pg3STT-NKN-F6SL>Z3EaAz`UEhP`1_x)wPM<;^#faV+8& z;h}K@fp_Z+FYaxWlqGDJbhEW`-+qI9k0h<`?@_mQrWB}w6SLik1`%*Q6}%&%B#&y1AbQC)DRiV#8I|XE zegVBNc*o`X8z-6emlXykDzRfkIaQ(4IObffQ;(84XQXp|Px2gtKaiT>Qt@u#pxjx2 z0<@x&d64W_KC;ZDR_6+&^v1!(B>|CqPbIRf$1GK|LE!$8chR|rd&6XG-{gEt|GzE*6w)Iu+dB*PPu7opZ$k0k$XMG;w zPdkc2lH98c`NPnVIe@!Dn)RTwTyh}6e5#ICrFw6Uo9dEm2J+^Vd0+(WB>nf>q@35e zU_pO1Tl1{7JZJN(!Qh^1M(@NnxWM5Z3bBv~zfePcf!$5tzd!RI+o)|kwi*a|gu2_K z$C9FZZrN;}DCT_pel>g@()2P}r;PQzSC`rEj9>l=-HOI^ zVsCYF$Q1LX{%co6Ac27x)W)uy00hXi)N_i&@mAO-dE>l8#rS}>5=RyXbQA;*0D^qf zfkkJ#!h?y73(^aM3$I0_x22`~XR83WHjQ~T3+T5!Cy;^f9+?q6_%vdZar7h1wmvVU+j#aA&nn(o#aR!Sm?rak-HZCQQC;?G^yyh<*XA(?iHGD)eqYc~i~^(K6}nRWnA)wGIr5XR0K zNsH62Tzc-rko_B7&$4#ogjb7htnbLS;)V(oHQCl6>82qT(h{EO`R+;S=3MtcLP}AR zlOapO<~Li~Tp)Vsg&Q@^V_VsW}j-gR#6et2~(j;Ls-NPkOUu@Fxr!5eatu%?_3?-1+7r$Oh-T`gg ze%EA%e*-y+IB9Xj8E42Gu6b)mn5Ir9rurKhC;6DS5Eof9YQTn4!5n9`)$eD7Hn&|W z-p}XjMQnss$gQtli#4$BuH^A=(N3VKUe3@yDY}JUm{r^0=8I5>oKdgyfY+15&IK|e6{?ZPy16&^Kz6ZB zPz(@tWqsE~fq4ykoy0KIxKd-kmfjCk(F_VN>py#Weo2DD7P?eRgDn0WcY6i~L+hAiGyB@<9*@B{X=}CRoCJ)$=zwb_BhdL{U=eIO6XJ)bkCbt=_1y7@-O2$=3Q-^PSQ=5GMMLT#RP=K#_cByzNP!&O7P}%SL0C-Q`h2s z`sbtd^&A81mTuo#!D6^^a$tBvhz(~Ui3@)EL*P37Cx_ZYMom=}>8J&6_|Ayxk7_ut z#1@cu;R@LkGIs9nr@5f%qZ7+{DrIM7XYe60Ns7ZYh;wkYfrm5c)hYG*PT_!7f>*9J z#Y{_o$5cZQ9N{Z~Ljrv((LE?eyK&bN*I4#iu~wwhxvs;a5G;JSjDG|ospb-4Xf#Sy zL$a_!Qu4s8+&4H8nvV`koDvOsy{^lc9r@jl6H#@#Vp`re@Vk1`IgS@aRkNGH9Ga~W zj)%t~stVF@*UZEm=Fg2@>L2fn4x*%kLllX+xwgR%`}N^nN$$ivvC_HHyqRL5Q&W@M zX1Zy%nZ!!<|J|4W(@;Ao;t1@akpbe`JIvL<_XXYUlZOFKbK12JLGe?B0^z4iV>- zK&obB-H*`7{E|;@6e}uk4{J({TkqoSl*V{BzS_|(y(;h~g2X<&h`X|mP{DB0$a;r8 zYSt8bF@;_krOQDb`RJd+5Y|M8nVaa!K1GdTS+k6fUO3AHx+fu2JnoBg$XL&EBM2f` zh`KL+aDSI}XC`X#`j?q_#DnCvgwpuST1Nnv>;RR|OmUu~gn}QF5AgW{gO>!0>zaU?8(m@lA4IMozrQf~AknQ$;ZP&HS8~v)3zj)|yX~ zu&-G6bFOQa{(jk&!ZI?*u%oqH$hq;x$>LchG2@-sOv{xp{Q24ZuITCCA7n^Op%jM_ zZGZM~$y-L3hh3h^zpg z3b4innNTuDLau?A<)r}<&lShYYM=r-BgCRJWC%ctqQ#jlJ@UMFb`?+KzAxl<^f5|v zIVXCYlfTxWQJ+#(j{2er{(fM-2p3s+b56~Cav5w6d-o>VYww03&~kbmHWwg*OU2RL zYbJJA_@zkkUF?B4x%-`z)=`acy>`*bpO$OqqYHFsys8=IKMRGPS@GTJLl)ObrS0Qv2y^ZUuUfe#s*1qnI~L|o~+62YL*yn?)2TZl4=1Xx)Ef!zhEKOe(XHYiPt^zCDPjVyf$xg3%in8ml6va=t{tXhm7a|W= zF#;ciXwcTf`V$3o&Y-)^PE`+GfCI?@5-}>1rsZb_B!nkNq1tlU@{qE#L`fD>!GA^% zPLq<>HsAZ~|1v(lq&Co!pa92pjw5nCiHoOt-jEz54nK*D69GQ!FV~k--~)rNIq+Ck`&{I@z4l%-tW6 zCH-Zw&c*C3Y*!C0zH*eemOnsAoHW4OECU(Pm+B5OU_Gf7Xi&^ym^ve zKe1_-@^m8J93qJf)-A#s8W@)rDeAlIyYn?9*h@u8L^F_a7%{`i{iD8^tByvNXX##l zGy|;#j9+&$J&GpLX2m{#?H4t_!@E8;PdC1e_(26gr`?_h@%vw}36S+zuY3Vd&q>Zts+_?o`clBa7 zxXwpfL$Ol+(1-4{BeY+;8L)bEHAt4au=o0!nTipcOXf$Jc>=?mmMDuT%AhQ$57~M+N)&we>|pt9xmj{+9~x4ioAggbwbGSVxTRc}R3T zl({;GRF%N&e(MKsvFObbk#}}4%>UiNuit5YGF!O}ZGdzjI3STNmDUXe7z$K8jbz^A zc1a1Tq_B~Z+JRR>Y3K^dnql1N3 z;_nEK;>8;K5)`zh^1p>U5iOG9QM+A!(Fvr39WGhI@AGMug|9!zxbHJl_25)AfQu zk#`#{mN@iS3yy3m<7R?C6&aCu_@LSpGhL$``!2rT=j#8G;Si#z5WkCrO`tQINY$s1 zzXim0Vy+0%gQ;8!aS!U4x^Lloee}L>kugRLjH3GjW=9@GX7@W>#mBkb*}$9_h8!;f z1G))*=-pUx=aVYVc+}*{`uHrrL-%Jyj2gL(d{{)YB+)Du8;!Yzlp78Cdb1JHd7?$c zAMLd^kqyLV&{*uEr>G=eDxqVKxoZ)lutV9Lz)C(Y$jhf*p)6r<_id%L2@`dvSY49^ z4h~}7Wvwgu)@b}=k$HtxAqGpg@G<3ZCx7SI^NhH$NLcEF1x{3@mVfx^pb4p9NVO{GnmQuaUC10 z?Y2;vCNeb@$p%5f{q_g6)uV+NuIjQd8~tL}o@0N#I&b)?mkBXUwimCuC)?TxH+e}g zp+7Ng4u!(2e$q;qQ(Y4P0-2Lv0ywH=)`Mi!BbXls$MST^d-OF&z3}(rp9uaXn5A8v zgixOL9Y&0eGDLMBJGH~kv#H%K4nlFvCE4};**Q{2ZIIakq|S`t7KV)J*>ukTV!uvU z*}FLJ3C;!F5p8@sJiR#llA806ZZsgaVP$zUhRc8Kpv16dou`PtPmzh@(FyCl^YLyx- z`TbaNOspc^LYfhE9Fy|7KWk?Q0hekVm4 z#+lvn)8zBxCNp<*d+_u7-{iQ*V(fn7?PH3&0#&=?xjf@hqaCl?qkZlFeLiQwu4uYV zwY`_OqKr`n^fuPW%Va3|4lv?>q;ti^m$z%Ny~t*v*`=5xPi}JSjR{O$zJX>p_>E={ z_Kj8g2fx1hNkSLZicIG>rIlo({k$pvGKLguJB?VIX)LPiy*;WoN@PX9_(K**BSW>?aiY}gcq`HU`YM zeoTA65B1*zKVwVH8ABtgZ@Y{(cT`I@9w=ka#-B~zOuU0Pd)aUCQ5$oLg(~_gIYr@^ z)x{){xuW`isS@tp!>&aOQ@Ty`X7*@koOKLK1O%pK%$#N+Y4)HV!KCezv{wcF*2F&$ zwc5qrYD0yuPPNXS4M!2U48*MfG=sfy7~A^X4jUV%AzFsMo7QF|$H>Sy_wangd6;|@ zZRS6*QH9EoWK~ni`9?ltA`5T!=U6?hHAgYGN}3~(dQAM~mCR}L^1j)?$IxC0BBl-r z{K^(_oIeXE8{g>V-_UXYbBcgYYf7C>OchUOb$^)YL`_+JHoms{4&(Foaqn5{FEWTG zQ$G&9D5H3FzfS=@LGKTRc?@;ZNijtrVb{{K<-9Y8nI=R8-(w=EWe7gy$>)sjir&%3 zHaGz>2?8i#Aq&iW^K?|Vjst@f9-;GJ6xemV#rPXioPqDgvinL%Bi1PD(T@I}I%Ajf zwX_6hGaUD=ZJy6jk%7K4TV5zIi~o}fU)JT6qb4?@xukJ_vsT!xbXozsx$#aVQ?icIYEhQfHx%41g(u<8s8JQ#NJs99_n9{!X;~-Bcm&rG78y<>qxmDP}np zDSYe|M4abx*73ajem(0}aIJtBF9)g60MuF*5gspAAkCDdj4uW&oU+_3f3NRC zQ=}(yg>s5^^@NPJ_dXv}BRx-|b}^Gy{p8$5nImZWca#^cSl8xdm;Fp%uWMPa!9jGZ5#<83ZlkfNlHujt2Y`L{G&m5 zrvT^iVWjRB$doM}xAJChWVn?0S+0y067_ zH|E;bksM$@E$q?vTTEqA2=z3kMMv^6^H{QzZdxpvVD*N9)ThHDLer=Jl?+eUe;r`9()0UMB3U6>lN&k+td<-7Bkv+_LDm($8T%x#+^P)BqcH~Ac9Z~Cpf2d9T4#hlLDLntPL##YpqgP2N2NcE>$Uiadg+&ke-;WGGLO!VIzo>28DlmjbVc+{xQ z@!8bqc$a^A%Es+7k33ocG)%ViumT7v7}ds9?jW?t=cUy^E&><^s4{&BKBd86*U7Jq_dQ0)}=S&KoLdwh?yVe#1r~AC2|H6 zx10HZ5A=jfDWIVO4)Raka2C+Ti@3OSp0#&oF*D3v;?lt$m81CL5fZII!-sPu6JR@4 z_L>P^xu*9^R7Yo28GW$be{BK^md}#adYsV`<9})850t5Q{yX=vr;n;{;)PI`8{kj3 zH|rnS{IEP5h*Ok844uc1a8;;E;RM}S-QRGml|Zc;i!HK%U&iXCoqv@C-qNjK@RGVI zrIjl|EtNF0XWb^PtavPvtiRnucP} z6&eMpeQG)4ju+=3mk=3KbQGz4z6f{5?*;N! zTA{jT@@cni9WUp3kHX5W^@WZmDax7B;bz%{h&wH7Ya2Z1E4AXnXN85CeS_!P;M7CD z3M{f3K>{2wNBWx!8u6L|-_{rds^gtdB8e#aT>0h zdnxCeS*pYA_*x|A^~&O-)u z8h*COK|f6uJ)ikki^Z?}BY|Fb)$YM7@1$YCwbjw*^G`-KRO{z)TpS_%q3zY9=DaEr zOp;8)ZafhLnwIr_fA+|?(Ym^#@W`xK;JatgNidSEBLmo)xg=2(6g-c-^{pHDAYeC9g_;1ZXp5 zh?sRU)Pg++*XARz>LBGu-G%+%`dqI`K1&DpL-9e zytW}>P0DwFQxx7xOsPTRZgVs9Gg@QWw{zWEGOiC4?JFw3)MU)UlG71Y3=#4GkTd_x zyX;ji>j*lxN%+p|j{f2Kh;z2d+*izo1}IyA+Jfv8Zv@yD)L6`6`Z`QyE4XNpup@0& zab+s|t7xVk;fZHuP@`%f|4||&V-=>-yTKH3;q^Hh2vS`nZezHI;4cqirJw4=F(Xvd z-!F*}yz#mh=}lhF>8#npe~XM;caE#KD<1piGrTkA6}WnE zD8@ki)z$cX;rEDN{iRe~)|xmq?1om=3yt~ErD1f49dg=0+PUIBg={k^MErevO?(e& zXv5dPCC0q{Oob68nE~V4;htuu&Nsgcrv$N_9Oq~8d-D6z$`<*}EGbNUFP;c`O8mnN z-BJBHCy5hU?iY3{fi*%xTo#9cc0$wE)p&-xk%4zcH{lR|hy zp8bfwa#LbhYvQnVe**;C2o@(<^PZ_uF9&!^C@YDPmNJ=P(sjHl8 z;k6-8qwAwA=~(#*jc`ZjH=oC|AAW~-hCYO9VFRToM`83>fAlbN4Z~6vfflXD^AF&Q zm!=>vptoVCAS-PzzWDEFoSE~sRKw3&uo^~}V9seiA)Z1}GjobNXeEVzv?x(x z!umkTOtE7jq6uE$vPec3u= zh~*hSe<$>%(`vJE@S;fhK_=9r6Cq zC-C0lnTTxQQYa1kz|Lg+w0R@al2~>M2oLHFzXxBJ9^VY7^}iu0iR-$EZyZn;oyMCE z#7&90n1&^14RUEqwl!J7WI@^B4)v#$Fb)E=dl>@4d{ySZY+6a9UODB z5Svqyu~y5|tZpkP@3Ju7={av;{fYm?4PY>Qtfu{V+d&xo&MSqk>${Dx>h5^zt55Oa zv)>`mm-E>Sb|*XQ7|#5+MtW>%sBP8KG5S`uO2)zP#)ZFt3mf;#rz2cS~( zYnkAHD>Z`iefXV=^eXT4_Tho0Ij1NwkDu{FIjiIm(fElZQR4Cwjp~oWSI@qViN8#O zC;Jm-Fr|9kXVY-}oFSvh(WweXjC~Gzhg*Nde-9pzCjCik9@Pm)m152#tJN=_{1uKx zr{Iw`z3|abGtjzQn8kY~=Pv`FemWWQB7F`De>~ExDKcwwdV+OOpF4^)4rks1ENAatnBa;Fl!GpGslnmCrm?@LkU{z4Ym z62e5q<3VNi9p@-99h_yYsr=F{%2FErT8zf`pS_Raul6nS#{Ku3E%@#9Md-9wr0tgB zJ&@Dz*2|H*9^HX6*9_}}xO+86y|!J8?Wv{-yL~S4GG1!e3X|u2ftq!%Q%DPtn-9!< zqGUZ7ZE>qD?Ug4dwSrhxO@elhbC6`OsYs8&2n~?A$3**Sw3`9w zI<9g|GZ_8WSTw0_vd#Q{6eWi9=*-Y-x|D1OG_DLk-kR+f4D%!yCL0*$2YZZ?Yz3lvTmQolLUT3i1jbI|LB$H{qXwnTyEwQAipY2uk{r(@3v zW?34(FsnK|*Sa|x4z0Xt;g*0=k`eDbiwEo3F2k(fl$Y?~m?z-nYYHVHDRCXX`+Ej5 z_b;@VI5C_zFtSjHp2n*r<4iPIvM}*=CZp%9u7(;-D(?7_IZQ@-d_bYX2>jmE3ONaa zo@Jq9bSJ`O9>Svw$}~r=E-Q(WLili1s`SH0ufKwZ?QUM*DN}e$o3^h;e5_#(h}yLQ z8cldpdVDJs`PsOfW~gr98`u8H?TBs0cu z&eBhpVEJhwFnU9pbFX?`k?(eM?@GpL_ZnoS8^)Icy}i)!(XwCr=ivTK%=qUI=rk<% zFRo5i7#`@-3%TCZZX2N2L7Q*Ei6SMy*-i9-1BwpORyH;-{~wZ!3*eDDwYy^An5Upr zOMNjrCBP?~W8Su<@$hc==+!Bx8_uO>455<5xV89d<0|B@`Jc3Xr>MhfBiPpz&Lf;6 zL;FZC=!Q_05!zqw=LJN=Y`TzYhE%UqnVp^<3XEvUfnWfC}so%XJ-kdxQt?LaF-DiWX zNVvKd|J$)ecuqe^+jjyDX{VZ|;?lH(^adEFqA>9f78TCpHXCg!oAkVc_%D=MBbAK4 z_l<_9w<&{>$B0WEvDeN-kwTR+N-IqKCB7kDFS=lyqeO3sy#q;ydXWc9XjEy8N}LF_`A5lshE`hijH2d#r$>ik)Okb9*F{->o!EI7oIk5+pz_C zqR;jDJ1=13n}hM$6VDF3@-wu660WYnm%?+(*)>Pnz7rZY zJ-8KfTsht5uceETmBwZA#V7P>i_kGIN{>rIa+YC!kgI!j z1VuI~IICsA#Y-B@ob{WjED{{ta9<1IEmdXaye)Bn-oduQRKrUx4gOYUGFE?yW0&YH z8SwV+gdy#EK;>k*?Csb9j!|Xirb3%{8UG#q4KF-;KTaMkSZTK;)M;A{_doJFIyUYl zZWqHKjwDX2{z2ZM<c%-j{t3P|V0mJLngrlQr z@QmM?V@-0%lVAF_goy{2%Eno{Ob@5!r7I?ROU^F7nAm#|nsqEbwDa$mtFR-=km}9S zL&NGfFLLp}n~{@msATNv;fMP76zZwi6sDgqc>N0;zO==-M;PnS?of@f{eU=^nb&J= zCrqp{80o`%uJWSI$--|^!h^9eu7f)U*Q<)=3h|Z}m9r>0*hyiWx3uKnMaW4%C~bTBw1MyNXQjs_MPppnAwM$*UIEm~vSHnl zD15zZJ9HYulFyujHFW5s&^bzn#ir;vPfBmQF(#Ed4DL?6=PEBr8-GT0tYLl~38~rz zUAl9so}IFwd&l;0bShW?^mNQp{BO#0h>tB@(obafNId({v#1hUP-yo`%)gkw?sw!} zIAq*YIv6=<8(t_8h+`*gG*EIlkG9ktj$_D8f+NLQ3ASIh#ul`>zb}IQOp}obkBj-7 zFXCqIn-D= zp{4j|-%iu~Iu4o-j_^nQp~cr@v|}A2di6k{zxmP{I_x{Q1mC{)3UZ7q%aoKx{hDLa zBNNT$Iq7it~2FO1pcD_nE1 z@=>*_3SEN&pe!i=Lv5jx!((I$d6q<&_=|iO9#|e^#_2e=#N0pW-PKNN^oksSz9UWX z)tizh4*%K}i=+hr30I4)I<)l5CG@KkCeEk|TJ04)Hhv;@t(FI-Z3BgH>5TudydY)$7O-uiA#r=;LqO`y_pe;JyGtRhW zr!4Rfb3@;1g{NQe{#$io1y+6fwPfW>L-XMs@cN*8;p}bVrL7E9@pTvWpk08en8rGaP-2s9wSOL2g_P$)gD5acy4Wj zx3_ShJ$X4)K4>v^Z(9OQuI$+>&^x>eYWBQ!{YE?1r%9bg;)W|6LS`s^=K4iAwqmDo zPsyNibbMwUUg+3YTC#+2$(>&h>_+yAsimHPs}lErfTuhTCfP_To*d4j9W}R-yv!&{ z92jN-yeST!oOKarPL-VKUQp;qxEAMqX~|)_h`lvOe#;g?pQJgM)%* zPPi|bM=^8P7Ub+J{i?f-Lkf*EQIKR|;>}6BDa##&iDE=iVg?6$xl)fkH~S)Xe0%39 z?B$V&77+;cDl|`v$Ayg^C67@bVI8l&8x!sKo8%7+m>0Ciar^cm9r)q`k&HK+|f~(Qu4C z>A}|I!te1dL`dJF4j z7^h81*o>c7|AU-p&b2H#Et_-%=OP)Qp*2r{Fc0TZm>9yNFr|ogOf;!mov+EoZ(G+R z`eG?(5?27Y z-`!rBp@^2%@p!jh2naQdU$e8j{nWoWKIa4DcJU}&ecpyQgrAOoxG*A_=LqoM_#J&~63Sdva8;LuMO% zl(rK=Tfy$d*)#i)lX29rL);?$s|3KSO5sc}JC*H9>F5Jd`K*=x7>w z$#HChs>|xntW0f4^A+sgu>zm1{14j~8Y<*e22|qP2Y7HuNzx#5cswL-CQO7Djt7$& z6U^;n7eaa`#!uZnTi~^?OXe+Qi2lHwUw(!*&tSBD{oBE**Ldl%rO=DsyVrzi7}H_A zX#kUBS#x);!@k|6n;Cv98r5ivo-Mpkt>$1jILaYREmD#<;=7GMVa}JYz;(_fD6WK#Wvk3L zq!3Y@Xf!CNtZgQBr*pECaQe&w40-YeTsUT!#9z^oLX*0D@zWG#;?2gr^3R(J6BV73 zM$0RQ@#r;4-jdqM8N-`)MU@)GdrRc>scdY$blSM3K))JQksWaJHJa`OpNHPWu&ytQ zZ)h;kxte60Uh=Q>*b?-g@(5m-G82<}^u|LS2Es46r|EA&pDB%TP2czr_I&d^+|L_Q z$!&>~qYZ^gS^0!Xe{cm75(~~-sCNz2yQgd>Oy4d1`_@PJWXndRB^6}3-YN8&{#f`s zk_;mUrN%4R6HkqP0B)XbjC+I~yRrsR3qLV#-I{9ESQE|^>ZYCLkcb@0GZ!ZQlpoVl zQ;;Z5g+|C-J$&)|vEPZxcUPak{`!&{9M->V?`fpaQ$9RCNu{roX}*8XhVYa7(I5+;R{ z@SH1`F!Ao#zI~lIe}+1YycK%avQ7uej)DDuug0{^D@^6dEXncMEjT!*^y%I*^@nuB zLoJ2Z#Ap*dPK))Zp?q(JG55+?4WF!3O9z=nZVE;M2sTXM#b zrri-*jmNKelGA{HcJ7qC2~yL)CNi2@zUZnt(w~s?^Gi@@3-u7JhRuXY?+kq< z6Q(R-D)vO5HEs!up{|XNy?Pn9>{yzFOUZcWr*Eai25Z0$#xJWD;A-^kPgK2k>J-!r zF|E8RC04DN_qie7bPF8A>p`V7W#-X#))S3}h$2M$Dhm?#(P+@lveX-ULd<+NWl!|% z+7<)+nxNN*AHvn$(`81#j|(i37kqWmJ4!lLEpyab*?N!*0weH?(NYy5;e~xfF7P5@!{i>FyVg> zn*3yI;UtdOn78i1-}7eRxz2;(8&ojWD>w5Pe%&<>M`uiwmV>u8Zfaxi&dM+?oy8_A z6WWv8k&$`9xFx!{ezj4nf7w;8ugtL5{PD}LAwF@fagTMl5W5|RzW?&JuT)2+Bi?)C z4XHS@%$^Ht5W9D^ajR&Q3N@Syr;Jm4cyrRWvax5+p%T7`Ld4}CxCAZ>F>>fcas&c= z12L@0Kn(i$Y1FF@gvkBg3QR(R4i{1j6{%&2f`(fbry@gn6d@i!X5(>k5Us3qkko25 zT2yI*h#Dm`ew;_Nb^brXO*gDlY`*$cwHY%S@J)8FmM2xG#(*7iosLkVD5$!^ReU8XW*D)5Ke31?P?wG@Om7? zjaOW+v7(W5c$G9ZA=9m@xcn>OJgxG;`d?RK`?+PtE$gAv#N+3Khj8Iw!Feq$H99oJ z@YW;6Bg3_z*Cpc5g)@(HwO!W5`VptT#Cmx97pU`(>N|Yq!XfXVh@Fhp`{yyiQDGmOB5!PrE_%j#lwSo zmiPcA3U6)cQD#3HOjy%^@KRS1zZJaS1+qEl;oT|8@$FHFz3bAA%nkZm$oqpxhmp;=!Tw{DN8r@sk*?=HqIu4=FuTW8NO zZruo@+Rx}z#hV&>*+Uq5Thk(jXF`|VlSkXzTgr8q4^nYQ9aZm9aT7LnT zlML}=&M)fR%&=6&o#L3Ag_y4<;_uzRir$OCC1i?tzXl`m&&sdSsw2;P+0t%E6MQ}Q ze;D7cH=Nw$m27o59kT=r-W?BRCg;;x9)CaE`1(3sDs*L`{8l0_J_=D81!>BzZZ%MI z2$vhPV`={TZ6#t;ZZDPlcF^mRG5c&3E^NB}xj>v0`sQ1oOLoS99vcrVg~ox2WJMF= z?Q5Qy$Ge3?BkZYik`u#r%|&A-7{hlaQ5w-|5SH%Rga%Cu)}5*>qzb=if%tA_&{N`a z0@t1(*vH&j90|(!o+XD$%!Q1U$W^&0=8~Z^^H}o2oPW`Zq&*$5GPbJAp|K z$G&+EOAao8USk+QW7f)}9Y*2TwO=DB%yMHa*M#Q9-)6cg@e6XpyZ_9kRvNc%k2XDOqg8_kj9a36pLGba|K7YVU$uY$Y0jq+K4AE+ zwrETyIq>g(B&6*&ZrKiYkoVu= z;(IiZZ+tdIivAe3HTd_yHfV3s53J(r4uwOZjy&xxixp6u7TQ+pi!#!(i0u(KKkGDex)c8in@C{=JbgQ2`nX5&{GYFvXf3Fcl9{E$ zytgLfh*LcL!|r^2B`0xIpPuzDUhFy;3P*W?EIqaz{{yj0En5dm9}o;jr5tXsE=h*d zP~oAx?z<8z4j%6dfrB%8bSaqiZ^v@{J&R*Y)=dDq8QWu1g;#uMovENe6~Y7M6>ov4 z#1qIpT6AWilTrnx6SYTz7K$6Rt-QEQp}TPZy4btp4Eo>KnwyoD1vA%S<0+HZO3#R6 zOH2@8T#BBCl#wS|Az|WU9xPdy45Rh}jxTi%af7E<$*ez1P8|AYF7mPs)gJu3>mjY( zQ0ejQ;(qxAe%riE%7`J(?v3%`(2*E7?IDY1t1#5?-6J33-=rKg%=1Hd9m^Pbrj)-l z>otsSF-RCbqa&)%!pha(S~OEH*Dn-Gm2E;$so6J=uU8~;#r1PMxnLk(?OGExdKFqC z+l~cD7ou?Rid+H4_M~LQ-_bae%mWWRA^tT)XYzB-;?T~;MP{fvD3roB6`JqK>^p|= zOrq4iK|gFgxE+mJgmJUd5+AR{=ih&U{JcWtrq+l?apHixJia7lRGFJ}9%)&aXxTX7 zNIG~lboBy4rJjFD$P|BK^)V(r7*1DX{~+k@VB#k$cdz+l8ZuH1OJ+DaRmH@%J#qhx zN2KkNGH>!ce0lF=9Nt_oSLJ#Ze7F!ZcI?E}*c7xHeV_0ajZatriqn+eKSZ+{EYfZu zC;JdGmx^Abgyw_tPEW;0bMY9GG79EZ__&1G$Cs?Y(M2nfnOU+zsMa7a zClOhv?_8L=4{HxMH|8xE*dn~`>?;LfU=u=^9F;7rT=Wpb%VR&poDH)O8d5n!cQSNT zW!roYc5O_AtpXh;^i?W3AIZE0_&H2J(@E|6LC7( zkPgT?a*djCCHC7#^VbJBaKSjC&A|gBn{>s*Kc+6^@&C0dNIs%lJn`9-sko(^jdmTo!d;!um5!4_oZ z7AogeVNpiyL}qG1XQhr`;p*{rEW?g1YYW1qWk7!JHRu@ zGHw;%pisdfp;sT~w?s z%|fZq4jGP?t#3cf^5FFGc(F}ucz9OFlHHpyYu`$I|L8OLd*Y{rSrpbQNO2{>v+9%dp znsY1GATi4}6KZdcBUDZd3f(!?>iVLVS?BWXl^AIDEjDf$&8?*5k#-qnUZ>f+gYwj`e2-2eQsL3-zrTAzq zFHdPX+>($fY}M8%VG$byS84hYV6t4Jv+c4p3RQI0Z zJq_=dk-8so7q1J+Q<%)g)`NWX2nmsf?JGUmIlxPUpTf!`3(ePEqPMhAG*(Kc;h+mm zZ;AWO22~1#iT!{PAQ&ryGPxFW#a-i`yGtQYXb? z$;I>1O0*;_q&pt}crw&xp7HGx5+uawgAXvOSpa+kn&JGZg-E=%PI|6Zcy;u8iFL88 z$F&444yP5|q+wMXBPXEH!eV!Xud8i@Nt>4nhvX>ZR>dSg<~(xp3$h~JlzwpWDB;C! zr^IBWT@wb1_NVMnIJ!fjw%onZ@u9Ks^fXqp(dA-URHC>i%rb0+C^;$$gh;K?TQr%f zyu#uN&-H&6b9T=|ejbO9BzW>ENGC<(JR);%>4vb&^`ShPeTtLG*K||`B2e_^OL{<$ zFJB{i4~CRbwM#AN+OXP2>2Y5-6K5|Qj2;sOo^0C^janHdwpoHY4ZJYop=Z&jg%|2I z5<+AwxSE!ekNrz5n~Y!%GZwc``xa-CjTf+kE4o#6g~r8lnRJ0(jKo?WttJ_IvuX(y z5r@R{;^ryvUj1CXOPqabr{ttbRay#lifxXwYc&M6C|LOKPVw|pp-0Vvv7#$+XQ0{p zk8w*}TjKjF91Da=Me|$9g(*vTO0N!j8Gr7ajr@GmeMM2?24i{@Cl2WHg5V(a4c-zl z59OH)lPM6ZauPkru#zv9cJ|0&(_AB8w`$Njmp-2AcI7m7YZH*3vQOHMtlkczKba^! zwj^!tX^hd`?nUzm2XyLs50p+daPjO~{P5dPxN@<;SXmNQl-QMU09T`TN!vdDtx+|^ zvJ>n4eL0U$KDAavpN)*9+ppSOE=Y?O!c?#}OqiFClyzmtvLvbsC0&&X0j|Lat7q9d zntl5Xf=X==r`*irIDW>gs)Z)EkjLbtbEwq1l$xp)qVzm|+VUIna!vOXg@=QSe~QK` z;`BB+sLP(YY2MQHn0rhN=dn?oOh;4YB2pitVTnCrP%dgK+D%mirj3$n#(hE$Qp5A0Z$ zTu0$Qn9t291DXVfz|wg^kwdzpN&wdYBzkO$O+s$Ep+K`j2$RCBG`OnBRfZ}D=MP`O zi-RA>FWdeSqGT#iO;Mr%%@$40#G^nkzQ-hpn_5`;NKwdTaiS=hj-Zpu4Z-zG<}qbv z0T(g~QiyB#naXjLifg$Skq~bvXVkP>Lkxc3vWEM5!Wx9(rTL%Zu>s@pa+_wT719h# zj>q9(L8(8BuzB%mEQpCl;QtA~J8)I6zL5~B%KC%8>S;DTyx}P`Vfrnnjteta_n`5#E6eUiYr6?T{jUhaS zS8f@?lPOA=*rdb9j68Rx3*1T=jXSpKG}3bnDWOb!s1bg91Nt61wP{Yk{&V{=|I>xi za=g|=A--|9acwa1uMeX$jC<=w>SmE~OWQY?V&IP=}T*I$)=C_^N#2VXL*miXb?+8Nz$r z-gtL$G;7r!Drdvox4g_#h`oGNx+nD7d}#$-8AqB}3*9TR=aRAYLq+%_Yw-z<-JEtDWOVLD6~ObHKX)sdQXD0+Mo)WymOfJ{Z#z<&O&IcEKY>jz&S77Qmk ztc^`Zmgrb3v>IVgXjTm2Z{iUfPL@n-e@T$SEX*aw)b zLc?(*(ag`|4!lmSu4lw^^+^E@&N85IT=djDh~(zry@#p)BMvDJ>#Q z9Q)q9ZYeTT4~zTnDlSDY!Ix{MV)#Sd(ZAJj{Pe~gY1tu5!>HBvB}Xx_*Hakx`Xnsb z`LE|y9#Yi|f8mdO*WE{byU%$evxBjtsQi3(1kpA=f3~V{c6m-J} zuT{4LT$?{EJ+>CXykl$t7i)|?XuH)&ElGZIGBPy<9+R8O#bRRsb_&BtMALem;puC+ zbnTMhJ^Tdx!tQ+enxddX#J)_=MopyU_(^8!G2}(<5_T$22vb2v=eo6-ZVTFz zS-+gZtZm;QH!m9+jYj+(o!GBIX2u0{y&858a2(5lp1Ajry}wdw-juXfnKD{Ul2xN9rPn^@o=a7o!#EGqCPTJn|%A zD)_FBX61=)hv+LQcxLo_c=+*1==7!lIz@pOF@LfNp~}bQ*md}7_2=k5{2p|O9Ea~; z_yZ?SzC7Z0t zQ2o^m)4@WJ`A~(07ew(XGZTFb02rFgvAN3+!JqnhL zuo?~?7~SS^jQ#KptID99P}{6aHPkh$N`EdP37J`!MW_BcVY^eOM&wle1x_0e`0qyU@fR;~UNc&eYw%=5Y zzxQW6KI&^cJ^V8~*mn|owH=EV4cnt_^J;kH-GTUZ^Orauz2^FLl=%KkZlFqiE!uL? z_KL5V;R+;*VXW` zPtd0BJ>oDMK3Zzh7QFoRxA^x*>s4s51he1z3#(4?dNG_AFTW1x9NGD{i`te32fu3K z{M~*iwKT+MT$8#@6%D;67x}pbH=LVVEoEokDJQp_#7WBuD7VaLora7t{aiQZ^M z93QnbO?uYD)4lIERpzz|TpFkSGk9Y9WVl*6qAhz&OoHfFD?#sgW(m)v zzz#A3RocrdIXMNNPnm`0O#=~AQ2vX;K%>7*v{j;Q7LDaa$?2x%C|7V_u>wQ z>{!9!E7AD3XoM+H%)I0JN}Qm{-d?dO;kSZ^mB-3loQ#KBJi(Ift|iNS$QBM;j(q2d z3&X_%H@J+(%?BMHo`S};h391acu5KC@$ARbux_5IoO-$9YE%Y3n!ga5T*G`UmOL6> zw>px#K493kGTP8uHk-?ol^cUxMX5VQWsY8(2d&1G;tF?RT`Y0g?pyvD-dnR6lUM$P zmwuaqC*F7%_m3QbSI3UW{FkR-=eK|1%+8C*5VvmyiAY-vjy3gr=}|n?YOpD}$&#=N z!&Cj=#Iyf^lW{uD26vslsR9fN_Qt;S-33&B`2aS6qXzUp# z`$aU)J)tl#gvQ47IQE3UGd4cv_(~y>lHr7BxfKCl&&EC{MUaUM97CcYtrv}68U;zN zeJ7V0;tj>$xg&$X!%8=VU3k{&U;vm^38lE1S{Sx|?FXZIui_^iUXIpa?)-m{k#xx9MUryvEm2EEqze zd7=j)HySAUF$Ij?4uy}xvt6_;qA{JAkNG~FD9cqfX;}Do`8O$k=3%=#?r-ss(o5n3 zN9MvL^)=MaLFm_U5Sn$Z!OdH8d|x~ktyqSvjB}#b5+)2)2=omtCrl*II~a|<0}y>B z9jR#saZ1ZLgWWr>plyrJ2(D5_^CYfg+uyq}<*zx&%jPvBE?jj4o@v_+sqIEc+qQt! zB+jf~B8j*4(Ie0y2vwVQH*Qx9bkT>g>DYGYG`x?2cD1`Aymcq|-Jvjrv}%T~U3#E# zj<~@TK}dB7L%voiyffazqKB$YLvD5yQp7cUKJF;C3$a>#a0gaxUXSGqmSFRe|8Q*e zR$M)N0hx(9WTfknou@{wMky^Pz=eZZ-iq`37^IN1RGGM836IYfI0>uMtXB^NT+!m- zwJhWdulu%f@I>3%Lons-kI-yLhY}fnOA#!bKKbu)ylK^}^@vVlp#yP_G%9#`1t2lr zAWU6r7cWe7UKY(~AvL>DDmFu7e~ZSk9KIoMY#uZU5MRhLKU8=&KBibur1(5* z(@{|Nh{oS%5mKge^C!32sDSjq=wel*N=ZA>9v4j-Sn~2|j2X{Q$AfQmZw#_x-M!PVG*je8VmR$~}`nmZG9>f2^w>CMn+bQs(1ajZNrPiz@1M*9Y%(EQo2 ztumSRR;+*L0i3*Cc=0l8(V*%u+&}5Jf;eymAbpXw)|56chTgL7f`#V zWi4B3Tqe#eU4e_o_hU`;RYavEA}#5Z@YD)c&AlZKuEOYfK%sDlQYE}92N!Aid>3&& zxjH$+O*AjH3p|{i;pwb~m#Z_p-CW@9>Iyww2c;7fqB*G4(ocoj8P0CbaP{^;t!_tNX~|3F)0d@G_d6D-vVE}_&LVCRUUYPqAaxx5QmG0hc@$}f2 zk=~-Wv~5d{PkRa*PtG!KSr;F_Zg~FP1#q#zTn$^H+43{qS@OL!ft$QOqD zT5PfySw)a!_yPHW%|m%yFIckdu5J++(5WvPwJ&}Z+I=ezWA*-R!X>`~5AS+t8R&)F zhW(7sTArM$_0dtK#o=g%MJ|aWtiz?Gb2z^#3f+hGgR3R8FG@)4$yiMK`cFhh8!s-= zv%Rl*Pv{0bW!SbcPA9hENVHiQek-C^xS?Z}+E4{loN*@I#ogF+_7JrBhNab;hIc?n zv-Wp9#&jz-WX9cwj5=aAZn@T>`?x;nI~s?a%lL8nte zua`ThrA62deXjWZbm%lm(B#EQAv1>ClM+rKI_fAcr3Pcv1EX&lUa|!=@{N16Lf4)n zP+#MXlng&4XXlA;Krhcrc#PgcSbgE|(+Xpnbj8>%y>Z`^*AZ0LirIa|;37V=H!Y_b zc@9qa{kMhCIFdMJW`9|~(4kIjlAReq6f|Svp8=tivK5Kr$jDKfKlW!#) zJTR)oXneVHMu{p`Sd!01zKoAHtVMo~VO%FPq(4Tz`W5m#ZIc~$J)Zx5gHIN`As+F1 zvN8%MyxI39cnx^nxTE}|U;ho>{%;m?GYy%Y54L|EU7vjq_3D~(WeQ=Ke=iFjk%W)0UvJ%aM1!=B{$p(AMts@u3O$IM(9w-Rzs1H zD9>Fi;DdW!!Lxt8EqxclvB9nD&SBK(UWiXMj7JM7N)!;TILHf=ab}L~u=hma;K`N4 zXqA@h9e5rx(_N|Hn)RxZg=REah8u*aot>oLwFSw&#^af8gQe0V5~6oFfBz!9HT*4U zRWbQ38>89%_h7)}XEE{Hk1+M`Z}HpmKe2etLaf=e8f!Oh!>TpgvEcW$ z`0IiHs#=ixLwSI6sIz zZF#~f$CMbpGfyujy`o3|@wBO^)-1xf*Us%wR7yWP6zW^7uSNsQB`W5BzZ1jmYbzcXTEB>1 z(O;r5V~(r6vZrUhupX_MO+sm9jk(8kgE(=$dZnsl-4ktornspM$IQt|0S5)g*=&b) z^cxu0qK|3P9y6XkUHm83%`pr-T9c*k&%&bfsp4-kc}5&Sg%A2X^osPjEXfM433nIU zEzXvjb`IKOhD8u70(v+9Lc)}hpNBh`4_R(Fe=HgAKKvy9x9nZyX2}!K6$lLOi}wcJ zhl$^R3;!AxTGQn&#<>W`$VrHmv$E(;ITK7$gv9*%$$z;2*^Z(UM+ZoPqQp5z6eW4m zD!1j#ySqx1h`BJ`fTIwml7{l~^Gyd?{GJ0G9BnI1#B<#H=-ITVPzFQFcvkvey!OE~ zoH@?WtaZ@jX|VqA2E@j&lVcz}paZnkjRhUc5YA<);wokS+XPMS1zb5<-~huwJij%T zcl7Ro>_r&2fJ}|1!g);QAQN^z{|t_a;-&-KE@YgW2$ zx#WdH7&fI-$wG1(ZLYWh3~$FD+MDzmtcljy4UM}$h9-^si_>oS`)9AL#79rQAyv?| z21?OIY8KEK5j6@DMB=iq!fj6ZNCK$iZ`)Nvw_LJ*r3s7S9a6|+O3G#ufpMdPk({i> z?=QZNw|<#o9C$Z;5zg~@qQfY>F>NO5^|fwA`@0F&#w#+#UL1CnW6>AUrt|0V{!inf zFX;VHlqg1=Y-qmHD>Lupbnv@Ql!$z2O@bsNd>-NX2w~!AUJ1}hnLviOFnCNhp6zBN zyM8MSs?!8@8w?g(;_K=1u=wakEc*OkX;!s0c<`I&@It$GaQ8N>HItRL7hmq$3g^FG zleWu}npJCx!%*~KtTnip8Ve0msVV>}FCSq<3Q{-I^D-)=$8>Jr6-=A>1b*H5C-QS? z+YI>lcE`ITCt~dMPvH|>KB}Tr7K#ev5t?v8vVwHc2(tHz=FfnLa0`jHfXgDEU8%wV$8(|`eeJ7%24a2N!qRG94*^3t< zgQwd{-2cTB_+-R5Axk!Da00Jq@IB8)QzdK7bF#n_?K&!QP?gqT~!E0E5YytFIV+k7v7c{9p9G^{|hAz*ItJHP( z$_mb5DhhUfH4cS)Hx#9OO+K{RLZZZMyOp9*oaAZ_jJgOkB%D>8OCR zq+?9bO5s1kq!gX1Yqz_TSe~1DOhQglbEzu^C`uZwMp|js2p=#@igk+Q zbZ}O4kk>D|>qUvkmYWAcJ{YEh;;3XOuVh(yhF>y<^2EF9R5qU7YKBu<0!|8Hx^#|( zcnx&v(N|ii%$iV_$9*yupFjF2d;<&xO_>nz)6Qkc`pNo>0~UqOQ3YSM?K1Po-YXSn z;lNs61r|n=ABX%{MpVi-$=9;5^!1nU?tcqRCAo-;TMN84_9Hy@zfa)rU$Xnw&h2qC zkBMZm!mbg?Tgo?WQ*`JRvF=?_qI6ef-rb7pJtosZ;Q@6N3ZvpEUkRXxWfCSjS+~gL zw>I9Je#5RkC&Y&`#P0mOy5iZ%W8h&UCvNnVVfg&-c;3DpQ3K2S-nIt3M9YzWoEuyH+haQ_PM>r%5fp*KszwI$h$Kf{Hn zg2DH$k;5=xRQ*!ju@-!xW|23-BxP`T}V^NBlza(?@LIU#hvx*AgG08z*wt^|)<|T&g zqI4ayxxPV^h)Q@&iktbY*I^#Yld^!yK$zTO&tlHu!{Wmj(v{uZ+u-xxKe*+EbUnGk z3tzQ321~zq6*1aaH0asP_{{Aw{PlY<{gH=E;>2?JOLqT^w0YKB66AWaBiq2$qqcF& zHk?d~L+<(g#;x*;W33i&b17K;I4$EEuI#pcwUE+qVAgWH@#W_@erBN%eM8A7Pw$SH zGVX2M_wgI>wA~zPI|A2Rj8$JdaS59IOwqYDW#&%m&;P}wk0&8B=MqL$3B`z4 z`xsww=Zt)R3_ck#LWomWXrb`7U{|CY>Qh;8HoOkN8$ZXXQD~phQ|1W9b4k+Z0dg( zNp?bI)C^$5rfjTyb2_FinuXNVo#;`&BZf_V8!i^kZW=r70Zi)L z0bc%wc~03G2Qd53PvM+s{W3m{YD9?BW7}otFI+v3{5|F0YpNMhS6sgZ6;AeCGb|if zmMnbZLwvSoj_I0qa;=Z2I^BzxzWxT`^=zAES>9lx+xhd%8?Zg@#boxLyLuIRU7?x$ z^p1EzF?@Gd7G%90%-tFuD5{I;fH9O;5GuVjI-yB&-7$^EwjPr@qX#D7`ex=Uze0n${Uip77JFc(_Tp zY5k~c@#}H*AL}ous0nQZR}b4xxXeyJgmb6XmHV!tBD^h})CDW=#-=ACX1j6W$ueO6 z>mOjo`njgeJ5QfZcxU*N82#x7aC5h9B%(ZW@b|g+_=V?;TegMkMBNO%u$-q7Qm-Fh z;?flqC03HQk1LfB`6`zW6n~lyTE0eG;v>k{DU3^9XvBH3WmaC)-(Q%@`}_NK#-L|j zz3I$snQXl~Y6dsQ$_qJIxn=>bMKgx4L-+a}5Z1Jg)pS**QsDl#-axOGjp5+vCC!~# za_TT#k6SmSmtDUde0^=VHj@Dz)?7|R?h%#)E5A5KG=#rTP2-lZAjwB?Vf{MemTj3k z=>vSRZjLlDo403p=s{1kM1@nL&uApvdvm2;bMI^hJe5{)I8Yy|VbZe>>(eYu>L z4sGGAPR?1Pdq>ZS-cq5#Cp$$$^DB!HWnTU%p%o9=3n&R3W0DsK)Mz=6$u^-pg(eqk z&d6f|3Jk4R4_*5gzt&8EFVO9cC-6Y4E^zZ~Y}^ZMJF^R0zx*9n;>33<;dd`Bby`(H z_t1uD(3oRb4#-U1gS6d$7@o8e8W(po@U(3xk6gX74Y4PeO4mnuh25#PAq8cd^x9-B zJ5f*!)mHrd>id|vVYW2>$gpe2x+C%Fr~gBfLAUn)?umkq+}pIBN1AiNtkF$DM}3C$*JKc`)cRrn0rj-;v}IJ-IJ15Tjnou#ap7)I6$Ga zjWD?!--WbPR*VN+y_%!T@X=C9`r@ERRJ{r?VfuJ{`^Lv;Qe!x{z+Y6%a!gtHE8ZCW zIF|ps&uYtMI4T|B=_!tpgCRB$onfUvWJa5C;T70;oQZ`+mz+;R&XI!hP~{CKKs2e_ z01C%~4$YDHI3!=PT|w84?@z;b+m}O|7Y#?JDj3 NziqdsJ&+yE=yzftZ#5;QXZo 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 S-AwW`#1E{fu22Os|L{NMEB7e? literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonclear6.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonclear6.png new file mode 100644 index 0000000000000000000000000000000000000000..5cec7a105d569aa2f9d4b37f709496b29eb96e2a GIT binary patch literal 78392 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

3s~@flweDA35(#PV^FB#0ww8-D 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

qC|Je@A{CPaWgAH#&us)?C)p`xW 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Ɛ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 H9i#sbzl81d literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonclear7.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonclear7.png new file mode 100644 index 0000000000000000000000000000000000000000..417af6c61166c727b9ab50cdd14a498c68744e0d GIT binary patch literal 77056 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 NziqdsJ&+yE=yzftZ#5;QXZo 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 S-AwW`#1E{fu22Os|L{NMEB7e? literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonclear8.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonclear8.png new file mode 100644 index 0000000000000000000000000000000000000000..de64264914cc8330093550eab1f0ffaa01e91be9 GIT binary patch literal 71177 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%0Dr63wml-6F*tONlh7MSGocvsq)+w z+6KaQFt(~SjU^wAhl78&7BNj_e@8M6t(p$I3%zx)m{%V{B_HjE|mg}#L`BIL_(40Fqkt+7B_{~Q{HqhAtIRnQrE^4&}y?1i! zEHXuz1*`A%f=yd{1P?i&c^oJq?+=!c9*-bL(W}mKNyJLv%xU50p{q;0dt5m!VB)@K zFO4=yy_1!>NND3R;&w#db&hom0v5beJLz?PcdTqMnYEW`_KiGQWY8_0Z=;M3YZgd~ z35Gcsl8JWNo!+<};9(E_ePFUYUWp4Ash z3(}_Ty%g|C;S@AfE3*{v1LY{6*MK94B<^o0{N{0Qel@w{rUGcjRxVWuANN#=p4*#x zSbQ;u2@gHon(;&YZ4x@!_%*nZ<={3pH_^BkyKHo-67aVuSnA<)_q*Frg=-#$j;42j zrNw8<2M@O2sIeS>e5hB<7Yd-Gv&u+*;b+k*n)mSC${VENREpRff1|O(-Bx{3?%S?f z?z>>vs<3Fl-`fNMWY3k_oMvB^7Cs6ll53k)Py^H}8|D(`rrcOwQcUOl+kTSN34w1>!beN0ih=*H(=()LpdjP(FalABKlVU zWQ{?Dv?99VEFgMj?j=MWZ$$$d{zcB)5^m~81&n2q`L*ZvDw&;*LYZCWN9)8WsT)?J zB0Foc3&A-v)G;Vyv3P8WZts$>`+UgMna&0^*Ho(hc8ZnkrnGG>&#MsMsJQ@RKJlq6 zyBd7vV!HiW_8;a(XVdWOa%sG<8?bu>`SglE{A#D%pOmOjotH7M2NO_&p6_K}9(H$( znJ3xN);|3Z`jN9&>Lny@Y*qj4*Y`=pU~2CPq4!DSSFJ@$rYX2$FQUNVLb`lI|L>+Q5dS zG#W#FW+i~IS1!w&ks*O6Vlf)ycEuif2_0(F_U~NN>!O1bMs}hLJ0rtm<*Pb%C|GQI zGum{HCL@<00Ipw2pbgL-n>gvdK4)O9xuj{i-z>DDS%y0jO4w8k@qShkvKZQQ=Yg~I zwON%)&r$OiSY_^!$H(!myj@utY3l2!*h^JO49Nn|7<G_+c@D)KWzzzGutfe@-c{ zFtM}o_WLHPy>49JFmr73)sTgJSaVT>5B_5?Ku8IGFl(?EN_P9OmA#p*XD_yk{wE!s zg!A58aSs39EmU?PQlAgp`5OULnJjBa+t@mqT>$r5T@Fa1x)%OGQ&U4jXdxUI;9tpG z^#Yzz?IvTF0lN(UBoWqz^@t7B$h+z!P%wpRb#tO8;N@BC6J{*O!|5uBi`w3?mLs`& z#6YWO2X@y5aJKWu3UL}>3*SAzTkKPck}gb?n`(%==Lbme?SXz>%hZMrC*(uN zr3wBt2@rY-^%uWSRAh=AYoc2QUCwghhYu|GB2Q)+qQttUah|r2b_*l9qIG>Feu^a& zcrBy66KL; z-gjtRp&e$Q5f!!)WOnhBA+poItl5WK><~yj$6~@Q>}Gf{s~wH6ny0e9C^5H~crf;G zRv{<$r!q1REG9v9b3Y;R<4#WR$b^-ahTk^XLp-~sRmb+?Pd6F+gNmRbLj6UUVX;u= z@e9IJZ$n;-#W+D4$=UM4w3UXgRPG;c>FWkWa?8|)acCrrG59*n2Tcd|Xb~NxMk^~y zk3~Fu3hy`>3W&q$PuQS6o1k4Dj!J$+Jx6-qR|yHBwf3#1JCnX+`Fyi^Pcb;Y=fQjZ zQ`H}QqNQY2Wc*hczqt5!e|hAcBxl4OLbtkSen`QPek|_QJW<-a@3AWmd&wCzs3k9N zIgaDUe!R<9Y2w`e+zLwbV*aADF8or1)Xu4`pa0EM>-Q(7QVFjb0&e-xatI(Yf4L{3;ujj0zMiN7|v3REA5W} zNMhOuw*cE=9Bj)gUCD#xRpg|{6K-%Hl5&YDl?&%sX7A2K<EK6-^#>I_4s;l?3>+ z2s|RzW6Ob*qKv6ON_tDa`HLYXZ{&*5U=DpB(n2E>!P~J`BM=A#m9dB2$=IyBsTl>y z9<`1iOsn0=QS@ILw{KqO*=S9{RD$IBb{w(nN77bTI@XaLRwpdK^8PwoDDPjskG@r897k8ysKDq*b{$)+0<`N6!-K zbWfH_S4&$bh|hKsMikaff%4Vq?RfYnU|Z$=o9Oc;O{>daCTrwodr99e@A{pe#Htyo z#MHCXcoBFE9sdcYX$^-PRC|OuB+d!6m}UyUvOvuZ2?bwok}ENxIWpA z14~EMSNatY0hzJfr%M**wvu0lp0}O5Yn+ci8yvA)&1#(d1~vEXw5v4l^hj?FnB%X4 zZwm(Ae1!NcvW<(y>5j4I*MoD}%h4n|?0$TS+QXLFj|`-Lr7TcwJXOqRu3mls7{+^_ za(HJB(-EyR4apDwkQF2bOXhB$;@mibB8(n6@{NXk9y!SsC_ZXqXrrQ*-4-7zL(P;w}`O6C_?dL@9XtO(2v zA_S2fVb7%W(4(prU`FK+LqS8(FL?~XL%}^Crpj;(VC5msx-2rNSsp1B%Xi+KxHcc| z^+CZ_eD3$o1^Hvxi0Znind5Q(VPIqf@v0_VI}!$>P#P{*12JR-e4>P!kr!0qsc@1I zjzke3uZej`Zn?~_QJZ`BR|qlpgQWPyP1K#O5ir;u8r+~tw7W=(|NX!RrB%9o#c`qO z9f%Ww$21c992;Ch{@z`{ylVg6?sz}s!b37owyo@`xR5Aa;l0(-9K|}-54SDG75!7& zJeNqxCmht8o57HAQ|@nMTi(00u)pZF9glXmyy#L_y7A8O<+j7k3-z7SG`73vMp?_? zv9rGnJBZKd5YIKj3tFa1P7xpL6?UZ0!h_x)C`@%Nz_Ym5*=&8Qsat zug!|O4DDDr-&Cqy@MH~k!UYBe78j?XVpvS=qg~T;DR#?yJ^(f0MlcRC*lsGJ^RiJ&k)OjG7HU?aQGW^*hnx|EhQ5~*3WUz z$F^Pfi}a?h{_QzrtVv?_zj(eE@#KJBvzG3k%27HvD(BF4e8R9Ud>Q0qn^ND~)5CjR zcrWYA3{7SgzAD8Yg+|%dJ`Gi>y79_D30Dn|D3?zcG&enZWN6UL{Pk;IZeE48Uq12H z^x?X@gb)lbZ^=Q5v*G2a0nsLCT)X-tGCPHy-Krry=V>GlJ`+#Q{E9KeP{09O$L*gw zL#in$;)M9jF@Ir0kMp!es5w!_+6VDgx`P+OaE@3W;;+hdSqN^XwybIj@AZb z?mg{!e1m+rF1YDt)6&Y+cbYcw{{Yn&OD4%jj{lEzUo1dUV3708omuXvJ?(ErB8ZH~ zkphsi7~p+;jm%YQj7WgVcE<#BP=$x?Kl1H>MUwjJ3<3`9<2&}sL;`7Znm4>DD@g{Y z{&1Tf*=MNzc!$`3!Mc?67HRt6J-|c3tW;0oaNKatbCc8BhFOozOHPRF^u+04Lnsp$FWj%@4Ir4Z(ko`-N{J%p|k#u|+x0o|)<#Nh>g*1%y L&GqVZUE=-^HyYM< literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonfail0.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonfail0.png new file mode 100644 index 0000000000000000000000000000000000000000..a25325815275dfe5e34358f3509f93fa273ba40f GIT binary patch literal 67970 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 literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonfail1.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonfail1.png new file mode 100644 index 0000000000000000000000000000000000000000..2dcaeb92e3bf4650be65948ebacd9a7ae8bc24ed GIT binary patch literal 69118 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

  • ATKFj*pA9VSx9n&4rRut6B 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 literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonfail2.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonfail2.png new file mode 100644 index 0000000000000000000000000000000000000000..0f0b1d4f59fa046156fb8911ba14cb73ccb60710 GIT binary patch literal 73351 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 BEpq?> literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonidle0.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonidle0.png new file mode 100644 index 0000000000000000000000000000000000000000..dde0adabb403485d9f92830f5ab95a4b4396ad83 GIT binary patch literal 68649 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 OK*OB3F|9W7i2WDz654kF literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonidle1.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonidle1.png new file mode 100644 index 0000000000000000000000000000000000000000..7295e95efe9c962cf5e5cee94ff928241b81765c GIT binary patch literal 69329 zcmV)@K!LxBP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D|D{PpK~#8N?41Q* z6h{}w|982#dq{|2!QI`xxRsWo#i3AIoVH&n6{tZ4D^lEx6|3Ox?h;5s+~ty-ee-s2 z?=BFzBzKn!&5usAvuAsIyZ?Ff=FJh(sdwRw@4hgdbbX3T<1c{|a^D)xSkW z_u`{%p)PC-btNmh`-J_%-%<)mUiW_~=1Hk&zxfD1B8jhVq5Z;NVVeqxwd9q*y06jY z)w;gCdeKo`D1tx>e--L&p$K)|*HF=g>cVq`*9hChtIz+>QYzXdk5bVt`RIzSAh1GR z_)CQZSSXgP&~tU4FVyKVE1jY%+`?y3A%wpLMY!nbudq#m`f4Gu6^dM4bobF~>3O

    8CJfgqHE01HJBS}KAVQxSw#5L@9f5@Bog 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%0DW8d%49(wA_m-Wzf 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 K%$b<;G5-Nz>y;7! literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonkiai0.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonkiai0.png new file mode 100644 index 0000000000000000000000000000000000000000..911378cbe91f2a6fbaa7d2ee1f8e71de3f7e2d75 GIT binary patch literal 76964 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

    g`m? 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 literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonkiai1.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonkiai1.png new file mode 100644 index 0000000000000000000000000000000000000000..f89568bca25c2a7bbef738d024c2f2f25ac398e0 GIT binary patch literal 75434 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%0DYz4H_ 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 E2j`LGcmMzZ literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs index dc89fa3a59..d11bfa05ba 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs @@ -6,9 +6,9 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; +using osu.Framework.Bindables; using osu.Framework.Testing; +using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; @@ -18,7 +18,6 @@ using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.Taiko.UI; using osu.Game.Rulesets.UI.Scrolling; -using osu.Game.Skinning; using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Taiko.Tests.Skinning @@ -38,35 +37,28 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning TimeRange = { Value = 5000 }, }; + [Cached(typeof(IBindable))] + private Bindable beatmap = new Bindable(); + private readonly List mascots = new List(); - private readonly List skinnables = new List(); private readonly List playfields = new List(); + private readonly List rulesets = new List(); [Test] public void TestStateTextures() { + AddStep("Set beatmap", () => setBeatmap()); + AddStep("Create mascot (idle)", () => - { - skinnables.Clear(); - SetContents(() => - { - var skinnable = getMascot(); - skinnables.Add(skinnable); - return skinnable; - }); - }); - - AddUntilStep("Wait for SkinnableDrawable", () => skinnables.Any(d => d.Drawable is DrawableTaikoMascot)); - - AddStep("Collect mascots", () => { mascots.Clear(); - foreach (var skinnable in skinnables) + SetContents(() => { - if (skinnable.Drawable is DrawableTaikoMascot mascot) - mascots.Add(mascot); - } + var mascot = new TestDrawableTaikoMascot(); + mascots.Add(mascot); + return mascot; + }); }); AddStep("Clear state", () => setState(TaikoMascotAnimationState.Clear)); @@ -76,59 +68,25 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning AddStep("Fail state", () => setState(TaikoMascotAnimationState.Fail)); } - private void setState(TaikoMascotAnimationState state) - { - foreach (var mascot in mascots) - { - if (mascot == null) - continue; - - mascot.Dumb = true; - mascot.State = state; - } - } - - private SkinnableDrawable getMascot() => - new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.TaikoDon), _ => new Container(), confineMode: ConfineMode.ScaleToFit) - { - RelativePositionAxes = Axes.Both - }; - [Test] public void TestPlayfield() { - AddStep("Create playfield", () => + AddStep("Set beatmap", () => setBeatmap()); + + AddStep("Create ruleset", () => { - playfields.Clear(); + rulesets.Clear(); SetContents(() => { - var playfield = new TaikoPlayfield(new ControlPointInfo()) - { - Height = 0.4f, - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - }; - - playfields.Add(playfield); - - return playfield; + var ruleset = new TaikoRuleset(); + var drawableRuleset = new DrawableTaikoRuleset(ruleset, beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo)); + rulesets.Add(drawableRuleset); + return drawableRuleset; }); }); - AddUntilStep("Wait for SkinnableDrawable", () => playfields.Any(p => p.ChildrenOfType().Any())); - - AddStep("Collect mascots", () => - { - mascots.Clear(); - - foreach (var playfield in playfields) - { - var mascot = playfield.ChildrenOfType().SingleOrDefault(); - - if (mascot != null) - mascots.Add(mascot); - } - }); + AddStep("Collect playfields", collectPlayfields); + AddStep("Collect mascots", collectMascots); AddStep("Create hit (miss)", () => { @@ -136,7 +94,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning addJudgement(playfield, HitResult.Miss); }); - AddAssert("Check if state is fail", () => mascots.Where(d => d != null).All(d => d.PlayfieldState.Value == TaikoMascotAnimationState.Fail)); + AddUntilStep("Wait for fail state", () => mascots.Where(d => d != null).All(d => d.State == TaikoMascotAnimationState.Fail)); AddStep("Create hit (great)", () => { @@ -144,12 +102,111 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning addJudgement(playfield, HitResult.Great); }); - AddAssert("Check if state is idle", () => mascots.Where(d => d != null).All(d => d.PlayfieldState.Value == TaikoMascotAnimationState.Idle)); + AddUntilStep("Wait for idle state", () => mascots.Where(d => d != null).All(d => d.State == TaikoMascotAnimationState.Idle)); + } + + [Test] + public void TestKiai() + { + AddStep("Set beatmap", () => setBeatmap(true)); + + AddUntilStep("Wait for beatmap to be loaded", () => beatmap.Value.Track.IsLoaded); + + AddStep("Create kiai ruleset", () => + { + beatmap.Value.Track.Start(); + + rulesets.Clear(); + SetContents(() => + { + var ruleset = new TaikoRuleset(); + var drawableRuleset = new DrawableTaikoRuleset(ruleset, beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo)); + rulesets.Add(drawableRuleset); + return drawableRuleset; + }); + }); + + AddStep("Collect playfields", collectPlayfields); + AddStep("Collect mascots", collectMascots); + + AddUntilStep("Wait for fail state", () => mascots.Where(d => d != null).All(d => d.State == TaikoMascotAnimationState.Fail)); + + AddStep("Create hit (great)", () => + { + foreach (var playfield in playfields) + addJudgement(playfield, HitResult.Great); + }); + + AddUntilStep("Wait for kiai state", () => mascots.Where(d => d != null).All(d => d.State == TaikoMascotAnimationState.Kiai)); + } + + private void setBeatmap(bool kiai = false) + { + var controlPointInfo = new ControlPointInfo(); + controlPointInfo.Add(0, new TimingControlPoint { BeatLength = 90 }); + + if (kiai) + controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true }); + + beatmap.Value = CreateWorkingBeatmap(new Beatmap + { + HitObjects = new List { new Hit { Type = HitType.Centre } }, + BeatmapInfo = new BeatmapInfo + { + BaseDifficulty = new BeatmapDifficulty(), + Metadata = new BeatmapMetadata + { + Artist = @"Unknown", + Title = @"Sample Beatmap", + AuthorString = @"Craftplacer", + }, + Ruleset = new TaikoRuleset().RulesetInfo + }, + ControlPointInfo = controlPointInfo + }); + } + + private void setState(TaikoMascotAnimationState state) + { + foreach (var mascot in mascots) + mascot?.ShowState(state); + } + + private void collectPlayfields() + { + playfields.Clear(); + foreach (var ruleset in rulesets) playfields.Add(ruleset.ChildrenOfType().Single()); + } + + private void collectMascots() + { + mascots.Clear(); + + foreach (var playfield in playfields) + { + var mascot = playfield.ChildrenOfType() + .SingleOrDefault(); + + if (mascot != null) mascots.Add(mascot); + } } private void addJudgement(TaikoPlayfield playfield, HitResult result) { playfield.OnNewResult(new DrawableRimHit(new Hit()), new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = result }); } + + private class TestDrawableTaikoMascot : DrawableTaikoMascot + { + public TestDrawableTaikoMascot(TaikoMascotAnimationState startingState = TaikoMascotAnimationState.Idle) + : base(startingState) + { + } + + protected override TaikoMascotAnimationState GetFinalAnimationState(EffectControlPoint effectPoint, TaikoMascotAnimationState playfieldState) + { + return State; + } + } } } diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs index f05c335456..2c94f5f1cb 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Allocation; using osu.Framework.Audio.Track; using osu.Framework.Bindables; @@ -11,68 +12,29 @@ using osu.Game.Graphics.Containers; namespace osu.Game.Rulesets.Taiko.UI { - public sealed class DrawableTaikoMascot : BeatSyncedContainer + public class DrawableTaikoMascot : BeatSyncedContainer { - private static TaikoMascotTextureAnimation idleDrawable, clearDrawable, kiaiDrawable, failDrawable; + private TaikoMascotTextureAnimation idleDrawable, clearDrawable, kiaiDrawable, failDrawable; private EffectControlPoint lastEffectControlPoint; - private TaikoMascotAnimationState state; public Bindable PlayfieldState; - ///

    - /// Determines if there should be no "state logic", intended for testing. - /// - public bool Dumb { get; set; } - - public TaikoMascotAnimationState State - { - get => state; - set - { - state = value; - - foreach (var child in InternalChildren) - child.Hide(); - - var drawable = getStateDrawable(State); - - drawable?.Show(); - } - } + public TaikoMascotAnimationState State { get; private set; } public DrawableTaikoMascot(TaikoMascotAnimationState startingState = TaikoMascotAnimationState.Idle) { RelativeSizeAxes = Axes.Both; + PlayfieldState = new Bindable(); PlayfieldState.BindValueChanged(b => { if (lastEffectControlPoint != null) - State = getFinalAnimationState(lastEffectControlPoint, b.NewValue); + ShowState(GetFinalAnimationState(lastEffectControlPoint, b.NewValue)); }); State = startingState; } - private TaikoMascotTextureAnimation getStateDrawable(TaikoMascotAnimationState state) - { - return state switch - { - TaikoMascotAnimationState.Idle => idleDrawable, - TaikoMascotAnimationState.Clear => clearDrawable, - TaikoMascotAnimationState.Kiai => kiaiDrawable, - TaikoMascotAnimationState.Fail => failDrawable, - _ => null - }; - } - - private TaikoMascotAnimationState getFinalAnimationState(EffectControlPoint effectPoint, TaikoMascotAnimationState playfieldState) - { - if (playfieldState == TaikoMascotAnimationState.Fail) - return playfieldState; - - return effectPoint.KiaiMode ? TaikoMascotAnimationState.Kiai : TaikoMascotAnimationState.Idle; - } - [BackgroundDependencyLoader] private void load(TextureStore textures) { @@ -84,21 +46,60 @@ namespace osu.Game.Rulesets.Taiko.UI failDrawable = new TaikoMascotTextureAnimation(TaikoMascotAnimationState.Fail), }; - // making sure we have the correct sprite set + ShowState(State); + } + + public void ShowState(TaikoMascotAnimationState state) + { + foreach (var child in InternalChildren) + child.Hide(); + State = state; + + var drawable = getStateDrawable(State); + drawable.Show(); + } + + private TaikoMascotTextureAnimation getStateDrawable(TaikoMascotAnimationState state) + { + switch (state) + { + case TaikoMascotAnimationState.Idle: + return idleDrawable; + + case TaikoMascotAnimationState.Clear: + return clearDrawable; + + case TaikoMascotAnimationState.Kiai: + return kiaiDrawable; + + case TaikoMascotAnimationState.Fail: + return failDrawable; + + default: + throw new ArgumentException($"There's no case for animation state ${state} available", nameof(state)); + } + } + + protected virtual TaikoMascotAnimationState GetFinalAnimationState(EffectControlPoint effectPoint, TaikoMascotAnimationState playfieldState) + { + if (playfieldState == TaikoMascotAnimationState.Fail) + return playfieldState; + + return effectPoint.KiaiMode ? TaikoMascotAnimationState.Kiai : TaikoMascotAnimationState.Idle; } protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) { base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); - if (!Dumb) - State = getFinalAnimationState(lastEffectControlPoint = effectPoint, PlayfieldState.Value); + var state = GetFinalAnimationState(lastEffectControlPoint = effectPoint, PlayfieldState.Value); + ShowState(state); - if (State == TaikoMascotAnimationState.Clear) + if (state == TaikoMascotAnimationState.Clear) return; - var drawable = getStateDrawable(State); + var drawable = getStateDrawable(state); drawable.Move(); } } diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs b/osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs index 2c04d3e1dc..c8e97b9f8b 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; @@ -39,7 +40,7 @@ namespace osu.Game.Rulesets.Taiko.UI { foreach (var textureIndex in clear_animation_sequence) { - var textureName = _getStateTextureName(textureIndex); + var textureName = getStateTextureName(textureIndex); Texture texture = skin.GetTexture(textureName); if (texture == null) @@ -52,7 +53,7 @@ namespace osu.Game.Rulesets.Taiko.UI { for (int i = 0; true; i++) { - var textureName = _getStateTextureName(i); + var textureName = getStateTextureName(i); Texture texture = skin.GetTexture(textureName); if (texture == null) @@ -63,10 +64,13 @@ namespace osu.Game.Rulesets.Taiko.UI } } - /// Advances the current frame by one. + /// + /// Advances the current frame by one. + /// public void Move() { - if (FrameCount == 0) // Frames are apparently broken + // Check whether there are frames before causing a crash. + if (FrameCount == 0) return; if (FrameCount <= currentFrame) @@ -77,18 +81,27 @@ namespace osu.Game.Rulesets.Taiko.UI currentFrame += 1; } - private string _getStateTextureName(int i) => $"pippidon{_getStateString(State)}{i}"; + private string getStateTextureName(int i) => $"pippidon{getStateString(State)}{i}"; - private string _getStateString(TaikoMascotAnimationState state) + private string getStateString(TaikoMascotAnimationState state) { - return state switch + switch (state) { - TaikoMascotAnimationState.Clear => "clear", - TaikoMascotAnimationState.Fail => "fail", - TaikoMascotAnimationState.Idle => "idle", - TaikoMascotAnimationState.Kiai => "kiai", - _ => null - }; + case TaikoMascotAnimationState.Clear: + return "clear"; + + case TaikoMascotAnimationState.Fail: + return "fail"; + + case TaikoMascotAnimationState.Idle: + return "idle"; + + case TaikoMascotAnimationState.Kiai: + return "kiai"; + + default: + throw new ArgumentException($"There's no case for animation state ${state} available", nameof(state)); + } } } } diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 6d9d263141..ebb3e0e786 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -282,15 +282,15 @@ namespace osu.Game.Rulesets.Taiko.UI mascot.PlayfieldState.Value = isFailing ? TaikoMascotAnimationState.Fail : TaikoMascotAnimationState.Idle; } } - } - internal class ProxyContainer : LifetimeManagementContainer - { - public new MarginPadding Padding + private class ProxyContainer : LifetimeManagementContainer { - set => base.Padding = value; - } + public new MarginPadding Padding + { + set => base.Padding = value; + } - public void Add(Drawable proxy) => AddInternal(proxy); + public void Add(Drawable proxy) => AddInternal(proxy); + } } } From fcded206559d688f0d363d12508b666ee779abd4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 27 Apr 2020 08:58:01 +0900 Subject: [PATCH 1307/2376] Don't specify IProvideVideo interface for now --- osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs b/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs index aea531e88d..9785b7e647 100644 --- a/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs +++ b/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs @@ -10,7 +10,7 @@ using osuTK.Graphics; namespace osu.Game.Tournament.Screens.Showcase { - public class ShowcaseScreen : BeatmapInfoScreen, IProvideVideo + public class ShowcaseScreen : BeatmapInfoScreen // IProvideVideo { [BackgroundDependencyLoader] private void load() From d46643ec5270942bb0e590fb5c2febea0101ba02 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Mon, 27 Apr 2020 02:10:12 +0200 Subject: [PATCH 1308/2376] Rework special case for strong hits --- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index ebb3e0e786..9a2cfad8a4 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -270,16 +270,12 @@ namespace osu.Game.Rulesets.Taiko.UI if (characterDrawable.Drawable is DrawableTaikoMascot mascot) { - var isFailing = result.Type == HitResult.Miss; + var miss = result.Type == HitResult.Miss; - // Only take combo in consideration when it's not a strong hit (it's always false) - if (!(judgedObject.HitObject is StrongHitObject)) - { - if (isFailing) - isFailing = result.Judgement.AffectsCombo; - } + if (miss && judgedObject.HitObject is StrongHitObject) + miss = result.Judgement.AffectsCombo; - mascot.PlayfieldState.Value = isFailing ? TaikoMascotAnimationState.Fail : TaikoMascotAnimationState.Idle; + mascot.PlayfieldState.Value = miss ? TaikoMascotAnimationState.Fail : TaikoMascotAnimationState.Idle; } } From 48168dddcec916967327057454f649fe884a02d1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 27 Apr 2020 10:54:32 +0900 Subject: [PATCH 1309/2376] Adjust editor timeline current marker to promote tick visibility --- .../Compose/Components/Timeline/CentreMarker.cs | 13 ++++++++++--- .../Edit/Compose/Components/Timeline/Timeline.cs | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.cs index 0d4c48b5f6..0d2b35132e 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.cs @@ -12,14 +12,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { public class CentreMarker : CompositeDrawable { - private const float triangle_width = 20; + private const float triangle_width = 15; private const float triangle_height = 10; private const float bar_width = 2; public CentreMarker() { RelativeSizeAxes = Axes.Y; - Size = new Vector2(20, 1); + Size = new Vector2(triangle_width, 1); Anchor = Anchor.Centre; Origin = Anchor.Centre; @@ -39,6 +39,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Origin = Anchor.BottomCentre, Size = new Vector2(triangle_width, triangle_height), Scale = new Vector2(1, -1) + }, + new Triangle + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Size = new Vector2(triangle_width, triangle_height), + Scale = new Vector2(1, 1) } }; } @@ -46,7 +53,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline [BackgroundDependencyLoader] private void load(OsuColour colours) { - Colour = colours.Red; + Colour = colours.RedDark; } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index 8e6b3d5424..25f3cfc285 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -50,7 +50,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline }); // We don't want the centre marker to scroll - AddInternal(new CentreMarker()); + AddInternal(new CentreMarker { Depth = float.MaxValue }); WaveformVisible.ValueChanged += visible => waveform.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint); From 104c61d622589b199dd859abf3dfb2e6f0d0e7f0 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 27 Apr 2020 11:06:23 +0900 Subject: [PATCH 1310/2376] Remove unnecessary scale --- .../Screens/Edit/Compose/Components/Timeline/CentreMarker.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.cs index 0d2b35132e..8c8b38d9ea 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/CentreMarker.cs @@ -45,7 +45,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, Size = new Vector2(triangle_width, triangle_height), - Scale = new Vector2(1, 1) } }; } From 8a47a615dbb44c3e236f8b4b61ebe1609a9bcefe Mon Sep 17 00:00:00 2001 From: Joehu Date: Sun, 26 Apr 2020 19:29:22 -0700 Subject: [PATCH 1311/2376] Remove unranked label from footer --- .../TestSceneModSelectOverlay.cs | 12 -------- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 29 ++----------------- 2 files changed, 2 insertions(+), 39 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 2294cd6966..769c660f48 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -117,8 +117,6 @@ namespace osu.Game.Tests.Visual.UserInterface public void TestManiaMods() { changeRuleset(3); - - testRankedText(new ManiaRuleset().GetModsFor(ModType.Conversion).First(m => m is ManiaModRandom)); } [Test] @@ -217,15 +215,6 @@ namespace osu.Game.Tests.Visual.UserInterface checkLabelColor(() => Color4.White); } - private void testRankedText(Mod mod) - { - AddUntilStep("check for ranked", () => modSelect.UnrankedLabel.Alpha == 0); - selectNext(mod); - AddUntilStep("check for unranked", () => modSelect.UnrankedLabel.Alpha != 0); - selectPrevious(mod); - AddUntilStep("check for ranked", () => modSelect.UnrankedLabel.Alpha == 0); - } - private void selectNext(Mod mod) => AddStep($"left click {mod.Name}", () => modSelect.GetModButton(mod)?.SelectNext(1)); private void selectPrevious(Mod mod) => AddStep($"right click {mod.Name}", () => modSelect.GetModButton(mod)?.SelectNext(-1)); @@ -272,7 +261,6 @@ namespace osu.Game.Tests.Visual.UserInterface } public new OsuSpriteText MultiplierLabel => base.MultiplierLabel; - public new OsuSpriteText UnrankedLabel => base.UnrankedLabel; public new TriangleButton DeselectAllButton => base.DeselectAllButton; public new Color4 LowMultiplierColour => base.LowMultiplierColour; diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 36d21c8b46..914e730c27 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -37,7 +37,6 @@ namespace osu.Game.Overlays.Mods protected readonly TriangleButton CloseButton; protected readonly OsuSpriteText MultiplierLabel; - protected readonly OsuSpriteText UnrankedLabel; protected override bool BlockNonPositionalInput => false; @@ -268,30 +267,11 @@ namespace osu.Game.Overlays.Mods Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, }, - new FillFlowContainer + MultiplierLabel = new OsuSpriteText { - AutoSizeAxes = Axes.Both, + Font = OsuFont.GetFont(size: 25, weight: FontWeight.Bold, fixedWidth: true), Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, - Direction = FillDirection.Vertical, - LayoutDuration = 100, - LayoutEasing = Easing.OutQuint, - Children = new Drawable[] - { - MultiplierLabel = new OsuSpriteText - { - Font = OsuFont.GetFont(size: 25, weight: FontWeight.Bold, fixedWidth: true), - Origin = Anchor.TopCentre, - Anchor = Anchor.TopCentre, - }, - UnrankedLabel = new OsuSpriteText - { - Text = @"(Unranked)", - Font = OsuFont.GetFont(size: 15, weight: FontWeight.Bold), - Origin = Anchor.TopCentre, - Anchor = Anchor.TopCentre, - }, - } }, }, }, @@ -340,7 +320,6 @@ namespace osu.Game.Overlays.Mods { LowMultiplierColour = colours.Red; HighMultiplierColour = colours.Green; - UnrankedLabel.Colour = colours.Blue; availableMods = osu.AvailableMods.GetBoundCopy(); @@ -444,12 +423,10 @@ namespace osu.Game.Overlays.Mods private void updateMods() { var multiplier = 1.0; - var ranked = true; foreach (var mod in SelectedMods.Value) { multiplier *= mod.ScoreMultiplier; - ranked &= mod.Ranked; } MultiplierLabel.Text = $"{multiplier:N2}x"; @@ -459,8 +436,6 @@ namespace osu.Game.Overlays.Mods MultiplierLabel.FadeColour(LowMultiplierColour, 200); else MultiplierLabel.FadeColour(Color4.White, 200); - - UnrankedLabel.FadeTo(ranked ? 0 : 1, 200); } private void updateModSettings(ValueChangedEvent> selectedMods) From 1b9362041a4e4019c84f41f8230aca4f3d194459 Mon Sep 17 00:00:00 2001 From: Joehu Date: Sun, 26 Apr 2020 19:50:11 -0700 Subject: [PATCH 1312/2376] Revert multiplier number changes and set width Safe arbitrary width taken from "0.00x" (highest width of 67), rounded to the nearest tenth. --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 914e730c27..09f4befbc1 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -269,9 +269,10 @@ namespace osu.Game.Overlays.Mods }, MultiplierLabel = new OsuSpriteText { - Font = OsuFont.GetFont(size: 25, weight: FontWeight.Bold, fixedWidth: true), + Font = OsuFont.GetFont(size: 30, weight: FontWeight.Bold), Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, + Width = 70, // to avoid footer from flowing when clicking mods }, }, }, From dd36b839b9bb4bd558a7218e8e86c37334f86b5a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 27 Apr 2020 12:01:31 +0900 Subject: [PATCH 1313/2376] Refactor --- .../Objects/Drawables/DrawableDrumRollTick.cs | 2 - osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 38 ++++++++++--------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs index 536dd1209c..e12d1a0ba9 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs @@ -24,8 +24,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables FillMode = FillMode.Fit; } - public override bool DisplayResult => false; - protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.DrumRollTick), _ => new TickPiece { diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index d1d2571ec7..6103dea1fd 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -181,29 +181,11 @@ namespace osu.Game.Rulesets.Taiko.UI } } - private void playDrumrollHit(DrawableDrumRollTick drumrollTick) - { - TaikoAction action = drumrollTick.JudgedAction; - bool isStrong = drumrollTick.HitObject.IsStrong; - double time = drumrollTick.HitObject.GetEndTime(); - - DrawableHit drawableHit; - if (action == TaikoAction.LeftRim || action == TaikoAction.RightRim) - drawableHit = new DrawableFlyingRimHit(time, isStrong); - else - drawableHit = new DrawableFlyingCentreHit(time, isStrong); - - drumRollHitContainer.Add(drawableHit); - } - internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result) { if (!DisplayJudgements.Value) return; - if ((judgedObject is DrawableDrumRollTick) && result.Type != HitResult.Miss) - playDrumrollHit((DrawableDrumRollTick)judgedObject); - if (!judgedObject.DisplayResult) return; @@ -214,6 +196,11 @@ namespace osu.Game.Rulesets.Taiko.UI hitExplosionContainer.Children.FirstOrDefault(e => e.JudgedObject == ((DrawableStrongNestedHit)judgedObject).MainObject)?.VisualiseSecondHit(); break; + case TaikoDrumRollTickJudgement _: + if (result.IsHit) + playDrumrollHit((DrawableDrumRollTick)judgedObject); + break; + default: judgementContainer.Add(new DrawableTaikoJudgement(result, judgedObject) { @@ -237,6 +224,21 @@ namespace osu.Game.Rulesets.Taiko.UI } } + private void playDrumrollHit(DrawableDrumRollTick drumrollTick) + { + TaikoAction action = drumrollTick.JudgedAction; + bool isStrong = drumrollTick.HitObject.IsStrong; + double time = drumrollTick.HitObject.GetEndTime(); + + DrawableHit drawableHit; + if (action == TaikoAction.LeftRim || action == TaikoAction.RightRim) + drawableHit = new DrawableFlyingRimHit(time, isStrong); + else + drawableHit = new DrawableFlyingCentreHit(time, isStrong); + + drumRollHitContainer.Add(drawableHit); + } + private class ProxyContainer : LifetimeManagementContainer { public new MarginPadding Padding From 81df22d2a77b1b85d4c14ade4da13532ede7a3f3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 27 Apr 2020 12:17:36 +0900 Subject: [PATCH 1314/2376] Improve test scene --- osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs index c2ca578dfa..1822180795 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Taiko.Tests [TestFixture] public class TestSceneHits : OsuTestScene { - private const double default_duration = 1000; + private const double default_duration = 3000; private const float scroll_time = 1000; protected override double TimePerAction => default_duration * 2; @@ -45,6 +45,7 @@ namespace osu.Game.Rulesets.Taiko.Tests AddStep("Miss :(", addMissJudgement); AddStep("DrumRoll", () => addDrumRoll(false)); AddStep("Strong DrumRoll", () => addDrumRoll(true)); + AddStep("Kiai DrumRoll", () => addDrumRoll(true, kiai: true)); AddStep("Swell", () => addSwell()); AddStep("Centre", () => addCentreHit(false)); AddStep("Strong Centre", () => addCentreHit(true)); @@ -192,7 +193,7 @@ namespace osu.Game.Rulesets.Taiko.Tests drawableRuleset.Playfield.Add(new DrawableSwell(swell)); } - private void addDrumRoll(bool strong, double duration = default_duration) + private void addDrumRoll(bool strong, double duration = default_duration, bool kiai = false) { addBarLine(true); addBarLine(true, scroll_time + duration); @@ -202,9 +203,13 @@ namespace osu.Game.Rulesets.Taiko.Tests StartTime = drawableRuleset.Playfield.Time.Current + scroll_time, IsStrong = strong, Duration = duration, + TickRate = 8, }; - d.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + var cpi = new ControlPointInfo(); + cpi.Add(-10000, new EffectControlPoint { KiaiMode = kiai }); + + d.ApplyDefaults(cpi, new BeatmapDifficulty()); drawableRuleset.Playfield.Add(new DrawableDrumRoll(d)); } From 7dc090cc24ccd64f19aa5f010ded2c99871f176d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 27 Apr 2020 12:23:53 +0900 Subject: [PATCH 1315/2376] Add support for hit explosions --- .../Objects/Drawables/DrawableDrumRollTick.cs | 7 ++-- osu.Game.Rulesets.Taiko/UI/HitExplosion.cs | 10 +++--- .../UI/KiaiHitExplosion.cs | 10 +++--- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 34 +++++++++++-------- 4 files changed, 31 insertions(+), 30 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs index e12d1a0ba9..62405cf047 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs @@ -13,10 +13,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables public class DrawableDrumRollTick : DrawableTaikoHitObject { /// - /// The action type that the user took which caused this tick to - /// have been judged as "hit" + /// The hit type corresponding to the that the user pressed to hit this . /// - public TaikoAction JudgedAction; + public HitType JudgementType; public DrawableDrumRollTick(DrumRollTick tick) : base(tick) @@ -57,7 +56,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables public override bool OnPressed(TaikoAction action) { - JudgedAction = action; + JudgementType = action == TaikoAction.LeftRim || action == TaikoAction.RightRim ? HitType.Rim : HitType.Centre; return UpdateResult(true); } diff --git a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs index d4118d38b6..fbaae7e322 100644 --- a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs @@ -21,16 +21,14 @@ namespace osu.Game.Rulesets.Taiko.UI public override bool RemoveWhenNotAlive => true; public readonly DrawableHitObject JudgedObject; + private readonly HitType type; private readonly Box innerFill; - private readonly bool isRim; - - public HitExplosion(DrawableHitObject judgedObject, bool isRim) + public HitExplosion(DrawableHitObject judgedObject, HitType type) { - this.isRim = isRim; - JudgedObject = judgedObject; + this.type = type; Anchor = Anchor.Centre; Origin = Anchor.Centre; @@ -58,7 +56,7 @@ namespace osu.Game.Rulesets.Taiko.UI [BackgroundDependencyLoader] private void load(OsuColour colours) { - innerFill.Colour = isRim ? colours.BlueDarker : colours.PinkDarker; + innerFill.Colour = type == HitType.Rim ? colours.BlueDarker : colours.PinkDarker; } protected override void LoadComplete() diff --git a/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs index e80b463481..3a307bb3bb 100644 --- a/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs @@ -18,14 +18,12 @@ namespace osu.Game.Rulesets.Taiko.UI public override bool RemoveWhenNotAlive => true; public readonly DrawableHitObject JudgedObject; + private readonly HitType type; - private readonly bool isRim; - - public KiaiHitExplosion(DrawableHitObject judgedObject, bool isRim) + public KiaiHitExplosion(DrawableHitObject judgedObject, HitType type) { - this.isRim = isRim; - JudgedObject = judgedObject; + this.type = type; Anchor = Anchor.CentreLeft; Origin = Anchor.Centre; @@ -53,7 +51,7 @@ namespace osu.Game.Rulesets.Taiko.UI EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow, - Colour = isRim ? colours.BlueDarker : colours.PinkDarker, + Colour = type == HitType.Rim ? colours.BlueDarker : colours.PinkDarker, Radius = 60, }; } diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 6103dea1fd..a19c08ae7e 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -10,7 +10,6 @@ using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.Taiko.Objects.Drawables; @@ -197,8 +196,13 @@ namespace osu.Game.Rulesets.Taiko.UI break; case TaikoDrumRollTickJudgement _: - if (result.IsHit) - playDrumrollHit((DrawableDrumRollTick)judgedObject); + if (!result.IsHit) + return; + + var drawableTick = (DrawableDrumRollTick)judgedObject; + + addDrumRollHit(drawableTick); + addExplosion(drawableTick, drawableTick.JudgementType); break; default: @@ -213,25 +217,19 @@ namespace osu.Game.Rulesets.Taiko.UI if (!result.IsHit) break; - bool isRim = (judgedObject.HitObject as Hit)?.Type == HitType.Rim; - - hitExplosionContainer.Add(new HitExplosion(judgedObject, isRim)); - - if (judgedObject.HitObject.Kiai) - kiaiExplosionContainer.Add(new KiaiHitExplosion(judgedObject, isRim)); + addExplosion(judgedObject, (judgedObject.HitObject as Hit)?.Type ?? HitType.Centre); break; } } - private void playDrumrollHit(DrawableDrumRollTick drumrollTick) + private void addDrumRollHit(DrawableDrumRollTick drawableTick) { - TaikoAction action = drumrollTick.JudgedAction; - bool isStrong = drumrollTick.HitObject.IsStrong; - double time = drumrollTick.HitObject.GetEndTime(); + bool isStrong = drawableTick.HitObject.IsStrong; + double time = drawableTick.HitObject.GetEndTime(); DrawableHit drawableHit; - if (action == TaikoAction.LeftRim || action == TaikoAction.RightRim) + if (drawableTick.JudgementType == HitType.Rim) drawableHit = new DrawableFlyingRimHit(time, isStrong); else drawableHit = new DrawableFlyingCentreHit(time, isStrong); @@ -239,6 +237,14 @@ namespace osu.Game.Rulesets.Taiko.UI drumRollHitContainer.Add(drawableHit); } + private void addExplosion(DrawableHitObject drawableObject, HitType type) + { + hitExplosionContainer.Add(new HitExplosion(drawableObject, type)); + + if (drawableObject.HitObject.Kiai) + kiaiExplosionContainer.Add(new KiaiHitExplosion(drawableObject, type)); + } + private class ProxyContainer : LifetimeManagementContainer { public new MarginPadding Padding From 2630fc1405260ce60ad9d7aa533c483d90a91147 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 27 Apr 2020 12:27:43 +0900 Subject: [PATCH 1316/2376] Break instead of return for consistency --- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index a19c08ae7e..1fce70122d 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -197,7 +197,7 @@ namespace osu.Game.Rulesets.Taiko.UI case TaikoDrumRollTickJudgement _: if (!result.IsHit) - return; + break; var drawableTick = (DrawableDrumRollTick)judgedObject; @@ -218,7 +218,6 @@ namespace osu.Game.Rulesets.Taiko.UI break; addExplosion(judgedObject, (judgedObject.HitObject as Hit)?.Type ?? HitType.Centre); - break; } } From 20ae973e4afba59b5e87645dffbf7964b0ea0676 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 27 Apr 2020 12:29:11 +0900 Subject: [PATCH 1317/2376] Use max result instead of GOOD --- .../Objects/Drawables/DrawableFlyingCentreHit.cs | 3 +-- .../Objects/Drawables/DrawableFlyingRimHit.cs | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingCentreHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingCentreHit.cs index f70d940bd2..105ff62812 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingCentreHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingCentreHit.cs @@ -3,7 +3,6 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { @@ -12,7 +11,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected override void LoadComplete() { base.LoadComplete(); - ApplyResult(r => r.Type = HitResult.Good); + ApplyResult(r => r.Type = r.Judgement.MaxResult); } public DrawableFlyingCentreHit(double time, bool isStrong = false) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingRimHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingRimHit.cs index 9005dac653..e8cbd6e54f 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingRimHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingRimHit.cs @@ -3,7 +3,6 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { @@ -12,7 +11,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected override void LoadComplete() { base.LoadComplete(); - ApplyResult(r => r.Type = HitResult.Good); + ApplyResult(r => r.Type = r.Judgement.MaxResult); } public DrawableFlyingRimHit(double time, bool isStrong = false) From 7731d45f13ce88392716c640dbdaa79b23d965ce Mon Sep 17 00:00:00 2001 From: Joehu Date: Sun, 26 Apr 2020 20:30:56 -0700 Subject: [PATCH 1318/2376] Remove unnecessary usings --- .../Visual/UserInterface/TestSceneModSelectOverlay.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 769c660f48..ec6ee6bc83 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -14,8 +14,6 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Mods; using osu.Game.Overlays.Mods.Sections; using osu.Game.Rulesets; -using osu.Game.Rulesets.Mania; -using osu.Game.Rulesets.Mania.Mods; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; From ff736a22dd9f4e1835b07ec8f25c7507045ed8b3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 27 Apr 2020 14:41:19 +0900 Subject: [PATCH 1319/2376] Fix typos in comment --- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 1fce70122d..cd05d8e2f9 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -150,8 +150,8 @@ namespace osu.Game.Rulesets.Taiko.UI rightArea.Padding = new MarginPadding { Left = leftArea.DrawWidth }; hitTargetOffsetContent.Padding = new MarginPadding { Left = HitTarget.DrawWidth / 2 }; - // When rewinding, make sure to remove any auxilliary hit notes that were - // spawned and played during a drumroll. + // When rewinding, make sure to remove any auxiliary hit notes that were + // spawned and played during a drum roll. if (Time.Elapsed < 0) { foreach (var o in drumRollHitContainer.Objects) From b9f28c83731a4ca823c0090f654fab57d9da979f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 27 Apr 2020 16:13:28 +0900 Subject: [PATCH 1320/2376] Combine hit types and remove old drumroll hits using a more efficient method --- .../Skinning/TestSceneDrawableHit.cs | 10 +++--- .../TestSceneHits.cs | 4 +-- .../Objects/Drawables/DrawableCentreHit.cs | 21 ----------- ...lyingCentreHit.cs => DrawableFlyingHit.cs} | 7 ++-- .../Objects/Drawables/DrawableFlyingRimHit.cs | 23 ------------ .../Objects/Drawables/DrawableHit.cs | 26 +++++++++----- .../Objects/Drawables/DrawableRimHit.cs | 21 ----------- .../UI/DrawableTaikoRuleset.cs | 5 +-- .../UI/DrumRollHitContainer.cs | 35 +++++++++++++++++++ osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 28 ++------------- 10 files changed, 67 insertions(+), 113 deletions(-) delete mode 100644 osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs rename osu.Game.Rulesets.Taiko/Objects/Drawables/{DrawableFlyingCentreHit.cs => DrawableFlyingHit.cs} (72%) delete mode 100644 osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingRimHit.cs delete mode 100644 osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs create mode 100644 osu.Game.Rulesets.Taiko/UI/DrumRollHitContainer.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs index 6d6da1fb5b..6a3c98a514 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs @@ -21,8 +21,6 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] { typeof(DrawableHit), - typeof(DrawableCentreHit), - typeof(DrawableRimHit), typeof(LegacyHit), typeof(LegacyCirclePiece), }).ToList(); @@ -30,25 +28,25 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning [BackgroundDependencyLoader] private void load() { - AddStep("Centre hit", () => SetContents(() => new DrawableCentreHit(createHitAtCurrentTime()) + AddStep("Centre hit", () => SetContents(() => new DrawableHit(createHitAtCurrentTime()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, })); - AddStep("Centre hit (strong)", () => SetContents(() => new DrawableCentreHit(createHitAtCurrentTime(true)) + AddStep("Centre hit (strong)", () => SetContents(() => new DrawableHit(createHitAtCurrentTime(true)) { Anchor = Anchor.Centre, Origin = Anchor.Centre, })); - AddStep("Rim hit", () => SetContents(() => new DrawableRimHit(createHitAtCurrentTime()) + AddStep("Rim hit", () => SetContents(() => new DrawableHit(createHitAtCurrentTime()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, })); - AddStep("Rim hit (strong)", () => SetContents(() => new DrawableRimHit(createHitAtCurrentTime(true)) + AddStep("Rim hit (strong)", () => SetContents(() => new DrawableHit(createHitAtCurrentTime(true)) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs index 1822180795..23adb79083 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs @@ -224,7 +224,7 @@ namespace osu.Game.Rulesets.Taiko.Tests h.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - drawableRuleset.Playfield.Add(new DrawableCentreHit(h)); + drawableRuleset.Playfield.Add(new DrawableHit(h)); } private void addRimHit(bool strong) @@ -237,7 +237,7 @@ namespace osu.Game.Rulesets.Taiko.Tests h.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - drawableRuleset.Playfield.Add(new DrawableRimHit(h)); + drawableRuleset.Playfield.Add(new DrawableHit(h)); } private class TestStrongNestedHit : DrawableStrongNestedHit diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs deleted file mode 100644 index a87da44415..0000000000 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; -using osu.Game.Skinning; - -namespace osu.Game.Rulesets.Taiko.Objects.Drawables -{ - public class DrawableCentreHit : DrawableHit - { - public override TaikoAction[] HitActions { get; } = { TaikoAction.LeftCentre, TaikoAction.RightCentre }; - - public DrawableCentreHit(Hit hit) - : base(hit) - { - } - - protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.CentreHit), - _ => new CentreHitCirclePiece(), confineMode: ConfineMode.ScaleToFit); - } -} diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingCentreHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingHit.cs similarity index 72% rename from osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingCentreHit.cs rename to osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingHit.cs index 105ff62812..86822e3ae8 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingCentreHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingHit.cs @@ -6,7 +6,10 @@ using osu.Game.Beatmaps.ControlPoints; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { - public class DrawableFlyingCentreHit : DrawableCentreHit + /// + /// A hit used specifically for drum rolls, where spawning flying hits is required. + /// + public class DrawableFlyingHit : DrawableHit { protected override void LoadComplete() { @@ -14,7 +17,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables ApplyResult(r => r.Type = r.Judgement.MaxResult); } - public DrawableFlyingCentreHit(double time, bool isStrong = false) + public DrawableFlyingHit(double time, bool isStrong = false) : base(new IgnoreHit { StartTime = time, IsStrong = isStrong }) { HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingRimHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingRimHit.cs deleted file mode 100644 index e8cbd6e54f..0000000000 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingRimHit.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; - -namespace osu.Game.Rulesets.Taiko.Objects.Drawables -{ - public class DrawableFlyingRimHit : DrawableRimHit - { - protected override void LoadComplete() - { - base.LoadComplete(); - ApplyResult(r => r.Type = r.Judgement.MaxResult); - } - - public DrawableFlyingRimHit(double time, bool isStrong = false) - : base(new IgnoreHit { StartTime = time, IsStrong = isStrong }) - { - HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - } - } -} diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index fe9a89f2be..d2671eadda 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -8,31 +8,45 @@ using osu.Framework.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { - public abstract class DrawableHit : DrawableTaikoHitObject + public class DrawableHit : DrawableTaikoHitObject { /// /// A list of keys which can result in hits for this HitObject. /// - public abstract TaikoAction[] HitActions { get; } + public TaikoAction[] HitActions { get; } /// /// The action that caused this to be hit. /// - public TaikoAction? HitAction { get; private set; } + public TaikoAction? HitAction + { + get; + private set; + } private bool validActionPressed; private bool pressHandledThisFrame; - protected DrawableHit(Hit hit) + public DrawableHit(Hit hit) : base(hit) { FillMode = FillMode.Fit; + + HitActions = + HitObject.Type == HitType.Centre + ? new[] { TaikoAction.LeftCentre, TaikoAction.RightCentre } + : new[] { TaikoAction.LeftRim, TaikoAction.RightRim }; } + protected override SkinnableDrawable CreateMainPiece() => HitObject.Type == HitType.Centre + ? new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.CentreHit), _ => new CentreHitCirclePiece(), confineMode: ConfineMode.ScaleToFit) + : new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.RimHit), _ => new RimHitCirclePiece(), confineMode: ConfineMode.ScaleToFit); + protected override void CheckForResult(bool userTriggered, double timeOffset) { Debug.Assert(HitObject.HitWindows != null); @@ -58,7 +72,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { if (pressHandledThisFrame) return true; - if (Judged) return false; @@ -66,14 +79,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables // Only count this as handled if the new judgement is a hit var result = UpdateResult(true); - if (IsHit) HitAction = action; // Regardless of whether we've hit or not, any secondary key presses in the same frame should be discarded // E.g. hitting a non-strong centre as a strong should not fall through and perform a hit on the next note pressHandledThisFrame = true; - return result; } @@ -81,7 +92,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { if (action == HitAction) HitAction = null; - base.OnReleased(action); } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs deleted file mode 100644 index f767403c65..0000000000 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; -using osu.Game.Skinning; - -namespace osu.Game.Rulesets.Taiko.Objects.Drawables -{ - public class DrawableRimHit : DrawableHit - { - public override TaikoAction[] HitActions { get; } = { TaikoAction.LeftRim, TaikoAction.RightRim }; - - public DrawableRimHit(Hit hit) - : base(hit) - { - } - - protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.RimHit), - _ => new RimHitCirclePiece(), confineMode: ConfineMode.ScaleToFit); - } -} diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs index e4a4b555a7..a6a00fe242 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs @@ -49,10 +49,7 @@ namespace osu.Game.Rulesets.Taiko.UI switch (h) { case Hit hit: - if (hit.Type == HitType.Centre) - return new DrawableCentreHit(hit); - else - return new DrawableRimHit(hit); + return new DrawableHit(hit); case DrumRoll drumRoll: return new DrawableDrumRoll(drumRoll); diff --git a/osu.Game.Rulesets.Taiko/UI/DrumRollHitContainer.cs b/osu.Game.Rulesets.Taiko/UI/DrumRollHitContainer.cs new file mode 100644 index 0000000000..9ef944b412 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/UI/DrumRollHitContainer.cs @@ -0,0 +1,35 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Taiko.Objects.Drawables; +using osu.Game.Rulesets.UI.Scrolling; + +namespace osu.Game.Rulesets.Taiko.UI +{ + internal class DrumRollHitContainer : ScrollingHitObjectContainer + { + protected override void Update() + { + base.Update(); + + // Remove any auxiliary hit notes that were spawned during a drum roll but subsequently rewound. + for (var i = AliveInternalChildren.Count - 1; i >= 0; i--) + { + var flyingHit = (DrawableFlyingHit)AliveInternalChildren[i]; + if (Time.Current < flyingHit.HitObject.StartTime) + Remove(flyingHit); + } + } + + protected override void OnChildLifetimeBoundaryCrossed(LifetimeBoundaryCrossedEvent e) + { + base.OnChildLifetimeBoundaryCrossed(e); + + // ensure all old hits are removed on becoming alive (may miss being in the AliveInternalChildren list above). + if (e.Kind == LifetimeBoundaryKind.Start && e.Direction == LifetimeBoundaryCrossingDirection.Backward) + Remove((DrawableHitObject)e.Child); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index cd05d8e2f9..28706ef0d3 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -93,10 +93,7 @@ namespace osu.Game.Rulesets.Taiko.UI Children = new Drawable[] { HitObjectContainer, - drumRollHitContainer = new ScrollingHitObjectContainer - { - Name = "Drumroll hit" - } + drumRollHitContainer = new DrumRollHitContainer() } }, kiaiExplosionContainer = new Container @@ -149,23 +146,11 @@ namespace osu.Game.Rulesets.Taiko.UI // This is basically allowing for correct alignment as relative pieces move around them. rightArea.Padding = new MarginPadding { Left = leftArea.DrawWidth }; hitTargetOffsetContent.Padding = new MarginPadding { Left = HitTarget.DrawWidth / 2 }; - - // When rewinding, make sure to remove any auxiliary hit notes that were - // spawned and played during a drum roll. - if (Time.Elapsed < 0) - { - foreach (var o in drumRollHitContainer.Objects) - { - if (o.HitObject.StartTime >= Time.Current) - drumRollHitContainer.Remove(o); - } - } } public override void Add(DrawableHitObject h) { h.OnNewResult += OnNewResult; - base.Add(h); switch (h) @@ -184,7 +169,6 @@ namespace osu.Game.Rulesets.Taiko.UI { if (!DisplayJudgements.Value) return; - if (!judgedObject.DisplayResult) return; @@ -226,20 +210,12 @@ namespace osu.Game.Rulesets.Taiko.UI { bool isStrong = drawableTick.HitObject.IsStrong; double time = drawableTick.HitObject.GetEndTime(); - - DrawableHit drawableHit; - if (drawableTick.JudgementType == HitType.Rim) - drawableHit = new DrawableFlyingRimHit(time, isStrong); - else - drawableHit = new DrawableFlyingCentreHit(time, isStrong); - - drumRollHitContainer.Add(drawableHit); + drumRollHitContainer.Add(new DrawableFlyingHit(time, isStrong)); } private void addExplosion(DrawableHitObject drawableObject, HitType type) { hitExplosionContainer.Add(new HitExplosion(drawableObject, type)); - if (drawableObject.HitObject.Kiai) kiaiExplosionContainer.Add(new KiaiHitExplosion(drawableObject, type)); } From 52cf1e18590fbef3e2f221e19a2dc126af2f4ad5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 27 Apr 2020 16:48:17 +0900 Subject: [PATCH 1321/2376] Fix hit type not being provided and hit time offset not being considered --- .../Objects/Drawables/DrawableFlyingHit.cs | 17 +++++++++++------ .../UI/DrumRollHitContainer.cs | 2 +- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 9 ++------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingHit.cs index 86822e3ae8..460e760629 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingHit.cs @@ -11,16 +11,21 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables ///
  • public class DrawableFlyingHit : DrawableHit { + public DrawableFlyingHit(DrawableDrumRollTick drumRollTick) + : base(new IgnoreHit + { + StartTime = drumRollTick.HitObject.StartTime + drumRollTick.Result.TimeOffset, + IsStrong = drumRollTick.HitObject.IsStrong, + Type = drumRollTick.JudgementType + }) + { + HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + } + protected override void LoadComplete() { base.LoadComplete(); ApplyResult(r => r.Type = r.Judgement.MaxResult); } - - public DrawableFlyingHit(double time, bool isStrong = false) - : base(new IgnoreHit { StartTime = time, IsStrong = isStrong }) - { - HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - } } } diff --git a/osu.Game.Rulesets.Taiko/UI/DrumRollHitContainer.cs b/osu.Game.Rulesets.Taiko/UI/DrumRollHitContainer.cs index 9ef944b412..fde42bec04 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumRollHitContainer.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumRollHitContainer.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Taiko.UI for (var i = AliveInternalChildren.Count - 1; i >= 0; i--) { var flyingHit = (DrawableFlyingHit)AliveInternalChildren[i]; - if (Time.Current < flyingHit.HitObject.StartTime) + if (Time.Current <= flyingHit.HitObject.StartTime) Remove(flyingHit); } } diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 28706ef0d3..4bc6cb8e4b 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -9,7 +9,6 @@ using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.Taiko.Objects.Drawables; @@ -206,12 +205,8 @@ namespace osu.Game.Rulesets.Taiko.UI } } - private void addDrumRollHit(DrawableDrumRollTick drawableTick) - { - bool isStrong = drawableTick.HitObject.IsStrong; - double time = drawableTick.HitObject.GetEndTime(); - drumRollHitContainer.Add(new DrawableFlyingHit(time, isStrong)); - } + private void addDrumRollHit(DrawableDrumRollTick drawableTick) => + drumRollHitContainer.Add(new DrawableFlyingHit(drawableTick)); private void addExplosion(DrawableHitObject drawableObject, HitType type) { From acf95fca9cdd8b12027d907838738fa0216845b4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 27 Apr 2020 17:14:00 +0900 Subject: [PATCH 1322/2376] Remove old, now unnecessary method --- .../Edit/ManiaSelectionHandler.cs | 23 ------------------- 1 file changed, 23 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs index 78f159b733..d290e4ec24 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs @@ -35,35 +35,12 @@ namespace osu.Game.Rulesets.Mania.Edit var maniaBlueprint = (ManiaSelectionBlueprint)moveEvent.Blueprint; int lastColumn = maniaBlueprint.DrawableObject.HitObject.Column; - // adjustOrigins(maniaBlueprint); performDragMovement(moveEvent); performColumnMovement(lastColumn, moveEvent); return true; } - /// - /// Ensures that the position of hitobjects remains centred to the mouse position. - /// E.g. The hitobject position will change if the editor scrolls while a hitobject is dragged. - /// - /// The that received the drag event. - private void adjustOrigins(ManiaSelectionBlueprint reference) - { - var referenceParent = (HitObjectContainer)reference.DrawableObject.Parent; - - float offsetFromReferenceOrigin = reference.DragPosition.Y - reference.DrawableObject.OriginPosition.Y; - float targetPosition = referenceParent.ToLocalSpace(reference.ScreenSpaceDragPosition).Y - offsetFromReferenceOrigin; - - // Flip the vertical coordinate space when scrolling downwards - if (scrollingInfo.Direction.Value == ScrollingDirection.Down) - targetPosition -= referenceParent.DrawHeight; - - float movementDelta = targetPosition - reference.DrawableObject.Position.Y; - - foreach (var b in SelectedBlueprints.OfType()) - b.DrawableObject.Y += movementDelta; - } - private void performDragMovement(MoveSelectionEvent moveEvent) { float delta = moveEvent.InstantDelta.Y; From 03863d901b221e31baa22af9f65de9490ae276d4 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2020 08:59:09 +0000 Subject: [PATCH 1323/2376] Bump Microsoft.NET.Test.Sdk from 16.5.0 to 16.6.1 Bumps [Microsoft.NET.Test.Sdk](https://github.com/microsoft/vstest) from 16.5.0 to 16.6.1. - [Release notes](https://github.com/microsoft/vstest/releases) - [Commits](https://github.com/microsoft/vstest/compare/v16.5.0...v16.6.1) Signed-off-by: dependabot-preview[bot] --- .../osu.Game.Rulesets.Catch.Tests.csproj | 2 +- .../osu.Game.Rulesets.Mania.Tests.csproj | 2 +- osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj | 2 +- .../osu.Game.Rulesets.Taiko.Tests.csproj | 2 +- osu.Game.Tests/osu.Game.Tests.csproj | 2 +- osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj index 8c371db257..cbd3dc5518 100644 --- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj +++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj @@ -2,7 +2,7 @@ - + diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj index 6855b99f28..77c871718b 100644 --- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj +++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj @@ -2,7 +2,7 @@ - + diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj index 217707b180..2fcfa1deb7 100644 --- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj +++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj @@ -2,7 +2,7 @@ - + diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj index f6054a5d6f..28b8476a22 100644 --- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj +++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj @@ -2,7 +2,7 @@ - + diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index 35eb3fa161..5ee887cb64 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -3,7 +3,7 @@ - + diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj index 3b45fc83fd..aa37326a49 100644 --- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj +++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj @@ -5,7 +5,7 @@ - + From c0b225ffc8e5c27bc4c624468994f61b698daab5 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2020 08:59:34 +0000 Subject: [PATCH 1324/2376] Bump ppy.osu.Game.Resources from 2020.412.0 to 2020.427.0 Bumps [ppy.osu.Game.Resources](https://github.com/ppy/osu-resources) from 2020.412.0 to 2020.427.0. - [Release notes](https://github.com/ppy/osu-resources/releases) - [Commits](https://github.com/ppy/osu-resources/compare/2020.412.0...2020.427.0) Signed-off-by: dependabot-preview[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 25942863c5..73fbe3ab2e 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 9c17c453a6..3b05eb82d7 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -24,7 +24,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 07ea4b9c2a..f3202693f3 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + From ca055581af91975ed44438b25863ad0b33c7e9c8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 27 Apr 2020 18:18:11 +0900 Subject: [PATCH 1325/2376] Fix taiko hit target alpha on legacy skins --- osu.Game.Rulesets.Taiko/Skinning/TaikoLegacyHitTarget.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacyHitTarget.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacyHitTarget.cs index 7c1e65f569..e522fb7c10 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacyHitTarget.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacyHitTarget.cs @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning { Texture = skin.GetTexture("approachcircle"), Scale = new Vector2(0.73f), - Alpha = 0.7f, + Alpha = 0.47f, // eyeballed to match stable Anchor = Anchor.Centre, Origin = Anchor.Centre, }, @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning { Texture = skin.GetTexture("taikobigcircle"), Scale = new Vector2(0.7f), - Alpha = 0.5f, + Alpha = 0.22f, // eyeballed to match stable Anchor = Anchor.Centre, Origin = Anchor.Centre, }, From b88dd442526c8a58a9bd992101b5d7b5da33e3a6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 27 Apr 2020 19:47:25 +0900 Subject: [PATCH 1326/2376] Fix movement not working correctly in down-scroll --- .../Blueprints/ManiaSelectionBlueprint.cs | 20 +++++++++++++++++++ .../Edit/ManiaSelectionHandler.cs | 19 +++++++----------- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs index 9f57160f99..14d52dd08e 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs @@ -74,5 +74,25 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints DrawableObject.AlwaysAlive = false; base.Hide(); } + + public override Vector2 GetInstantDelta(Vector2 screenSpacePosition) + { + var baseDelta = base.GetInstantDelta(screenSpacePosition); + + if (scrollingInfo.Direction.Value == ScrollingDirection.Down) + { + // The parent of DrawableObject is the scrolling hitobject container (SHOC). + // In the coordinate-space of the SHOC, the screen-space position at the hit target is equal to the height of the SHOC, + // but this is not what we want as it means a slight movement downwards results in a delta greater than the height of the SHOC. + // To get around this issue, the height of the SHOC is subtracted from the delta. + // + // Ideally this should be a _negative_ value in the case described above, however this code gives a _positive_ delta. + // This is intentional as the delta is added to the hitobject's position (see: ManiaSelectionHandler) and a negative delta would move them towards the top of the screen instead, + // which would cause the delta to get increasingly larger as additional movements are performed. + return new Vector2(baseDelta.X, baseDelta.Y - DrawableObject.Parent.DrawHeight); + } + + return baseDelta; + } } } diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs index d290e4ec24..8dfc97f87a 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs @@ -45,11 +45,6 @@ namespace osu.Game.Rulesets.Mania.Edit { float delta = moveEvent.InstantDelta.Y; - // When scrolling downwards the anchor position is at the bottom of the screen, however the movement event assumes the anchor is at the top of the screen. - // This causes the delta to assume a positive hitobject position, and which can be corrected for by subtracting the parent height. - if (scrollingInfo.Direction.Value == ScrollingDirection.Down) - delta -= moveEvent.Blueprint.Parent.DrawHeight; // todo: definitely wrong - foreach (var selectionBlueprint in SelectedBlueprints) { var b = (OverlaySelectionBlueprint)selectionBlueprint; @@ -57,24 +52,24 @@ namespace osu.Game.Rulesets.Mania.Edit var hitObject = b.DrawableObject; var objectParent = (HitObjectContainer)hitObject.Parent; - // StartTime could be used to adjust the position if only one movement event was received per frame. - // However this is not the case and ScrollingHitObjectContainer performs movement in UpdateAfterChildren() so the position must also be updated to be valid for further movement events + // We receive multiple movement events per frame such that we can't rely on updating the start time + // since the scrolling hitobject container requires at least one update frame to update the position. + // However the position needs to be valid for future movement events to calculate the correct deltas. hitObject.Y += delta; float targetPosition = hitObject.Position.Y; - // The scrolling algorithm always assumes an anchor at the top of the screen, so the position must be flipped when scrolling downwards to reflect a top anchor if (scrollingInfo.Direction.Value == ScrollingDirection.Down) + { + // When scrolling downwards, the position is _negative_ when the object's start time is after the current time (e.g. in the middle of the stage). + // However all scrolling algorithms upwards scrolling, meaning that a positive (inverse) position is expected in the same scenario. targetPosition = -targetPosition; - - objectParent.Remove(hitObject); + } hitObject.HitObject.StartTime = scrollingInfo.Algorithm.TimeAt(targetPosition, editorClock.CurrentTime, scrollingInfo.TimeRange.Value, objectParent.DrawHeight); - - objectParent.Add(hitObject); } } From ece6e2db5cca33bacff5301804a18cc6b9dd3eaa Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Mon, 27 Apr 2020 13:12:31 +0200 Subject: [PATCH 1327/2376] Change removed class --- .../Skinning/TestSceneDrawableTaikoMascot.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs index d11bfa05ba..93b5803e87 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs @@ -193,7 +193,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning private void addJudgement(TaikoPlayfield playfield, HitResult result) { - playfield.OnNewResult(new DrawableRimHit(new Hit()), new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = result }); + playfield.OnNewResult(new DrawableHit(new Hit()), new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = result }); } private class TestDrawableTaikoMascot : DrawableTaikoMascot From cebc0fc0466d61db6f22c19358c8d22ce5f12d09 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 27 Apr 2020 20:35:24 +0900 Subject: [PATCH 1328/2376] Attempt to fix multiple selection movements --- .../Edit/ManiaHitObjectComposer.cs | 28 ++++++++++++++++++- .../Edit/ManiaSelectionHandler.cs | 14 ---------- osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs | 16 +++++++++++ .../Compose/Components/BlueprintContainer.cs | 10 +++---- 4 files changed, 47 insertions(+), 21 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index 62b609610f..8d012c63ac 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -10,6 +10,7 @@ using osu.Framework.Allocation; using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; +using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Screens.Edit.Compose.Components; using osuTK; @@ -37,7 +38,32 @@ namespace osu.Game.Rulesets.Mania.Edit protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - public int TotalColumns => ((ManiaPlayfield)drawableRuleset.Playfield).TotalColumns; + public ManiaPlayfield Playfield => ((ManiaPlayfield)drawableRuleset.Playfield); + + public int TotalColumns => Playfield.TotalColumns; + + public override (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time) + { + var hoc = Playfield.GetColumn(0).HitObjectContainer; + + position.Y -= ToLocalSpace(hoc.ScreenSpaceDrawQuad.TopLeft).Y; + + float targetPosition = position.Y; + + if (drawableRuleset.ScrollingInfo.Direction.Value == ScrollingDirection.Down) + { + // When scrolling downwards, the position is _negative_ when the object's start time is after the current time (e.g. in the middle of the stage). + // However all scrolling algorithms upwards scrolling, meaning that a positive (inverse) position is expected in the same scenario. + targetPosition = -targetPosition; + } + + double targetTime = drawableRuleset.ScrollingInfo.Algorithm.TimeAt(targetPosition, + EditorClock.CurrentTime, + drawableRuleset.ScrollingInfo.TimeRange.Value, + hoc.DrawHeight); + + return base.GetSnappedPosition(position, targetTime); + } protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) { diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs index 8dfc97f87a..989e0f5b01 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs @@ -56,20 +56,6 @@ namespace osu.Game.Rulesets.Mania.Edit // since the scrolling hitobject container requires at least one update frame to update the position. // However the position needs to be valid for future movement events to calculate the correct deltas. hitObject.Y += delta; - - float targetPosition = hitObject.Position.Y; - - if (scrollingInfo.Direction.Value == ScrollingDirection.Down) - { - // When scrolling downwards, the position is _negative_ when the object's start time is after the current time (e.g. in the middle of the stage). - // However all scrolling algorithms upwards scrolling, meaning that a positive (inverse) position is expected in the same scenario. - targetPosition = -targetPosition; - } - - hitObject.HitObject.StartTime = scrollingInfo.Algorithm.TimeAt(targetPosition, - editorClock.CurrentTime, - scrollingInfo.TimeRange.Value, - objectParent.DrawHeight); } } diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs index 2dec468654..f9faa262ed 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs @@ -87,6 +87,22 @@ namespace osu.Game.Rulesets.Mania.UI return found; } + public Column GetColumn(int index) + { + foreach (var stage in stages) + { + if (index >= stage.Columns.Count) + { + index -= stage.Columns.Count; + continue; + } + + return stage.Columns[index]; + } + + throw new ArgumentOutOfRangeException(nameof(index)); + } + /// /// Retrieves the total amount of columns across all stages in this playfield. /// diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 0823be01f8..8910684463 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -401,18 +401,16 @@ namespace osu.Game.Screens.Edit.Compose.Components HitObject draggedObject = movementBlueprint.HitObject; - // The final movement position, relative to movementBlueprintOriginalPosition + // The final movement position, relative to movementBlueprintOriginalPosition. Vector2 movePosition = movementBlueprintOriginalPosition.Value + e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition; - (Vector2 snappedPosition, _) = snapProvider.GetSnappedPosition(ToLocalSpace(movePosition), draggedObject.StartTime); + // Retrieve a snapped position. + (Vector2 snappedPosition, double snappedTime) = snapProvider.GetSnappedPosition(ToLocalSpace(movePosition), draggedObject.StartTime); - // Move the hitobjects + // Move the hitobjects. if (!selectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprint, ToScreenSpace(snappedPosition)))) return true; - // Todo: Temp - (_, double snappedTime) = snapProvider.GetSnappedPosition(ToLocalSpace(snappedPosition), draggedObject.StartTime); - // Apply the start time at the newly snapped-to position double offset = snappedTime - draggedObject.StartTime; foreach (HitObject obj in selectionHandler.SelectedHitObjects) From be59ee945a5ead6fe5d8d7cbeaad058e8180bf7f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 27 Apr 2020 22:22:32 +0900 Subject: [PATCH 1329/2376] Add taiko hit explosion skinning support --- .../Skinning/TestSceneHitExplosion.cs | 79 +++++++++++++++++++ .../Skinning/LegacyHitExplosion.cs | 29 +++++++ .../Skinning/TaikoLegacySkinTransformer.cs | 27 +++++++ .../TaikoSkinComponents.cs | 5 +- .../UI/DefaultHitExplosion.cs | 54 +++++++++++++ osu.Game.Rulesets.Taiko/UI/HitExplosion.cs | 50 ++++++------ osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 7 +- 7 files changed, 221 insertions(+), 30 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs create mode 100644 osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs create mode 100644 osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs new file mode 100644 index 0000000000..ca0c60b67a --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs @@ -0,0 +1,79 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Objects.Drawables; +using osu.Game.Rulesets.Taiko.Skinning; +using osu.Game.Rulesets.Taiko.UI; + +namespace osu.Game.Rulesets.Taiko.Tests.Skinning +{ + [TestFixture] + public class TestSceneHitExplosion : TaikoSkinnableTestScene + { + public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] + { + typeof(HitExplosion), + typeof(LegacyHitExplosion), + typeof(DefaultHitExplosion), + }).ToList(); + + [BackgroundDependencyLoader] + private void load() + { + AddStep("Great", () => SetContents(() => getContentFor(HitResult.Great))); + AddStep("Good", () => SetContents(() => getContentFor(HitResult.Good))); + AddStep("Miss", () => SetContents(() => getContentFor(HitResult.Miss))); + } + + private Drawable getContentFor(HitResult type) + { + DrawableTaikoHitObject hit; + + return new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + hit = createHit(type), + new HitExplosion(hit) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + } + }; + } + + private DrawableTaikoHitObject createHit(HitResult type) => new TestDrawableHit(new Hit { StartTime = Time.Current }, type); + + private class TestDrawableHit : DrawableTaikoHitObject + { + private readonly HitResult type; + + public TestDrawableHit(Hit hit, HitResult type) + : base(hit) + { + this.type = type; + } + + [BackgroundDependencyLoader] + private void load() + { + Result.Type = type; + } + + public override bool OnPressed(TaikoAction action) => false; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs new file mode 100644 index 0000000000..d29b574866 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs @@ -0,0 +1,29 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; + +namespace osu.Game.Rulesets.Taiko.Skinning +{ + public class LegacyHitExplosion : CompositeDrawable + { + public LegacyHitExplosion(Drawable sprite) + { + InternalChild = sprite; + + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + AutoSizeAxes = Axes.Both; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + this.FadeIn(120); + this.ScaleTo(0.6f).Then().ScaleTo(1, 240, Easing.OutElastic); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs index 447d6ae455..bea1c6bdcf 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs @@ -81,11 +81,38 @@ namespace osu.Game.Rulesets.Taiko.Skinning return new LegacyBarLine(); return null; + + case TaikoSkinComponents.TaikoExplosionGood: + case TaikoSkinComponents.TaikoExplosionGreat: + case TaikoSkinComponents.TaikoExplosionMiss: + + var sprite = this.GetAnimation(getHitname(taikoComponent.Component), true, false); + if (sprite != null) + return new LegacyHitExplosion(sprite); + + return null; } return source.GetDrawableComponent(component); } + private string getHitname(TaikoSkinComponents component) + { + switch (component) + { + case TaikoSkinComponents.TaikoExplosionMiss: + return "taiko-hit0"; + + case TaikoSkinComponents.TaikoExplosionGood: + return "taiko-hit100"; + + case TaikoSkinComponents.TaikoExplosionGreat: + return "taiko-hit300"; + } + + return string.Empty; + } + public Texture GetTexture(string componentName) => source.GetTexture(componentName); public SampleChannel GetSample(ISampleInfo sampleInfo) => source.GetSample(new LegacyTaikoSampleInfo(sampleInfo)); diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs index a90ce608b2..fd091f97d0 100644 --- a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs +++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs @@ -14,6 +14,9 @@ namespace osu.Game.Rulesets.Taiko HitTarget, PlayfieldBackgroundLeft, PlayfieldBackgroundRight, - BarLine + BarLine, + TaikoExplosionMiss, + TaikoExplosionGood, + TaikoExplosionGreat, } } diff --git a/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs new file mode 100644 index 0000000000..aa444d0494 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs @@ -0,0 +1,54 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko.Objects; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Taiko.UI +{ + internal class DefaultHitExplosion : CircularContainer + { + [Resolved] + private DrawableHitObject judgedObject { get; set; } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + RelativeSizeAxes = Axes.Both; + + BorderColour = Color4.White; + BorderThickness = 1; + + Alpha = 0.15f; + Masking = true; + + if (judgedObject.Result.Type == HitResult.Miss) + return; + + bool isRim = (judgedObject.HitObject as Hit)?.Type == HitType.Rim; + + InternalChildren = new[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = isRim ? colours.BlueDarker : colours.PinkDarker, + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + this.ScaleTo(3f, 1000, Easing.OutQuint); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs index fbaae7e322..9bef93d834 100644 --- a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs @@ -1,15 +1,15 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osuTK; -using osuTK.Graphics; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.UI { @@ -20,15 +20,12 @@ namespace osu.Game.Rulesets.Taiko.UI { public override bool RemoveWhenNotAlive => true; + [Cached(typeof(DrawableHitObject))] public readonly DrawableHitObject JudgedObject; - private readonly HitType type; - private readonly Box innerFill; - - public HitExplosion(DrawableHitObject judgedObject, HitType type) + public HitExplosion(DrawableHitObject judgedObject) { JudgedObject = judgedObject; - this.type = type; Anchor = Anchor.Centre; Origin = Anchor.Centre; @@ -37,35 +34,36 @@ namespace osu.Game.Rulesets.Taiko.UI Size = new Vector2(TaikoHitObject.DEFAULT_SIZE); RelativePositionAxes = Axes.Both; - - BorderColour = Color4.White; - BorderThickness = 1; - - Alpha = 0.15f; - Masking = true; - - Children = new[] - { - innerFill = new Box - { - RelativeSizeAxes = Axes.Both, - } - }; } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load() { - innerFill.Colour = type == HitType.Rim ? colours.BlueDarker : colours.PinkDarker; + Child = new SkinnableDrawable(new TaikoSkinComponent(getComponentName(JudgedObject.Result?.Type ?? HitResult.Great)), _ => new DefaultHitExplosion()); + } + + private TaikoSkinComponents getComponentName(HitResult resultType) + { + switch (resultType) + { + case HitResult.Miss: + return TaikoSkinComponents.TaikoExplosionMiss; + + case HitResult.Good: + return TaikoSkinComponents.TaikoExplosionGood; + + case HitResult.Great: + return TaikoSkinComponents.TaikoExplosionGreat; + } + + throw new ArgumentException("Invalid result type", nameof(resultType)); } protected override void LoadComplete() { base.LoadComplete(); - this.ScaleTo(3f, 1000, Easing.OutQuint); this.FadeOut(500); - Expire(true); } diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 4bc6cb8e4b..6a78c0a1fb 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -185,7 +185,6 @@ namespace osu.Game.Rulesets.Taiko.UI var drawableTick = (DrawableDrumRollTick)judgedObject; addDrumRollHit(drawableTick); - addExplosion(drawableTick, drawableTick.JudgementType); break; default: @@ -200,7 +199,9 @@ namespace osu.Game.Rulesets.Taiko.UI if (!result.IsHit) break; - addExplosion(judgedObject, (judgedObject.HitObject as Hit)?.Type ?? HitType.Centre); + var type = (judgedObject.HitObject as Hit)?.Type ?? HitType.Centre; + + addExplosion(judgedObject, type); break; } } @@ -210,7 +211,7 @@ namespace osu.Game.Rulesets.Taiko.UI private void addExplosion(DrawableHitObject drawableObject, HitType type) { - hitExplosionContainer.Add(new HitExplosion(drawableObject, type)); + hitExplosionContainer.Add(new HitExplosion(drawableObject)); if (drawableObject.HitObject.Kiai) kiaiExplosionContainer.Add(new KiaiHitExplosion(drawableObject, type)); } From df55439f8b15d167491fba706987278f50bd4ac0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 27 Apr 2020 23:24:12 +0900 Subject: [PATCH 1330/2376] Remove undetected usings --- osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs index ca0c60b67a..3a9779c93b 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs @@ -8,8 +8,6 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects.Drawables; From a34ec03efc685c706004fb578da9fddb1f5855d2 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Mon, 27 Apr 2020 12:44:20 -0700 Subject: [PATCH 1331/2376] Reword width comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Bartłomiej Dach --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 09f4befbc1..b32875f723 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -272,7 +272,7 @@ namespace osu.Game.Overlays.Mods Font = OsuFont.GetFont(size: 30, weight: FontWeight.Bold), Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, - Width = 70, // to avoid footer from flowing when clicking mods + Width = 70, // to prevent footer from flowing when clicking mods }, }, }, From c0493026500db4fe91dd727176c84c2fcfec383f Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Mon, 27 Apr 2020 22:23:04 +0200 Subject: [PATCH 1332/2376] Update osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Bartłomiej Dach --- osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs b/osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs index c8e97b9f8b..be1864049a 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs @@ -100,7 +100,7 @@ namespace osu.Game.Rulesets.Taiko.UI return "kiai"; default: - throw new ArgumentException($"There's no case for animation state ${state} available", nameof(state)); + throw new ArgumentOutOfRangeException(nameof(state), $"There's no case for animation state {state} available"); } } } From 5caa4dedc2cd28b3bbd098a47a80c62349000b43 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Mon, 27 Apr 2020 22:27:03 +0200 Subject: [PATCH 1333/2376] Update osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Bartłomiej Dach --- osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs index 2c94f5f1cb..a90bf11af5 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs @@ -77,7 +77,7 @@ namespace osu.Game.Rulesets.Taiko.UI return failDrawable; default: - throw new ArgumentException($"There's no case for animation state ${state} available", nameof(state)); + throw new ArgumentOutOfRangeException(nameof(state), $"There's no animation available for state {state}"); } } From 0972442b3a6bbc64845d7a79b926454310c014ef Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Mon, 27 Apr 2020 23:17:19 +0200 Subject: [PATCH 1334/2376] Use test scene beatmap bindable --- .../.idea/projectSettingsUpdater.xml | 2 +- .../Skinning/TestSceneDrawableTaikoMascot.cs | 19 ++++++++++++------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml b/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml index 4bb9f4d2a0..7515e76054 100644 --- a/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml +++ b/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs index 93b5803e87..3a78ad76a6 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs @@ -37,13 +37,18 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning TimeRange = { Value = 5000 }, }; - [Cached(typeof(IBindable))] - private Bindable beatmap = new Bindable(); + private Bindable workingBeatmap; private readonly List mascots = new List(); private readonly List playfields = new List(); private readonly List rulesets = new List(); + [BackgroundDependencyLoader] + private void load(Bindable beatmap) + { + workingBeatmap = beatmap; + } + [Test] public void TestStateTextures() { @@ -79,7 +84,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning SetContents(() => { var ruleset = new TaikoRuleset(); - var drawableRuleset = new DrawableTaikoRuleset(ruleset, beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo)); + var drawableRuleset = new DrawableTaikoRuleset(ruleset, workingBeatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo)); rulesets.Add(drawableRuleset); return drawableRuleset; }); @@ -110,17 +115,17 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning { AddStep("Set beatmap", () => setBeatmap(true)); - AddUntilStep("Wait for beatmap to be loaded", () => beatmap.Value.Track.IsLoaded); + AddUntilStep("Wait for beatmap to be loaded", () => workingBeatmap.Value.Track.IsLoaded); AddStep("Create kiai ruleset", () => { - beatmap.Value.Track.Start(); + workingBeatmap.Value.Track.Start(); rulesets.Clear(); SetContents(() => { var ruleset = new TaikoRuleset(); - var drawableRuleset = new DrawableTaikoRuleset(ruleset, beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo)); + var drawableRuleset = new DrawableTaikoRuleset(ruleset, workingBeatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo)); rulesets.Add(drawableRuleset); return drawableRuleset; }); @@ -148,7 +153,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning if (kiai) controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true }); - beatmap.Value = CreateWorkingBeatmap(new Beatmap + workingBeatmap.Value = CreateWorkingBeatmap(new Beatmap { HitObjects = new List { new Hit { Type = HitType.Centre } }, BeatmapInfo = new BeatmapInfo From c8ee941952c9e9de3c55dae188e40037f1a2366f Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Mon, 27 Apr 2020 23:17:33 +0200 Subject: [PATCH 1335/2376] Fix formatting --- .../Skinning/TestSceneDrawableTaikoMascot.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs index 3a78ad76a6..a0ab3e5c25 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs @@ -180,7 +180,8 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning private void collectPlayfields() { playfields.Clear(); - foreach (var ruleset in rulesets) playfields.Add(ruleset.ChildrenOfType().Single()); + foreach (var ruleset in rulesets) + playfields.Add(ruleset.ChildrenOfType().Single()); } private void collectMascots() From 9b3c1e41267e5e6f59effceddcb361f17df54490 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Mon, 27 Apr 2020 23:17:52 +0200 Subject: [PATCH 1336/2376] Remove unused bindables --- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 020df5e9e4..d94503fa67 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -3,7 +3,6 @@ using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps.ControlPoints; @@ -44,8 +43,6 @@ namespace osu.Game.Rulesets.Taiko.UI private SkinnableDrawable mascotDrawable; - private Bindable frameTime = new Bindable(100); - public TaikoPlayfield(ControlPointInfo controlPoints) { this.controlPoints = controlPoints; From 834eeb6d9862fdd1f0badc6125b1ae5a0007d70e Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Mon, 27 Apr 2020 23:18:26 +0200 Subject: [PATCH 1337/2376] Reduce duplicate texture retrieval code --- .../UI/TaikoMascotTextureAnimation.cs | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs b/osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs index c8e97b9f8b..6fc8df66ae 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs @@ -5,7 +5,6 @@ using System; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; -using osu.Framework.Graphics.Textures; using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.UI @@ -40,30 +39,33 @@ namespace osu.Game.Rulesets.Taiko.UI { foreach (var textureIndex in clear_animation_sequence) { - var textureName = getStateTextureName(textureIndex); - Texture texture = skin.GetTexture(textureName); - - if (texture == null) + if (!addFrame(skin, textureIndex)) break; - - AddFrame(texture); } } else { for (int i = 0; true; i++) { - var textureName = getStateTextureName(i); - Texture texture = skin.GetTexture(textureName); - - if (texture == null) + if (!addFrame(skin, i)) break; - - AddFrame(texture); } } } + private bool addFrame(ISkinSource skin, int textureIndex) + { + var textureName = getStateTextureName(textureIndex); + var texture = skin.GetTexture(textureName); + + if (texture == null) + return false; + + AddFrame(texture); + + return true; + } + /// /// Advances the current frame by one. /// From 96660b2cca73a642f07cb35e0dd8e5bfd22cb89a Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Mon, 27 Apr 2020 23:18:40 +0200 Subject: [PATCH 1338/2376] Flip frame count check --- osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs b/osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs index 6fc8df66ae..6ceb5cd08e 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs @@ -75,7 +75,7 @@ namespace osu.Game.Rulesets.Taiko.UI if (FrameCount == 0) return; - if (FrameCount <= currentFrame) + if (currentFrame >= FrameCount) currentFrame = 0; GotoFrame(currentFrame); From 74d36cad784f2c1c3f3ca94f6eca84745d4b50f0 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Mon, 27 Apr 2020 23:19:18 +0200 Subject: [PATCH 1339/2376] Change state variables --- .../UI/DrawableTaikoMascot.cs | 27 +++++++++++-------- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 2 +- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs index 2c94f5f1cb..e66a045881 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs @@ -4,7 +4,6 @@ using System; using osu.Framework.Allocation; using osu.Framework.Audio.Track; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Textures; using osu.Game.Beatmaps.ControlPoints; @@ -16,8 +15,7 @@ namespace osu.Game.Rulesets.Taiko.UI { private TaikoMascotTextureAnimation idleDrawable, clearDrawable, kiaiDrawable, failDrawable; private EffectControlPoint lastEffectControlPoint; - - public Bindable PlayfieldState; + private TaikoMascotAnimationState playfieldState; public TaikoMascotAnimationState State { get; private set; } @@ -25,13 +23,6 @@ namespace osu.Game.Rulesets.Taiko.UI { RelativeSizeAxes = Axes.Both; - PlayfieldState = new Bindable(); - PlayfieldState.BindValueChanged(b => - { - if (lastEffectControlPoint != null) - ShowState(GetFinalAnimationState(lastEffectControlPoint, b.NewValue)); - }); - State = startingState; } @@ -60,6 +51,20 @@ namespace osu.Game.Rulesets.Taiko.UI drawable.Show(); } + /// + /// Sets the playfield state used for determining the final state. + /// + /// + /// If you're looking to change the state manually, please look at . + /// + public void SetPlayfieldState(TaikoMascotAnimationState state) + { + playfieldState = state; + + if (lastEffectControlPoint != null) + ShowState(GetFinalAnimationState(lastEffectControlPoint, playfieldState)); + } + private TaikoMascotTextureAnimation getStateDrawable(TaikoMascotAnimationState state) { switch (state) @@ -93,7 +98,7 @@ namespace osu.Game.Rulesets.Taiko.UI { base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); - var state = GetFinalAnimationState(lastEffectControlPoint = effectPoint, PlayfieldState.Value); + var state = GetFinalAnimationState(lastEffectControlPoint = effectPoint, playfieldState); ShowState(state); if (state == TaikoMascotAnimationState.Clear) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index d94503fa67..41879f173e 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -220,7 +220,7 @@ namespace osu.Game.Rulesets.Taiko.UI if (miss && judgedObject.HitObject is StrongHitObject) miss = result.Judgement.AffectsCombo; - mascot.PlayfieldState.Value = miss ? TaikoMascotAnimationState.Fail : TaikoMascotAnimationState.Idle; + mascot.SetPlayfieldState(miss ? TaikoMascotAnimationState.Fail : TaikoMascotAnimationState.Idle); } } From f387fe310f04cf87f182961e6209477bb6a9394a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 28 Apr 2020 11:07:31 +0900 Subject: [PATCH 1340/2376] Fix regressing hits test --- .../DrawableTestHit.cs | 29 +++++++++++++++++++ .../Skinning/TestSceneHitExplosion.cs | 21 +------------- .../TestSceneHits.cs | 12 +++----- 3 files changed, 34 insertions(+), 28 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs b/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs new file mode 100644 index 0000000000..1db07b3244 --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs @@ -0,0 +1,29 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Objects.Drawables; + +namespace osu.Game.Rulesets.Taiko.Tests +{ + internal class DrawableTestHit : DrawableTaikoHitObject + { + private readonly HitResult type; + + public DrawableTestHit(Hit hit, HitResult type = HitResult.Great) + : base(hit) + { + this.type = type; + } + + [BackgroundDependencyLoader] + private void load() + { + Result.Type = type; + } + + public override bool OnPressed(TaikoAction action) => false; + } +} diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs index 3a9779c93b..791c438c94 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs @@ -53,25 +53,6 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning }; } - private DrawableTaikoHitObject createHit(HitResult type) => new TestDrawableHit(new Hit { StartTime = Time.Current }, type); - - private class TestDrawableHit : DrawableTaikoHitObject - { - private readonly HitResult type; - - public TestDrawableHit(Hit hit, HitResult type) - : base(hit) - { - this.type = type; - } - - [BackgroundDependencyLoader] - private void load() - { - Result.Type = type; - } - - public override bool OnPressed(TaikoAction action) => false; - } + private DrawableTaikoHitObject createHit(HitResult type) => new DrawableTestHit(new Hit { StartTime = Time.Current }, type); } } diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs index 23adb79083..44452d70c1 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs @@ -149,6 +149,8 @@ namespace osu.Game.Rulesets.Taiko.Tests var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == HitResult.Good ? -0.1f : -0.05f, hitResult == HitResult.Good ? 0.1f : 0.05f) }; + Add(h); + ((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = hitResult }); } @@ -164,6 +166,8 @@ namespace osu.Game.Rulesets.Taiko.Tests var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == HitResult.Good ? -0.1f : -0.05f, hitResult == HitResult.Good ? 0.1f : 0.05f) }; + Add(h); + ((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = hitResult }); ((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(new TestStrongNestedHit(h), new JudgementResult(new HitObject(), new TaikoStrongJudgement()) { Type = HitResult.Great }); } @@ -249,13 +253,5 @@ namespace osu.Game.Rulesets.Taiko.Tests public override bool OnPressed(TaikoAction action) => false; } - - private class DrawableTestHit : DrawableHitObject - { - public DrawableTestHit(TaikoHitObject hitObject) - : base(hitObject) - { - } - } } } From 84641765c5858976fc2fa43606e77f60d4f7e7f1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 28 Apr 2020 11:08:19 +0900 Subject: [PATCH 1341/2376] Adjust exceptions and fix capitalisation --- .../Skinning/TaikoLegacySkinTransformer.cs | 7 ++++--- osu.Game.Rulesets.Taiko/UI/HitExplosion.cs | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs index bea1c6bdcf..f0df612e18 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.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.Audio.Sample; using osu.Framework.Bindables; @@ -86,7 +87,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning case TaikoSkinComponents.TaikoExplosionGreat: case TaikoSkinComponents.TaikoExplosionMiss: - var sprite = this.GetAnimation(getHitname(taikoComponent.Component), true, false); + var sprite = this.GetAnimation(getHitName(taikoComponent.Component), true, false); if (sprite != null) return new LegacyHitExplosion(sprite); @@ -96,7 +97,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning return source.GetDrawableComponent(component); } - private string getHitname(TaikoSkinComponents component) + private string getHitName(TaikoSkinComponents component) { switch (component) { @@ -110,7 +111,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning return "taiko-hit300"; } - return string.Empty; + throw new ArgumentOutOfRangeException(nameof(component), "Invalid result type"); } public Texture GetTexture(string componentName) => source.GetTexture(componentName); diff --git a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs index 9bef93d834..35a54d6ea7 100644 --- a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs @@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Taiko.UI return TaikoSkinComponents.TaikoExplosionGreat; } - throw new ArgumentException("Invalid result type", nameof(resultType)); + throw new ArgumentOutOfRangeException(nameof(resultType), "Invalid result type"); } protected override void LoadComplete() From 62be138aa912ca3cd77e53d3df39cff877b3f7f9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 28 Apr 2020 11:46:08 +0900 Subject: [PATCH 1342/2376] Avoid calls on MusicController executing before it may have finished loading --- .../TestSceneNowPlayingOverlay.cs | 17 +++++++++-------- osu.Game/Overlays/MusicController.cs | 18 +++++++++++------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs index 2ea9aec50a..e2913833a7 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs @@ -47,18 +47,19 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void TestPrevTrackBehavior() { - AddStep(@"Play track", () => - { - musicController.NextTrack(); - currentBeatmap = Beatmap.Value; - }); + AddStep(@"Next track", () => musicController.NextTrack()); + AddStep("Store track", () => currentBeatmap = Beatmap.Value); AddStep(@"Seek track to 6 second", () => musicController.SeekTo(6000)); AddUntilStep(@"Wait for current time to update", () => currentBeatmap.Track.CurrentTime > 5000); - AddAssert(@"Check action is restart track", () => musicController.PreviousTrack() == PreviousTrackResult.Restart); - AddUntilStep("Wait for current time to update", () => Precision.AlmostEquals(currentBeatmap.Track.CurrentTime, 0)); + + AddStep(@"Set previous", () => musicController.PreviousTrack()); + AddAssert(@"Check track didn't change", () => currentBeatmap == Beatmap.Value); - AddAssert(@"Check action is not restart", () => musicController.PreviousTrack() != PreviousTrackResult.Restart); + AddUntilStep("Wait for current time to update", () => currentBeatmap.Track.CurrentTime < 5000); + + AddStep(@"Set previous", () => musicController.PreviousTrack()); + AddAssert(@"Check track did change", () => currentBeatmap != Beatmap.Value); } } } diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index d788929739..6d269aa944 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -172,10 +172,15 @@ namespace osu.Game.Overlays } /// - /// Play the previous track or restart the current track if it's current time below + /// Play the previous track or restart the current track if it's current time below . /// - /// The that indicate the decided action - public PreviousTrackResult PreviousTrack() + public void PreviousTrack() => Schedule(() => prev()); + + /// + /// Play the previous track or restart the current track if it's current time below . + /// + /// The that indicate the decided action. + private PreviousTrackResult prev() { var currentTrackPosition = current?.Track.CurrentTime; @@ -204,8 +209,7 @@ namespace osu.Game.Overlays /// /// Play the next random or playlist track. /// - /// Whether the operation was successful. - public bool NextTrack() => next(); + public void NextTrack() => Schedule(() => next()); private bool next(bool instant = false) { @@ -319,13 +323,13 @@ namespace osu.Game.Overlays return true; case GlobalAction.MusicNext: - if (NextTrack()) + if (next()) onScreenDisplay?.Display(new MusicControllerToast("Next track")); return true; case GlobalAction.MusicPrev: - switch (PreviousTrack()) + switch (prev()) { case PreviousTrackResult.Restart: onScreenDisplay?.Display(new MusicControllerToast("Restart track")); From 4fff08d241a2158da662863426e07fa1f1621f8a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 28 Apr 2020 12:19:39 +0900 Subject: [PATCH 1343/2376] Remove unused using --- .../Visual/UserInterface/TestSceneNowPlayingOverlay.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs index e2913833a7..9944e6f9d0 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs @@ -4,7 +4,6 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Overlays; using osu.Game.Rulesets.Osu; From 81e73acb1a05863fbda05603e3505a8fe5c79931 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 28 Apr 2020 12:14:22 +0900 Subject: [PATCH 1344/2376] Fix tests failing when not run in certain order --- .../TestSceneNowPlayingOverlay.cs | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs index 9944e6f9d0..a9c52bd189 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -43,9 +44,29 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep(@"hide", () => nowPlayingOverlay.Hide()); } + [Resolved] + private BeatmapManager manager { get; set; } + [Test] public void TestPrevTrackBehavior() { + // ensure we have at least two beatmaps available. + AddRepeatStep("import beatmap", () => manager.Import(new BeatmapSetInfo + { + Beatmaps = new List + { + new BeatmapInfo + { + BaseDifficulty = new BeatmapDifficulty(), + } + }, + Metadata = new BeatmapMetadata + { + Artist = "a test map", + Title = "title", + } + }).Wait(), 5); + AddStep(@"Next track", () => musicController.NextTrack()); AddStep("Store track", () => currentBeatmap = Beatmap.Value); @@ -54,11 +75,11 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep(@"Set previous", () => musicController.PreviousTrack()); - AddAssert(@"Check track didn't change", () => currentBeatmap == Beatmap.Value); + AddAssert(@"Check beatmap didn't change", () => currentBeatmap == Beatmap.Value); AddUntilStep("Wait for current time to update", () => currentBeatmap.Track.CurrentTime < 5000); AddStep(@"Set previous", () => musicController.PreviousTrack()); - AddAssert(@"Check track did change", () => currentBeatmap != Beatmap.Value); + AddAssert(@"Check beatmap did change", () => currentBeatmap != Beatmap.Value); } } } From 743a92bbbedcd696d7df90bb07dd1bdb049e0b79 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 28 Apr 2020 12:40:56 +0900 Subject: [PATCH 1345/2376] Use a local database for now playing test --- .../UserInterface/TestSceneNowPlayingOverlay.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs index a9c52bd189..ee922a073a 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs @@ -4,9 +4,12 @@ using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Audio; using osu.Framework.Graphics; +using osu.Framework.Platform; using osu.Game.Beatmaps; using osu.Game.Overlays; +using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; namespace osu.Game.Tests.Visual.UserInterface @@ -21,9 +24,14 @@ namespace osu.Game.Tests.Visual.UserInterface private NowPlayingOverlay nowPlayingOverlay; + private RulesetStore rulesets; + [BackgroundDependencyLoader] - private void load() + private void load(AudioManager audio, GameHost host) { + Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); + Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, host, Beatmap.Default)); + Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); nowPlayingOverlay = new NowPlayingOverlay @@ -44,9 +52,10 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep(@"hide", () => nowPlayingOverlay.Hide()); } - [Resolved] private BeatmapManager manager { get; set; } + private int importId = 0; + [Test] public void TestPrevTrackBehavior() { @@ -62,7 +71,7 @@ namespace osu.Game.Tests.Visual.UserInterface }, Metadata = new BeatmapMetadata { - Artist = "a test map", + Artist = $"a test map {importId++}", Title = "title", } }).Wait(), 5); From 0d752dc7b847cc162a3076f72c20e487833e724e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 28 Apr 2020 12:55:57 +0900 Subject: [PATCH 1346/2376] Remove redundant initialisation --- .../Visual/UserInterface/TestSceneNowPlayingOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs index ee922a073a..532744a0fc 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs @@ -54,7 +54,7 @@ namespace osu.Game.Tests.Visual.UserInterface private BeatmapManager manager { get; set; } - private int importId = 0; + private int importId; [Test] public void TestPrevTrackBehavior() From 832fa74a5e06146fc7572b841f7d957c9d44afdc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 28 Apr 2020 13:26:42 +0900 Subject: [PATCH 1347/2376] Reword comment slightly --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index b32875f723..3d0ad1a594 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -272,7 +272,7 @@ namespace osu.Game.Overlays.Mods Font = OsuFont.GetFont(size: 30, weight: FontWeight.Bold), Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, - Width = 70, // to prevent footer from flowing when clicking mods + Width = 70, // make width fixed so reflow doesn't occur when multiplier number changes. }, }, }, From 7342e0015161a7b7e339370ac4aa6aadcee6f3ce Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 28 Apr 2020 14:00:14 +0900 Subject: [PATCH 1348/2376] Convert positions to local HOC coordinate space --- osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index 8d012c63ac..b415c9f0c9 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -46,9 +46,7 @@ namespace osu.Game.Rulesets.Mania.Edit { var hoc = Playfield.GetColumn(0).HitObjectContainer; - position.Y -= ToLocalSpace(hoc.ScreenSpaceDrawQuad.TopLeft).Y; - - float targetPosition = position.Y; + float targetPosition = hoc.ToLocalSpace(ToScreenSpace(position)).Y; if (drawableRuleset.ScrollingInfo.Direction.Value == ScrollingDirection.Down) { From da30eafa3020d3c1479e32ca4e37f43e114098ad Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 28 Apr 2020 14:47:45 +0900 Subject: [PATCH 1349/2376] Prevent potential exception --- osu.Game/Rulesets/UI/DrawableRuleset.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 5062c92afe..7170d425e2 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.UI ///
    public PassThroughInputManager KeyBindingInputManager; - public override double GameplayStartTime => Objects.First().StartTime - 2000; + public override double GameplayStartTime => Objects.FirstOrDefault()?.StartTime - 2000 ?? 0; private readonly Lazy playfield; From d905ef53b37ac287c2072fb8bcb26c8704211c27 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 28 Apr 2020 14:47:52 +0900 Subject: [PATCH 1350/2376] Add test scene for mania composer --- .../TestSceneManiaHitObjectComposer.cs | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs new file mode 100644 index 0000000000..3cd2530f36 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs @@ -0,0 +1,69 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Rulesets.Mania.Edit; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Screens.Edit; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Mania.Tests +{ + public class TestSceneManiaHitObjectComposer : EditorClockTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(ManiaBlueprintContainer) + }; + + [Cached(typeof(EditorBeatmap))] + [Cached(typeof(IBeatSnapProvider))] + private readonly EditorBeatmap editorBeatmap; + + protected override Container Content { get; } + + private ManiaHitObjectComposer composer; + + public TestSceneManiaHitObjectComposer() + { + base.Content.Add(new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + editorBeatmap = new EditorBeatmap(new ManiaBeatmap(new StageDefinition { Columns = 4 })) + { + BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo } + }, + Content = new Container + { + RelativeSizeAxes = Axes.Both, + } + }, + }); + + for (int i = 0; i < 10; i++) + { + editorBeatmap.Add(new Note { StartTime = 100 * i }); + } + } + + [SetUp] + public void Setup() => Schedule(() => + { + Children = new Drawable[] + { + composer = new ManiaHitObjectComposer(new ManiaRuleset()) + }; + + BeatDivisor.Value = 8; + }); + } +} From 330521a2ae7f268b1645c03fc7feab010c48ce74 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 28 Apr 2020 15:34:10 +0900 Subject: [PATCH 1351/2376] Fix lifetime override not working --- .../Drawables/DrawableManiaHitObject.cs | 62 +++++++++++++++++-- 1 file changed, 56 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs index 88888001b4..a44d8b09aa 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs @@ -13,11 +13,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { public abstract class DrawableManiaHitObject : DrawableHitObject { - /// - /// Whether this should always remain alive. - /// - internal bool AlwaysAlive; - /// /// The which causes this to be hit. /// @@ -54,7 +49,62 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables Direction.BindValueChanged(OnDirectionChanged, true); } - protected override bool ShouldBeAlive => AlwaysAlive || base.ShouldBeAlive; + private double computedLifetimeStart; + + public override double LifetimeStart + { + get => base.LifetimeStart; + set + { + computedLifetimeStart = value; + + if (!AlwaysAlive) + base.LifetimeStart = value; + } + } + + private double computedLifetimeEnd; + + public override double LifetimeEnd + { + get => base.LifetimeEnd; + set + { + computedLifetimeEnd = value; + + if (!AlwaysAlive) + base.LifetimeEnd = value; + } + } + + private bool alwaysAlive; + + /// + /// Whether this should always remain alive. + /// + internal bool AlwaysAlive + { + get => alwaysAlive; + set + { + if (alwaysAlive == value) + return; + + alwaysAlive = value; + + if (value) + { + // Set the base lifetimes directly, to avoid mangling the computed lifetimes + base.LifetimeStart = double.MinValue; + base.LifetimeEnd = double.MaxValue; + } + else + { + LifetimeStart = computedLifetimeStart; + LifetimeEnd = computedLifetimeEnd; + } + } + } protected virtual void OnDirectionChanged(ValueChangedEvent e) { From 3eb7c8755c1ed9e21a36ab0cee16047aab20f6e1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 28 Apr 2020 15:34:41 +0900 Subject: [PATCH 1352/2376] Cleanup --- .../TestSceneManiaHitObjectComposer.cs | 4 +-- .../Blueprints/ManiaSelectionBlueprint.cs | 29 ------------------- .../Edit/ManiaSelectionHandler.cs | 11 ------- 3 files changed, 1 insertion(+), 43 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs index 3cd2530f36..180ceb94f4 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs @@ -29,8 +29,6 @@ namespace osu.Game.Rulesets.Mania.Tests protected override Container Content { get; } - private ManiaHitObjectComposer composer; - public TestSceneManiaHitObjectComposer() { base.Content.Add(new Container @@ -60,7 +58,7 @@ namespace osu.Game.Rulesets.Mania.Tests { Children = new Drawable[] { - composer = new ManiaHitObjectComposer(new ManiaRuleset()) + new ManiaHitObjectComposer(new ManiaRuleset()) }; BeatDivisor.Value = 8; diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs index 14d52dd08e..b03bf7c078 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs @@ -3,8 +3,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Input.Events; -using osu.Framework.Timing; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables; @@ -15,13 +13,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints { public class ManiaSelectionBlueprint : OverlaySelectionBlueprint { - public Vector2 ScreenSpaceDragPosition { get; private set; } - public Vector2 DragPosition { get; private set; } - public new DrawableManiaHitObject DrawableObject => (DrawableManiaHitObject)base.DrawableObject; - protected IClock EditorClock { get; private set; } - [Resolved] private IScrollingInfo scrollingInfo { get; set; } @@ -34,12 +27,6 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints RelativeSizeAxes = Axes.None; } - [BackgroundDependencyLoader] - private void load(IAdjustableClock clock) - { - EditorClock = clock; - } - protected override void Update() { base.Update(); @@ -47,22 +34,6 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints Position = Parent.ToLocalSpace(DrawableObject.ToScreenSpace(Vector2.Zero)); } - protected override bool OnMouseDown(MouseDownEvent e) - { - ScreenSpaceDragPosition = e.ScreenSpaceMousePosition; - DragPosition = DrawableObject.ToLocalSpace(e.ScreenSpaceMousePosition); - - return base.OnMouseDown(e); - } - - protected override void OnDrag(DragEvent e) - { - base.OnDrag(e); - - ScreenSpaceDragPosition = e.ScreenSpaceMousePosition; - DragPosition = DrawableObject.ToLocalSpace(e.ScreenSpaceMousePosition); - } - public override void Show() { DrawableObject.AlwaysAlive = true; diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs index 989e0f5b01..11e9b56a53 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs @@ -4,11 +4,9 @@ using System; using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Timing; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Edit.Blueprints; using osu.Game.Rulesets.Mania.Objects; -using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Screens.Edit.Compose.Components; @@ -22,14 +20,6 @@ namespace osu.Game.Rulesets.Mania.Edit [Resolved] private IManiaHitObjectComposer composer { get; set; } - private IClock editorClock; - - [BackgroundDependencyLoader] - private void load(IAdjustableClock clock) - { - editorClock = clock; - } - public override bool HandleMovement(MoveSelectionEvent moveEvent) { var maniaBlueprint = (ManiaSelectionBlueprint)moveEvent.Blueprint; @@ -50,7 +40,6 @@ namespace osu.Game.Rulesets.Mania.Edit var b = (OverlaySelectionBlueprint)selectionBlueprint; var hitObject = b.DrawableObject; - var objectParent = (HitObjectContainer)hitObject.Parent; // We receive multiple movement events per frame such that we can't rely on updating the start time // since the scrolling hitobject container requires at least one update frame to update the position. From 31c3fd86b9ffc1117cff3628a4441309255c504b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 28 Apr 2020 16:22:00 +0900 Subject: [PATCH 1353/2376] Avoid using internal EF methods in tests --- osu.Game.Tests/Beatmaps/TestSceneEditorBeatmap.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/TestSceneEditorBeatmap.cs b/osu.Game.Tests/Beatmaps/TestSceneEditorBeatmap.cs index 2d4587341d..b7b48ec06a 100644 --- a/osu.Game.Tests/Beatmaps/TestSceneEditorBeatmap.cs +++ b/osu.Game.Tests/Beatmaps/TestSceneEditorBeatmap.cs @@ -1,9 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; -using Microsoft.EntityFrameworkCore.Internal; using NUnit.Framework; using osu.Framework.Testing; using osu.Game.Rulesets.Objects; @@ -137,7 +137,7 @@ namespace osu.Game.Tests.Beatmaps var hitCircle = new HitCircle { StartTime = 1000 }; editorBeatmap.Add(hitCircle); Assert.That(editorBeatmap.HitObjects.Count(h => h == hitCircle), Is.EqualTo(1)); - Assert.That(editorBeatmap.HitObjects.IndexOf(hitCircle), Is.EqualTo(3)); + Assert.That(Array.IndexOf(editorBeatmap.HitObjects.ToArray(), hitCircle), Is.EqualTo(3)); } /// @@ -161,7 +161,7 @@ namespace osu.Game.Tests.Beatmaps hitCircle.StartTime = 0; Assert.That(editorBeatmap.HitObjects.Count(h => h == hitCircle), Is.EqualTo(1)); - Assert.That(editorBeatmap.HitObjects.IndexOf(hitCircle), Is.EqualTo(1)); + Assert.That(Array.IndexOf(editorBeatmap.HitObjects.ToArray(), hitCircle), Is.EqualTo(1)); } /// From f3fbb3cdc6595bda5e66382b01027bcdf4498ce1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 28 Apr 2020 16:49:43 +0900 Subject: [PATCH 1354/2376] Add to banned symbols --- CodeAnalysis/BannedSymbols.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CodeAnalysis/BannedSymbols.txt b/CodeAnalysis/BannedSymbols.txt index a92191a439..4d7135a195 100644 --- a/CodeAnalysis/BannedSymbols.txt +++ b/CodeAnalysis/BannedSymbols.txt @@ -4,3 +4,4 @@ M:System.ValueType.Equals(System.Object)~System.Boolean;Don't use object.Equals( M:System.Nullable`1.Equals(System.Object)~System.Boolean;Use == instead. T:System.IComparable;Don't use non-generic IComparable. Use generic version instead. M:osu.Framework.Graphics.Sprites.SpriteText.#ctor;Use OsuSpriteText. +T:Microsoft.EntityFrameworkCore.Internal.EnumerableExtensions;Don't use internal extension methods. \ No newline at end of file From c3a41c8476d97a5710e90b6da19e72787b026449 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 28 Apr 2020 16:56:36 +0900 Subject: [PATCH 1355/2376] Also avoid using internal TypeExtensions --- CodeAnalysis/BannedSymbols.txt | 3 ++- osu.Game/Screens/OsuScreen.cs | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CodeAnalysis/BannedSymbols.txt b/CodeAnalysis/BannedSymbols.txt index 4d7135a195..e34626a59e 100644 --- a/CodeAnalysis/BannedSymbols.txt +++ b/CodeAnalysis/BannedSymbols.txt @@ -4,4 +4,5 @@ M:System.ValueType.Equals(System.Object)~System.Boolean;Don't use object.Equals( M:System.Nullable`1.Equals(System.Object)~System.Boolean;Use == instead. T:System.IComparable;Don't use non-generic IComparable. Use generic version instead. M:osu.Framework.Graphics.Sprites.SpriteText.#ctor;Use OsuSpriteText. -T:Microsoft.EntityFrameworkCore.Internal.EnumerableExtensions;Don't use internal extension methods. \ No newline at end of file +T:Microsoft.EntityFrameworkCore.Internal.EnumerableExtensions;Don't use internal extension methods. +T:Microsoft.EntityFrameworkCore.Internal.TypeExtensions;Don't use internal extension methods. diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs index 61e94ae969..2124a66a75 100644 --- a/osu.Game/Screens/OsuScreen.cs +++ b/osu.Game/Screens/OsuScreen.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using Microsoft.EntityFrameworkCore.Internal; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -31,7 +30,7 @@ namespace osu.Game.Screens /// /// A user-facing title for this screen. /// - public virtual string Title => GetType().ShortDisplayName(); + public virtual string Title => GetType().Name; public string Description => Title; From e5131400e77d7ba9c213259ad883fc715206b9f7 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 28 Apr 2020 18:34:39 +0900 Subject: [PATCH 1356/2376] Remove now unnecessary position manipulation --- .../Edit/ManiaSelectionHandler.cs | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs index 11e9b56a53..55245198c8 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs @@ -4,7 +4,6 @@ using System; using System.Linq; using osu.Framework.Allocation; -using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Edit.Blueprints; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.UI.Scrolling; @@ -25,29 +24,11 @@ namespace osu.Game.Rulesets.Mania.Edit var maniaBlueprint = (ManiaSelectionBlueprint)moveEvent.Blueprint; int lastColumn = maniaBlueprint.DrawableObject.HitObject.Column; - performDragMovement(moveEvent); performColumnMovement(lastColumn, moveEvent); return true; } - private void performDragMovement(MoveSelectionEvent moveEvent) - { - float delta = moveEvent.InstantDelta.Y; - - foreach (var selectionBlueprint in SelectedBlueprints) - { - var b = (OverlaySelectionBlueprint)selectionBlueprint; - - var hitObject = b.DrawableObject; - - // We receive multiple movement events per frame such that we can't rely on updating the start time - // since the scrolling hitobject container requires at least one update frame to update the position. - // However the position needs to be valid for future movement events to calculate the correct deltas. - hitObject.Y += delta; - } - } - private void performColumnMovement(int lastColumn, MoveSelectionEvent moveEvent) { var currentColumn = composer.ColumnAt(moveEvent.ScreenSpacePosition); From a7a680b4862e5adf6d0579b81534f07b169488c4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 28 Apr 2020 18:34:55 +0900 Subject: [PATCH 1357/2376] Fix horizontal drag not working --- osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs index f9faa262ed..6960d15f31 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs @@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Mania.UI { foreach (var column in stage.Columns) { - if (column.ReceivePositionalInputAt(screenSpacePosition)) + if (column.ReceivePositionalInputAt(new Vector2(screenSpacePosition.X, column.ScreenSpaceDrawQuad.Centre.Y))) { found = column; break; From f93291e25b4967edd658d3e309d33905684d5343 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 28 Apr 2020 18:35:22 +0900 Subject: [PATCH 1358/2376] Remove unused override --- .../Blueprints/ManiaSelectionBlueprint.cs | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs index b03bf7c078..b8574b804e 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs @@ -45,25 +45,5 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints DrawableObject.AlwaysAlive = false; base.Hide(); } - - public override Vector2 GetInstantDelta(Vector2 screenSpacePosition) - { - var baseDelta = base.GetInstantDelta(screenSpacePosition); - - if (scrollingInfo.Direction.Value == ScrollingDirection.Down) - { - // The parent of DrawableObject is the scrolling hitobject container (SHOC). - // In the coordinate-space of the SHOC, the screen-space position at the hit target is equal to the height of the SHOC, - // but this is not what we want as it means a slight movement downwards results in a delta greater than the height of the SHOC. - // To get around this issue, the height of the SHOC is subtracted from the delta. - // - // Ideally this should be a _negative_ value in the case described above, however this code gives a _positive_ delta. - // This is intentional as the delta is added to the hitobject's position (see: ManiaSelectionHandler) and a negative delta would move them towards the top of the screen instead, - // which would cause the delta to get increasingly larger as additional movements are performed. - return new Vector2(baseDelta.X, baseDelta.Y - DrawableObject.Parent.DrawHeight); - } - - return baseDelta; - } } } From 7d54d4b800234bb017602dc49e8b2c4a5134193e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 28 Apr 2020 18:35:37 +0900 Subject: [PATCH 1359/2376] Improve test scene --- .../TestSceneManiaHitObjectComposer.cs | 171 ++++++++++++++---- .../Edit/ManiaHitObjectComposer.cs | 2 + osu.Game/Tests/Visual/EditorClockTestScene.cs | 2 +- 3 files changed, 140 insertions(+), 35 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs index 180ceb94f4..4ce2424ffa 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs @@ -3,16 +3,23 @@ using System; using System.Collections.Generic; +using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Edit; using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Screens.Edit; using osu.Game.Tests.Visual; +using osuTK; +using osuTK.Input; namespace osu.Game.Rulesets.Mania.Tests { @@ -23,45 +30,141 @@ namespace osu.Game.Rulesets.Mania.Tests typeof(ManiaBlueprintContainer) }; - [Cached(typeof(EditorBeatmap))] - [Cached(typeof(IBeatSnapProvider))] - private readonly EditorBeatmap editorBeatmap; - - protected override Container Content { get; } - - public TestSceneManiaHitObjectComposer() - { - base.Content.Add(new Container - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - editorBeatmap = new EditorBeatmap(new ManiaBeatmap(new StageDefinition { Columns = 4 })) - { - BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo } - }, - Content = new Container - { - RelativeSizeAxes = Axes.Both, - } - }, - }); - - for (int i = 0; i < 10; i++) - { - editorBeatmap.Add(new Note { StartTime = 100 * i }); - } - } + private TestComposer composer; [SetUp] public void Setup() => Schedule(() => { - Children = new Drawable[] - { - new ManiaHitObjectComposer(new ManiaRuleset()) - }; - BeatDivisor.Value = 8; + Clock.Seek(0); + + Child = composer = new TestComposer { RelativeSizeAxes = Axes.Both }; }); + + [Test] + public void TestDragOffscreenSelectionVerticallyUpScroll() + { + DrawableHitObject lastObject = null; + Vector2 originalPosition = Vector2.Zero; + + AddStep("seek to last object", () => + { + lastObject = this.ChildrenOfType().Single(d => d.HitObject == composer.EditorBeatmap.HitObjects.Last()); + Clock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime); + }); + + AddStep("select all objects", () => composer.EditorBeatmap.SelectedHitObjects.AddRange(composer.EditorBeatmap.HitObjects)); + + AddStep("click last object", () => + { + originalPosition = lastObject.DrawPosition; + + InputManager.MoveMouseTo(lastObject); + InputManager.PressButton(MouseButton.Left); + }); + + AddStep("move mouse downwards", () => + { + InputManager.MoveMouseTo(lastObject, new Vector2(0, 20)); + InputManager.ReleaseButton(MouseButton.Left); + }); + + AddAssert("hitobjects not moved columns", () => composer.EditorBeatmap.HitObjects.All(h => ((ManiaHitObject)h).Column == 0)); + AddAssert("hitobjects moved downwards", () => lastObject.DrawPosition.Y - originalPosition.Y > 0); + AddAssert("hitobjects not moved too far", () => lastObject.DrawPosition.Y - originalPosition.Y < 50); + } + + [Test] + public void TestDragOffscreenSelectionVerticallyDownScroll() + { + DrawableHitObject lastObject = null; + Vector2 originalPosition = Vector2.Zero; + + AddStep("set down scroll", () => ((Bindable)composer.Composer.ScrollingInfo.Direction).Value = ScrollingDirection.Down); + + AddStep("seek to last object", () => + { + lastObject = this.ChildrenOfType().Single(d => d.HitObject == composer.EditorBeatmap.HitObjects.Last()); + Clock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime); + }); + + AddStep("select all objects", () => composer.EditorBeatmap.SelectedHitObjects.AddRange(composer.EditorBeatmap.HitObjects)); + + AddStep("click last object", () => + { + originalPosition = lastObject.DrawPosition; + + InputManager.MoveMouseTo(lastObject); + InputManager.PressButton(MouseButton.Left); + }); + + AddStep("move mouse upwards", () => + { + InputManager.MoveMouseTo(lastObject, new Vector2(0, -20)); + InputManager.ReleaseButton(MouseButton.Left); + }); + + AddAssert("hitobjects not moved columns", () => composer.EditorBeatmap.HitObjects.All(h => ((ManiaHitObject)h).Column == 0)); + AddAssert("hitobjects moved upwards", () => originalPosition.Y - lastObject.DrawPosition.Y > 0); + AddAssert("hitobjects not moved too far", () => originalPosition.Y - lastObject.DrawPosition.Y < 50); + } + + [Test] + public void TestDragOffscreenSelectionHorizontally() + { + DrawableHitObject lastObject = null; + Vector2 originalPosition = Vector2.Zero; + + AddStep("seek to last object", () => + { + lastObject = this.ChildrenOfType().Single(d => d.HitObject == composer.EditorBeatmap.HitObjects.Last()); + originalPosition = lastObject.DrawPosition; + + Clock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime); + }); + + AddStep("select all objects", () => composer.EditorBeatmap.SelectedHitObjects.AddRange(composer.EditorBeatmap.HitObjects)); + + AddStep("click last object", () => + { + InputManager.MoveMouseTo(lastObject); + InputManager.PressButton(MouseButton.Left); + }); + + AddStep("move mouse right", () => + { + InputManager.MoveMouseTo(lastObject, new Vector2(40, 0)); + InputManager.ReleaseButton(MouseButton.Left); + }); + + AddAssert("hitobjects moved columns", () => composer.EditorBeatmap.HitObjects.All(h => ((ManiaHitObject)h).Column == 1)); + + // Todo: They'll have moved vertically by half the height of a note. Probably a problem. + AddAssert("hitobjects not moved vertically", () => lastObject.DrawPosition.Y - originalPosition.Y < 10); + } + + private class TestComposer : CompositeDrawable + { + [Cached(typeof(EditorBeatmap))] + [Cached(typeof(IBeatSnapProvider))] + public readonly EditorBeatmap EditorBeatmap; + + public readonly ManiaHitObjectComposer Composer; + + public TestComposer() + { + InternalChildren = new Drawable[] + { + EditorBeatmap = new EditorBeatmap(new ManiaBeatmap(new StageDefinition { Columns = 4 })) + { + BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo } + }, + Composer = new ManiaHitObjectComposer(new ManiaRuleset()) + }; + + for (int i = 0; i < 10; i++) + EditorBeatmap.Add(new Note { StartTime = 100 * i }); + } + } } } diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index b415c9f0c9..fba80f92d2 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -40,6 +40,8 @@ namespace osu.Game.Rulesets.Mania.Edit public ManiaPlayfield Playfield => ((ManiaPlayfield)drawableRuleset.Playfield); + public IScrollingInfo ScrollingInfo => drawableRuleset.ScrollingInfo; + public int TotalColumns => Playfield.TotalColumns; public override (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time) diff --git a/osu.Game/Tests/Visual/EditorClockTestScene.cs b/osu.Game/Tests/Visual/EditorClockTestScene.cs index 58a443ed3d..830e6ed363 100644 --- a/osu.Game/Tests/Visual/EditorClockTestScene.cs +++ b/osu.Game/Tests/Visual/EditorClockTestScene.cs @@ -15,7 +15,7 @@ namespace osu.Game.Tests.Visual /// Provides a clock, beat-divisor, and scrolling capability for test cases of editor components that /// are preferrably tested within the presence of a clock and seek controls. /// - public abstract class EditorClockTestScene : OsuTestScene + public abstract class EditorClockTestScene : OsuManualInputManagerTestScene { protected readonly BindableBeatDivisor BeatDivisor = new BindableBeatDivisor(); protected new readonly EditorClock Clock; From ff24a15760167b1db112239b6c3418870654f5a1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 28 Apr 2020 18:36:24 +0900 Subject: [PATCH 1360/2376] Fix vertical drag in down-scroll scenarios --- osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index fba80f92d2..af465af16a 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -52,9 +52,9 @@ namespace osu.Game.Rulesets.Mania.Edit if (drawableRuleset.ScrollingInfo.Direction.Value == ScrollingDirection.Down) { - // When scrolling downwards, the position is _negative_ when the object's start time is after the current time (e.g. in the middle of the stage). - // However all scrolling algorithms upwards scrolling, meaning that a positive (inverse) position is expected in the same scenario. - targetPosition = -targetPosition; + // We're dealing with screen coordinates in which the position decreases towards the centre of the screen resulting in an increase in start time. + // The scrolling algorithm assumes a top anchor meaning an increase in time corresponds to an increase in position, so when scrolling downwards the coordinates need to be flipped. + targetPosition = hoc.DrawHeight - targetPosition; } double targetTime = drawableRuleset.ScrollingInfo.Algorithm.TimeAt(targetPosition, From db12fafc2c3187220488e6235acc4f93cfa97687 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 28 Apr 2020 18:53:30 +0900 Subject: [PATCH 1361/2376] Update comment --- .../TestSceneManiaHitObjectComposer.cs | 5 +++-- osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs index 4ce2424ffa..a84fe83245 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs @@ -14,6 +14,7 @@ using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Edit; using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Screens.Edit; @@ -139,8 +140,8 @@ namespace osu.Game.Rulesets.Mania.Tests AddAssert("hitobjects moved columns", () => composer.EditorBeatmap.HitObjects.All(h => ((ManiaHitObject)h).Column == 1)); - // Todo: They'll have moved vertically by half the height of a note. Probably a problem. - AddAssert("hitobjects not moved vertically", () => lastObject.DrawPosition.Y - originalPosition.Y < 10); + // Todo: They'll move vertically by the height of a note since there's no snapping and the selection point is the middle of the note. + AddAssert("hitobjects not moved vertically", () => lastObject.DrawPosition.Y - originalPosition.Y <= DefaultNotePiece.NOTE_HEIGHT); } private class TestComposer : CompositeDrawable diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index af465af16a..dfa933baad 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -53,7 +53,8 @@ namespace osu.Game.Rulesets.Mania.Edit if (drawableRuleset.ScrollingInfo.Direction.Value == ScrollingDirection.Down) { // We're dealing with screen coordinates in which the position decreases towards the centre of the screen resulting in an increase in start time. - // The scrolling algorithm assumes a top anchor meaning an increase in time corresponds to an increase in position, so when scrolling downwards the coordinates need to be flipped. + // The scrolling algorithm instead assumes a top anchor meaning an increase in time corresponds to an increase in position, + // so when scrolling downwards the coordinates need to be flipped. targetPosition = hoc.DrawHeight - targetPosition; } From ff3928465c06a7e578afe1088a9b0a045ea2eca7 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 28 Apr 2020 18:55:58 +0900 Subject: [PATCH 1362/2376] Add xmldoc --- osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs index 6960d15f31..1af7d06998 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs @@ -87,8 +87,17 @@ namespace osu.Game.Rulesets.Mania.UI return found; } + /// + /// Retrieves a by index. + /// + /// The index of the column. + /// The corresponding to the given index. + /// If is less than 0 or greater than . public Column GetColumn(int index) { + if (index < 0 || index > TotalColumns - 1) + throw new ArgumentOutOfRangeException(nameof(index)); + foreach (var stage in stages) { if (index >= stage.Columns.Count) From 1aaab40228baa33ccb6cadadf07e84805fa48f66 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 28 Apr 2020 19:23:33 +0900 Subject: [PATCH 1363/2376] Fix mods affecting mania scroll speed --- .../UI/DrawableManiaRuleset.cs | 25 ++++++++++++++++--- osu.Game/Rulesets/UI/DrawableRuleset.cs | 8 +++--- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index 14cad39b04..39f3331fbb 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -2,7 +2,9 @@ // 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.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; @@ -48,6 +50,10 @@ namespace osu.Game.Rulesets.Mania.UI protected new ManiaRulesetConfigManager Config => (ManiaRulesetConfigManager)base.Config; private readonly Bindable configDirection = new Bindable(); + private readonly Bindable configTimeRange = new Bindable(); + + // Stores the current speed adjustment active in gameplay. + private readonly Track speedAdjustmentTrack = new TrackVirtual(1000); public DrawableManiaRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) : base(ruleset, beatmap, mods) @@ -58,6 +64,9 @@ namespace osu.Game.Rulesets.Mania.UI [BackgroundDependencyLoader] private void load() { + foreach (var mod in Mods.OfType()) + mod.ApplyToTrack(speedAdjustmentTrack); + bool isForCurrentRuleset = Beatmap.BeatmapInfo.Ruleset.Equals(Ruleset.RulesetInfo); foreach (var p in ControlPoints) @@ -76,7 +85,8 @@ namespace osu.Game.Rulesets.Mania.UI Config.BindWith(ManiaRulesetSetting.ScrollDirection, configDirection); configDirection.BindValueChanged(direction => Direction.Value = (ScrollingDirection)direction.NewValue, true); - Config.BindWith(ManiaRulesetSetting.ScrollTime, TimeRange); + Config.BindWith(ManiaRulesetSetting.ScrollTime, configTimeRange); + configTimeRange.BindValueChanged(_ => updateTimeRange()); } protected override void AdjustScrollSpeed(int amount) @@ -86,10 +96,19 @@ namespace osu.Game.Rulesets.Mania.UI private double relativeTimeRange { - get => MAX_TIME_RANGE / TimeRange.Value; - set => TimeRange.Value = MAX_TIME_RANGE / value; + get => MAX_TIME_RANGE / configTimeRange.Value; + set => configTimeRange.Value = MAX_TIME_RANGE / value; } + protected override void Update() + { + base.Update(); + + updateTimeRange(); + } + + private void updateTimeRange() => TimeRange.Value = configTimeRange.Value * speedAdjustmentTrack.AggregateTempo.Value * speedAdjustmentTrack.AggregateFrequency.Value; + /// /// Retrieves the column that intersects a screen-space position. /// diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 5062c92afe..0a1c35c7c6 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -107,7 +107,7 @@ namespace osu.Game.Rulesets.UI /// The mods which are to be applied. /// [Cached(typeof(IReadOnlyList))] - private readonly IReadOnlyList mods; + protected readonly IReadOnlyList Mods; private FrameStabilityContainer frameStabilityContainer; @@ -129,7 +129,7 @@ namespace osu.Game.Rulesets.UI throw new ArgumentException($"{GetType()} expected the beatmap to contain hitobjects of type {typeof(TObject)}.", nameof(beatmap)); Beatmap = tBeatmap; - this.mods = mods?.ToArray() ?? Array.Empty(); + Mods = mods?.ToArray() ?? Array.Empty(); RelativeSizeAxes = Axes.Both; @@ -204,7 +204,7 @@ namespace osu.Game.Rulesets.UI .WithChild(ResumeOverlay))); } - applyRulesetMods(mods, config); + applyRulesetMods(Mods, config); loadObjects(cancellationToken); } @@ -224,7 +224,7 @@ namespace osu.Game.Rulesets.UI Playfield.PostProcess(); - foreach (var mod in mods.OfType()) + foreach (var mod in Mods.OfType()) mod.ApplyToDrawableHitObjects(Playfield.AllHitObjects); } From 7868c0dad5cff824fe5bf7c90c0a8d4dea71d5eb Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 28 Apr 2020 20:15:56 +0900 Subject: [PATCH 1364/2376] Fix test case failures --- .../TestSceneManiaHitObjectComposer.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs index a84fe83245..286e3f6e50 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs @@ -119,8 +119,6 @@ namespace osu.Game.Rulesets.Mania.Tests AddStep("seek to last object", () => { lastObject = this.ChildrenOfType().Single(d => d.HitObject == composer.EditorBeatmap.HitObjects.Last()); - originalPosition = lastObject.DrawPosition; - Clock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime); }); @@ -128,13 +126,18 @@ namespace osu.Game.Rulesets.Mania.Tests AddStep("click last object", () => { + originalPosition = lastObject.DrawPosition; + InputManager.MoveMouseTo(lastObject); InputManager.PressButton(MouseButton.Left); }); AddStep("move mouse right", () => { - InputManager.MoveMouseTo(lastObject, new Vector2(40, 0)); + var firstColumn = composer.Composer.Playfield.GetColumn(0); + var secondColumn = composer.Composer.Playfield.GetColumn(1); + + InputManager.MoveMouseTo(lastObject, new Vector2(secondColumn.ScreenSpaceDrawQuad.Centre.X - firstColumn.ScreenSpaceDrawQuad.Centre.X + 1, 0)); InputManager.ReleaseButton(MouseButton.Left); }); From 119000f1ab2157762863743d25d111b936f6a4a5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 28 Apr 2020 21:43:35 +0900 Subject: [PATCH 1365/2376] Reduce database includes where possible --- osu.Game/Beatmaps/BeatmapManager.cs | 54 ++++++++++++++++++++-- osu.Game/Beatmaps/BeatmapStore.cs | 12 +++++ osu.Game/Overlays/MusicController.cs | 2 +- osu.Game/Screens/Menu/IntroScreen.cs | 2 +- osu.Game/Screens/Select/BeatmapCarousel.cs | 2 +- osu.Game/Screens/Select/SongSelect.cs | 2 +- 6 files changed, 67 insertions(+), 7 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 6542866936..5651d07566 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -246,6 +246,12 @@ namespace osu.Game.Beatmaps if (beatmapInfo?.BeatmapSet == null || beatmapInfo == DefaultBeatmap?.BeatmapInfo) return DefaultBeatmap; + if (beatmapInfo.BeatmapSet.Files == null) + { + var info = beatmapInfo; + beatmapInfo = QueryBeatmap(b => b.ID == info.ID); + } + lock (workingCache) { var working = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == beatmapInfo.ID); @@ -287,13 +293,34 @@ namespace osu.Game.Beatmaps /// Returns a list of all usable s. ///
    /// A list of available . - public List GetAllUsableBeatmapSets() => GetAllUsableBeatmapSetsEnumerable().ToList(); + public List GetAllUsableBeatmapSets(IncludedDetails includes = IncludedDetails.All) => GetAllUsableBeatmapSetsEnumerable(includes).ToList(); /// - /// Returns a list of all usable s. + /// Returns a list of all usable s. Note that files are not populated. /// + /// The level of detail to include in the returned objects. /// A list of available . - public IQueryable GetAllUsableBeatmapSetsEnumerable() => beatmaps.ConsumableItems.Where(s => !s.DeletePending && !s.Protected); + public IQueryable GetAllUsableBeatmapSetsEnumerable(IncludedDetails includes) + { + IQueryable queryable; + + switch (includes) + { + case IncludedDetails.Minimal: + queryable = beatmaps.BeatmapSetsOverview; + break; + + case IncludedDetails.AllButFiles: + queryable = beatmaps.BeatmapSetsWithoutFiles; + break; + + default: + queryable = beatmaps.ConsumableItems; + break; + } + + return queryable.Where(s => !s.DeletePending && !s.Protected); + } /// /// Perform a lookup query on available s. @@ -482,4 +509,25 @@ namespace osu.Game.Beatmaps } } } + + /// + /// 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. + /// + All + } } diff --git a/osu.Game/Beatmaps/BeatmapStore.cs b/osu.Game/Beatmaps/BeatmapStore.cs index a2279fdb14..642bafd2ac 100644 --- a/osu.Game/Beatmaps/BeatmapStore.cs +++ b/osu.Game/Beatmaps/BeatmapStore.cs @@ -87,6 +87,18 @@ namespace osu.Game.Beatmaps base.Purge(items, context); } + public IQueryable BeatmapSetsOverview => ContextFactory.Get().BeatmapSetInfo + .Include(s => s.Metadata) + .Include(s => s.Beatmaps) + .AsNoTracking(); + + public IQueryable BeatmapSetsWithoutFiles => ContextFactory.Get().BeatmapSetInfo + .Include(s => s.Metadata) + .Include(s => s.Beatmaps).ThenInclude(s => s.Ruleset) + .Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty) + .Include(s => s.Beatmaps).ThenInclude(b => b.Metadata) + .AsNoTracking(); + public IQueryable Beatmaps => ContextFactory.Get().BeatmapInfo .Include(b => b.BeatmapSet).ThenInclude(s => s.Metadata) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 6d269aa944..c872f82b32 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -66,7 +66,7 @@ namespace osu.Game.Overlays beatmaps.ItemAdded += handleBeatmapAdded; beatmaps.ItemRemoved += handleBeatmapRemoved; - beatmapSets.AddRange(beatmaps.GetAllUsableBeatmapSets().OrderBy(_ => RNG.Next())); + beatmapSets.AddRange(beatmaps.GetAllUsableBeatmapSets(IncludedDetails.Minimal).OrderBy(_ => RNG.Next())); } protected override void LoadComplete() diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index 26455b1dbd..d2296573a6 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -73,7 +73,7 @@ namespace osu.Game.Screens.Menu if (!MenuMusic.Value) { - var sets = beatmaps.GetAllUsableBeatmapSets(); + var sets = beatmaps.GetAllUsableBeatmapSets(IncludedDetails.Minimal); if (sets.Count > 0) setInfo = beatmaps.QueryBeatmapSet(s => s.ID == sets[RNG.Next(0, sets.Count - 1)].ID); } diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index f989ab2787..5a4a03662a 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -169,7 +169,7 @@ namespace osu.Game.Screens.Select loadBeatmapSets(GetLoadableBeatmaps()); } - protected virtual IEnumerable GetLoadableBeatmaps() => beatmaps.GetAllUsableBeatmapSetsEnumerable(); + protected virtual IEnumerable GetLoadableBeatmaps() => beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.AllButFiles); public void RemoveBeatmapSet(BeatmapSetInfo beatmapSet) => Schedule(() => { diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 0d07a335cf..c07465ca44 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -286,7 +286,7 @@ namespace osu.Game.Screens.Select Schedule(() => { // if we have no beatmaps but osu-stable is found, let's prompt the user to import. - if (!beatmaps.GetAllUsableBeatmapSetsEnumerable().Any() && beatmaps.StableInstallationAvailable) + if (!beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.Minimal).Any() && beatmaps.StableInstallationAvailable) { dialogOverlay.Push(new ImportFromStablePopup(() => { From 902326e7ac175923a5a1dbcc40adcbdb218f8f29 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Tue, 28 Apr 2020 22:24:19 +0200 Subject: [PATCH 1366/2376] Slight refactoring --- .idea/.idea.osu.Desktop/.idea/discord.xml | 9 ++++ .../Skinning/TestSceneDrawableTaikoMascot.cs | 41 +++++++------------ 2 files changed, 24 insertions(+), 26 deletions(-) create mode 100644 .idea/.idea.osu.Desktop/.idea/discord.xml diff --git a/.idea/.idea.osu.Desktop/.idea/discord.xml b/.idea/.idea.osu.Desktop/.idea/discord.xml new file mode 100644 index 0000000000..59b11d1d39 --- /dev/null +++ b/.idea/.idea.osu.Desktop/.idea/discord.xml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs index a0ab3e5c25..41d7156e7e 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs @@ -93,21 +93,11 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning AddStep("Collect playfields", collectPlayfields); AddStep("Collect mascots", collectMascots); - AddStep("Create hit (miss)", () => - { - foreach (var playfield in playfields) - addJudgement(playfield, HitResult.Miss); - }); + AddStep("Create hit (great)", () => addJudgement(HitResult.Miss)); + AddUntilStep("Wait for idle state", () => checkForState(TaikoMascotAnimationState.Fail)); - AddUntilStep("Wait for fail state", () => mascots.Where(d => d != null).All(d => d.State == TaikoMascotAnimationState.Fail)); - - AddStep("Create hit (great)", () => - { - foreach (var playfield in playfields) - addJudgement(playfield, HitResult.Great); - }); - - AddUntilStep("Wait for idle state", () => mascots.Where(d => d != null).All(d => d.State == TaikoMascotAnimationState.Idle)); + AddStep("Create hit (great)", () => addJudgement(HitResult.Great)); + AddUntilStep("Wait for idle state", () => checkForState(TaikoMascotAnimationState.Idle)); } [Test] @@ -134,15 +124,10 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning AddStep("Collect playfields", collectPlayfields); AddStep("Collect mascots", collectMascots); - AddUntilStep("Wait for fail state", () => mascots.Where(d => d != null).All(d => d.State == TaikoMascotAnimationState.Fail)); + AddUntilStep("Wait for idle state", () => checkForState(TaikoMascotAnimationState.Fail)); - AddStep("Create hit (great)", () => - { - foreach (var playfield in playfields) - addJudgement(playfield, HitResult.Great); - }); - - AddUntilStep("Wait for kiai state", () => mascots.Where(d => d != null).All(d => d.State == TaikoMascotAnimationState.Kiai)); + AddStep("Create hit (great)", () => addJudgement(HitResult.Great)); + AddUntilStep("Wait for idle state", () => checkForState(TaikoMascotAnimationState.Kiai)); } private void setBeatmap(bool kiai = false) @@ -190,18 +175,22 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning foreach (var playfield in playfields) { - var mascot = playfield.ChildrenOfType() + var mascot = playfield.ChildrenOfType() .SingleOrDefault(); - if (mascot != null) mascots.Add(mascot); + if (mascot != null) + mascots.Add(mascot); } } - private void addJudgement(TaikoPlayfield playfield, HitResult result) + private void addJudgement(HitResult result) { - playfield.OnNewResult(new DrawableHit(new Hit()), new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = result }); + foreach (var playfield in playfields) + playfield.OnNewResult(new DrawableHit(new Hit()), new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = result }); } + private bool checkForState(TaikoMascotAnimationState state) => mascots.All(d => d.State == state); + private class TestDrawableTaikoMascot : DrawableTaikoMascot { public TestDrawableTaikoMascot(TaikoMascotAnimationState startingState = TaikoMascotAnimationState.Idle) From 0d285ac0f40e8343eb4462389c9366177bd409b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 28 Apr 2020 23:17:52 +0200 Subject: [PATCH 1367/2376] Revert unrelated change to Rider configuration --- .idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml b/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml index 7515e76054..4bb9f4d2a0 100644 --- a/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml +++ b/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml @@ -1,6 +1,6 @@ - \ No newline at end of file From 24216b6600106282f68565b85677dd260ef186c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 28 Apr 2020 23:22:50 +0200 Subject: [PATCH 1368/2376] Remove unnecessary lists --- .../Skinning/TestSceneDrawableTaikoMascot.cs | 50 ++----------------- 1 file changed, 5 insertions(+), 45 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs index a0ab3e5c25..ae7dc71cc9 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs @@ -39,9 +39,8 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning private Bindable workingBeatmap; - private readonly List mascots = new List(); - private readonly List playfields = new List(); - private readonly List rulesets = new List(); + private IEnumerable mascots => this.ChildrenOfType(); + private IEnumerable playfields => this.ChildrenOfType(); [BackgroundDependencyLoader] private void load(Bindable beatmap) @@ -56,14 +55,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning AddStep("Create mascot (idle)", () => { - mascots.Clear(); - - SetContents(() => - { - var mascot = new TestDrawableTaikoMascot(); - mascots.Add(mascot); - return mascot; - }); + SetContents(() => new TestDrawableTaikoMascot()); }); AddStep("Clear state", () => setState(TaikoMascotAnimationState.Clear)); @@ -80,19 +72,13 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning AddStep("Create ruleset", () => { - rulesets.Clear(); SetContents(() => { var ruleset = new TaikoRuleset(); - var drawableRuleset = new DrawableTaikoRuleset(ruleset, workingBeatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo)); - rulesets.Add(drawableRuleset); - return drawableRuleset; + return new DrawableTaikoRuleset(ruleset, workingBeatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo)); }); }); - AddStep("Collect playfields", collectPlayfields); - AddStep("Collect mascots", collectMascots); - AddStep("Create hit (miss)", () => { foreach (var playfield in playfields) @@ -121,19 +107,13 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning { workingBeatmap.Value.Track.Start(); - rulesets.Clear(); SetContents(() => { var ruleset = new TaikoRuleset(); - var drawableRuleset = new DrawableTaikoRuleset(ruleset, workingBeatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo)); - rulesets.Add(drawableRuleset); - return drawableRuleset; + return new DrawableTaikoRuleset(ruleset, workingBeatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo)); }); }); - AddStep("Collect playfields", collectPlayfields); - AddStep("Collect mascots", collectMascots); - AddUntilStep("Wait for fail state", () => mascots.Where(d => d != null).All(d => d.State == TaikoMascotAnimationState.Fail)); AddStep("Create hit (great)", () => @@ -177,26 +157,6 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning mascot?.ShowState(state); } - private void collectPlayfields() - { - playfields.Clear(); - foreach (var ruleset in rulesets) - playfields.Add(ruleset.ChildrenOfType().Single()); - } - - private void collectMascots() - { - mascots.Clear(); - - foreach (var playfield in playfields) - { - var mascot = playfield.ChildrenOfType() - .SingleOrDefault(); - - if (mascot != null) mascots.Add(mascot); - } - } - private void addJudgement(TaikoPlayfield playfield, HitResult result) { playfield.OnNewResult(new DrawableHit(new Hit()), new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = result }); From 15bbedca8796a4c91975592ae6005d211195449b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 28 Apr 2020 23:24:21 +0200 Subject: [PATCH 1369/2376] Remove unnecessary beatmap field --- .../Skinning/TestSceneDrawableTaikoMascot.cs | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs index ae7dc71cc9..8f45fec3f9 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; @@ -37,17 +36,9 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning TimeRange = { Value = 5000 }, }; - private Bindable workingBeatmap; - private IEnumerable mascots => this.ChildrenOfType(); private IEnumerable playfields => this.ChildrenOfType(); - [BackgroundDependencyLoader] - private void load(Bindable beatmap) - { - workingBeatmap = beatmap; - } - [Test] public void TestStateTextures() { @@ -75,7 +66,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning SetContents(() => { var ruleset = new TaikoRuleset(); - return new DrawableTaikoRuleset(ruleset, workingBeatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo)); + return new DrawableTaikoRuleset(ruleset, Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo)); }); }); @@ -101,16 +92,16 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning { AddStep("Set beatmap", () => setBeatmap(true)); - AddUntilStep("Wait for beatmap to be loaded", () => workingBeatmap.Value.Track.IsLoaded); + AddUntilStep("Wait for beatmap to be loaded", () => Beatmap.Value.Track.IsLoaded); AddStep("Create kiai ruleset", () => { - workingBeatmap.Value.Track.Start(); + Beatmap.Value.Track.Start(); SetContents(() => { var ruleset = new TaikoRuleset(); - return new DrawableTaikoRuleset(ruleset, workingBeatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo)); + return new DrawableTaikoRuleset(ruleset, Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo)); }); }); @@ -133,7 +124,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning if (kiai) controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true }); - workingBeatmap.Value = CreateWorkingBeatmap(new Beatmap + Beatmap.Value = CreateWorkingBeatmap(new Beatmap { HitObjects = new List { new Hit { Type = HitType.Centre } }, BeatmapInfo = new BeatmapInfo From e7e529ab99944f62bbc31f711295519950426b84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 28 Apr 2020 23:26:10 +0200 Subject: [PATCH 1370/2376] Remove unnecessary null checks --- .../Skinning/TestSceneDrawableTaikoMascot.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs index 8f45fec3f9..83bf64eb46 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs @@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning addJudgement(playfield, HitResult.Miss); }); - AddUntilStep("Wait for fail state", () => mascots.Where(d => d != null).All(d => d.State == TaikoMascotAnimationState.Fail)); + AddUntilStep("Wait for fail state", () => mascots.All(d => d.State == TaikoMascotAnimationState.Fail)); AddStep("Create hit (great)", () => { @@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning addJudgement(playfield, HitResult.Great); }); - AddUntilStep("Wait for idle state", () => mascots.Where(d => d != null).All(d => d.State == TaikoMascotAnimationState.Idle)); + AddUntilStep("Wait for idle state", () => mascots.All(d => d.State == TaikoMascotAnimationState.Idle)); } [Test] @@ -105,7 +105,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning }); }); - AddUntilStep("Wait for fail state", () => mascots.Where(d => d != null).All(d => d.State == TaikoMascotAnimationState.Fail)); + AddUntilStep("Wait for fail state", () => mascots.All(d => d.State == TaikoMascotAnimationState.Fail)); AddStep("Create hit (great)", () => { @@ -113,7 +113,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning addJudgement(playfield, HitResult.Great); }); - AddUntilStep("Wait for kiai state", () => mascots.Where(d => d != null).All(d => d.State == TaikoMascotAnimationState.Kiai)); + AddUntilStep("Wait for kiai state", () => mascots.All(d => d.State == TaikoMascotAnimationState.Kiai)); } private void setBeatmap(bool kiai = false) From d7b072dd6e90138a6607deff4c9943c019733cf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 29 Apr 2020 00:00:01 +0200 Subject: [PATCH 1371/2376] Fix post-merge test regression --- .../Skinning/TestSceneDrawableTaikoMascot.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs index 83bf64eb46..4eeeb52e2f 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs @@ -14,7 +14,6 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Judgements; using osu.Game.Rulesets.Taiko.Objects; -using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.Taiko.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Tests.Visual; @@ -150,7 +149,10 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning private void addJudgement(TaikoPlayfield playfield, HitResult result) { - playfield.OnNewResult(new DrawableHit(new Hit()), new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = result }); + var hit = new DrawableTestHit(new Hit(), result); + Add(hit); + + playfield.OnNewResult(hit, new JudgementResult(hit.HitObject, new TaikoJudgement()) { Type = result }); } private class TestDrawableTaikoMascot : DrawableTaikoMascot From 00918ecb6d6c47f6c131fca8991448825e9297e3 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 29 Apr 2020 04:43:49 +0300 Subject: [PATCH 1372/2376] Replace interval collection with a more-specific immutable component Covers all small changes into one commit: - Remove generics and use `double` type instead. - Make the component immutable and not enumerable for simplicity of it's worth. - Make the component more-specific (to time period tracking) - Apply small adjustments to the component --- osu.Game/Lists/IntervalList.cs | 111 -------------------------------- osu.Game/Utils/PeriodTracker.cs | 90 ++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 111 deletions(-) delete mode 100644 osu.Game/Lists/IntervalList.cs create mode 100644 osu.Game/Utils/PeriodTracker.cs diff --git a/osu.Game/Lists/IntervalList.cs b/osu.Game/Lists/IntervalList.cs deleted file mode 100644 index 580015bb96..0000000000 --- a/osu.Game/Lists/IntervalList.cs +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Collections; -using System.Collections.Generic; -using osu.Framework.Lists; - -namespace osu.Game.Lists -{ - /// - /// Represents a list of intervals that can be used for whether a specific value falls into one of them. - /// - /// The type of interval values. - public class IntervalList : IEnumerable> - where T : struct, IConvertible - { - private static readonly IComparer type_comparer = Comparer.Default; - - private readonly SortedList> intervals = new SortedList>((x, y) => type_comparer.Compare(x.Start, y.Start)); - private int nearestIndex; - - public Interval this[int i] - { - get => intervals[i]; - set => intervals[i] = value; - } - - /// - /// Whether the provided value is in any interval added to this list. - /// - /// The value to check for. - public bool IsInAnyInterval(T value) - { - if (intervals.Count == 0) - return false; - - // Clamp the nearest index in case there were intervals - // removed from the list causing the index to go out of range. - nearestIndex = Math.Clamp(nearestIndex, 0, intervals.Count - 1); - - if (type_comparer.Compare(value, this[nearestIndex].End) > 0) - { - while (type_comparer.Compare(value, this[nearestIndex].End) > 0 && nearestIndex < intervals.Count - 1) - nearestIndex++; - } - else - { - while (type_comparer.Compare(value, this[nearestIndex].Start) < 0 && nearestIndex > 0) - nearestIndex--; - } - - var nearestInterval = this[nearestIndex]; - - return type_comparer.Compare(value, nearestInterval.Start) >= 0 && - type_comparer.Compare(value, nearestInterval.End) <= 0; - } - - /// - /// Adds a new interval to the list. - /// - /// The start value of the interval. - /// The end value of the interval. - public void Add(T start, T end) => Add(new Interval(start, end)); - - /// - /// Adds a new interval to the list - /// - /// The interval to add. - public void Add(Interval interval) => intervals.Add(interval); - - /// - /// Removes an existing interval from the list. - /// - /// The interval to remove. - /// Whether the provided interval exists in the list and has been removed. - public bool Remove(Interval interval) => intervals.Remove(interval); - - /// - /// Removes all intervals from the list. - /// - public void Clear() => intervals.Clear(); - - public IEnumerator> GetEnumerator() => intervals.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - } - - public readonly struct Interval - where T : struct, IConvertible - { - /// - /// The start value of this interval. - /// - public readonly T Start; - - /// - /// The end value of this interval. - /// - public readonly T End; - - public Interval(T start, T end) - { - if (Comparer.Default.Compare(start, end) >= 0) - throw new ArgumentException($"Invalid interval, {nameof(start)} must be less than {nameof(end)}", nameof(start)); - - Start = start; - End = end; - } - } -} diff --git a/osu.Game/Utils/PeriodTracker.cs b/osu.Game/Utils/PeriodTracker.cs new file mode 100644 index 0000000000..589f061c1d --- /dev/null +++ b/osu.Game/Utils/PeriodTracker.cs @@ -0,0 +1,90 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace osu.Game.Utils +{ + /// + /// Represents a tracking component used for whether a + /// specific time falls into any of the provided periods. + /// + public class PeriodTracker + { + private readonly List periods = new List(); + private int nearestIndex; + + /// + /// The list of periods to add to the tracker for using the required check methods. + /// + public IEnumerable Periods + { + set + { + var sortedValue = value?.ToList(); + sortedValue?.Sort(); + + if (sortedValue != null && periods.SequenceEqual(sortedValue)) + return; + + periods.Clear(); + nearestIndex = 0; + + if (value?.Any() != true) + return; + + periods.AddRange(sortedValue); + } + } + + /// + /// Whether the provided time is in any of the added periods. + /// + /// The time value to check for. + public bool Contains(double time) + { + if (periods.Count == 0) + return false; + + if (time > periods[nearestIndex].End) + { + while (time > periods[nearestIndex].End && nearestIndex < periods.Count - 1) + nearestIndex++; + } + else + { + while (time < periods[nearestIndex].Start && nearestIndex > 0) + nearestIndex--; + } + + var nearest = periods[nearestIndex]; + return time >= nearest.Start && time <= nearest.End; + } + } + + public readonly struct Period : IComparable + { + /// + /// The start time of this period. + /// + public readonly double Start; + + /// + /// The end time of this period. + /// + public readonly double End; + + public Period(double start, double end) + { + if (start >= end) + throw new ArgumentException($"Invalid period provided, {nameof(start)} must be less than {nameof(end)}", nameof(start)); + + Start = start; + End = end; + } + + public int CompareTo(Period other) => Start.CompareTo(other.Start); + } +} From 587f946dc63ba8315b8f90b8b1cd9b5d42a9fc06 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 29 Apr 2020 04:58:08 +0300 Subject: [PATCH 1373/2376] Adjust, simplify and make the test code more flexible to some degree --- osu.Game.Tests/Lists/IntervalListTest.cs | 115 ------------------ osu.Game.Tests/NonVisual/PeriodTrackerTest.cs | 103 ++++++++++++++++ 2 files changed, 103 insertions(+), 115 deletions(-) delete mode 100644 osu.Game.Tests/Lists/IntervalListTest.cs create mode 100644 osu.Game.Tests/NonVisual/PeriodTrackerTest.cs diff --git a/osu.Game.Tests/Lists/IntervalListTest.cs b/osu.Game.Tests/Lists/IntervalListTest.cs deleted file mode 100644 index 0958f0fa7c..0000000000 --- a/osu.Game.Tests/Lists/IntervalListTest.cs +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using NUnit.Framework; -using osu.Game.Lists; - -namespace osu.Game.Tests.Lists -{ - [TestFixture] - public class IntervalListTest - { - // this is intended to be unordered to test adding intervals in unordered way. - private static readonly (double, double)[] test_intervals = - { - (-9.1d, -8.3d), - (-3.4d, 2.1d), - (9.0d, 50.0d), - (5.25d, 10.50d), - }; - - [Test] - public void TestCheckValueInsideSingleInterval() - { - var list = new IntervalList { { 1.0d, 2.0d } }; - - Assert.IsTrue(list.IsInAnyInterval(1.0d)); - Assert.IsTrue(list.IsInAnyInterval(1.5d)); - Assert.IsTrue(list.IsInAnyInterval(2.0d)); - } - - [Test] - public void TestCheckValuesInsideIntervals() - { - var list = new IntervalList(); - - foreach (var (start, end) in test_intervals) - list.Add(start, end); - - Assert.IsTrue(list.IsInAnyInterval(-8.75d)); - Assert.IsTrue(list.IsInAnyInterval(1.0d)); - Assert.IsTrue(list.IsInAnyInterval(7.89d)); - Assert.IsTrue(list.IsInAnyInterval(9.8d)); - Assert.IsTrue(list.IsInAnyInterval(15.83d)); - } - - [Test] - public void TestCheckValuesInRandomOrder() - { - var list = new IntervalList(); - - foreach (var (start, end) in test_intervals) - list.Add(start, end); - - Assert.IsTrue(list.IsInAnyInterval(9.8d)); - Assert.IsTrue(list.IsInAnyInterval(7.89d)); - Assert.IsTrue(list.IsInAnyInterval(1.0d)); - Assert.IsTrue(list.IsInAnyInterval(15.83d)); - Assert.IsTrue(list.IsInAnyInterval(-8.75d)); - } - - [Test] - public void TestCheckValuesOutOfIntervals() - { - var list = new IntervalList(); - - foreach (var (start, end) in test_intervals) - list.Add(start, end); - - Assert.IsFalse(list.IsInAnyInterval(-9.2d)); - Assert.IsFalse(list.IsInAnyInterval(2.2d)); - Assert.IsFalse(list.IsInAnyInterval(5.15d)); - Assert.IsFalse(list.IsInAnyInterval(51.2d)); - } - - [Test] - public void TestCheckValueAfterRemovedInterval() - { - var list = new IntervalList { { 50, 100 }, { 150, 200 }, { 250, 300 } }; - - Assert.IsTrue(list.IsInAnyInterval(75)); - Assert.IsTrue(list.IsInAnyInterval(175)); - Assert.IsTrue(list.IsInAnyInterval(275)); - - list.Remove(list[1]); - - Assert.IsFalse(list.IsInAnyInterval(175)); - Assert.IsTrue(list.IsInAnyInterval(75)); - Assert.IsTrue(list.IsInAnyInterval(275)); - } - - [Test] - public void TestCheckValueAfterAddedInterval() - { - var list = new IntervalList { { 50, 100 }, { 250, 300 } }; - - Assert.IsFalse(list.IsInAnyInterval(175)); - Assert.IsTrue(list.IsInAnyInterval(75)); - Assert.IsTrue(list.IsInAnyInterval(275)); - - list.Add(150, 200); - - Assert.IsTrue(list.IsInAnyInterval(175)); - } - - [Test] - public void TestReversedIntervalThrows() - { - var list = new IntervalList(); - - Assert.Throws(() => list.Add(50, 25)); - Assert.Throws(() => list.Add(new Interval(50, 25))); - } - } -} diff --git a/osu.Game.Tests/NonVisual/PeriodTrackerTest.cs b/osu.Game.Tests/NonVisual/PeriodTrackerTest.cs new file mode 100644 index 0000000000..39eea2b386 --- /dev/null +++ b/osu.Game.Tests/NonVisual/PeriodTrackerTest.cs @@ -0,0 +1,103 @@ +// 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.Utils; +using osu.Game.Utils; + +namespace osu.Game.Tests.NonVisual +{ + [TestFixture] + public class PeriodTrackerTest + { + private static readonly Period[] test_single_period = { new Period(1.0, 2.0) }; + + // this is intended to be unordered to test adding periods in unordered way. + private static readonly Period[] test_periods = + { + new Period(-9.1, -8.3), + new Period(-3.4, 2.1), + new Period(9.0, 50.0), + new Period(5.25, 10.50) + }; + + [Test] + public void TestCheckValueInsideSinglePeriod() + { + var tracker = new PeriodTracker { Periods = test_single_period }; + + var period = test_single_period.Single(); + Assert.IsTrue(tracker.Contains(period.Start)); + Assert.IsTrue(tracker.Contains(getMidTime(period))); + Assert.IsTrue(tracker.Contains(period.End)); + } + + [Test] + public void TestCheckValuesInsidePeriods() + { + var tracker = new PeriodTracker { Periods = test_periods }; + + foreach (var period in test_periods) + Assert.IsTrue(tracker.Contains(getMidTime(period))); + } + + [Test] + public void TestCheckValuesInRandomOrder() + { + var tracker = new PeriodTracker { Periods = test_periods }; + + foreach (var period in test_periods.OrderBy(_ => RNG.Next())) + Assert.IsTrue(tracker.Contains(getMidTime(period))); + } + + [Test] + public void TestCheckValuesOutOfPeriods() + { + var tracker = new PeriodTracker + { + Periods = new[] + { + new Period(1.0, 2.0), + new Period(3.0, 4.0) + } + }; + + Assert.IsFalse(tracker.Contains(0.9), "Time before first period is being considered inside"); + + Assert.IsFalse(tracker.Contains(2.1), "Time right after first period is being considered inside"); + Assert.IsFalse(tracker.Contains(2.9), "Time right before second period is being considered inside"); + + Assert.IsFalse(tracker.Contains(4.1), "Time after last period is being considered inside"); + } + + [Test] + public void TestNullRemovesExistingPeriods() + { + var tracker = new PeriodTracker { Periods = test_single_period }; + + var period = test_single_period.Single(); + Assert.IsTrue(tracker.Contains(getMidTime(period))); + + tracker.Periods = null; + Assert.IsFalse(tracker.Contains(getMidTime(period))); + } + + [Test] + public void TestReversedPeriodHandling() + { + var tracker = new PeriodTracker(); + + Assert.Throws(() => + { + tracker.Periods = new[] + { + new Period(2.0, 1.0) + }; + }); + } + + private double getMidTime(Period period) => period.Start + (period.End - period.Start) / 2; + } +} From 8d899f4e7742e93b8afa0432e865685e3b92e71f Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 29 Apr 2020 05:07:58 +0300 Subject: [PATCH 1374/2376] Apply changes to the BreakTracker and more adjustment --- osu.Game/Screens/Play/BreakTracker.cs | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/Play/BreakTracker.cs b/osu.Game/Screens/Play/BreakTracker.cs index c2eb069ee6..e30c0c6dec 100644 --- a/osu.Game/Screens/Play/BreakTracker.cs +++ b/osu.Game/Screens/Play/BreakTracker.cs @@ -2,20 +2,22 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Beatmaps.Timing; -using osu.Game.Lists; using osu.Game.Rulesets.Scoring; +using osu.Game.Utils; namespace osu.Game.Screens.Play { public class BreakTracker : Component { private readonly ScoreProcessor scoreProcessor; - private readonly double gameplayStartTime; + private readonly PeriodTracker tracker = new PeriodTracker(); + /// /// Whether the gameplay is currently in a break. /// @@ -23,22 +25,14 @@ namespace osu.Game.Screens.Play private readonly BindableBool isBreakTime = new BindableBool(); - private readonly IntervalList breakIntervals = new IntervalList(); - public IReadOnlyList Breaks { set { isBreakTime.Value = false; - breakIntervals.Clear(); - foreach (var b in value) - { - if (!b.HasEffect) - continue; - - breakIntervals.Add(b.StartTime, b.EndTime - BreakOverlay.BREAK_FADE_DURATION); - } + tracker.Periods = value?.Where(b => b.HasEffect) + .Select(b => new Period(b.StartTime, b.EndTime - BreakOverlay.BREAK_FADE_DURATION)); } } @@ -54,7 +48,7 @@ namespace osu.Game.Screens.Play var time = Clock.CurrentTime; - isBreakTime.Value = breakIntervals.IsInAnyInterval(time) + isBreakTime.Value = tracker.Contains(time) || time < gameplayStartTime || scoreProcessor?.HasCompleted == true; } From 6e76e5900a4d965eddecb7dc2223d47cf8d2c38b Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 29 Apr 2020 05:08:38 +0300 Subject: [PATCH 1375/2376] Rename is-in-any check method to a more legible name --- osu.Game.Tests/NonVisual/PeriodTrackerTest.cs | 22 +++++++++---------- osu.Game/Screens/Play/BreakTracker.cs | 2 +- osu.Game/Utils/PeriodTracker.cs | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/NonVisual/PeriodTrackerTest.cs b/osu.Game.Tests/NonVisual/PeriodTrackerTest.cs index 39eea2b386..f033672576 100644 --- a/osu.Game.Tests/NonVisual/PeriodTrackerTest.cs +++ b/osu.Game.Tests/NonVisual/PeriodTrackerTest.cs @@ -29,9 +29,9 @@ namespace osu.Game.Tests.NonVisual var tracker = new PeriodTracker { Periods = test_single_period }; var period = test_single_period.Single(); - Assert.IsTrue(tracker.Contains(period.Start)); - Assert.IsTrue(tracker.Contains(getMidTime(period))); - Assert.IsTrue(tracker.Contains(period.End)); + Assert.IsTrue(tracker.IsInAny(period.Start)); + Assert.IsTrue(tracker.IsInAny(getMidTime(period))); + Assert.IsTrue(tracker.IsInAny(period.End)); } [Test] @@ -40,7 +40,7 @@ namespace osu.Game.Tests.NonVisual var tracker = new PeriodTracker { Periods = test_periods }; foreach (var period in test_periods) - Assert.IsTrue(tracker.Contains(getMidTime(period))); + Assert.IsTrue(tracker.IsInAny(getMidTime(period))); } [Test] @@ -49,7 +49,7 @@ namespace osu.Game.Tests.NonVisual var tracker = new PeriodTracker { Periods = test_periods }; foreach (var period in test_periods.OrderBy(_ => RNG.Next())) - Assert.IsTrue(tracker.Contains(getMidTime(period))); + Assert.IsTrue(tracker.IsInAny(getMidTime(period))); } [Test] @@ -64,12 +64,12 @@ namespace osu.Game.Tests.NonVisual } }; - Assert.IsFalse(tracker.Contains(0.9), "Time before first period is being considered inside"); + Assert.IsFalse(tracker.IsInAny(0.9), "Time before first period is being considered inside"); - Assert.IsFalse(tracker.Contains(2.1), "Time right after first period is being considered inside"); - Assert.IsFalse(tracker.Contains(2.9), "Time right before second period is being considered inside"); + Assert.IsFalse(tracker.IsInAny(2.1), "Time right after first period is being considered inside"); + Assert.IsFalse(tracker.IsInAny(2.9), "Time right before second period is being considered inside"); - Assert.IsFalse(tracker.Contains(4.1), "Time after last period is being considered inside"); + Assert.IsFalse(tracker.IsInAny(4.1), "Time after last period is being considered inside"); } [Test] @@ -78,10 +78,10 @@ namespace osu.Game.Tests.NonVisual var tracker = new PeriodTracker { Periods = test_single_period }; var period = test_single_period.Single(); - Assert.IsTrue(tracker.Contains(getMidTime(period))); + Assert.IsTrue(tracker.IsInAny(getMidTime(period))); tracker.Periods = null; - Assert.IsFalse(tracker.Contains(getMidTime(period))); + Assert.IsFalse(tracker.IsInAny(getMidTime(period))); } [Test] diff --git a/osu.Game/Screens/Play/BreakTracker.cs b/osu.Game/Screens/Play/BreakTracker.cs index e30c0c6dec..eb77cb9369 100644 --- a/osu.Game/Screens/Play/BreakTracker.cs +++ b/osu.Game/Screens/Play/BreakTracker.cs @@ -48,7 +48,7 @@ namespace osu.Game.Screens.Play var time = Clock.CurrentTime; - isBreakTime.Value = tracker.Contains(time) + isBreakTime.Value = tracker.IsInAny(time) || time < gameplayStartTime || scoreProcessor?.HasCompleted == true; } diff --git a/osu.Game/Utils/PeriodTracker.cs b/osu.Game/Utils/PeriodTracker.cs index 589f061c1d..49b372bb27 100644 --- a/osu.Game/Utils/PeriodTracker.cs +++ b/osu.Game/Utils/PeriodTracker.cs @@ -43,7 +43,7 @@ namespace osu.Game.Utils /// Whether the provided time is in any of the added periods. /// /// The time value to check for. - public bool Contains(double time) + public bool IsInAny(double time) { if (periods.Count == 0) return false; From 024f10a494f124e632652cae331bbcce1dbf3c39 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 29 Apr 2020 13:24:31 +0900 Subject: [PATCH 1376/2376] Use non-generic bindable 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/UI/DrawableManiaRuleset.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index 39f3331fbb..fa26e6d713 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Mania.UI protected new ManiaRulesetConfigManager Config => (ManiaRulesetConfigManager)base.Config; private readonly Bindable configDirection = new Bindable(); - private readonly Bindable configTimeRange = new Bindable(); + private readonly Bindable configTimeRange = new BindableDouble(); // Stores the current speed adjustment active in gameplay. private readonly Track speedAdjustmentTrack = new TrackVirtual(1000); From 0c95d11fdb0db19fc1610774961ffe376241d3e5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 29 Apr 2020 13:27:33 +0900 Subject: [PATCH 1377/2376] Remove unnecessary value change binding --- osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index fa26e6d713..00fa68d088 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -86,7 +86,6 @@ namespace osu.Game.Rulesets.Mania.UI configDirection.BindValueChanged(direction => Direction.Value = (ScrollingDirection)direction.NewValue, true); Config.BindWith(ManiaRulesetSetting.ScrollTime, configTimeRange); - configTimeRange.BindValueChanged(_ => updateTimeRange()); } protected override void AdjustScrollSpeed(int amount) From 4f332ace1426b1c6840d0b4af3aad2e4a8e58e14 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 29 Apr 2020 14:27:21 +0900 Subject: [PATCH 1378/2376] Use 0 length --- osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index 00fa68d088..f3f843f366 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Mania.UI private readonly Bindable configTimeRange = new BindableDouble(); // Stores the current speed adjustment active in gameplay. - private readonly Track speedAdjustmentTrack = new TrackVirtual(1000); + private readonly Track speedAdjustmentTrack = new TrackVirtual(0); public DrawableManiaRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) : base(ruleset, beatmap, mods) From c73d45bc01de6d2d2175c028400bb4c6d727c2f4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Apr 2020 15:23:28 +0900 Subject: [PATCH 1379/2376] Reduce initial channel load overhead by only loading history on active channel --- osu.Game/Online/Chat/ChannelManager.cs | 12 ++++++------ osu.Game/Overlays/Chat/DrawableChannel.cs | 8 ++++++++ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index 822f628dd2..53872ddcba 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -93,6 +93,12 @@ namespace osu.Game.Online.Chat { if (!(e.NewValue is ChannelSelectorTabItem.ChannelSelectorTabChannel)) JoinChannel(e.NewValue); + + if (e.NewValue?.MessagesLoaded == false) + { + // let's fetch a small number of messages to bring us up-to-date with the backlog. + fetchInitalMessages(e.NewValue); + } } /// @@ -375,12 +381,6 @@ namespace osu.Game.Online.Chat if (CurrentChannel.Value == null) CurrentChannel.Value = channel; - if (!channel.MessagesLoaded) - { - // let's fetch a small number of messages to bring us up-to-date with the backlog. - fetchInitalMessages(channel); - } - return channel; } diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index 6019657cf0..d63faebae4 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -105,6 +105,14 @@ namespace osu.Game.Overlays.Chat private void newMessagesArrived(IEnumerable newMessages) { + if (newMessages.Min(m => m.Id) < chatLines.Max(c => c.Message.Id)) + { + // there is a case (on initial population) that we may receive past messages and need to reorder. + // easiest way is to just combine messages and recreate drawables (less worrying about day separators etc.) + newMessages = newMessages.Concat(chatLines.Select(c => c.Message)).OrderBy(m => m.Id).ToList(); + ChatLineFlow.Clear(); + } + bool shouldScrollToEnd = scroll.IsScrolledToEnd(10) || !chatLines.Any() || newMessages.Any(m => m is LocalMessage); // Add up to last Channel.MAX_HISTORY messages From d1ec99ffd9c790bf57f10a9592bd9e06b7b4df53 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Apr 2020 16:51:22 +0900 Subject: [PATCH 1380/2376] Further improve beatmap carousel load performance by avoiding incorrect query construction --- osu.Game/Beatmaps/BeatmapManager.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 5651d07566..b8dfac0342 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -300,7 +300,7 @@ namespace osu.Game.Beatmaps /// /// The level of detail to include in the returned objects. /// A list of available . - public IQueryable GetAllUsableBeatmapSetsEnumerable(IncludedDetails includes) + public IEnumerable GetAllUsableBeatmapSetsEnumerable(IncludedDetails includes) { IQueryable queryable; @@ -319,7 +319,10 @@ namespace osu.Game.Beatmaps break; } - return queryable.Where(s => !s.DeletePending && !s.Protected); + // 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 && !s.Protected); } /// public class SimpleUpdateManager : UpdateManager { + public override bool CanPerformUpdate => true; + private string version; [Resolved] diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs index f628bde324..f8c8bfe967 100644 --- a/osu.Game/Updater/UpdateManager.cs +++ b/osu.Game/Updater/UpdateManager.cs @@ -17,6 +17,8 @@ namespace osu.Game.Updater /// From 4a101ca7151cbb1bc75bb0ce5143cc863e7aa265 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Wed, 29 Apr 2020 10:46:32 +0200 Subject: [PATCH 1381/2376] Revert project config file change --- .idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml b/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml index 4bb9f4d2a0..7515e76054 100644 --- a/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml +++ b/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml @@ -1,6 +1,6 @@ - \ No newline at end of file From 48733a7e2f5c4d0d02666950e5d61fe04d435f25 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Apr 2020 17:53:25 +0900 Subject: [PATCH 1382/2376] Change taiko hit explosion animation to match stable for skins --- .../Skinning/LegacyHitExplosion.cs | 10 ++++++++-- .../UI/DefaultHitExplosion.cs | 3 +++ osu.Game.Rulesets.Taiko/UI/HitExplosion.cs | 16 +++++++--------- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs index d29b574866..42d4a34b9d 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs @@ -22,8 +22,14 @@ namespace osu.Game.Rulesets.Taiko.Skinning { base.LoadComplete(); - this.FadeIn(120); - this.ScaleTo(0.6f).Then().ScaleTo(1, 240, Easing.OutElastic); + const double animation_time = 120; + + this.FadeInFromZero(animation_time).Then().FadeOut(animation_time * 1.5); + + this.ScaleTo(0.6f) + .Then().ScaleTo(1.1f, animation_time * 0.8) + .Then().ScaleTo(0.9f, animation_time * 0.4) + .Then().ScaleTo(1f, animation_time * 0.2); } } } diff --git a/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs index aa444d0494..a0ca5f1c39 100644 --- a/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs @@ -49,6 +49,9 @@ namespace osu.Game.Rulesets.Taiko.UI base.LoadComplete(); this.ScaleTo(3f, 1000, Easing.OutQuint); + this.FadeOut(500); + + Expire(true); } } } diff --git a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs index 35a54d6ea7..f0585b9c50 100644 --- a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs @@ -23,6 +23,12 @@ namespace osu.Game.Rulesets.Taiko.UI [Cached(typeof(DrawableHitObject))] public readonly DrawableHitObject JudgedObject; + private SkinnableDrawable skinnable; + + public override double LifetimeStart => skinnable.Drawable.LifetimeStart; + + public override double LifetimeEnd => skinnable.Drawable.LifetimeEnd; + public HitExplosion(DrawableHitObject judgedObject) { JudgedObject = judgedObject; @@ -39,7 +45,7 @@ namespace osu.Game.Rulesets.Taiko.UI [BackgroundDependencyLoader] private void load() { - Child = new SkinnableDrawable(new TaikoSkinComponent(getComponentName(JudgedObject.Result?.Type ?? HitResult.Great)), _ => new DefaultHitExplosion()); + Child = skinnable = new SkinnableDrawable(new TaikoSkinComponent(getComponentName(JudgedObject.Result?.Type ?? HitResult.Great)), _ => new DefaultHitExplosion()); } private TaikoSkinComponents getComponentName(HitResult resultType) @@ -59,14 +65,6 @@ namespace osu.Game.Rulesets.Taiko.UI throw new ArgumentOutOfRangeException(nameof(resultType), "Invalid result type"); } - protected override void LoadComplete() - { - base.LoadComplete(); - - this.FadeOut(500); - Expire(true); - } - /// /// Transforms this hit explosion to visualise a secondary hit. /// From 511f7aeb28084553d2d60fb035aa4c296f856df8 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Wed, 29 Apr 2020 10:55:39 +0200 Subject: [PATCH 1383/2376] Remove rider plugin config --- .idea/.idea.osu.Desktop/.idea/discord.xml | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 .idea/.idea.osu.Desktop/.idea/discord.xml diff --git a/.idea/.idea.osu.Desktop/.idea/discord.xml b/.idea/.idea.osu.Desktop/.idea/discord.xml deleted file mode 100644 index 59b11d1d39..0000000000 --- a/.idea/.idea.osu.Desktop/.idea/discord.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - \ No newline at end of file From 43e768240f8a1dcba8179efef6dcd09c8f150934 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Wed, 29 Apr 2020 10:57:07 +0200 Subject: [PATCH 1384/2376] Revert "Revert project config file change" This reverts commit 4a101ca7151cbb1bc75bb0ce5143cc863e7aa265. --- .idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml b/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml index 7515e76054..4bb9f4d2a0 100644 --- a/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml +++ b/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml @@ -1,6 +1,6 @@ - \ No newline at end of file From 6e2ed0c4f3f4389e32e84d548262b5037daa015e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 29 Apr 2020 20:28:46 +0200 Subject: [PATCH 1385/2376] Refactor mascot to only contain state transitions --- .../Skinning/TestSceneDrawableTaikoMascot.cs | 47 ++++---- .../UI/DrawableTaikoMascot.cs | 109 +++++++----------- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 8 +- 3 files changed, 67 insertions(+), 97 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs index 2966c90b5e..f37c723a36 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; @@ -41,26 +42,25 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning [Test] public void TestStateTextures() { - AddStep("Set beatmap", () => setBeatmap()); + AddStep("set beatmap", () => setBeatmap()); - AddStep("Create mascot (idle)", () => + AddStep("create mascot", () => { SetContents(() => new TestDrawableTaikoMascot()); }); - AddStep("Clear state", () => setState(TaikoMascotAnimationState.Clear)); - - AddStep("Kiai state", () => setState(TaikoMascotAnimationState.Kiai)); - - AddStep("Fail state", () => setState(TaikoMascotAnimationState.Fail)); + AddStep("clear state", () => setState(TaikoMascotAnimationState.Clear)); + AddStep("kiai state", () => setState(TaikoMascotAnimationState.Kiai)); + AddStep("fail state", () => setState(TaikoMascotAnimationState.Fail)); + AddStep("idle state", () => setState(TaikoMascotAnimationState.Idle)); } [Test] public void TestPlayfield() { - AddStep("Set beatmap", () => setBeatmap()); + AddStep("set beatmap", () => setBeatmap()); - AddStep("Create ruleset", () => + AddStep("create drawable ruleset", () => { SetContents(() => { @@ -69,21 +69,21 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning }); }); - AddStep("Create hit (great)", () => addJudgement(HitResult.Miss)); - AddUntilStep("Wait for idle state", () => checkForState(TaikoMascotAnimationState.Fail)); + AddStep("new judgement (miss)", () => addJudgement(HitResult.Miss)); + AddUntilStep("wait for fail state", () => assertState(TaikoMascotAnimationState.Fail)); - AddStep("Create hit (great)", () => addJudgement(HitResult.Great)); - AddUntilStep("Wait for idle state", () => checkForState(TaikoMascotAnimationState.Idle)); + AddStep("new judgement (great)", () => addJudgement(HitResult.Great)); + AddUntilStep("wait for idle state", () => assertState(TaikoMascotAnimationState.Idle)); } [Test] public void TestKiai() { - AddStep("Set beatmap", () => setBeatmap(true)); + AddStep("set beatmap", () => setBeatmap(true)); - AddUntilStep("Wait for beatmap to be loaded", () => Beatmap.Value.Track.IsLoaded); + AddUntilStep("wait for beatmap to be loaded", () => Beatmap.Value.Track.IsLoaded); - AddStep("Create kiai ruleset", () => + AddStep("create drawable ruleset", () => { Beatmap.Value.Track.Start(); @@ -94,10 +94,10 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning }); }); - AddUntilStep("Wait for idle state", () => checkForState(TaikoMascotAnimationState.Fail)); + AddUntilStep("wait for fail state", () => assertState(TaikoMascotAnimationState.Fail)); - AddStep("Create hit (great)", () => addJudgement(HitResult.Great)); - AddUntilStep("Wait for kiai state", () => checkForState(TaikoMascotAnimationState.Kiai)); + AddStep("new judgement (great)", () => addJudgement(HitResult.Great)); + AddUntilStep("wait for kiai state", () => assertState(TaikoMascotAnimationState.Kiai)); } private void setBeatmap(bool kiai = false) @@ -129,7 +129,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning private void setState(TaikoMascotAnimationState state) { foreach (var mascot in mascots) - mascot?.ShowState(state); + mascot.State.Value = state; } private void addJudgement(HitResult result) @@ -143,7 +143,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning } } - private bool checkForState(TaikoMascotAnimationState state) => mascots.All(d => d.State == state); + private bool assertState(TaikoMascotAnimationState state) => mascots.All(d => d.State.Value == state); private class TestDrawableTaikoMascot : DrawableTaikoMascot { @@ -152,10 +152,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning { } - protected override TaikoMascotAnimationState GetFinalAnimationState(EffectControlPoint effectPoint, TaikoMascotAnimationState playfieldState) - { - return State; - } + public new Bindable State => base.State; } } } diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs index 7c4dfe2da7..be744de5f4 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs @@ -1,29 +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; +using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Audio.Track; +using osu.Framework.Bindables; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Textures; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics.Containers; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Taiko.UI { public class DrawableTaikoMascot : BeatSyncedContainer { - private TaikoMascotTextureAnimation idleDrawable, clearDrawable, kiaiDrawable, failDrawable; - private EffectControlPoint lastEffectControlPoint; - private TaikoMascotAnimationState playfieldState; + protected Bindable State { get; } - public TaikoMascotAnimationState State { get; private set; } + private readonly Dictionary animations; + private Drawable currentAnimation; + + private bool lastHitMissed; + private bool kiaiMode; public DrawableTaikoMascot(TaikoMascotAnimationState startingState = TaikoMascotAnimationState.Idle) { RelativeSizeAxes = Axes.Both; - State = startingState; + State = new Bindable(startingState); + animations = new Dictionary(); } [BackgroundDependencyLoader] @@ -31,81 +38,53 @@ namespace osu.Game.Rulesets.Taiko.UI { InternalChildren = new[] { - idleDrawable = new TaikoMascotTextureAnimation(TaikoMascotAnimationState.Idle), - clearDrawable = new TaikoMascotTextureAnimation(TaikoMascotAnimationState.Clear), - kiaiDrawable = new TaikoMascotTextureAnimation(TaikoMascotAnimationState.Kiai), - failDrawable = new TaikoMascotTextureAnimation(TaikoMascotAnimationState.Fail), + animations[TaikoMascotAnimationState.Idle] = new TaikoMascotTextureAnimation(TaikoMascotAnimationState.Idle), + animations[TaikoMascotAnimationState.Clear] = new TaikoMascotTextureAnimation(TaikoMascotAnimationState.Clear), + animations[TaikoMascotAnimationState.Kiai] = new TaikoMascotTextureAnimation(TaikoMascotAnimationState.Kiai), + animations[TaikoMascotAnimationState.Fail] = new TaikoMascotTextureAnimation(TaikoMascotAnimationState.Fail), }; - ShowState(State); + updateState(); } - public void ShowState(TaikoMascotAnimationState state) + protected override void LoadComplete() { - foreach (var child in InternalChildren) - child.Hide(); + base.LoadComplete(); - State = state; - - var drawable = getStateDrawable(State); - drawable.Show(); + animations.Values.ForEach(animation => animation.Hide()); + State.BindValueChanged(mascotStateChanged, true); } - /// - /// Sets the playfield state used for determining the final state. - /// - /// - /// If you're looking to change the state manually, please look at . - /// - public void SetPlayfieldState(TaikoMascotAnimationState state) + public void OnNewResult(JudgementResult result) { - playfieldState = state; - - if (lastEffectControlPoint != null) - ShowState(GetFinalAnimationState(lastEffectControlPoint, playfieldState)); - } - - private TaikoMascotTextureAnimation getStateDrawable(TaikoMascotAnimationState state) - { - switch (state) - { - case TaikoMascotAnimationState.Idle: - return idleDrawable; - - case TaikoMascotAnimationState.Clear: - return clearDrawable; - - case TaikoMascotAnimationState.Kiai: - return kiaiDrawable; - - case TaikoMascotAnimationState.Fail: - return failDrawable; - - default: - throw new ArgumentOutOfRangeException(nameof(state), $"There's no animation available for state {state}"); - } - } - - protected virtual TaikoMascotAnimationState GetFinalAnimationState(EffectControlPoint effectPoint, TaikoMascotAnimationState playfieldState) - { - if (playfieldState == TaikoMascotAnimationState.Fail) - return playfieldState; - - return effectPoint.KiaiMode ? TaikoMascotAnimationState.Kiai : TaikoMascotAnimationState.Idle; + lastHitMissed = result.Type == HitResult.Miss && result.Judgement.AffectsCombo; + updateState(); } protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) { - base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); + kiaiMode = effectPoint.KiaiMode; + updateState(); + } - var state = GetFinalAnimationState(lastEffectControlPoint = effectPoint, playfieldState); - ShowState(state); + private void updateState() + { + State.Value = getNextState(); + } - if (state == TaikoMascotAnimationState.Clear) - return; + private TaikoMascotAnimationState getNextState() + { + if (lastHitMissed) + return TaikoMascotAnimationState.Fail; - var drawable = getStateDrawable(state); - drawable.Move(); + return kiaiMode ? TaikoMascotAnimationState.Kiai : TaikoMascotAnimationState.Idle; + } + + private void mascotStateChanged(ValueChangedEvent state) + { + currentAnimation?.Hide(); + currentAnimation = animations[state.NewValue]; + currentAnimation.Show(); } } } diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index c6e867a5d9..084a11d523 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -15,7 +15,6 @@ using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.Taiko.Judgements; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Skinning; -using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Taiko.UI { @@ -216,12 +215,7 @@ namespace osu.Game.Rulesets.Taiko.UI if (mascotDrawable.Drawable is DrawableTaikoMascot mascot) { - var miss = result.Type == HitResult.Miss; - - if (miss && judgedObject.HitObject is StrongHitObject) - miss = result.Judgement.AffectsCombo; - - mascot.SetPlayfieldState(miss ? TaikoMascotAnimationState.Fail : TaikoMascotAnimationState.Idle); + mascot.OnNewResult(result); } } From e81d33dcec78ce816fc823214ea99a0e524c69da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 29 Apr 2020 21:27:02 +0200 Subject: [PATCH 1386/2376] Refactor mascot animations to split logic paths --- .../Skinning/TestSceneDrawableTaikoMascot.cs | 35 ++---- .../UI/DrawableTaikoMascot.cs | 19 +-- .../UI/TaikoMascotAnimation.cs | 116 ++++++++++++++++++ .../UI/TaikoMascotTextureAnimation.cs | 109 ---------------- 4 files changed, 133 insertions(+), 146 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs delete mode 100644 osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs index f37c723a36..28065c401c 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; @@ -27,6 +26,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] { typeof(DrawableTaikoMascot), + typeof(TaikoMascotAnimation) }).ToList(); [Cached(typeof(IScrollingInfo))] @@ -36,23 +36,18 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning TimeRange = { Value = 5000 }, }; - private IEnumerable mascots => this.ChildrenOfType(); + private IEnumerable mascots => this.ChildrenOfType(); private IEnumerable playfields => this.ChildrenOfType(); [Test] - public void TestStateTextures() + public void TestStateAnimations() { AddStep("set beatmap", () => setBeatmap()); - AddStep("create mascot", () => - { - SetContents(() => new TestDrawableTaikoMascot()); - }); - - AddStep("clear state", () => setState(TaikoMascotAnimationState.Clear)); - AddStep("kiai state", () => setState(TaikoMascotAnimationState.Kiai)); - AddStep("fail state", () => setState(TaikoMascotAnimationState.Fail)); - AddStep("idle state", () => setState(TaikoMascotAnimationState.Idle)); + AddStep("clear state", () => SetContents(() => new TaikoMascotAnimation(TaikoMascotAnimationState.Clear))); + AddStep("idle state", () => SetContents(() => new TaikoMascotAnimation(TaikoMascotAnimationState.Idle))); + AddStep("kiai state", () => SetContents(() => new TaikoMascotAnimation(TaikoMascotAnimationState.Kiai))); + AddStep("fail state", () => SetContents(() => new TaikoMascotAnimation(TaikoMascotAnimationState.Fail))); } [Test] @@ -126,12 +121,6 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning }); } - private void setState(TaikoMascotAnimationState state) - { - foreach (var mascot in mascots) - mascot.State.Value = state; - } - private void addJudgement(HitResult result) { foreach (var playfield in playfields) @@ -144,15 +133,5 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning } private bool assertState(TaikoMascotAnimationState state) => mascots.All(d => d.State.Value == state); - - private class TestDrawableTaikoMascot : DrawableTaikoMascot - { - public TestDrawableTaikoMascot(TaikoMascotAnimationState startingState = TaikoMascotAnimationState.Idle) - : base(startingState) - { - } - - public new Bindable State => base.State; - } } } diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs index be744de5f4..bfc1d958c2 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs @@ -17,9 +17,10 @@ namespace osu.Game.Rulesets.Taiko.UI { public class DrawableTaikoMascot : BeatSyncedContainer { - protected Bindable State { get; } + public IBindable State => state; - private readonly Dictionary animations; + private readonly Bindable state; + private readonly Dictionary animations; private Drawable currentAnimation; private bool lastHitMissed; @@ -29,8 +30,8 @@ namespace osu.Game.Rulesets.Taiko.UI { RelativeSizeAxes = Axes.Both; - State = new Bindable(startingState); - animations = new Dictionary(); + state = new Bindable(startingState); + animations = new Dictionary(); } [BackgroundDependencyLoader] @@ -38,10 +39,10 @@ namespace osu.Game.Rulesets.Taiko.UI { InternalChildren = new[] { - animations[TaikoMascotAnimationState.Idle] = new TaikoMascotTextureAnimation(TaikoMascotAnimationState.Idle), - animations[TaikoMascotAnimationState.Clear] = new TaikoMascotTextureAnimation(TaikoMascotAnimationState.Clear), - animations[TaikoMascotAnimationState.Kiai] = new TaikoMascotTextureAnimation(TaikoMascotAnimationState.Kiai), - animations[TaikoMascotAnimationState.Fail] = new TaikoMascotTextureAnimation(TaikoMascotAnimationState.Fail), + animations[TaikoMascotAnimationState.Idle] = new TaikoMascotAnimation(TaikoMascotAnimationState.Idle), + animations[TaikoMascotAnimationState.Clear] = new TaikoMascotAnimation(TaikoMascotAnimationState.Clear), + animations[TaikoMascotAnimationState.Kiai] = new TaikoMascotAnimation(TaikoMascotAnimationState.Kiai), + animations[TaikoMascotAnimationState.Fail] = new TaikoMascotAnimation(TaikoMascotAnimationState.Fail), }; updateState(); @@ -69,7 +70,7 @@ namespace osu.Game.Rulesets.Taiko.UI private void updateState() { - State.Value = getNextState(); + state.Value = getNextState(); } private TaikoMascotAnimationState getNextState() diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs b/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs new file mode 100644 index 0000000000..1e289c1a74 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs @@ -0,0 +1,116 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Audio.Track; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Animations; +using osu.Framework.Graphics.Textures; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics.Containers; +using osu.Game.Skinning; + +namespace osu.Game.Rulesets.Taiko.UI +{ + public sealed class TaikoMascotAnimation : BeatSyncedContainer + { + private readonly TextureAnimation textureAnimation; + + private int currentFrame; + + public TaikoMascotAnimation(TaikoMascotAnimationState state) + { + InternalChild = textureAnimation = createTextureAnimation(state).With(animation => + { + animation.Origin = animation.Anchor = Anchor.BottomLeft; + RelativeSizeAxes = Axes.Both; + }); + + RelativeSizeAxes = Axes.Both; + Origin = Anchor = Anchor.BottomLeft; + } + + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) + { + // assume that if the animation is playing on its own, it's independent from the beat and doesn't need to be touched. + if (textureAnimation.FrameCount == 0 || textureAnimation.IsPlaying) + return; + + textureAnimation.GotoFrame(currentFrame); + currentFrame = (currentFrame + 1) % textureAnimation.FrameCount; + } + + private static TextureAnimation createTextureAnimation(TaikoMascotAnimationState state) + { + switch (state) + { + case TaikoMascotAnimationState.Clear: + return new ClearMascotTextureAnimation(); + + case TaikoMascotAnimationState.Idle: + case TaikoMascotAnimationState.Kiai: + case TaikoMascotAnimationState.Fail: + return new ManualMascotTextureAnimation(state); + + default: + throw new ArgumentOutOfRangeException(nameof(state), $"Mascot animations for state {state} are not supported"); + } + } + + private class ManualMascotTextureAnimation : TextureAnimation + { + private readonly TaikoMascotAnimationState state; + + public ManualMascotTextureAnimation(TaikoMascotAnimationState state) + { + this.state = state; + + IsPlaying = false; + } + + [BackgroundDependencyLoader] + private void load(ISkinSource skin) + { + for (int frameIndex = 0; true; frameIndex++) + { + var texture = getAnimationFrame(skin, state, frameIndex); + + if (texture == null) + break; + + AddFrame(texture); + } + } + } + + private class ClearMascotTextureAnimation : TextureAnimation + { + private const float clear_animation_speed = 1000 / 10f; + + private static readonly int[] clear_animation_sequence = { 0, 1, 2, 3, 4, 5, 6, 5, 6, 5, 4, 3, 2, 1, 0 }; + + public ClearMascotTextureAnimation() + { + DefaultFrameLength = clear_animation_speed; + } + + [BackgroundDependencyLoader] + private void load(ISkinSource skin) + { + foreach (var frameIndex in clear_animation_sequence) + { + var texture = getAnimationFrame(skin, TaikoMascotAnimationState.Clear, frameIndex); + + if (texture == null) + continue; + + AddFrame(texture); + } + } + } + + private static Texture getAnimationFrame(ISkinSource skin, TaikoMascotAnimationState state, int frameIndex) + => skin.GetTexture($"pippidon{state.ToString().ToLower()}{frameIndex}"); + } +} diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs b/osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs deleted file mode 100644 index 080d30c3f2..0000000000 --- a/osu.Game.Rulesets.Taiko/UI/TaikoMascotTextureAnimation.cs +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Animations; -using osu.Game.Skinning; - -namespace osu.Game.Rulesets.Taiko.UI -{ - public sealed class TaikoMascotTextureAnimation : TextureAnimation - { - private const float clear_animation_speed = 1000 / 10f; - private static readonly int[] clear_animation_sequence = { 0, 1, 2, 3, 4, 5, 6, 5, 6, 5, 4, 3, 2, 1, 0 }; - private int currentFrame; - - public TaikoMascotAnimationState State { get; } - - public TaikoMascotTextureAnimation(TaikoMascotAnimationState state) - : base(true) - { - State = state; - - // We're animating on beat if it's not the clear animation - if (state == TaikoMascotAnimationState.Clear) - DefaultFrameLength = clear_animation_speed; - else - this.Stop(); - - Origin = Anchor.BottomLeft; - Anchor = Anchor.BottomLeft; - } - - [BackgroundDependencyLoader] - private void load(ISkinSource skin) - { - if (State == TaikoMascotAnimationState.Clear) - { - foreach (var textureIndex in clear_animation_sequence) - { - if (!addFrame(skin, textureIndex)) - break; - } - } - else - { - for (int i = 0; true; i++) - { - if (!addFrame(skin, i)) - break; - } - } - } - - private bool addFrame(ISkinSource skin, int textureIndex) - { - var textureName = getStateTextureName(textureIndex); - var texture = skin.GetTexture(textureName); - - if (texture == null) - return false; - - AddFrame(texture); - - return true; - } - - /// - /// Advances the current frame by one. - /// - public void Move() - { - // Check whether there are frames before causing a crash. - if (FrameCount == 0) - return; - - if (currentFrame >= FrameCount) - currentFrame = 0; - - GotoFrame(currentFrame); - - currentFrame += 1; - } - - private string getStateTextureName(int i) => $"pippidon{getStateString(State)}{i}"; - - private string getStateString(TaikoMascotAnimationState state) - { - switch (state) - { - case TaikoMascotAnimationState.Clear: - return "clear"; - - case TaikoMascotAnimationState.Fail: - return "fail"; - - case TaikoMascotAnimationState.Idle: - return "idle"; - - case TaikoMascotAnimationState.Kiai: - return "kiai"; - - default: - throw new ArgumentOutOfRangeException(nameof(state), $"There's no case for animation state {state} available"); - } - } - } -} From 9d6720e7e6b11f327b56c6135c46bd6711f5580b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 29 Apr 2020 21:30:13 +0200 Subject: [PATCH 1387/2376] Scope up parameter --- osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs b/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs index 1e289c1a74..b9af8f0106 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs @@ -110,7 +110,7 @@ namespace osu.Game.Rulesets.Taiko.UI } } - private static Texture getAnimationFrame(ISkinSource skin, TaikoMascotAnimationState state, int frameIndex) + private static Texture getAnimationFrame(ISkin skin, TaikoMascotAnimationState state, int frameIndex) => skin.GetTexture($"pippidon{state.ToString().ToLower()}{frameIndex}"); } } From 47b040b7d8888283497858e4be2734bc2a6c5341 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 29 Apr 2020 21:42:28 +0200 Subject: [PATCH 1388/2376] Cover strong hit miss exemption in tests --- .../Skinning/TestSceneDrawableTaikoMascot.cs | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs index 28065c401c..81ea9c0755 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs @@ -64,11 +64,14 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning }); }); - AddStep("new judgement (miss)", () => addJudgement(HitResult.Miss)); - AddUntilStep("wait for fail state", () => assertState(TaikoMascotAnimationState.Fail)); + AddStep("miss result for normal hit", () => addJudgement(HitResult.Miss, new TaikoJudgement())); + AddUntilStep("state is fail", () => assertState(TaikoMascotAnimationState.Fail)); - AddStep("new judgement (great)", () => addJudgement(HitResult.Great)); - AddUntilStep("wait for idle state", () => assertState(TaikoMascotAnimationState.Idle)); + AddStep("great result for normal hit", () => addJudgement(HitResult.Great, new TaikoJudgement())); + AddUntilStep("state is idle", () => assertState(TaikoMascotAnimationState.Idle)); + + AddStep("miss result for strong hit", () => addJudgement(HitResult.Miss, new TaikoStrongJudgement())); + AddAssert("state remains idle", () => assertState(TaikoMascotAnimationState.Idle)); } [Test] @@ -89,10 +92,10 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning }); }); - AddUntilStep("wait for fail state", () => assertState(TaikoMascotAnimationState.Fail)); + AddUntilStep("state is fail", () => assertState(TaikoMascotAnimationState.Fail)); - AddStep("new judgement (great)", () => addJudgement(HitResult.Great)); - AddUntilStep("wait for kiai state", () => assertState(TaikoMascotAnimationState.Kiai)); + AddStep("great result for normal hit", () => addJudgement(HitResult.Great, new TaikoJudgement())); + AddUntilStep("state is kiai", () => assertState(TaikoMascotAnimationState.Kiai)); } private void setBeatmap(bool kiai = false) @@ -121,14 +124,14 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning }); } - private void addJudgement(HitResult result) + private void addJudgement(HitResult result, Judgement judgement) { foreach (var playfield in playfields) { var hit = new DrawableTestHit(new Hit(), result); Add(hit); - playfield.OnNewResult(hit, new JudgementResult(hit.HitObject, new TaikoJudgement()) { Type = result }); + playfield.OnNewResult(hit, new JudgementResult(hit.HitObject, judgement) { Type = result }); } } From 0d917ca339fbc6783587913a7190eab9733add91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 29 Apr 2020 21:52:09 +0200 Subject: [PATCH 1389/2376] Ensure correct behaviour for clear animation --- osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs b/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs index b9af8f0106..ee1389147d 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs @@ -103,7 +103,8 @@ namespace osu.Game.Rulesets.Taiko.UI var texture = getAnimationFrame(skin, TaikoMascotAnimationState.Clear, frameIndex); if (texture == null) - continue; + // as per https://osu.ppy.sh/help/wiki/Skinning/osu!taiko#pippidon + break; AddFrame(texture); } From b0e97793b6bea128746817734051055dcb484dfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 30 Apr 2020 00:14:27 +0200 Subject: [PATCH 1390/2376] Implement transitions into and from clear state --- .../Skinning/TestSceneDrawableTaikoMascot.cs | 40 ++++++++++++++++--- .../UI/DrawableTaikoMascot.cs | 16 ++++---- .../UI/TaikoMascotAnimation.cs | 10 +++++ 3 files changed, 53 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs index 81ea9c0755..0018899769 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs @@ -6,6 +6,8 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; @@ -50,6 +52,31 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning AddStep("fail state", () => SetContents(() => new TaikoMascotAnimation(TaikoMascotAnimationState.Fail))); } + [Test] + public void TestClearStateTransition() + { + AddStep("set beatmap", () => setBeatmap()); + + // the bindables need to be independent for each content cell to prevent interference, + // as if some of the skins don't implement the animation they'll immediately revert to the previous state from the clear state. + var states = new List>(); + + AddStep("create mascot", () => SetContents(() => + { + var state = new Bindable(TaikoMascotAnimationState.Clear); + states.Add(state); + return new DrawableTaikoMascot { State = { BindTarget = state } }; + })); + + AddStep("set clear state", () => states.ForEach(state => state.Value = TaikoMascotAnimationState.Clear)); + AddStep("miss", () => mascots.ForEach(mascot => mascot.OnNewResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Miss }))); + AddAssert("skins with animations remain in clear state", () => mascots.Any(mascot => mascot.State.Value == TaikoMascotAnimationState.Clear)); + AddUntilStep("state reverts to fail", () => someMascotsIn(TaikoMascotAnimationState.Fail)); + + AddStep("set clear state again", () => states.ForEach(state => state.Value = TaikoMascotAnimationState.Clear)); + AddAssert("skins with animations change to clear", () => someMascotsIn(TaikoMascotAnimationState.Clear)); + } + [Test] public void TestPlayfield() { @@ -65,13 +92,13 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning }); AddStep("miss result for normal hit", () => addJudgement(HitResult.Miss, new TaikoJudgement())); - AddUntilStep("state is fail", () => assertState(TaikoMascotAnimationState.Fail)); + AddUntilStep("state is fail", () => allMascotsIn(TaikoMascotAnimationState.Fail)); AddStep("great result for normal hit", () => addJudgement(HitResult.Great, new TaikoJudgement())); - AddUntilStep("state is idle", () => assertState(TaikoMascotAnimationState.Idle)); + AddUntilStep("state is idle", () => allMascotsIn(TaikoMascotAnimationState.Idle)); AddStep("miss result for strong hit", () => addJudgement(HitResult.Miss, new TaikoStrongJudgement())); - AddAssert("state remains idle", () => assertState(TaikoMascotAnimationState.Idle)); + AddAssert("state remains idle", () => allMascotsIn(TaikoMascotAnimationState.Idle)); } [Test] @@ -92,10 +119,10 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning }); }); - AddUntilStep("state is fail", () => assertState(TaikoMascotAnimationState.Fail)); + AddUntilStep("state is fail", () => allMascotsIn(TaikoMascotAnimationState.Fail)); AddStep("great result for normal hit", () => addJudgement(HitResult.Great, new TaikoJudgement())); - AddUntilStep("state is kiai", () => assertState(TaikoMascotAnimationState.Kiai)); + AddUntilStep("state is kiai", () => allMascotsIn(TaikoMascotAnimationState.Kiai)); } private void setBeatmap(bool kiai = false) @@ -135,6 +162,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning } } - private bool assertState(TaikoMascotAnimationState state) => mascots.All(d => d.State.Value == state); + private bool allMascotsIn(TaikoMascotAnimationState state) => mascots.All(d => d.State.Value == state); + private bool someMascotsIn(TaikoMascotAnimationState state) => mascots.Any(d => d.State.Value == state); } } diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs index bfc1d958c2..f4bc841c15 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Taiko.UI private readonly Bindable state; private readonly Dictionary animations; - private Drawable currentAnimation; + private TaikoMascotAnimation currentAnimation; private bool lastHitMissed; private bool kiaiMode; @@ -44,8 +44,6 @@ namespace osu.Game.Rulesets.Taiko.UI animations[TaikoMascotAnimationState.Kiai] = new TaikoMascotAnimation(TaikoMascotAnimationState.Kiai), animations[TaikoMascotAnimationState.Fail] = new TaikoMascotAnimation(TaikoMascotAnimationState.Fail), }; - - updateState(); } protected override void LoadComplete() @@ -53,28 +51,32 @@ namespace osu.Game.Rulesets.Taiko.UI base.LoadComplete(); animations.Values.ForEach(animation => animation.Hide()); - State.BindValueChanged(mascotStateChanged, true); + state.BindValueChanged(mascotStateChanged, true); } public void OnNewResult(JudgementResult result) { lastHitMissed = result.Type == HitResult.Miss && result.Judgement.AffectsCombo; - updateState(); } protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) { kiaiMode = effectPoint.KiaiMode; - updateState(); } - private void updateState() + protected override void Update() { + base.Update(); state.Value = getNextState(); } private TaikoMascotAnimationState getNextState() { + // don't change state if current animation is playing + // (used for clear state - others are manually animated on new beats) + if (currentAnimation != null && !currentAnimation.Completed) + return state.Value; + if (lastHitMissed) return TaikoMascotAnimationState.Fail; diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs b/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs index ee1389147d..165e00cc73 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs @@ -29,6 +29,15 @@ namespace osu.Game.Rulesets.Taiko.UI RelativeSizeAxes = Axes.Both; Origin = Anchor = Anchor.BottomLeft; + AlwaysPresent = true; + } + + public bool Completed => !textureAnimation.IsPlaying || textureAnimation.PlaybackPosition >= textureAnimation.Duration; + + public override void Show() + { + base.Show(); + textureAnimation.Seek(0); } protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) @@ -93,6 +102,7 @@ namespace osu.Game.Rulesets.Taiko.UI public ClearMascotTextureAnimation() { DefaultFrameLength = clear_animation_speed; + Loop = false; } [BackgroundDependencyLoader] From 783dc58ef0f600073ef115f4e4868902f96473ea Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Apr 2020 09:41:56 +0900 Subject: [PATCH 1391/2376] Move taiko additive blending locally to avoid applying to legacy skins --- osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs | 2 ++ osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs | 2 ++ osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 3 --- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs index 42d4a34b9d..c44da9ce1e 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs @@ -16,6 +16,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning Origin = Anchor.Centre; AutoSizeAxes = Axes.Both; + + Blending = BlendingParameters.Additive; } protected override void LoadComplete() diff --git a/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs index 3a307bb3bb..067d390894 100644 --- a/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs @@ -31,6 +31,8 @@ namespace osu.Game.Rulesets.Taiko.UI RelativeSizeAxes = Axes.Both; Size = new Vector2(TaikoHitObject.DEFAULT_SIZE, 1); + Blending = BlendingParameters.Additive; + Masking = true; Alpha = 0.25f; diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 6a78c0a1fb..5c763cb332 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -68,7 +68,6 @@ namespace osu.Game.Rulesets.Taiko.UI hitExplosionContainer = new Container { RelativeSizeAxes = Axes.Both, - Blending = BlendingParameters.Additive, }, HitTarget = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.HitTarget), _ => new TaikoHitTarget()) { @@ -100,13 +99,11 @@ namespace osu.Game.Rulesets.Taiko.UI Name = "Kiai hit explosions", RelativeSizeAxes = Axes.Both, FillMode = FillMode.Fit, - Blending = BlendingParameters.Additive }, judgementContainer = new JudgementContainer { Name = "Judgements", RelativeSizeAxes = Axes.Y, - Blending = BlendingParameters.Additive }, } }, From 49a98fde7374055a8b2aa5286b733ec3673f55ad Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Apr 2020 09:57:14 +0900 Subject: [PATCH 1392/2376] Move to non-legacy class --- osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs | 2 -- osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs index c44da9ce1e..42d4a34b9d 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs @@ -16,8 +16,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning Origin = Anchor.Centre; AutoSizeAxes = Axes.Both; - - Blending = BlendingParameters.Additive; } protected override void LoadComplete() diff --git a/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs index a0ca5f1c39..9943a58e3e 100644 --- a/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs @@ -26,6 +26,8 @@ namespace osu.Game.Rulesets.Taiko.UI BorderColour = Color4.White; BorderThickness = 1; + Blending = BlendingParameters.Additive; + Alpha = 0.15f; Masking = true; From cf4e79cf38490ecb85f213b154dc7eef3ffb14ab Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Apr 2020 11:51:06 +0900 Subject: [PATCH 1393/2376] Show loading spinner when carousel is not ready to be displayed --- .../SongSelect/TestScenePlaySongSelect.cs | 1 + osu.Game/Screens/Select/SongSelect.cs | 33 +++++++++++-------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 39e04ed39a..aed8e19fb2 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -797,6 +797,7 @@ namespace osu.Game.Tests.Visual.SongSelect { AddStep("create song select", () => LoadScreen(songSelect = new TestSongSelect())); AddUntilStep("wait for present", () => songSelect.IsCurrentScreen()); + AddUntilStep("wait for carousel loaded", () => songSelect.Carousel.IsAlive); } private void addManyTestMaps() diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index c07465ca44..6b896694ea 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -34,6 +34,7 @@ using System.Linq; using System.Threading.Tasks; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; +using osu.Game.Graphics.UserInterface; using osu.Game.Scoring; namespace osu.Game.Screens.Select @@ -92,6 +93,8 @@ namespace osu.Game.Screens.Select private SampleChannel sampleChangeDifficulty; private SampleChannel sampleChangeBeatmap; + private Container carouselContainer; + protected BeatmapDetailArea BeatmapDetails { get; private set; } private readonly Bindable decoupledRuleset = new Bindable(); @@ -105,9 +108,22 @@ namespace osu.Game.Screens.Select // initial value transfer is required for FilterControl (it uses our re-cached bindables in its async load for the initial filter). transferRulesetValue(); + LoadComponentAsync(Carousel = new BeatmapCarousel + { + AllowSelection = false, // delay any selection until our bindables are ready to make a good choice. + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.Both, + BleedTop = FilterControl.HEIGHT, + BleedBottom = Footer.HEIGHT, + SelectionChanged = updateSelectedBeatmap, + BeatmapSetsChanged = carouselBeatmapsLoaded, + GetRecommendedBeatmap = (recommender = new DifficultyRecommender()).GetRecommendedBeatmap, + }, c => carouselContainer.Child = c); + AddRangeInternal(new Drawable[] { - recommender = new DifficultyRecommender(), + recommender, new ResetScrollContainer(() => Carousel.ScrollToSelected()) { RelativeSizeAxes = Axes.Y, @@ -139,7 +155,7 @@ namespace osu.Game.Screens.Select Padding = new MarginPadding { Right = -150 }, }, }, - new Container + carouselContainer = new Container { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding @@ -147,18 +163,7 @@ namespace osu.Game.Screens.Select Top = FilterControl.HEIGHT, Bottom = Footer.HEIGHT }, - Child = Carousel = new BeatmapCarousel - { - AllowSelection = false, // delay any selection until our bindables are ready to make a good choice. - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - RelativeSizeAxes = Axes.Both, - BleedTop = FilterControl.HEIGHT, - BleedBottom = Footer.HEIGHT, - SelectionChanged = updateSelectedBeatmap, - BeatmapSetsChanged = carouselBeatmapsLoaded, - GetRecommendedBeatmap = recommender.GetRecommendedBeatmap, - }, + Child = new LoadingSpinner(true) { State = { Value = Visibility.Visible } } } }, } From 21c6ac8c43725f8739dcaea3a2b6105e7feab142 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Apr 2020 12:12:28 +0900 Subject: [PATCH 1394/2376] Allow closing the game during intro --- osu.Game/OsuGame.cs | 5 +---- osu.Game/Screens/Menu/IntroScreen.cs | 2 -- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index f5f7d0cef4..8e62819c95 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -914,10 +914,7 @@ namespace osu.Game if (ScreenStack.CurrentScreen is Loader) return false; - if (introScreen == null) - return true; - - if (!introScreen.DidLoadMenu || !(ScreenStack.CurrentScreen is IntroScreen)) + if (introScreen.DidLoadMenu && !(ScreenStack.CurrentScreen is IntroScreen)) { Scheduler.Add(introScreen.MakeCurrent); return true; diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index d2296573a6..736202ee52 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -96,8 +96,6 @@ namespace osu.Game.Screens.Menu Track = introBeatmap.Track; } - public override bool OnExiting(IScreen next) => !DidLoadMenu; - public override void OnResuming(IScreen last) { this.FadeIn(300); From 48af4d4eb4fc3ef8abf757ab512211defddee949 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Apr 2020 16:18:15 +0900 Subject: [PATCH 1395/2376] Fix skinned taiko hit explosions not being removed on rewind --- osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs index 42d4a34b9d..b5ec2e8def 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs @@ -30,6 +30,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning .Then().ScaleTo(1.1f, animation_time * 0.8) .Then().ScaleTo(0.9f, animation_time * 0.4) .Then().ScaleTo(1f, animation_time * 0.2); + + Expire(true); } } } From d0a8c0fa71bfe372a535380d258eb259b7f60dd4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Apr 2020 16:42:38 +0900 Subject: [PATCH 1396/2376] Add kiai support to osu!taiko skinned playfields --- .../Skinning/TestSceneTaikoPlayfield.cs | 18 ++++++ .../TaikoLegacyPlayfieldBackgroundRight.cs | 57 +++++++++++++++++++ .../Skinning/TaikoLegacySkinTransformer.cs | 9 +-- osu.Game/Beatmaps/WorkingBeatmap.cs | 4 +- osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs | 4 ++ 5 files changed, 82 insertions(+), 10 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/Skinning/TaikoLegacyPlayfieldBackgroundRight.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs index 16b3c036a3..ae5dd1e622 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs @@ -9,6 +9,7 @@ using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Taiko.Beatmaps; using osu.Game.Rulesets.Taiko.Skinning; using osu.Game.Rulesets.Taiko.UI; using osu.Game.Rulesets.UI.Scrolling; @@ -34,6 +35,18 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning public TestSceneTaikoPlayfield() { + TaikoBeatmap beatmap; + bool kiai = false; + + AddStep("set beatmap", () => + { + Beatmap.Value = CreateWorkingBeatmap(beatmap = new TaikoBeatmap()); + + beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 }); + + Beatmap.Value.Track.Start(); + }); + AddStep("Load playfield", () => SetContents(() => new TaikoPlayfield(new ControlPointInfo()) { Anchor = Anchor.CentreLeft, @@ -41,6 +54,11 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning })); AddRepeatStep("change height", () => this.ChildrenOfType().ForEach(p => p.Height = Math.Max(0.2f, (p.Height + 0.2f) % 1f)), 50); + + AddStep("Toggle kiai", () => + { + Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new EffectControlPoint { KiaiMode = (kiai = !kiai) }); + }); } } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacyPlayfieldBackgroundRight.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacyPlayfieldBackgroundRight.cs new file mode 100644 index 0000000000..7508c75231 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacyPlayfieldBackgroundRight.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 osu.Framework.Allocation; +using osu.Framework.Audio.Track; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics.Containers; +using osu.Game.Skinning; +using osuTK; + +namespace osu.Game.Rulesets.Taiko.Skinning +{ + public class TaikoLegacyPlayfieldBackgroundRight : BeatSyncedContainer + { + private Sprite kiai; + + private bool kiaiDisplayed; + + [BackgroundDependencyLoader] + private void load(ISkinSource skin) + { + RelativeSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + new Sprite + { + Texture = skin.GetTexture("taiko-bar-right"), + RelativeSizeAxes = Axes.Both, + Size = Vector2.One, + }, + kiai = new Sprite + { + Texture = skin.GetTexture("taiko-bar-right-glow"), + RelativeSizeAxes = Axes.Both, + Size = Vector2.One, + Alpha = 0, + } + }; + } + + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) + { + base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); + + if (effectPoint.KiaiMode != kiaiDisplayed) + { + kiaiDisplayed = effectPoint.KiaiMode; + + kiai.ClearTransforms(); + kiai.FadeTo(kiaiDisplayed ? 1 : 0, 200); + } + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs index f0df612e18..5dfc7ec0df 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Textures; using osu.Game.Audio; using osu.Game.Skinning; -using osuTK; namespace osu.Game.Rulesets.Taiko.Skinning { @@ -60,13 +59,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning case TaikoSkinComponents.PlayfieldBackgroundRight: if (GetTexture("taiko-bar-right") != null) - { - return this.GetAnimation("taiko-bar-right", false, false).With(d => - { - d.RelativeSizeAxes = Axes.Both; - d.Size = Vector2.One; - }); - } + return new TaikoLegacyPlayfieldBackgroundRight(); return null; diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index f30340956a..d2804bdc05 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -197,7 +197,7 @@ namespace osu.Game.Beatmaps public override string ToString() => BeatmapInfo.ToString(); - public bool BeatmapLoaded => beatmapLoadTask?.IsCompleted ?? false; + public virtual bool BeatmapLoaded => beatmapLoadTask?.IsCompleted ?? false; public IBeatmap Beatmap { @@ -233,7 +233,7 @@ namespace osu.Game.Beatmaps protected abstract Texture GetBackground(); private readonly RecyclableLazy background; - public bool TrackLoaded => track.IsResultAvailable; + public virtual bool TrackLoaded => track.IsResultAvailable; public Track Track => track.Value; protected abstract Track GetTrack(); private RecyclableLazy track; diff --git a/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs b/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs index 8f8afb87d4..cdf9170701 100644 --- a/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs +++ b/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs @@ -27,6 +27,10 @@ namespace osu.Game.Tests.Beatmaps this.storyboard = storyboard; } + public override bool TrackLoaded => true; + + public override bool BeatmapLoaded => true; + protected override IBeatmap GetBeatmap() => beatmap; protected override Storyboard GetStoryboard() => storyboard ?? base.GetStoryboard(); From 9bec42bc7ee8e59a1e9faebdc9388350259a7212 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 30 Apr 2020 20:03:46 +0900 Subject: [PATCH 1397/2376] Fix mania crashing on undo/redo --- osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs b/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs index 17eba87076..45a0a5c485 100644 --- a/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs +++ b/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs @@ -84,7 +84,11 @@ namespace osu.Game.Screens.Edit { using (var stream = new MemoryStream(state)) using (var reader = new LineBufferedReader(stream, true)) - return new PassThroughWorkingBeatmap(Decoder.GetDecoder(reader).Decode(reader)).GetPlayableBeatmap(editorBeatmap.BeatmapInfo.Ruleset); + { + var decoded = Decoder.GetDecoder(reader).Decode(reader); + decoded.BeatmapInfo.Ruleset = editorBeatmap.BeatmapInfo.Ruleset; + return new PassThroughWorkingBeatmap(decoded).GetPlayableBeatmap(editorBeatmap.BeatmapInfo.Ruleset); + } } private class PassThroughWorkingBeatmap : WorkingBeatmap From c96bc5c51cdfe0f607ebd9925ab160358fb10a81 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 30 Apr 2020 20:39:41 +0900 Subject: [PATCH 1398/2376] Fix undo/redo behaving poorly with simultaneous objects --- .../Editing/LegacyEditorBeatmapPatcherTest.cs | 25 +++++++++++++++++++ osu.Game/Screens/Edit/EditorBeatmap.cs | 20 ++++++++++++--- .../Edit/LegacyEditorBeatmapPatcher.cs | 6 +++-- 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs b/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs index a3ab677d96..ff17f23d50 100644 --- a/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs +++ b/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs @@ -304,6 +304,31 @@ namespace osu.Game.Tests.Editing runTest(patch); } + [Test] + public void TestChangeHitObjectAtSameTime() + { + current.AddRange(new[] + { + new HitCircle { StartTime = 500, Position = new Vector2(50) }, + new HitCircle { StartTime = 500, Position = new Vector2(100) }, + new HitCircle { StartTime = 500, Position = new Vector2(150) }, + new HitCircle { StartTime = 500, Position = new Vector2(200) }, + }); + + var patch = new OsuBeatmap + { + HitObjects = + { + new HitCircle { StartTime = 500, Position = new Vector2(150) }, + new HitCircle { StartTime = 500, Position = new Vector2(100) }, + new HitCircle { StartTime = 500, Position = new Vector2(50) }, + new HitCircle { StartTime = 500, Position = new Vector2(200) }, + } + }; + + runTest(patch); + } + private void runTest(IBeatmap patch) { // Due to the method of testing, "patch" comes in without having been decoded via a beatmap decoder. diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index a2d2f08ce9..2e8e03bc73 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -136,14 +136,26 @@ namespace osu.Game.Screens.Edit /// The to add. public void Add(HitObject hitObject) { - trackStartTime(hitObject); - // Preserve existing sorting order in the beatmap var insertionIndex = findInsertionIndex(PlayableBeatmap.HitObjects, hitObject.StartTime); - mutableHitObjects.Insert(insertionIndex + 1, hitObject); + Insert(insertionIndex + 1, hitObject); + } + + /// + /// Inserts a into this . + /// + /// + /// It is the invoker's responsibility to make sure that sorting order is maintained. + /// + /// The index to insert the at. + /// The to insert. + public void Insert(int index, HitObject hitObject) + { + trackStartTime(hitObject); + + mutableHitObjects.Insert(index, hitObject); HitObjectAdded?.Invoke(hitObject); - updateHitObject(hitObject, true); } diff --git a/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs b/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs index 17eba87076..04faba6478 100644 --- a/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs +++ b/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs @@ -63,8 +63,10 @@ namespace osu.Game.Screens.Edit } } - // Make the removal indices are sorted so that iteration order doesn't get messed up post-removal. + // Sort the indices to ensure that removal + insertion indices don't get jumbled up post-removal or post-insertion. + // This isn't strictly required, but the differ makes no guarantees about order. toRemove.Sort(); + toAdd.Sort(); // Apply the changes. for (int i = toRemove.Count - 1; i >= 0; i--) @@ -74,7 +76,7 @@ namespace osu.Game.Screens.Edit { IBeatmap newBeatmap = readBeatmap(newState); foreach (var i in toAdd) - editorBeatmap.Add(newBeatmap.HitObjects[i]); + editorBeatmap.Insert(i, newBeatmap.HitObjects[i]); } } From 000c34dc26ea9d815f0f76c32510f1a0b5f600a7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Apr 2020 21:01:53 +0900 Subject: [PATCH 1399/2376] Move recommender to field construction --- osu.Game/Screens/Select/SongSelect.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 6b896694ea..a7e27c27ba 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -78,7 +78,7 @@ namespace osu.Game.Screens.Select protected BeatmapCarousel Carousel { get; private set; } - private DifficultyRecommender recommender; + private readonly DifficultyRecommender recommender = new DifficultyRecommender(); private BeatmapInfoWedge beatmapInfoWedge; private DialogOverlay dialogOverlay; @@ -118,7 +118,7 @@ namespace osu.Game.Screens.Select BleedBottom = Footer.HEIGHT, SelectionChanged = updateSelectedBeatmap, BeatmapSetsChanged = carouselBeatmapsLoaded, - GetRecommendedBeatmap = (recommender = new DifficultyRecommender()).GetRecommendedBeatmap, + GetRecommendedBeatmap = recommender.GetRecommendedBeatmap, }, c => carouselContainer.Child = c); AddRangeInternal(new Drawable[] From 99677ac17149bff748d96954bae2e8ab0fe6bafe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 30 Apr 2020 21:41:57 +0200 Subject: [PATCH 1400/2376] Expand test coverage of state transitions --- .../Skinning/TestSceneDrawableTaikoMascot.cs | 123 ++++++++++++------ 1 file changed, 86 insertions(+), 37 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs index 0018899769..cdd2a38e19 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Humanizer; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -16,6 +17,7 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Judgements; using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Scoring; using osu.Game.Rulesets.Taiko.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Tests.Visual; @@ -38,9 +40,17 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning TimeRange = { Value = 5000 }, }; + private TaikoScoreProcessor scoreProcessor; + private IEnumerable mascots => this.ChildrenOfType(); private IEnumerable playfields => this.ChildrenOfType(); + [SetUp] + public void SetUp() + { + scoreProcessor = new TaikoScoreProcessor(); + } + [Test] public void TestStateAnimations() { @@ -78,51 +88,63 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning } [Test] - public void TestPlayfield() + public void TestIdleState() { AddStep("set beatmap", () => setBeatmap()); - AddStep("create drawable ruleset", () => - { - SetContents(() => - { - var ruleset = new TaikoRuleset(); - return new DrawableTaikoRuleset(ruleset, Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo)); - }); - }); + createDrawableRuleset(); - AddStep("miss result for normal hit", () => addJudgement(HitResult.Miss, new TaikoJudgement())); - AddUntilStep("state is fail", () => allMascotsIn(TaikoMascotAnimationState.Fail)); - - AddStep("great result for normal hit", () => addJudgement(HitResult.Great, new TaikoJudgement())); - AddUntilStep("state is idle", () => allMascotsIn(TaikoMascotAnimationState.Idle)); - - AddStep("miss result for strong hit", () => addJudgement(HitResult.Miss, new TaikoStrongJudgement())); - AddAssert("state remains idle", () => allMascotsIn(TaikoMascotAnimationState.Idle)); + assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle); + assertStateAfterResult(new JudgementResult(new StrongHitObject(), new TaikoStrongJudgement()) { Type = HitResult.Miss }, TaikoMascotAnimationState.Idle); } [Test] - public void TestKiai() + public void TestKiaiState() { AddStep("set beatmap", () => setBeatmap(true)); - AddUntilStep("wait for beatmap to be loaded", () => Beatmap.Value.Track.IsLoaded); + createDrawableRuleset(); - AddStep("create drawable ruleset", () => - { - Beatmap.Value.Track.Start(); + assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Good }, TaikoMascotAnimationState.Kiai); + assertStateAfterResult(new JudgementResult(new Hit(), new TaikoStrongJudgement()) { Type = HitResult.Miss }, TaikoMascotAnimationState.Kiai); + assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Miss }, TaikoMascotAnimationState.Fail); + } - SetContents(() => - { - var ruleset = new TaikoRuleset(); - return new DrawableTaikoRuleset(ruleset, Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo)); - }); - }); + [Test] + public void TestMissState() + { + AddStep("set beatmap", () => setBeatmap()); - AddUntilStep("state is fail", () => allMascotsIn(TaikoMascotAnimationState.Fail)); + createDrawableRuleset(); - AddStep("great result for normal hit", () => addJudgement(HitResult.Great, new TaikoJudgement())); - AddUntilStep("state is kiai", () => allMascotsIn(TaikoMascotAnimationState.Kiai)); + assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle); + assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Miss }, TaikoMascotAnimationState.Fail); + assertStateAfterResult(new JudgementResult(new DrumRoll(), new TaikoDrumRollJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Fail); + assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Good }, TaikoMascotAnimationState.Idle); + } + + [TestCase(true)] + [TestCase(false)] + public void TestClearStateOnComboMilestone(bool kiai) + { + AddStep("set beatmap", () => setBeatmap(kiai)); + + createDrawableRuleset(); + + AddRepeatStep("reach 49 combo", () => applyNewResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }), 49); + + assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Good }, TaikoMascotAnimationState.Clear); + } + + [TestCase(true)] + [TestCase(false)] + public void TestClearStateOnClearedSwell(bool kiai) + { + AddStep("set beatmap", () => setBeatmap(kiai)); + + createDrawableRuleset(); + + assertStateAfterResult(new JudgementResult(new Swell(), new TaikoSwellJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Clear); } private void setBeatmap(bool kiai = false) @@ -141,24 +163,51 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning BaseDifficulty = new BeatmapDifficulty(), Metadata = new BeatmapMetadata { - Artist = @"Unknown", - Title = @"Sample Beatmap", - AuthorString = @"Craftplacer", + Artist = "Unknown", + Title = "Sample Beatmap", + AuthorString = "Craftplacer", }, Ruleset = new TaikoRuleset().RulesetInfo }, ControlPointInfo = controlPointInfo }); + + scoreProcessor.ApplyBeatmap(Beatmap.Value.Beatmap); } - private void addJudgement(HitResult result, Judgement judgement) + private void createDrawableRuleset() + { + AddUntilStep("wait for beatmap to be loaded", () => Beatmap.Value.Track.IsLoaded); + + AddStep("create drawable ruleset", () => + { + Beatmap.Value.Track.Start(); + + SetContents(() => + { + var ruleset = new TaikoRuleset(); + return new DrawableTaikoRuleset(ruleset, Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo)); + }); + }); + } + + private void assertStateAfterResult(JudgementResult judgementResult, TaikoMascotAnimationState expectedState) + { + AddStep($"{judgementResult.Type.ToString().ToLower()} result for {judgementResult.Judgement.GetType().Name.Humanize(LetterCasing.LowerCase)}", + () => applyNewResult(judgementResult)); + + AddAssert($"state is {expectedState.ToString().ToLower()}", () => allMascotsIn(expectedState)); + } + + private void applyNewResult(JudgementResult judgementResult) { foreach (var playfield in playfields) { - var hit = new DrawableTestHit(new Hit(), result); + var hit = new DrawableTestHit(new Hit(), judgementResult.Type); Add(hit); - playfield.OnNewResult(hit, new JudgementResult(hit.HitObject, judgement) { Type = result }); + playfield.OnNewResult(hit, judgementResult); + scoreProcessor.ApplyResult(judgementResult); } } From 22fde8d2a0e7ac158358af6839f91c528853d07e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 30 Apr 2020 21:58:05 +0200 Subject: [PATCH 1401/2376] Implement partial clear transition logic --- .../Skinning/TestSceneDrawableTaikoMascot.cs | 3 ++- .../UI/DrawableTaikoMascot.cs | 25 ++++++++++++++++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs index cdd2a38e19..f74de47425 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs @@ -201,13 +201,14 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning private void applyNewResult(JudgementResult judgementResult) { + scoreProcessor.ApplyResult(judgementResult); + foreach (var playfield in playfields) { var hit = new DrawableTestHit(new Hit(), judgementResult.Type); Add(hit); playfield.OnNewResult(hit, judgementResult); - scoreProcessor.ApplyResult(judgementResult); } } diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs index f4bc841c15..2df0cae2e3 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs @@ -11,7 +11,7 @@ using osu.Framework.Graphics.Textures; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics.Containers; using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko.Judgements; namespace osu.Game.Rulesets.Taiko.UI { @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Taiko.UI private readonly Dictionary animations; private TaikoMascotAnimation currentAnimation; - private bool lastHitMissed; + private bool lastObjectHit; private bool kiaiMode; public DrawableTaikoMascot(TaikoMascotAnimationState startingState = TaikoMascotAnimationState.Idle) @@ -56,7 +56,18 @@ namespace osu.Game.Rulesets.Taiko.UI public void OnNewResult(JudgementResult result) { - lastHitMissed = result.Type == HitResult.Miss && result.Judgement.AffectsCombo; + // TODO: missing support for clear/fail state transition at end of beatmap gameplay + + if (triggerComboClear(result) || triggerSwellClear(result)) + { + state.Value = TaikoMascotAnimationState.Clear; + return; + } + + if (!result.Judgement.AffectsCombo) + return; + + lastObjectHit = result.IsHit; } protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) @@ -77,7 +88,7 @@ namespace osu.Game.Rulesets.Taiko.UI if (currentAnimation != null && !currentAnimation.Completed) return state.Value; - if (lastHitMissed) + if (!lastObjectHit) return TaikoMascotAnimationState.Fail; return kiaiMode ? TaikoMascotAnimationState.Kiai : TaikoMascotAnimationState.Idle; @@ -89,5 +100,11 @@ namespace osu.Game.Rulesets.Taiko.UI currentAnimation = animations[state.NewValue]; currentAnimation.Show(); } + + private bool triggerComboClear(JudgementResult judgementResult) + => (judgementResult.ComboAtJudgement + 1) % 50 == 0 && judgementResult.Judgement.AffectsCombo && judgementResult.IsHit; + + private bool triggerSwellClear(JudgementResult judgementResult) + => judgementResult.Judgement is TaikoSwellJudgement && judgementResult.IsHit; } } From 5cfc05e12afe54aca75b6c92ba91e3d0ad35a277 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 30 Apr 2020 22:03:39 +0200 Subject: [PATCH 1402/2376] Ensure correct initial state --- .../Skinning/TestSceneDrawableTaikoMascot.cs | 8 ++++++++ osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs index f74de47425..e23f63e245 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs @@ -62,6 +62,14 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning AddStep("fail state", () => SetContents(() => new TaikoMascotAnimation(TaikoMascotAnimationState.Fail))); } + [Test] + public void TestInitialState() + { + AddStep("create mascot", () => SetContents(() => new DrawableTaikoMascot())); + + AddAssert("mascot initially idle", () => allMascotsIn(TaikoMascotAnimationState.Idle)); + } + [Test] public void TestClearStateTransition() { diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs index 2df0cae2e3..1b7d011d8a 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Taiko.UI private readonly Dictionary animations; private TaikoMascotAnimation currentAnimation; - private bool lastObjectHit; + private bool lastObjectHit = true; private bool kiaiMode; public DrawableTaikoMascot(TaikoMascotAnimationState startingState = TaikoMascotAnimationState.Idle) From db4c8b2ba59c6a59ab4a19eec2d548f28022242e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 30 Apr 2020 22:16:25 +0200 Subject: [PATCH 1403/2376] Fix transition out of clear state --- .../Skinning/TestSceneDrawableTaikoMascot.cs | 7 ++++--- osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs | 3 ++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs index e23f63e245..aee057602a 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs @@ -144,15 +144,16 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Good }, TaikoMascotAnimationState.Clear); } - [TestCase(true)] - [TestCase(false)] - public void TestClearStateOnClearedSwell(bool kiai) + [TestCase(true, TaikoMascotAnimationState.Kiai)] + [TestCase(false, TaikoMascotAnimationState.Idle)] + public void TestClearStateOnClearedSwell(bool kiai, TaikoMascotAnimationState expectedStateAfterClear) { AddStep("set beatmap", () => setBeatmap(kiai)); createDrawableRuleset(); assertStateAfterResult(new JudgementResult(new Swell(), new TaikoSwellJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Clear); + AddUntilStep($"state reverts to {expectedStateAfterClear.ToString().ToLower()}", () => allMascotsIn(expectedStateAfterClear)); } private void setBeatmap(bool kiai = false) diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs index 1b7d011d8a..089f5c87a2 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs @@ -61,7 +61,8 @@ namespace osu.Game.Rulesets.Taiko.UI if (triggerComboClear(result) || triggerSwellClear(result)) { state.Value = TaikoMascotAnimationState.Clear; - return; + // never play fail immediately after clear. + lastObjectHit = true; } if (!result.Judgement.AffectsCombo) From f5526890cc3ffe85b45861fc3f46c1d24df8db05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 30 Apr 2020 22:51:22 +0200 Subject: [PATCH 1404/2376] Add comment about animation presence --- osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs b/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs index 165e00cc73..452272211d 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs @@ -24,11 +24,12 @@ namespace osu.Game.Rulesets.Taiko.UI InternalChild = textureAnimation = createTextureAnimation(state).With(animation => { animation.Origin = animation.Anchor = Anchor.BottomLeft; - RelativeSizeAxes = Axes.Both; }); RelativeSizeAxes = Axes.Both; Origin = Anchor = Anchor.BottomLeft; + + // needs to be always present to prevent the animation clock consuming time spent when not present. AlwaysPresent = true; } From 1e7b10320f8ba2cd7148f2734f216495f38846d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 1 May 2020 00:19:12 +0200 Subject: [PATCH 1405/2376] Adjust mascot positioning in playfield layout --- osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs | 2 +- .../UI/TaikoMascotAnimation.cs | 2 ++ osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 15 +++++++++------ 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs index 089f5c87a2..eb885872c5 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Taiko.UI public DrawableTaikoMascot(TaikoMascotAnimationState startingState = TaikoMascotAnimationState.Idle) { - RelativeSizeAxes = Axes.Both; + Origin = Anchor = Anchor.BottomLeft; state = new Bindable(startingState); animations = new Dictionary(); diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs b/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs index 452272211d..0bf6bc7d49 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Textures; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics.Containers; using osu.Game.Skinning; +using osuTK; namespace osu.Game.Rulesets.Taiko.UI { @@ -24,6 +25,7 @@ namespace osu.Game.Rulesets.Taiko.UI InternalChild = textureAnimation = createTextureAnimation(state).With(animation => { animation.Origin = animation.Anchor = Anchor.BottomLeft; + animation.Scale = new Vector2(0.6f); }); RelativeSizeAxes = Axes.Both; diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 084a11d523..2edc697d66 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -130,18 +130,21 @@ namespace osu.Game.Rulesets.Taiko.UI }, } }, + mascotDrawable = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.TaikoDon), _ => Empty()) + { + Origin = Anchor.BottomLeft, + Anchor = Anchor.TopLeft, + RelativePositionAxes = Axes.None, + RelativeSizeAxes = Axes.None, + X = 15, + Y = 45 + }, topLevelHitContainer = new ProxyContainer { Name = "Top level hit objects", RelativeSizeAxes = Axes.Both, }, drumRollHitContainer.CreateProxy(), - mascotDrawable = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.TaikoDon), _ => new Container(), confineMode: ConfineMode.ScaleToFit) - { - Origin = Anchor.BottomLeft, - Anchor = Anchor.TopLeft, - RelativePositionAxes = Axes.None - } }; } From 05183c6e6fb1683bd6fa94021c7e69c2572cb8e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 1 May 2020 00:24:39 +0200 Subject: [PATCH 1406/2376] Final test touch-ups --- .../Skinning/TestSceneDrawableTaikoMascot.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs index aee057602a..492f628482 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs @@ -9,6 +9,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; @@ -65,7 +66,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning [Test] public void TestInitialState() { - AddStep("create mascot", () => SetContents(() => new DrawableTaikoMascot())); + AddStep("create mascot", () => SetContents(() => new DrawableTaikoMascot { RelativeSizeAxes = Axes.Both })); AddAssert("mascot initially idle", () => allMascotsIn(TaikoMascotAnimationState.Idle)); } @@ -83,13 +84,13 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning { var state = new Bindable(TaikoMascotAnimationState.Clear); states.Add(state); - return new DrawableTaikoMascot { State = { BindTarget = state } }; + return new DrawableTaikoMascot { State = { BindTarget = state }, RelativeSizeAxes = Axes.Both }; })); AddStep("set clear state", () => states.ForEach(state => state.Value = TaikoMascotAnimationState.Clear)); AddStep("miss", () => mascots.ForEach(mascot => mascot.OnNewResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Miss }))); - AddAssert("skins with animations remain in clear state", () => mascots.Any(mascot => mascot.State.Value == TaikoMascotAnimationState.Clear)); - AddUntilStep("state reverts to fail", () => someMascotsIn(TaikoMascotAnimationState.Fail)); + AddAssert("skins with animations remain in clear state", () => someMascotsIn(TaikoMascotAnimationState.Clear)); + AddUntilStep("state reverts to fail", () => allMascotsIn(TaikoMascotAnimationState.Fail)); AddStep("set clear state again", () => states.ForEach(state => state.Value = TaikoMascotAnimationState.Clear)); AddAssert("skins with animations change to clear", () => someMascotsIn(TaikoMascotAnimationState.Clear)); From d021e213b216b994c343184d400c5aa7bfa25fd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 1 May 2020 00:29:03 +0200 Subject: [PATCH 1407/2376] Reword comment --- osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs index eb885872c5..9328b607e6 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs @@ -61,7 +61,7 @@ namespace osu.Game.Rulesets.Taiko.UI if (triggerComboClear(result) || triggerSwellClear(result)) { state.Value = TaikoMascotAnimationState.Clear; - // never play fail immediately after clear. + // always consider a clear equivalent to a hit to avoid clear -> miss transitions lastObjectHit = true; } From e0ae9f791c357a040f0ff06180451bf20dca2428 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 1 May 2020 11:56:40 +0900 Subject: [PATCH 1408/2376] 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 73fbe3ab2e..336479c40a 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 3b05eb82d7..acb7fe5fbe 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -23,7 +23,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index f3202693f3..6662e57dcd 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From 700214d249d2938977c6161e4dcea22b712b1025 Mon Sep 17 00:00:00 2001 From: Joehu Date: Thu, 30 Apr 2020 22:13:38 -0700 Subject: [PATCH 1409/2376] Truncate beatmap title and artist on score panel --- .../Ranking/TestSceneExpandedPanelMiddleContent.cs | 5 ++++- .../Ranking/Expanded/ExpandedPanelMiddleContent.cs | 10 ++++++++-- osu.Game/Screens/Ranking/ScorePanel.cs | 4 ++-- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs index 52d8ea0480..328a0e0c27 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs @@ -20,6 +20,7 @@ using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; +using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking.Expanded; using osu.Game.Screens.Ranking.Expanded.Accuracy; using osu.Game.Screens.Ranking.Expanded.Statistics; @@ -74,6 +75,8 @@ namespace osu.Game.Tests.Visual.Ranking { var beatmap = new TestBeatmap(rulesetStore.GetRuleset(0)); beatmap.Metadata.Author = author; + beatmap.Metadata.Title = "Verrrrrrrrrrrrrrrrrrry looooooooooooooooooooooooong beatmap title"; + beatmap.Metadata.Artist = "Verrrrrrrrrrrrrrrrrrry looooooooooooooooooooooooong beatmap artist"; return new TestWorkingBeatmap(beatmap); } @@ -114,7 +117,7 @@ namespace osu.Game.Tests.Visual.Ranking Anchor = Anchor.Centre; Origin = Anchor.Centre; - Size = new Vector2(500, 700); + Size = new Vector2(ScorePanel.EXPANDED_WIDTH, 700); Children = new Drawable[] { new Box diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index b058cc142b..fd8ac33aef 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -35,6 +35,8 @@ namespace osu.Game.Screens.Ranking.Expanded private RollingCounter scoreCounter; + private const float padding = 10; + /// /// Creates a new . /// @@ -46,7 +48,7 @@ namespace osu.Game.Screens.Ranking.Expanded RelativeSizeAxes = Axes.Both; Masking = true; - Padding = new MarginPadding { Vertical = 10, Horizontal = 10 }; + Padding = new MarginPadding(padding); } [BackgroundDependencyLoader] @@ -92,13 +94,17 @@ namespace osu.Game.Screens.Ranking.Expanded Origin = Anchor.TopCentre, Text = new LocalisedString((metadata.TitleUnicode, metadata.Title)), Font = OsuFont.Torus.With(size: 20, weight: FontWeight.SemiBold), + MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2, + Truncate = true, }, new OsuSpriteText { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Text = new LocalisedString((metadata.ArtistUnicode, metadata.Artist)), - Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold) + Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold), + MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2, + Truncate = true, }, new Container { diff --git a/osu.Game/Screens/Ranking/ScorePanel.cs b/osu.Game/Screens/Ranking/ScorePanel.cs index a1adfcc500..c055df7ccc 100644 --- a/osu.Game/Screens/Ranking/ScorePanel.cs +++ b/osu.Game/Screens/Ranking/ScorePanel.cs @@ -31,7 +31,7 @@ namespace osu.Game.Screens.Ranking /// /// Width of the panel when expanded. /// - private const float expanded_width = 360; + public const float EXPANDED_WIDTH = 360; /// /// Height of the panel when expanded. @@ -183,7 +183,7 @@ namespace osu.Game.Screens.Ranking switch (state) { case PanelState.Expanded: - this.ResizeTo(new Vector2(expanded_width, expanded_height), resize_duration, Easing.OutQuint); + this.ResizeTo(new Vector2(EXPANDED_WIDTH, expanded_height), resize_duration, Easing.OutQuint); topLayerBackground.FadeColour(expanded_top_layer_colour, resize_duration, Easing.OutQuint); middleLayerBackground.FadeColour(expanded_middle_layer_colour, resize_duration, Easing.OutQuint); From 8955b98cbb5be1a6768b97ad4b3ef7390b81bef6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Apr 2020 17:38:37 +0900 Subject: [PATCH 1410/2376] Add basic taiko scroller implementation --- .../old-skin/taiko-slider-fail@2x.png | Bin 0 -> 141234 bytes .../Resources/old-skin/taiko-slider@2x.png | Bin 0 -> 127176 bytes .../Skinning/TestSceneTaikoPlayfield.cs | 2 + .../Skinning/TestSceneTaikoScroller.cs | 16 ++++ .../Skinning/LegacyTaikoScroller.cs | 83 ++++++++++++++++++ .../Skinning/TaikoLegacySkinTransformer.cs | 6 ++ .../TaikoSkinComponents.cs | 1 + osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 7 ++ 8 files changed, 115 insertions(+) create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-slider-fail@2x.png create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-slider@2x.png create mode 100644 osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs create mode 100644 osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-slider-fail@2x.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-slider-fail@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..ef597210f1c9e6e805b9d3f4399b70e057b51866 GIT binary patch literal 141234 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⩔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%0DbICSRi1->iw)J4JfN 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 literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-slider@2x.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-slider@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..05b8c5783f4bde04d74aab93db8c17f5e73c5060 GIT binary patch literal 127176 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,o z@E!mDeW%&1T_tI$;n$k@dx(GeS}b)rp2VmrrPL+c5x^WV6)ajf1( zI)&f9ex$VM2*EpEgF4Ls~H>WlBsIV>UK|S-37^6e?tGO9$bk({tadf7EFI7<09w-f5t4#p<^HX zmJ1I72M<%%Z6kXfo2y#=d9|b!r$mngnvcqyyPDzsHk(&LukYq;r%+n?HzP(=ur5@Zaw8Z8=wPR zR#*#h5B|Z4m_>yXBOE9*W%20efrJO|!K4x!Eme+}K-|JhJxEN{d}~$Pw>g!etJz(> zG0`PGCAIg;4a=TwjS2Vr=F}J)dRS*)E*4KpGEhr0qfwit24Wo&@kxYS3y?P1dU#u6 zIoVDY9D+=-o&-QA?cRl{YUSC|J1l~tDG1WV4~LkH=0<$Rr%2M%y)7Cygy^)0LH^ed zuvfAKZPz!uL{1YsP%fvvv2Q>I3A5Jka%GSyc?vY@Q)h(Dvudq{0^m=tNy|*eXVw>l z@!D9qr4$&o7E4{?FI^>qXflU?%BOG-n6d}{-Xc6~VW_hd4bHIYg3<$GzC2I$Kr)=F z(db0LWB_9T?2Wi@l}>(RW@f4`PNod{2pd1F<8U0fIu{-Qw0>pi#PG*x{|f;`y=Pa! zTw+Ucl6ds>$s(4FNh%hugi2j;SONf4_I&9a%>m&paTU-t0sojK{|HT|Q)YfOJG;}? zi5Gk?j=FDH_fdY;9!%se&Ba6r-sHx5aOk{=OKeA|$PuOiASZ6;{ML>795B0In z%^3Hh&R38`|8g+{ivCPFdC;^U)3!KY)ZZCpvi{mGXm5e>@&hOIIiggvkd`O}wyt|O z$AzsDOJPW`6^mcA4fKU}6ek&fhkRO80_&KSS z(Zi&*Va9z64RbZ4^@l&87;n!plCpB~goCSuOq#P_onCa2Nz$o3HW!~L{!~nCr89pc z+rjoooBA=)A8>@*8cxwpHon{ZYM#I#(_rGbg*KFA!{7jj;$gzV?C(49>=?Y~ci-Sz zKEaxJ=rZb%E#=E)*w}B5|4{bVcYh7t`XF^QD-W)ev-nB?wc~N|Hal_S&(zw0q;F=> z$xP>9r#ER+fLTYI*?_PSP0HhjM<-=4T8Y_}fegSbUV@`QO@XL*%71Sf<{7}K8^6Ze zmdC*&jF3&Ux_-Ls^+5QkL%}*93{jLJYYM`7!ZRmtIFzG%RUY!pE!fWU+#*+`TscAr21!?Gh>_$uC{@sDpWX)?f$`4mpfcA(osY5O#ev&aCz9G&c9rH_RZA3Lb zNVu;#E#flsX;2EpuboNuy*|%_!Oc(c;@)S3H%G6AVxP;EpU@AsP>6ZW9~X@YDqcSr zK6rr`F&63C?HaC)ayY>&@?g5r^_hu4W)|#MFwr~n9*#r(g zb3nQ1WL>VG{5{?fpBNKj{^e}DIpKr}={D|u;5+F?VkY`jHv#!-Y}*C!W)PnleCC2% z^|g1tny1Y_{Solk$QB?rSoUi{&a_FlvG~u()hzwu?2^T_nq*dYn8da9o|{73c@3sb zo<9+Y#F|4_2>>$W@h$F}wtvu&5{{~{+-k%93w}4&fDdox(8XZ@|D(CjqfoKFo?hul zamrtdSiW#GX$HZhX%3Cjt%4n7IR3_Y`+mhh28@~m8VT!Kju5C}Uht5makrY-=XQ%M zwU0C6X-mOejzXC$J0fjWkAPmBLd3rn;8-?S$3a3TQ0C6fhMH$*_s@e0BaZKf=8%J? z5jpxt51IOF;eszn_kDMS2>_lFXm>wxF!{wLt_7;S%;v0JIKA(Fy zUDl|COq7JV5ZVhtq?%?HUF>l8j&qNFv50fo?ZMrO7AEElj*u5K_zuc{<>i1n;I{VN z$3gC=qw9%)qqnTJL88_i6T8t3Z%!4ln+2+0rXQ)Wf<`)UnjWOXqZ3|aWl<~}E)lh@ z#V%YrT90B)U}Av~3yI3#%F3c|U*lIUC<$LJbz@3jWH*Igo`rk9A|~WlR|@2|Cm#lT zE<@;+2OQ|kbhXnr{8d>ja*@_OK(RkRpDI#%
    }yM>XgHr~ImQgC~?`F>Gh7oE%p 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 z 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

    5};$)8hHQlgnx)m3IOv=u%u;#}^7!mm6_O`JX-W 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 literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs index ae5dd1e622..e02ad53ed8 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs @@ -24,6 +24,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning typeof(TaikoHitTarget), typeof(TaikoLegacyHitTarget), typeof(PlayfieldBackgroundRight), + typeof(LegacyTaikoScroller), }).ToList(); [Cached(typeof(IScrollingInfo))] @@ -51,6 +52,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, + Height = 0.6f, })); AddRepeatStep("change height", () => this.ChildrenOfType().ForEach(p => p.Height = Math.Max(0.2f, (p.Height + 0.2f) % 1f)), 50); diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs new file mode 100644 index 0000000000..e4673430d6 --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.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 osu.Framework.Graphics; +using osu.Game.Skinning; + +namespace osu.Game.Rulesets.Taiko.Tests.Skinning +{ + public class TestSceneTaikoScroller : TaikoSkinnableTestScene + { + public TestSceneTaikoScroller() + { + AddStep("Load scroller", () => SetContents(() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.TaikoScroller), _ => Drawable.Empty()))); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs new file mode 100644 index 0000000000..6276eb1e8a --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.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.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Skinning; + +namespace osu.Game.Rulesets.Taiko.Skinning +{ + public class LegacyTaikoScroller : CompositeDrawable + { + public LegacyTaikoScroller() + { + RelativeSizeAxes = Axes.Both; + } + + protected override void Update() + { + base.Update(); + + foreach (var sprite in InternalChildren) + { + sprite.X -= (float)Time.Elapsed * 0.1f; + + if (sprite.X + sprite.DrawWidth < 0) + sprite.Expire(); + } + + var last = InternalChildren.LastOrDefault(); + + if (last == null || last.ScreenSpaceDrawQuad.TopRight.X < ScreenSpaceDrawQuad.TopRight.X) + { + AddInternal(new ScrollerSprite { X = last == null ? 0 : last.X + last.DrawWidth }); + } + } + + private class ScrollerSprite : CompositeDrawable + { + private Sprite passingSprite; + private Sprite failingSprite; + + private bool passing = true; + + public bool Passing + { + get => passing; + set + { + if (value == passing) + return; + + passing = value; + + if (passing) + { + passingSprite.Show(); + failingSprite.FadeOut(200); + } + else + { + failingSprite.FadeIn(200); + passingSprite.Delay(200).FadeOut(); + } + } + } + + [BackgroundDependencyLoader] + private void load(ISkinSource skin) + { + AutoSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + passingSprite = new Sprite { Texture = skin.GetTexture("taiko-slider") }, + failingSprite = new Sprite { Texture = skin.GetTexture("taiko-slider-fail"), Alpha = 0 }, + }; + } + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs index 5dfc7ec0df..0212cdfd9e 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs @@ -85,6 +85,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning return new LegacyHitExplosion(sprite); return null; + + case TaikoSkinComponents.TaikoScroller: + if (GetTexture("taiko-slider") != null) + return new LegacyTaikoScroller(); + + return null; } return source.GetDrawableComponent(component); diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs index fd091f97d0..877351534a 100644 --- a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs +++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs @@ -18,5 +18,6 @@ namespace osu.Game.Rulesets.Taiko TaikoExplosionMiss, TaikoExplosionGood, TaikoExplosionGreat, + TaikoScroller } } diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 5c763cb332..a5edcc1357 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -50,6 +50,13 @@ namespace osu.Game.Rulesets.Taiko.UI { InternalChildren = new[] { + new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.TaikoScroller), _ => Drawable.Empty()) + { + Origin = Anchor.BottomLeft, + Anchor = Anchor.TopLeft, + RelativeSizeAxes = Axes.X, + Height = 100, + }, new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.PlayfieldBackgroundRight), _ => new PlayfieldBackgroundRight()), rightArea = new Container { From 5e430c726cd73c5bdafeb9fffdcd5be53c3e530a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 May 2020 19:25:55 +0900 Subject: [PATCH 1411/2376] Add testing resources --- .../metrics-skin/taiko-slider-fail@2x.png | Bin 0 -> 187725 bytes .../metrics-skin/taiko-slider@2x.png | Bin 0 -> 189590 bytes .../old-skin/taiko-slider-fail@2x.png | Bin 141234 -> 102010 bytes .../Resources/old-skin/taiko-slider@2x.png | Bin 127176 -> 96449 bytes .../special-skin/taiko-slider-fail.png | Bin 0 -> 61156 bytes .../Resources/special-skin/taiko-slider.png | Bin 0 -> 65012 bytes 6 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taiko-slider-fail@2x.png create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taiko-slider@2x.png create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/taiko-slider-fail.png create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/taiko-slider.png diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taiko-slider-fail@2x.png b/osu.Game.Rulesets.Taiko.Tests/Resources/metrics-skin/taiko-slider-fail@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..ac0fef86266d4b0667f4b62a4513175bcb5c5968 GIT binary patch literal 187725 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 z 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

    9ce#)%<# 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

    >%B8t;Um@gU=Ni|7yxlLXfD89Lc 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 z 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 literal 141234 zcmYhh1ymf(@;SSxGwJQ?(PyCg3BVoEx5ZwfZz^)-h1!w``$Xe zr_auu?wYBtsebCIj!;sNL_s7(1ONai(o$k7001mH001Ed5BqUtkjcgEql2~(mKO#9 z>f?}Jj9@VK}y>h06@b0=La#f?)&mF93U+wtmeM*=T$pxZy3ac=nES*r8=IG zAZj)-P#-BCSxBpV?EsoQ11W6ITZ1;w4T|!n6<=4n?A$gR7tYwFBxmM@B1{t0aH4<< zx+y8nrsLa(kXTDc#td_&S zM2q*qj_Z8uKT_x9urtyKcarf%>K{?!0i`@2!x<0YUotG4Ovm1^4oUczU#lVwgx*u|L;Njog5;t&(mUvTg$n$ zq!W9!>3IGnK$KXa)b2@E)P#2v&;`Qe^R*K^PcrrW11YFKs-!m^?_q zva#Agl&jLyVd($NhR<*#t+jDz8A9KtwlwGL%NyjWfG0!GbNZj#|EJfQ2XY(JSi4wQ z!#7sypEq@ft)~!u-w_mMP^nLm)c7(%om7Qgu~mijzxDV}_w(=i%i|RpHLAJ0IsGvU z5<$;a_&?KzSwQ0Pk2Bw~J3FSd9Zl^&((Zv$vdx%FqkaSx@Wm?q%arbe+h5fG?gLDK zX^52K_{E=x7q7qb;yY9RW8Z%Dz7#U#RD+1IyW=sPjRE@8it$4l9HwL?2h)E!N2+xfw*G6U{~9990^JTR7)Nr!7jA3`t=Dx5>HvuG&#N-wF?}D_r|L5_%Bxm?dYSl^ znoSX0b9R6wesE3TViX+s`QL`g#o&Q73NfO9!<7driTR@>Y)L`Gh0TlSI`-V|GDIV} z|Ge%}h@$(uR(r$Sv|6>V8(z3ZZ6D>0D+?UAC?QK7NI>|vXI}SXz%xW@4+nWvVaS;Z zE60WHqn(f6&~Qq<*=DrXN{1V@>2$+E|KIa5 zp)?#Q*eUFNd@<;KAJc|+LPOvPoR01SDSCZ`J`ro6jk%b?^7KB=Y-N+qietdC?|(5# z)qI?9sn9w2T`1e{uxTt6vi$Sr&@SiyOc$0Svf+`$)SX4j%Wutc`)QFh7v;gbB>RDn zWl-oV7Cpblonk*#h(Ye5jBWmSvNJgoFHfs{$|vHcGu+90wympvbik{a||YdxK}Is z&2EB<9?lmz$?O@##eE)Fh zNFoYhyxjd}DN3lTc=(US8WYcrd!pb{;C#l)R{yUL_xCv}M^)R5?HnkfRt8@mF~`ca5;{QlMfMCv3Q3YI zVIs`mQZ$1#B;33$}FY9@e(`Cmlg;StlCBZ1RSw?N(FvUAmf;i-j1 z_1HFeBe>j`TncDq1@bBKwRva%Dwqryv?E}CIp)(M=VLBVGG2O~U*vtjyRKF93`#K1s;?9JAPuKgNtli)CFz{@?;G+(2iA&zTk$6^wg)~aP(9e51V z1&*Q~mR74U9bSDQT1AR`L(pNH6Ar&0mIxNYv|(hHjBC(c6LTM4nf)Xd*`7&4n56&T z_JPRdoO}Jvu?yHDK8X_DPhq%LV|aGcWHhYD}ax>d=W6QQ512g@NXm}$ba9|+JKwl}RX64U1TKg8gs5j5Oaud(MSw%V z@RL$SDqM1ja4J>bEjLkl)iLSy$~6xED~MbFYt^|I*_*`i52JtFxfhrU!=)jQO)Fc- zzdg;#&7{?r0Q)%FYS=)gdE@L^X4|M8p6D4GVMnqHwJV>v2=HPEkz+HYTR><`K+qhZ zHzGJV`Ira6T=`r3+rbuDzeX9`l#ljntfg7n%l-b@uHEe&rh|A^BCC0Fn#GwcMYd*8 zB}xliAB@mpneL4!^@T_NTOhKD$|2mzqU_R>(}Y&)iaExG%@a3kkD)7KG#`H z{076vY8fg2zQ}?cHi0i@an2O6DPgWOQs$AT(2eN^|4fl$kR;w?_}y6(QMDNZqs6`# z$#?k0E4TKA;;O;3OP(Mi$x08~ww(ZjiV>`LIsBH{&u27k+Go}zW0+ADeJ)Ucp62)B z=AeaZerDoC*tcE-=HHkQrY?U_e}huwMrFkHI!M_O#F zsTFf2f{yyIB#XM5qIxq&7H`MaP&??y*XGDvu# z_G8<-VyO)2r2wLr<8aSK2Jm|oRq^4TVX5Obr!qt9(OSk^ni=kPd}kG(jOOZhPCuTm zo;kJOUJ2`*n4X@K z_xL))m-Q?kVE5*VHSoiNv#vR>a_hKbJ)RiA{x*nQLZ~o5)un@q${xVM9BkeRNKS!z zIvwN}Mk-Twm!tGAsYxRB(T$Ds&E*e#qn$1l1pnl1=eG><^*#3q)q2KdE z=Aee4AW`3RjqxSjRqv+I!CN*@xX=7PwLyl7=UG3R0tibRC0e6X;m33_zGpiCdlZjD zw28)L39A4`b>T6&cuLVoq6gOYYXK}Bxj*3muLm$1-u{dQbz*GW^jcIv(D<1D_0*rg zK%?C5gO`FYJWHQz-QZ5V7t|N98>-0VufmTvv=6l^`O#$QC4UZy=z>--*rp&4>RGf9 zreG-|92^U)`mqJT$z#V%xY{|?mGVh?6CNl0YdAS?F<+3O2w(vNp<&iw^)X;j;raQilS4Y`G>=eD<&Zy%fLxp#c)71>kqF@bVH7Q|`oOq% zPtv66pPQJZW}HAyF1gx-A(d@S+3%`DoBa3q35)W|3W6nLHvLHCC=ip@TZ#b@cDIfo&wIBj(?#B#b zoX@j8z^r6LJ<@0!LRw9;!z4IpM6|7k%RQG$V$w#hj!dvSW1Jy?!j736k9k<5JX^ic5`qa@dCo3B=r*cn4lBM-nyEn z^5gQ8d2`I^mP3Z&Y#c5tczktJ*TA_oa(H#qWhkq5?sDyZ0RZT4L=bpC6m_|mKw=29 z%N1Xj9f3%*yWp)1q1T+Ea57sVeRA_j^}6~)P-vY5L<;V_XeZR1oKqX&C0c;+|BOxv z1YybdbAIwL6r~J5)G??oZ2!9YnXwi}$?h4SILt2E1}EIk#0)NMzi;r*Z!s~uP8F!@ znR{+m+1596tTQx()%ID+85lR7g5hL@{VC|9MkUwdLa5+xB!MctnV2LP(MlIN~ zB^US9y6=yD8ot9=Q@5O977!i>a?_5_u2cfdoQUp^%q@TaR%7wM%DbIknKI5`_RlqB ztPMsU8td7t%P}MKCXH8G>tu3LPE%`IMvs5Fm}7*!;a7}=0(>#BH$1FFfyRmrifebj znj~-+I<$WlIK`zf@0#^EA(mj{94*Z2n;&3TBm+88No}-hYrsv{MsS43R%Qusfi-d} zNzT6f^A%y{S%6%0aMuuOP*Gj$ngr^o(C|&ZZ_HhUHC*5FwJS>Uba4Y%z8DF`z_Hg7 zxAwR;wVNwZpPZPfS`6X`lJ&5$MD@MuN(X8G$4(yMk>k;at!@M?E&+1ZhM{s!y^&h^ zJ4@f_&@-QO7Z9Szu-GnUlYLOr!-GTQenni`U|`)vQAdA(YNYF7yhhzTb>X7}V^qyg z?I|-E?P-Bdd2nkFXSh~hsMg-M5dtr5UI#<@)5Cc3pKes1AG1d$8!&x0_<0UB&Mtv5 zG0o2b;n!YdHbpUM)~%P-8pMZBMDjoFQZGj~NyzXs7_H~r)+9E`O3g$gfn~jkY+r)^ zYTBoI2}tQTh(82RT#H<*ZEW+>`1O~Mu!7DXk-2@#1n!HQZguHgO(^GMuPt*&cukyI zi6xpw8Xt3cdB%gj1C`?DPp|9?d+4zPR!TEyx?jB*yF@E{A&5$nLuMGP4kt5Pr&qmyWUyQYS|-uV+h7VfnquEvnzfP%Qgy4EDM7veAi- zR=;yv356$&3gKY=SYF*_!6;B4Zz952gN#&K|At;XT{m{vatXs`o*hG$_4IAj`R5M^ zet&Zws0-8lg^$OLRMFCul|YX`UXoqP3Rb}?-hIh#!5%L^8t_OVGm4d$Tm0SxbK~0 zU239UmM?2x5snL20rCuG#C;k2t!{%~!v9tZ(c#F-DK(*`QKMTlNnmnmza~1zvj6R- z<<%Tzz})EEm8l7lw)XLeG^WXRyvT`eNF^P==8KzyLps8aR>G}LhsNZbA&v}*-TOA7 zz++aBdy4#AnRxgszcT9896p&I5)v>ki-B7;3tw}OUNW4j550$|Z z6;;$JnkD@d8 zyn`@HChF!AI|1ruh$(D}3CHm)c>^EqU(I<+M*F34POCH*wRX6deR;N9N#HL+EHcLN z7fstd%|gGAvHJq0Jq(7~*>B%1{I#@r@gy2E^N!B5%v9UO9~*wM@=l$$OQBe=(HfnJ zcSR+3eqW~>fELlnGS8Ug)s#R*y{luT8&|=v0LPqCRd!aM3fR54BHjDB8|&#?p<(A_ zeQx>_MFKaB&|0!-$QGTon>jPO@=R{sf2{4GzW^&sgMurDKQ4>SU%6Q^E>L8)JLgjV z9D&58f#`t{l0Ej`G#yji<5Y!d4*Enn8p^j6k8+HBV=J^S80TjW7d95k?=wodnK4u6 zh?d#{Zu32Im@mk}s&KRcw}nvjpKaO~-DQ!Gyz9_VrJ3F?cD_yw+EyL&ux~p5d27@C zg$c^zp6YZOZsZU!zeT%kX;%yTm)TaB62pD0^6Mp0egd&AtRROQ)vcXlGO zTIJCZ+~p?1Es+pKajD4JR3sN8R|>*2_uboa2ocKIP&tnlf$XiRtp;mJTv+0Sk>2jG z{4lV14KHkZ^Vb=g0`69w z)u;ccB{olJipfgXh94`QF=(_3OL}FIp=Ib+JJL;|F4Fl%RXl1XOdWn@`BZ8Xey^;7 zK)swq|NVdx*!bpz&lx-M<$#x`3Ua&#twLa6#FuyJbG%09A}QZjFco5b z4I{`h_>td!y!!^uHC|KH!I=8-(CoM?5)$H(Iq3gWguUwQ@#YnH&ztNd=*iUHJYh9- z<8y4iyt_G%Ph|UqfMam4qdlhYrVrNml_RL%fB5bWZ?}Cb3)-a$Rc7k^b>A;mH?^Su z#I~}Lw_5m~n23mH6;Stcf{EiXUBhyZaT8eCC6|?44yHy5{{0iK^-hQLVSk|6sA9XI zU0LkHP)vWZesxY}4{3F8sCOH&*R+K4?T3-?h^X1uO?2?H4$2MM(9v1kj!VN+#Ry~o zAJ;r9L!QT~(w936E#f8LN?NnRqaWWl)H%1iN~73+u1ChhHV5|tO?xT2IoW9(r#E?> zLKfv(EPtd0X$@0i&s@e_twbgw+%d&sA|YqpO@3xSjezd>8NI08UZdSmklCE{PRzt$ za~NGJklv!GViIMC)8H=Ob-wkts&}+sH1@`7RY3n?jFbPNm!RG2XU#8D6vI{0fj0Gm zy@GmGk$+G(*WmOz2|lu=L=<)%q0gqo1=rMCRAe5N8*s+T6eg3WNKbC@j|*}%tJ>lO zE<6Td2i4kRKj%kuHi1sn2{ZNJL?27sA5@JNo#*s>UiMNN{yGINUO>bXAd%pZ$V zv02_oDidg?>;ppgt;m|qxusn{WKO0079_S9WX~X7PJ}o9_L0Vt+YCu13F(40M!ROx zJ$lYAn*NS#5*x9>M>~RcY3A=im(I@?i^=_5HHRL>@zouQX_Gmdn*f0K+ba}Xw+!L9 zF;itI^W2KzLdpwn0-ikK9+h0G#N;@(W9bnRRNLq#{&LMY*Dm6tf+{9!!Gq#xZmhK^ zB()=_S#E+H8Sc%LO-c`cd)NHVk~fmg{;as%ZwSM)HhS0YjbG*LgX^Tk#{BTnSYLHY zN>yF>bTy7l!+;qgi|?bhq}&@e1r7SYj$Z{xUS7mDiV)3rdIobMKDM+p3@$OfRA2G0 zlaX4d?QW_UxKpk2!Gz&Ja^q*K{kbUOou*a*vj}_J~qkVh)89mK;r5Pk&#v}**3Tp* z)cSScBt(A0fywSZAz9C|9~`Hqm+0#fg`WNzr!$Du8x?q zaVE|8$LE~ePu!WUZ!zZX4SK(B&!O*qowDcVITk94Y3BzuN6^*t zQI++g&5V(S$&jp^h~;&FiH3T2LbBBpKIWoo$7-Vo7USO z!I1WfeZ4uO9XhARLBYq71XE_5{rXjlLmDhn+}Cy%&@;T8$D|+pVwSX70ECskVWgRY zy*+Ga@zKrrq?k58PQY@!#=xTc?ut|8U(#FaQErF(dzDml9WG!1%hgj5nrQP#**nYP zXc#)of}qq^Iz@B0#=xwbSB3RXSl*Q$tw>i*ER>adVi>Qy zbCxinb0X1vGkf*7d@vNi;p}wcA;{!_Bh_lZxdW#mT!?4cZf zIwSrc4GJ@h#7ZgWc}a1B-RHT9h1+>d91gcJ9xo2~i@HDd<{ACB++kxdO=v2wa)r<>F9wKm?7z z?)nZ%ZnycEs1aU3|I3(ZL~f>)!|PHSh>0?>BZdhYiPoIf@45n1Riy*qp|>R+ADfix zTJfxb7O>oTi&HR3`xc!XhhpjU?NDlB8lgLeX>WSF<+037F*+pMHXV8FQlQ_!UHk2{IclWqhA7m9JTwvlfo8K{|YSi3p-ff*qqkHYZ17OKKVRJ+PmJ z6tLX>k-dCAK}iaPhfUOC(P9`Er~N+KNN);RvDs256TUNaE5Dv(aIQ9Gk)S7T zYTvA~T@o56GTm>A9c<$JvlgmhT`RNi6BBNumVeec_|KGks)v5AhoL(p|9uze0mn18 zg$Inf7W#%_>D##-Q{ zZVsW5SVelf7#KDs6eEi(z~oHTx9n5`y?yNExPjBJRq zUlcFqp8AqCFG-=DOr8KHFe(4}nvpVphBvwe@%Jx{#R{8pl)1}Unr51z2+1cWHHDnp zUr#`>15Zw-@n2PiW^8eI7vL z-LEuwX^k>MM!r2;u0|94<69Ox_3j#|Op`8HPiJft`%rj1Xs620ib{q2<1E?Aq!^6S zUjM$LNpwk%0x%@0yPl^$e|@0k6p$v$wv2`FkCKAg`N8K8U#ErV;rH@pF+m)7x*m>`o50fbVEn736!uO=fCL zQ`dV}V)yPpyn$7o4GH3H{JzI0HKRrJ&E2~F=iE9g_f-uCMc1ZMV*$XgcNHdv3vK<6 zJ+70Ssw?dmH<6;;lANPRx{o;~LU9ox09YiMnMfP1g9&bR7h zejnR6?5K_^-ENAE9je9jfH3naVB!-_z_-R-&6hnMl8Utr40bq@5b1D?z(<>p?6%cKZ9q6g2M_b#OOCGG}5Pui-0SmUA^YDjQ=> z^=|(NV0*+Kia26+G2rlfr%fiXd(($4DWOT2UO@&>AWs)TE0eW6*re3VoX8=S z5xkP3YVewEDy&~fX2JdKc1nc=W9%n}K`u%{zc)%TBux_ejQSvA1VE~+$(=2LCG58Y zQlYvCe=NFVKSEOHvF8gBRtg(YFBO|mllib~Rz;M(jh8pC2W$w$RJ4B1vr^he8V$TI0#;l6&O7wX-69>KgU*D z1_CfGgb@DR9h3UZ;5^yPOjYx&y@QGnN zj*f+KlThK2<-$^)jT^1*2-R}uU_%%I%~!>;+V~V9f?d0rbze0w*2_%jqOVP7O|J;T zZAqHgaZ)e6i#b5Q*Dtmlq{(7gOPxW$NJ_)?EQ1`AcPO0wTS(V#?V+L;KBQVOk3jrl zV(4%uF2e?aiitNivuv$g%wqye$7>38x}wd)e%K<(+m0CFYWXfp7o{MpJ^D5PeNV|o zhhf(!qjUnXL_7hO9TV1L1akd;M?{Yxho&_|&a6PO*i)56g`6`0kHR7WVCqRW80;|u z&BXKW(LSY1oebl;} z@*)9zDG^6j6F}U^ENtIC$1>3|SphJ5V_>l>)o3EzQMwk zh}E!R7&$o#vN3yfA|Dksy3BB?68JKe1~>2#d6YMqb{qZprP^Y1s4?HL{TC(>zqQZ< zcm$r`gnvxxwhmE-#T!*b=6IeHITY zY8pa2Yr+*)#oJ>FjzP`tNJm+E^j>?(J-3pkmt06`^3~e!!RB8OZNnqR`?I47 zu&KcTRN5?2pg^W-?U^=~y}B#*L}XY7D5s{HP_?>76)DBQn+1d!J>$aT(`M5^deT#= zz;Z<_4TP(78XUTm+?2Sjqvg6A&PgoURsIG#Q1_b(PRHgXH5wqlL1S(-aLztCk4X-g zlysUJ#P3|CP1D>wWv4ymp^W7;OU!877fq0*+R%!@cXeM;&# z3qIBnzh$`0B-FfjBgu+o5@F(R6YMd?kN3o0$2U~@#?;ha-`QWh3twT9`yyd9hpxX# zx83yK8Z`BG44}Ek{G?u?E%dAbbGE9^;VOG+95p$|H&gRFKW|nYpNkWeuPasY&cE6C zCXt}caIrC%#;^rZcRyyQ0$U>2wxMa}n`E)=kxHUk)}s4!tv#_gFhD6@51%nYjwQfp zL^$Q!Vz^6CS*@}iFp<*i13Tq}#5`Iyr2n#EQ$UIvsr}PT%X?ReoVCePws`r|yj@h`X`cw;oSQdrH>>g0 z94BZO;VJA!B}ZIN0kn(2D*^44KB=b8nT69Io~=adRbGRmZ1-6H(Zhcv#YpES3QZF!Pn!Yr?x%b$VW4u!hx_8nqVGuOvAh*Y6zZ{Ap5U_Pir z;*Y$dI_~WiZK{t8v2|}p!_B6jOt-W^rECioFKgIG?B0<6tgr9O$blxq7FO)*y=R5J zX84}gTuCM_!VF40>Zns$_Fmoso#`jlL#xbYEnXF2+E{xmI$*ueQ}Y6x_MsI%1Ix%o z23_q@rd?Bodyb6#!lrhOWj}u1YxSs_BZ&3;8hzotOXBhZMZ=O{%7ki$04wu4qVcCS zM53@UKo$0 z{RG!H?lIbrG$`4E$X<2=Qf30uKl0|47!C5nDR|lz`-AdGld|X{Z0VY?$ft4{933Ev zd#i$lNgB2?eu~to|DO2nBSa`>Egcf=74a60UyC&j+(+Fzez^v zLi$R*8nd{b!71$5k-ad_<@Fw$#T%+up?2~7uL;I=r zw)>w8Uv*(Vk_Ov%%vIJgzTVz#w%(hw!Ww4d@v#^xQ0)k9{GDLLsGYuda%@LD{ekpg zB=C1K^X~LVl48_J5)Dc0y%e+mZ5@BOO^fE~$t<86m5bbaj^`NcG#0l%BW`?pOYh#} z_|Qw{3<6iPt;;L|GB}Gt337bR^#XMsTv1xzEoV{3LSISXoxjnrm>-_%*hbUZ?1u2! z`MIH7yru@OW z3}2a`_&Z5PX~)6*y_EF2{58bjwtMaxebV{4$QiPsA5rwvX}PsA@$J&j=uoJS2$IHS z{(yqE=-UwsCm4S(%7e#^&Hr_w+yq{AFjb6%@{S9AMyo?r+^j3zl*1Y$LI=qV?KTIK zKX3bODsGoplWY=&N#4kHEiI|Vsz5dX5D zXAdvAH6mCpm1@^Zo6q2~71R;Ee!&&ZCQwF?kPoye1`g1e3am(esh14A>CDmPMw4@r zbooTK5L+P=_9uGpfspz)Pw$lWp!P~1m%Vfa;2T$wZvpWTDDKHNVEf>rY1k*F;diGV z>_*|CvUg_OQp?ZN_<4c-r}TbAa_e!--J_2>C$HAsnq>|5J8}EOE z_6X0C9zG|R0+Q;spp{koxgMy{ezfO^ZvTv#61?q+WL^l=$E+VGOi&?8QgbP;o3mz} zjCjKI;|E%sLe%4ZcbWM6N8Y<7gfB7>%rDc_8}H1Dd!LpN?o{+%#ps&mUS1l)7AELr zVjMVA)Wl)2BRABR^OC2oGwQCa3Bf&EbU`Ep2O;;ef3+G;JHcyV_CX>U1i#Psm2?GSoZO9{2p4o+|M>I^5@bolhRq|kQD|W{(>(CJTkF=Y8)R> zqS_>%Wm{0TO3fHb{}UN{9(=TV_>Jbf1&cBDRqfBpbSzS3F_4;z17Qlt+F&V?@3YCU zitj+-?V{5yXh0J`o zJc_g!@|*P{5x(S1nG?MqeH0?aHhjykWottBn*`7grB612`^ZRM-L@V#+6G_z(k;|E zw41aMpkfqQ#@J%KbEQrvu2S7DxGe^sRcyssk=lr;NNrSdZmlIovy4fxV(C_cj-G(e z^xw2e71>uj8WBkVLwU%hwX+Mh*{CkGw* zjNAHbG!9oYJ2rT?r@lg(Ba=uMc$_S`FjmmQF|d5_R{$QXEb+A$4WfHL&tglmODIY_ zrHxggZnrZ99JlTkatt`UcT8QEH*&%jZ9BG5kz{k50j-50K;y({1JIs!j8_OjQ{D{xzzBb44c2RX22a|0rpUu5E#iK zRgx4(JIo=ZvMgpZPxSa$5b*`J*GhgQHJ2{r)El=D@?>}!q1$hBHkB;!_Xx! zm>KaDn58D%tg+(4`oxk=v{H7MKJyaKlyyhhBj6qOXz9RXza5GrT75C4Ik%|R%XL02 zinjxy^x9T55pOC^fw7@d13A2clDvER;e#}!WSfEx>dCI7*5!ON{~ONP#^lCP-;Lvj z<*v^zIkZ&NX)Dz8Q(BlcBvFp|P?_AaOYSAcdJ>3dt5i3+)VP8P7vI}*K)xjdd_Sgo zZW+Ut4Nqk6EQi5DckX#5CMqD|ldcg^RD-_KJwAx~NPwX9$*nz(=Jm_^eFb8=*^Bl` zZ6kD(w5FPsmw4zxwbL7$!t@vXf<|sBwyhBo`03j8a-ejDjQoohVsoWkr7c)QTS+P+-Q5; zi6=(NZDD7678Kg1z~F03Y%$GeKgl-oyqnwWzCwhM9V#&*Uvz0 z^u@q(>yHuX(EXDHE}wNt$f3tpc9GJ}-#${i07kd%^Ad5}7C>zey4;?_KZhvl45X_N zxU1Eot};Tj-70N}H6Wh)( z{LWWY=w4xN39W8D+`@f^$Rf&4@H~b}3hN4O6i}ytu{M-?{`(K{s zrDJAShSWP`aZSRZiM}K=J*H;yS%2y#L}$NC==aWu{Nm~oH$L}vq4d|Xp8)0HKoR6% z?x~K=UST0-EFeT8XJSe-jzG`M2+6NvbEl*#38bVcvDMnA)w`C#{{8Jq_q2=E_K85t z>H?~sQDqZ{`8B6P@7{6O?ewxq8$-K0W6*p(^84p--jJlTKSr69nN_{E=PlCQ)@=6Q zTs}(0*2^}#OruXiZq-^0hW9O5=^oO@*b_Qt{BJD^^!&2RiSzfmn@+<);NfGq#({i528+=UkJ5lL9Vx{~)=HV?#jfK@f|+m52fm3SfNtSG@%ou_$?3 zUST|3EGm+ZQ$@5bNU9BFlqiv$6NrRNa+&yb&k{sl7db{t1Y=%=gR?ZL!d%KeE{_vg zH4A^D#Qb5f;47k25Rw>;@=03kG?JtJ54&3eHec-HSWgs+ANaV%sT*63n}p79&Y>&N z^&<+JRmJWvwJ4DLQXF%0RDKtJ0-2v%5qij_eGs#d+CdJu9%TCL2rxfbbl3GKVMnnM z%|flgdj0)=#}w%fXAc=vzXyk4qoBIPRTINBktg(qf&lP|r^4u_r8q_oe1li4oRgV?T*eW3f${Ku)ihKFRBsEq-+btjcVd7CR179`^JOuR` z;dA+AwX4VpZn~v2)CuVdzBQ=~Q(!x3(TvMdn`(cQ3sd5C731!`J#|-s+hgzBe8m)H>phuiuQd)U52FD$X~9cCH`N-K*xqXruEtgxxf6OMBrJm0)B9F{v0sB;k?L= zWoBW(^)fU|m3MD|EgUNArSQ$PF*7w&uwrrNG&FdhLeT-wAfBkc_pw1l$62E4P{lK#jR0%r$2N4md?Z2f4f-& zAm-`FyYm)dUx`1X~l?X=PgW zfE9UC0^gN}R>PuU)0g_U_sEbbcD~>6NJ=Axjet`EMc@m}xX}qKC>Q60x`n13C9?y4 zB|I!Q0F4(CV@EE&{s>b!^&YCb3Jr^?O}V`J%bXuQm5u+*3wI3ws|gkKZE5mj zkCMC~1{pB+FniIe;YX%GzC^--cLF>L>evZjmb@;I?>G382hkmMF?|?*;9|WdVFp`V4dtWBNsmut#qaOolj(Iu`fv2)_P!E9cj^ z&xNv#UmfiUWT~r1ZK6R$M*H&HK*jzVk)%@vL%-f%WI9aqZ_f)PKNwJ}1i0?{yJ%tv z!|aWQM3R0e&Cr`M)WhGV2)8SJk_`vp-qTA15hx81$RNX4M34Tp$x7Y-(J&q;8wh9A zhS_zzl`6;SHe0mw^ap1yA`_IhBA-QrK!J=IJ;6UmI8=)zB-&@473*c-xkV28Kpr*` zHEF8?O7YhbnD><=g!7mVU6h)c&d5`c$ci6vQ@(F=7EsiPFsMi3$1FxA2kP)Y1?yye z+mNX2*lXVlFfgG_#-}baEz!DRCJZXtJ!<#NrbGJ?*E8ZK5x%Le+B)G8EXR#n*d)lx zjJ2zLSi#0{>B5#;5j{uD&7fd4MRm!KSn5mQGl}0nm{y6@glW4$U*yJ<_>{SH`0J4D zPV`4y#hGKmIUlhGp0+b_mY4cCTPS-kRTH1$OpHf+o9bC;+{v9w?{D7jyJlM+p6q-- zwuL+Eb+=S!5_>h9KZgVZ=}|07cnP$H6rxvM%braEtDD+k26r40W)^s5r%*BLm>(2< z$${2o?F?5tOGWj8WfoPR7$ke$9IWL{ zqm3 zs>DbYr!Lm8ci`2v2vPd+SY9(-X_eJs_NRNgZ@WwnTZZJt2_ghjw17jMOWseB^w#mO zvgYbEFWmY24LtM9_Q5DJY-Q+)E5!U6J+)};+blwRoap;uk-`e3DTeL$M#72eL`N;0 z@rIp=pONy>CeNCy0z>MMkl&bbS$U8hTfZ}G#rzv&iU#9*9luBXEqQmCuqp{GUPa^)csTuv96O|3fO%yo zLmba=Q8NGAbU*lrf0yr*!bVmQj~2Y8Aq6tb zs&+3%SrH!zWj3!WAku4^V<<~q%w>4slIh3_zYo%tIL_2%UO(};-AD@af&R?Yc4lg| zqw+b_sKgwi#QaHb3nDKOVSBT+zn6x1b+f9Ey^9aGvQ#a3G4kjf#WE1#VBVzEJ`7)y z(WJSpH7L9@6|OlI0iywkbWqR#yPB;Tp|Q)(#Dk3F6o@)kx1xceV~}O9ULCn`hIzL! zp)sn0#C;_|1Q*8tRG__kvKDs~E1wES#{fkYYd~lC97CVkQp#8A?7nXCs3E=pSL%vU z8=)~_3P$~|ttUU!Aj0;+yD>#L%?@3I!2t^~k<`=6Boo)0t39>%Bm-F+<{2d6Pn9-# zkCj24n^jd$-xfU%A>N!(mEP`rDX{bd8Rfyo?Oe<0w)#xN{ep zE_Qys#jOEvWNzdalgf8c6T^|ZCXG}k((pJuNaIx#;n;Ml$(Gdr2pSa6S28WXcq+dL z9Ua})_TNZiZI6>Y;wSiwzubWof&Nrxq5<~5lA{cPn5E}hV|USlRf+r@#k1qV4~q$& z7Svm@%06+u)YY5{U-B%%rZ*tiO7Rzo=&h7P2viZlb`RP2KKoTqF5*)e9fa0#`H)~m zRE|r!|volX4G%?6iU?PgSNSH0H-Q!c0I^H74hi z5JIZXUh4eHX{5ZvqNscTB_Yx)Ma^sBJw8GTRgS(?I0>J7P!h<0p&KlTtDC^xQp z7)q}0TN1rToSIczaa*z?vJxi7tI_|G7`A@FQ3}1klWWknU$l0^%0oc38}XWO5~Mvf z*h?;(hcy(za%lb8m#AvgJfV!o!fJ!b=rCqQWG%wXs7BydKvGA)I|Z# zjkBdAGP$Zw+B6TT|2nsp_VMLao?St+w#+D-TU6LbA++;vSeE-cm|qwjde7=oZC##H z6c)m9xYng{(PUZz-3FO zSY_s(*St?9Gx3^SA&%gOZHUn`$P=9sH9k}h8I(|!qVT&Y@xOp0(&%Xq4w1u4_}3uE zb2JOnAEr+^gU~{B_%5U(_sE}c!%TsDZ96Q&*Vvy_!O>K^iSA5q6^kNRYf&Ck+7L@` z)rsQc1F2ISozZ+IQY-flBp#~N^o*0d^k<39!z=1yvs4gamcC2%~^L}ZFd4w{FlYThh*&<;@%&94bFfeJ`1M{(X)>sO(Fv(%~uy|zgWIuwNgk~JN*mn{k z#U~rYIg}gt5q1im7xHP58Z^Mho!4tult{k`n&W1Jr_d}{%aiP*mWxw=Mc8j1+Hu^g z{Y(Rmt_%zPw|7&Z_pTDfrc+DO%r@5NB^;8oCe7iMNS^=U5ko9P^eKhL?`f2vGd-Ke z#q^6=J)MpZ^0SrSbA8%r0nu6JTlB9}7b5VQth0bI4nnBE|>pYNr_8(JoLk!`$` zBecLM$9;S-%ItEb;EvXYKbWef1;GF#IOS#EpaAb9VAfBq?^78ZlXkk~ULPv5oE0Ph z5P0u}7z`qt&nP^l2n0ObkfQxvd2hvf>c*XMtn-IcYxe$VHolB5ZgW1wi3k>GaQLpK zS$Pfv__HKBuN1G|#6DYfK*~44>j8}ZIg?wwKKzlrl^fVR}7js*x8%ez;=og8fkb_DD{IV3RGNSLM06o z4YN^U{PZd!tzOXzW$}+z|KXDQAqFKdvlgYK`yqz~OI3wSRn$h=CUZoFE(=3{tHQRm z(RR`0&XaT1CO`}PyiXM5`CArzyG_%pvQ`7R-@d$L4&>>rOP3G|KKlymu6q93U ziU1!d`WKR4tGEE{S)G_1cDgvfg5EG0X8D#%M}AIqaCgS6N#2b$`9;97rg+()v?S&C zUToeVz76lDfE9Cu4*QzzkHdX`Z-NwB7bB@BZt+f9Z*w@YE6Fc8NuZxBXz0K2D?WP{ zT}iPeIN7yR_hIK|wyIVIxMzQ-I^I1tcrIZ(;f|7hF6FRGsk_(bJ{QFPe-xc_RAyZi z#l0Wf4!^K?cMjBz4!Cm&&lhw;25IF z_U9bm9;KAC$p6cHNot&<fgtD%C*;J^s-ZGTYiJS z7sSSC2LEvi1F_8~bHbrVPj2sjsS(l&G7$T^7cWy-%P2IA9a!$dZ<6!ErWN=_xF66e zcwkC!kE6NYwu6^w^1X3t4cFVo+rnhgCD-9ex;zOUIf$32PHt$Ri#|LQD9WNKSGJK)*@k$Jhs$<;`?e@h)&XXTytQMC_Ou-?FKV=iHRF;&vGC6BnirlJ6pfFAK6$>BD|0au4K?8vJr zo0UW1U$!riI}1N?zM5C@GyT2Vs8(X-PM&zt_*?}OLqppgx~r+9@gX@63p`TSmB{z< zgivKuu%}tanvfyVgR%nEj}<_p!}v53VZZ>5=Lv#{h1uujpMr9Rw$=kGB6b$YVBLXa zRG5mMc-bzj{O2K=e2hV&aL)qqNpL%57N|T)j}Z{6aYsPBEo#Dxx?7e*(Jk#c(&-D4 zuhoC5;PM&Z|KV&5Ys^r_cLSr>E;nqZ{Xg33<7{?PInWo$#@&Z{;@9r-pF$$xE2Itn}-G|GHe0Q;*VK`q1GBiG#?$5IhGwg`gePG)R zWPdU?d)sz({_{pvSLgIfItiYK_=0eI!6QoVMqI^wbK+Wep(DQ0I-Ap(SEvaNXv?~j z{8r)XWN8jr>!joq&<)Q@pQ@3Cws-m`vNYGTd2oS82@uI~otXqXb%e{P`m#}z`w56OfSUpAI5q5!0BAgR&q#8Muzo7cX z{fj;9aWCuiBkz`k(c?{`;D1|&52)pB{;0n5u?ed+dmv*jHsCB`VYt0P!k1*%i3}OzW#*o|T9UITVnv>vid(K}$je2aU|5C8 zcr@8#2hHJDm6n<%&fmn}!M#p8j}RzBw>Y_pcehz$a;u!Nx$r30@dnCOdTkaODee*) zui3HM+{h4dN42S6=#sg<%d$kqL!PQmK3lY0&$6spP~hVK+Aiv!THBT-KTuxMb9J}yC8pQwCudptTQ7_)m+0=YnB zg^@wVZ)xGbVf4ABRp*2=N2_ZwG^`cVn@rXjXzV!}m`1EF6B^zo_ebm0YIav{>i69* zWxCy5&_4*B7`~1n>%HnCM*Z8QbEJg3;bGgBm=C7iD!ri|QX$ZBO4XLq{;gIOwHG`o z&#idSrSEU_$j32aaQnXi)!XwbaFfC9i_zELn!ILyEGR9KRm{WxZ8{bo6tr59>625U zvE(8Y+u0tF*U(*aUr12ZF+(;~+*;HBC@9>FfA#QQCEaLR8;?$3hHMDZ<(8tGDH zdwMuXc{D4AIjUo?n5Uft%DP={cOb_csF+ z5I%IPpuVvVs^D2(lTPp8*w}Z0n~DH;qqyFd+hG;U>3j_ogG#(ZFg}OK_A7>}AcIrZr(8*b zw1|amd7PsJ>z?>go$IKu9FpR!z7$dSmqJBbVdN%)rC)~$)?8%=5qBz+T^q_s*?vLA zngjT$T&9)f+5-W)EF(GRN@K-3#ReApPcc!Wf5#>cYdu>9f!i{)8%V1Sm;RIM@s|Hr zvSA4g>NYC=GzYX=v?$?WP^?|rG%DEXdgEg4p>p9;#lYVJfTp31hnT4<^fn=9p$Tn0 z+0uVzDKkMR%#Ggi*)c(9^+OJ_G7TU`sIoKr5^4Xsm@|Ij1jlz-a4sJkWUkHy&d#vD z$$r_GW`8bM=On34HsZsMAyW>95D8yEQB=16J=Gg?FoA(06jyn^a1%L2#>+*lc1_P+ z9#kIAj|0|Z#QCgpNfmeTIg5w!g+1)qEd&q;nmhl7oE$(@qD=a;qp_Y)Uo_VMAN``bBhP7v?VDo-aA$RjODge$*iC_+8a{9n;oKvC zcRwwpaJ}m8Eo_0i*bu+m-|}xg6|dss2$;th28YMato#k^(!KkFDIdE;v#yi=v%2JV z?tIRadt~?raWhDBap{hqm6&wv%8saIaR~VPW9t77sjyS*3Wcfn53{4j!G4!hk~nxh zam8NulG^6ZXBmLw2uyc8PC7hTk}+C4Eyg+nYPNu+*@VFv?6ALh?wQf~vo*iU1!=Uy zi-TX{t3Z|jfgxMUI`IS>xFc1dnEoC)Dvl8Z3(4vS_9^sJBjdpjyFs6qyZ7UNEM#z| zB77J%CADO_&}wuZbW6cMFkoMXDA#;X;iPgNcXm2Hb4X19g{`@>BgBa@+`t+Ocy4(~ zfA*2^P8kt2X|sr`NKtPu0p;%<)Mg2NU!>vI5eN`P$+)a)gc%_LnXi&y^c-fi5=Is| z0OTO%X{BR)d<2+xNke+M&!4At$G`cArpG#x2zJy^Y zV48>wb3R$zsvL@f*m0*A177804qG7$^3m5R>fv108hSq%6F%c>IG z)rkhI{)BD&dq{@w_A}}2{C9B$ydo)*gHxB%qGZ~lT$y>LS)?RR-e`&p15=+YIc%=- z)2+>CbC)bfKND&w{-m0-I>~2D8|Im}Lx^a2Zl^}Zw?8>ZJc1vu9mkRe{8@igei*;S z+)zzftvv~%0EB!VP`-*@<7ZjvkpcgWm!I&2EQ|lc(*#RnR|rV!`MEW!Dj&(&jR&o_ zW?s-lRaEBGsM?mVQlw%+;MlRZZyY}(OD^@15rf=K?^S*UD1C0z2OR$+y|Sca6-`qg z^p}ZyLOs|jvBRppFNEp#cNFUG$RXOPzZ8;qbihJ5@9o;yMpN?f4p8w8c=?H+O^Vb++1vlpu0&P>pAq=kJV{mHVPa6v_ypDyg64%u`LK zV~2j2+mO>xCf2eEl&$NQ`Hs$nHY^u;KkbMMHIaksKIIucaNr&FbnPXl#|)kPx3pwS zDmaw8I{i-`2oRDnz+y%m@VaozvNGDm^mt(?ZosaIX9oj<)2^0cU}Mky5; zSVS(=yYMl7Z&P2K1+Xg9xtClf)r~;RaWQ`FKA^eApxjMeLbU8q5708j%aRA-JDTur z!&J3dc{3NG+e1T zN3VtU=bh=M%f*kR4ehY?FL7+Eq&;u}9Ak*_df&z6;@0natq~Pk;ZFx!Y^`&X6C!PI zNmYY7lDRLtJQYL@&ZOosVv~`0s=F$#m0{Op(wL|HPgR}yND;14R~L)FWZkZnnrnU* zvr(g(dpzy3CHicvsIDP+I*ZhFRXoRM&wQgA7 zcf5QbV}f%gL?WE%)?vtfZCrERo`;H77G3ejj>la}c}adv?J@i?_h8z1Y_LWGmNbVb zPw?!w1FYB$!9h3aiXv>bbSR=&AJ>2m)XD$#M?M4lkW6{S`~oHvDpiq9G}Z56yd~RC z7+EMw1&PNPGO4jWvt+mW*FmgF-&~WH2uiBBwzaUAlu?{b^l>F_4{Tn4Ka5GRSN-_o z=nd(G@dwy3Kep*#q>4v2;EB=~p}i=gT5(d~^Vq&MV8tc9=2fIEG58;8(L73K<7V@} zG-KMf>jfHI7dRCdg5=^({_GV?oD{hb%8B=~`{WMSTjkS_f&9J53D2_q&*KBjRKm!*LHi5Ms}%$NK3pMzexP`1I4)#D4Pw<*=u&3d z8sz%+@59NN*?S?lBQM13-J_(xmjDjIgi*4qN_na50KvGd4dWmz2`fJtZu?pr9jlAs}P4jy^|Wo-ej73A~(cCg~$>hnbyMoN?&$X7GOV* z47z}}Tz5iE3@jrF(RG$1<5n2+UFj$|=sstb2*%Pj~v z5f3~B)7g@Tl>vFoc8AQh5?l_s*+(_e_hXNaOdYvFq(kei4fwbbDgjD9RVc3%h=Vb2UzV?7N;x z3gfJH+AnDYfAcTZ@JL#PWxc1mw^*O~v^MGU?%&?3o>JH@SmC z`dzE@)ZR;<-DNjj&>m{lGl-ArmBZv~3^)>9vkkDox?*}{aM#~4kx-OC_!64MWR-Be z1QIkN0eQWcFTqLJa_h95h1LUccFAb2cuJgDaZIQIAm{`7iK&`TBU}L9&#$|X+lI-9 z?4j_tSG3yh(9;%RFTcl(gyOZ|l|+vww7WwYbbHZ<9s2beriV#&p86JUG*kP+sJJzI z8^|++VUY|51{@IPZPj&};;%oUhYaovQ|9W20>ytKT;3y)*uO9GHtE~^*_tcb7U}L8 zBI*eWRN@X`g+&!D1UnQ4A@yz!TRV;eFU%{=_G4Bo`jGz>p*}1c)zCsE#G*b=mpAIH z9TfY|NbV2*6`_#gwz`$jdAFvefxCG&~}(d8kqf{?`uM)5XUoW`VF>w_*nK<@2H=?O*g{crq6 z5HDAU#dOu!Dyu&;84RdU1kbr&y0=Apw0G)xc{zhwBgoNWk&Uu1a{*_F2ShV-43UJ zPrpVZR&)bYo89&;z_fl0sYxg$=(tsl58190efR7L{$1U)$G-3;0HbNl{bQ;g_gSEP zlWF_u!bQS_db|0F9f7Z{kxBf ztY=Z=3~|L=vb(`a5A2f%j0(rP2U)%{mqa(1TME8L)39;WoCiZhw; z=Q|dzc>hEJeFJmaDhxuZYFk$jlQNeYOz&amrw%N@b%qBj@wPnPiq+}S)PcdRDrm_o z%7@_aC>DmuD70%IU{2=0mYS#>g!@o;K$?WT-Pa%2zNEGtfbUO;{`*+df6|G-R^DJo zF7VS32O<5%USt&JH+TBXTO%1T$uFeH$;{Jk6Vy#wrQ{$sC{6=anNv_G zL%=m!_{D(Gss|k9Spd7dG;l(e`5A9s`P$edmW6riIV)J>sDW}>0DGbB#@!Sel=E#x z>Cy#pHuO1N?Jd!Azg3if67a(G7@-^u{^sB2{S4e;jl`CVFc2lLm}NFTA7In{HF{`a zSJUuOOSU5{b8z{+@=^_K+YP&OZ zbK}*lWFE3EP`!RoLjja*vmCz@C%40Jav^>at2 z|6xtabOqMd^LHf0wj%)n$;&P7<*(obZ2sZIWaJPDfA-qMo1~pNjoQO#T(i*xf2T=k zzRtRGQj?{jkq}S>Ed_v3Zt*5bkQ`qI(16@!phY;3v*aLM5K#eXeV0Z%Ts%G>>WCCs zl?xKcwgGb|oGR}Q|94ct{c3}La4!0Uu2Sl*s-(+$B_ZccB8EdNNm_YTCh430Q{b2R zX7gX`qx>On>C*x|E3rSK|0kKJ?{hH zxj!VAz)Z7Gd&QaT&wgoR$={~C$DXd5vHvo|^%^&6xsK)p#@2jp-UHn)k-;1@LUVrB zT#G7zyvZ3FnPv?aO)XkjM$6Y8YG2JUfsR6g7pVra1^be}xufcztUyhdj8Pj2Jh+Gp zD*ENQP#0!BR9vKCL(tsj~-p-fiWkd%Z)BQQ-m_z+w#Nbn--^1qPcyMuRLyOrx1^# zyeht&!K!zM!0p8*;7v5t#YlPL!sN1w8O=<42L94(%cuxINn(~3Xr_x?!=z@T>=WHbHE|w^i zI|L+P-~-segi9~-efa^V2Ytm|(>)f^=>wytx2{vy!ulim{&nqt0#TLNIS~B(n+6E1 zICj2cZ0}HNVkE(-_NjfAA1{=HS|n9Zy8nrr1{F!=8h z#CP-@3)HKq3A=3kkADeE8j5h{dnv0cQyuQyaT7(r$}VY#-sPrW4uDR$*EGrg+*$_j8?UG8|%fU;V%sMv2PmQTgjzI9nmr(ox@l^NDuy7iV6s zxDS3*%T4;-gU1YQ-%5zCHt|&f&ZT{#c)O(4<3ArWc#l4ASw2HN*Idwzv;mfdOyX2G z^BsthmO|9$>38ogFu_8rA6*|B7Y~aH*V)4p;-Pm8%qqjqs}~%1`4rfHq;4-w_FR2njIZAH@(%Uo{QiH$4JG`6dAb!H zV^0402n%X{_35I)QmBlLU|ma(FO=7b>STV6QS zAf9z5wXJVy_=+Ze-=yiuiZQG93QQFv_qdszZN|Ol`>CCtZ%4;88^8o=IMJugJ~rPI z>6NXYxZfHw8Drt{FQ8y^TxuRS@Ly<6+@Sx=-$g&}4M-%yFl2Cz0-p)J21!q76ub>^ z$k6Z8B<@5IY92buhz}wA(k)9{s89@GAK;W_rkN_F=;UUu+ zv1FA)K{)BZporU9x+4u-DODc!a*rreF&q)Hti9G^?JxPA^)V-!gu!_&T%m*}TIlmn zMK9?ZeUt^+r4&O2-iD`rjfsuaU0fAgxlQYmc7LNCeZwv`(0M&W(~vN!T}{cV)*s2z zmk|<8Ur35JAVZ{Yu(keOTa^s)%XeT0i$mnJk#Ae!{!-dF{_`%1A6%EZl1R2+#W|^; z%iiLa*U;sJd{>Iq+scWdL-LHBh=*a|;PYX}yqhUypVVP~50t^18!5|M&w~Wl=4kHE zbzoHIN~ZIA-ux5Z@e*&BHSQudq;}J7V!JnvgI4uWpcFKyVue%tT9h7TyIHpLEVR~N z^L4OFvH1+DbJreN z87qd)`ETOMWkkj~EqWDHS{`PX^TA^-?+ZGypu(HMV}j(vPIBCsKxk?qm*u98CYE2V zg`n%I?%F(gM~RqY-^O3}moecBqtBdZ^2AwqG7&Hn(WOKjTzO99uVyxdL=KOIJAKC; zU792F|9*G)z-o-f(B&%Q_FP*^O1)?|#+QGTSjuT%7crZdYA&s##Hs+DDEu!OI7$^6 zj!~3f6BF6SOD!AjHo<|F5mMWB>WRHRBOnYoukHMhss0qj_Qe5k-k4V*-WVEU}+PW(7%nSetc3>)7Jl$ z0$Jd2eJ`T9W!f+#PZy3f%S3|Ue>cNj>Rj+c)A&*lrCmF;M@Iwg7Ga8(oyz=GF1SsZ z=*!8fS^JFzpWDkdzTn3Ytu|DOB9->MH+B!t(8@UhkR(mcX?V0qrZp7~CT0LcL$ zh}`pWs5djYaMU{>15@B7mU@gpb&f;wwxj(QE?mC9!Ei;>NHCBiAc71I4ukxSdIWlo zqg&5n!qrLNG)#!g^N&c&Q&92bfCGp8o7r6505eOn5Zq8OMBmF8$633+RH+WyNygF* znFV!y#w44-9HrrSQn=(Yv)%!b+tu7$(!NL;nDJr!9TJRA$ce*sOQF>&l6S3Us-2kR z5%b~na$-Yb3feGQ+;OUMBfs>HB%wzECo2u7!O0o&|FPCn!j~JtG;mIWG_y=vK+||K zI5Pj9ol1At4<2LO?%sX96J-B{_6 zz?M8ALKH^j(UeKf>s|JxGAxjZB9K*m#dW@U2{?EQ*Pwazn3*!)=Z7YQ8U< z=r3TCodf4p3guosHDNAj1ll2hqqFFOT^WNIvfr|H!!OYWtK85~H^(_spA%~t<2=X_+IgKy89;tw^|KQ^tQhe#E zZ2zL|*%|Wi8-c+l;N`SeCh$&z&}aLF@C%Ue?LLWUmDOW$anO!d1tHf859LRUjdXsQ z3|B1>jzd|(fX1KlcQT?XOf)O}qrml57C}A%{d`y4Xr6_PwA^=wL-VTUUY8iywesib z(CTFLMVIZ{Ye+P4ghAty%vAloGeZJ*4^kr({le6+>KDT9q5YBl&PX{DlKvc*!*!Ma zo631zb!X9CQ>R#@!Y% z9j&Fao8}=9AHU~$@%FQ2_VG3+i~do`L{65%8Y9K^JE;%Jl^1ICDWRp_W;Z|$P9{RS zpfg1fKOy-S#4`7T)K|JX8>1E|@qn<{j}LT$RctI9 z=p<`FSyv%`z^6$Q@rrR7vGD)+duflH{67+5(~$apt=Y0>2{z%w0>xVK=N(q{do1v5 z`U#SPut`h&dapmRy;LP0N8l_TrMNvo{iY3RwHggg5$jxVUBF!tbAMgv4P54U&_ENh zZxmv!+fyYfO8eqLj(0JV00}M7@5Zfppu6pA?N82;f;u$_T)RP^{qcgduZe!nhux4< z-p^P&z)#PV&0Z5#HkJlH^|=k>bKmvAdJ>zjp)&U@&qj)|8mwzqpb=({EvCi<*ak4d z-|xkm#>dHES>7x$;Vc#!QTtB5RoUR2NFTG(dohnePO4l9*m{@7*!*Buu8_Lq!Cm=! z`0K0L39o_LmmtF$Y4O$IqJB}(f!kALqP8#EDH``XZL9jB)jO3I4%rR8=O1cVd1$i? z!!iR?41|ma`PRw>D#n6C*wV4ZBy~3OdOVu&Tuyuyk9OOTTkN%k;fgc_bM_)f35(6HA#E3&*Md|Kh2U}jNL$49) zuQbrTQ^wduQeG*fNV%CXY}<6z9vKJeu>>lF4Eh$!kw;dx);7B2uJTTU5jr3uoVPw= zWBWRg;=IQ4Z!KdXt;W%ydi{pk&mq;vcwHB`YwXAh8KWSi z^0H;FqaEb_&+pME4kKhSN+<4hpZ?E8U<^Rt*nkcajHGK*>NBVYMg0}Q(S8@x1$(2> zI6tCsP@&=CEJPDN78TeKRp$?O3Rnt|IM;s&sWcphK2E-d*Q8IT$~9u9my_B`z(+8SeXwln;%q+bcZ8kBDO( z9;IO2J|4MPiX64@q5m{}6Sr#)JO9H;jTyq-A=K{{7q#6~{+a(#oSoQZ)SJLhV=8@k zSb@fjS`>SHp#-o~&TCJDD{C^NIpv_hjcd5e&t|FId;d7s6l@6G&UBTPna7w~(E10< zklT$t>_Rlf%9O@*n?`^Et@LbG^`5A{k=$~LHsjXw=4L;v?<;rqr}QnE#3XJp=82xa z=G(XViJeYmg1oidVd#RSJYC#s9)c{FU*&u8o@XQI-z~hm!?=z0j^7&___v7Yo{&}z ztae^bC;o{c(C)|SoMe3&{uiN~u3(=SLxWDp!`$Y2*})8Bk*HjZMun(RAb>`e_$|+e z{DobWNbgPdu#r1%g@2ullD9x3xH9!8;yPmRNq+Y1_!j0E>8uYYgbl5?s7(&;W^ZuJ*SenFMO)ky8OgN3`wZ z>c89{?WFbLNi{^93{?lIH#rEU!m`~(_F;1e*4)OTdP)7sbLxc)1463Hu94<+v*d>Y zI{p3z%z2I`h=kQ^qIMRp0yUkDzpi)`X;WYD=bOLBQRo*%tJ@?R-=h~rC? zr6ZGeq<>AI{Zs)EM*!Y_Ux8sbhw0`S?C|I3ktP1Qa z#{jO|Q8xdR;c>LruWhc?SK5NFV(ui9-}UqDPpH!8GyTQq8%TeEL`=o>B;YXlHDzZ5 zPP=W*o^k3Zrt}vUShv5CMZiSr(ST}k?5o?`MYyK97Vg`>1{Mx3^~dxR(rA^k7Z2k; z@F?&?N$_YOYi?JSu=@))Wr6n#@+zDlRpyLlK1Io>rfpZC9^7F@$~ zO}ku^Zw6$jdF>?<74rw9J9tKKNDeWMEW*H647g0sc(TEmq*<9Pw=N(xAu=R&kLsf5 zZ$Mcny^Od<-xzRl%|K5m;uVa=H7fbLDe8VP#TZw|o1#M9snZb@s-a@){AwQ%Bv%_r8n)#_g#)!@XYqov;&eo=g_X9N%M35uj{o*q|r z)u6JzY&CQw^SKJ-M!e5VG=nEw5Tg{HQX)DE2hV_-#)Ra&W;~rx=4snt7Xk}SmP|q9 z1Ufa%5n2`Q+Ffc9eM^@~S65mBQElLRBkgp2GebH{E=J{ks`U}7w=^ris&a4l(0)t7 zrbS8i%ChQoVlVP5t`kbI9kEA^U4Y12SP`m~`XFH3<`&G?BKe*s2^*)oYP6-Dg7M`P z%!8u(NQ>4a3-fL3;xbUC5AY?a_GRsPJ2!!GZNA>)T_65V+PaRb^7Qr**WZ@{x@{7o zlMkb67w)6-l^4;yv6rq64<$;Ye87CUAELeEeWY*Q-u+w4=OQ`a$*;~u6dnhU3K$0G z{j3Z--L(Dlm#cpS7cy(@RL*k>@cJ_lZ~ZFU$x`$Pmcb23D`Qf%k00=o>#X{3`F3tT z8v^8WkjBy^{85hrfAkntv*EMHxxoqjS_7lfl~B~O*A;wO(D zGUZeM)K1#v9dYfz^oUNOrK|QnDOUPA-^<5qe!;jHbhp1TnZb z$M!wvlR>F(Rhw3ub$JfKqB2P)`qv4xLeQOXTDD>4Xi>ema^HfdXY7#W8Wa4-B#U-_ zS6)!>av!+hXR#R2860#gT9DHuMS_Jz1g1wLSVJBr81!Gqz9)S9?u_7zF z9e4!52ze^!-aTGe)F9NqaYWSY#ezq38XA!_D0E{X8e}N#qTu(ZncBY3I0Q=oXfwt` zlGMjwyn#J5@95&QYojp_JtjK{Hf6%?&OSRTRUnKiK!2%yf}K&U)ks_3}fO$#J*wIY3!e z@4GRS|0{--J5%xC=8Lerh)D_PYsw1=3-_^v4!N=;(518OSV^jiZNv2X=<~kGG%=B= zXiMfu4s00R7CyX218}@<1TBkp9bP=;3f$=ymB9lWSN5t@WjYVMM;f?6hGV1~{#c0s zX5`RFXfwM4UBa-T`5&LALBQB)g^*S~6?#dnU#>5E@2l^-G)2AmDdblXf&oew?udI) zwv>XovIg%;?B23Cn1pY_u>chnF?9J`@lb5!E!1NCTOE!tda;XRi$Wn>a0h3$S5^hw z5)&uKB&DlI@0nBF{^&cxG(<*8d6F;wwO+rscHgfh7F>tjOdGf7sb;`|0%6L47=J*T zeOzZeU5I!zI&hcu;&Y4Y?5jv!v&dPKv{&=|eMG5%c%Ff)QLH_5Y^xxyd7+_I?iMV# z20~HHB}41$})QM?Q@<`MU8TGpI7n{x>X9?51Gf94c?!Z0zYjfw-ZW)+a)eIZ}Ao zP#uwH>L`cTX09IoCZ~5MmdV0chw?x3!9bmPbcP&^+D0aha1acxGzh2R5OWcph}-FD z^hP$B`f_1gd-g~l`xHFh>%ye#5B=K1uHr3KXwxgW+z zIQ6A5t=dOR)?1JAzdD0{7TK#TUuCWhG}<| z@D9kc{Axm3F#xcNaK)PK*CCcH8;V+Y5xG>Le1-mW&bs@nlx zRdcvUOQ!_~G((TCZcjgkfTx(PGcd)*DXfkvF8E6^0Z||kwjoz9jmGE>&Z{vR+cM(u zdgn+A5CXLidS4WGM_y^+Xp-t=m8gym+-97MD#+xIa-yyWfa3sQ><%}>w4fR*F|5B) z>-pA)Uwohem<9JoUg&nZZF-!x4)u_SSy~0u*o~DB&)_Fx7<9wyaw7P~>lEtsXpVtc z;ctEIWPzovr=!ap`7fw>p0m!r?pYW^@XxPt|A2|MOtX<@GTx#O#Y<$s>{&5+qG-J$ zbA}>sVa=AH5Rg*;rzAdH4BEI<-A-NTRSX~WA@R*9U+ZL==Eg7v+YZC!)}&6une1~M z4f7t-YS(|!gcFiTrQG@~_eFCpIlSLt z3gn&;?;<0jv~v}X2Fl%nrAAsdx)~A}E}KCK5rWeI_e!iAZMr9A&F8oXkH820aVaWH z^;8Lp0K+`EAc({-$HGSosAav{7AeHvm6FqyS4u8o>Vh<9lF<+S@A$Y$wnwJ;S|n}D zI3yY|Cj#!&9AwRa))XKkhlyH0ZFja`F15z{Nsq-V7R3lbPxLZi39jO&(58DKn+wnu3g_J<*cVu7E$TK3b zft(cMl9*UmNLr0{F-Mu=m%oVmssE*qvdQThq5FZ6n*T+^ATCpwWb86yg4MNig>G?F z+t(5I{P5?rw^LaC5Ky2>%6yYugqty?&bnhQMGPH{8}3j!DnfPyP`C;K12MaDJQNEi zSp!Qp>)!65iTdDhl;u5nep8pWxExo6JpuVP7mw;#sUM{Ix9kb9kViC@wN7GXC6;Ct zgjsVvkyIGHcban@@sk=sR$t9B-Bi}Qo|Xkyn#*f>J~n;;Hip)G#}(mroT^2`NZ*%Z z1uyez)p`%g?*x%i9!l(RR92acA17$qU@3_<&$o~4Q3DgFcVx4Cr7i|`SSyJZ2&%Y`(m>0+VBs)*H*;Kv}DX=qKUlzH-oahg?dRs8Nx@3FAiP>c^lv$Ek zI|cn{N|B~a6-9fLsO!ksL}VkR4X~95K)ObKjVzc8!bwij-*c%_MLwRyabVg)DfIJs z-;X=U7Y!=^%ya%Iu=Z-v0&>{L>!))yc}WrTHTJ)$5(M3NfO82GkUw5xoYqFQ_)pN_)Di{zZ?d9_}Q4k zbr5?GMqyC8bXcOISQ{--pV9>?;Pi~?@|YM*wZ%`8pTD55!dD=zXRigbZn_Q%PKQrj zW+0EGn?5Io1?%V||34HK<&gh>;2J)~MtE2IwkA4YIF}XJ+0?lvmmBx6CGGOVRwU8? zZd&I4;_d}*5dnNT@_w;H#nJq_sjTYoGI=kQilJ$yCX`TL9Y{aKZ=9e2YC87k5vQ6n zoNv>_a5qx6+e2ZGUtY4|vZJ^Uw;*FCcgYEsOX!^iVH+cs}1l}b_ z)DM$r;-}xtD|Fd{`MAdsR7zT37X{0JJ4BjT3?cX5pbc!DT3Jkou%UN$r(U={vs#!D z+`v#`z~*+P-7sPxU6If^B~ylQYIWFm!)Rj4#&MNOpnovBJ*dOz8(Z)sZN3 zwj%v*E;kh5AiM9lyt#Vg0l9w4Wext3qca#=bnC?vd zU2rLX?3xc%;(ZPS<%Is|WHJ1C0rgBX2kfoLL{uyKYX!Q1K(aP^{cmf zSWwZggfp{`v+C?YIs|dzLw5%3EQk% z?R69sp;Lf~4jXbs$&R{i+oZ4BdFo04v6ohs*VtYn<)Rd|A7p-I|I?--J8?^C=5};U zUT2Poi=qkSAh(PMcZ(4$_| z`rH!z_Yvf|%5-j5MV|H#F(G*OL->fGeTPQx5+2EuXM&S7MYSdF0~bmt5e8#(K8yY2 zWLmJAfKbs=xT!}KI-IR%!Sw)_k{AXrJH2bYG%CwMAFQstIIWk^{;G_>`Zr&qcZ_k6&EnY00mngf zlD9q6ldECTa~?yvJaefI!$^9;{Sd7c0F0kJEZlgduvLgI6R$x!%%RAJ-_-a>ur=)$bV` zYerkc-#wcVY*UIsi=qv$Q&*ptx^Y{(%nzpm8?1f(Qs|2ru^+BXY6b@82PPvfqAp&9 z%PhGoDNj{!sWul;i5hfse3K&XXr&6CwMMu#0^EJWM+7g6VK(PEcfuSP-qZjwKFGFH zh6XS0?P@l{5L%g#T{gF68)Wd-B{C_y${)4k}G%G0@g_J1z`l>Qfn+Jb^@(&IV z^vxp(4(rhee8{w$^@4V(kP~AiY71VJx?WZPUca{T>uIV4o^~9x&T+;{%BWZ>Dw=M@ z%RWZC6MC@f&0%Wfm8>9wgwP@+GbCpnxCzoiQ*7#IO}Ozw9@E@ox_j_hOU6UwNRSNL z%STSwA^4Lih5=;k+S2v7%PxfV=nN?DtS}O)R{u#Dm8&H_$5M(bx^B_mVBeWTU$b*Hymk8o+qy!FkKDQ z8{psybkfPWbF9eb6Pa>OgpTlV&?JDQp@I8K>avS!r0v4DL&oC1Re=gv+0{DmZa?i5 z!*V|r1oCzL4PP;L|LDh6>mHy^ehv8URrOpa_~8zlXhgRM6Oce(m0Dvg^}Ui3acYM2 zdx!=FN<)Evku4F@|8oaioGRAyTU&o1IgE6=zDpJg?3z+S2D6EEob}z}lf0E>GK|$f z*)bR{havS+5@heA2sVMAR&Z1eXSKF+uxMHfWQ9o zATH`P>OaTw@_#ePQ!$a zXgOISh(lNYNtu)>*dFlFsUjq7bdnG%+NuF{i$`N3Aw=oL2w~<=D?tDnERc|nPvBtq z6~P|1Vn@KvO$fM!NxH6w!s}GqgAQ6=-^HRr6I+-l9Kpy68PR3@!`R~{-CsWTJUpTn z7@m=x@$A-9MDYG4K&Oa{cbtGF(5T})XvZH$czSvy;meWnN>a>$at zyFWpm(tqR_048aczdtq-|E`dN=#53uo~N;u1doP~KK+^h)$iY7k6`b#_8MT?{8a1Q zxe=VB?&|f5&zmP7RQ~v%a*(*82ktpt@NH|Lq-+14h*agwQd=_@7D9`yrnyA_Iy8{E zw~mEw-`>+#*V+?Ad`WV%GY}v=Y!@&uUD=&8wg`Aj65gK)_EDAy$^?2SuL6hfQMteI z&I#~|0SgU|F1IpAD2+Enw(^hy0SX5Rg}3K75`1u~4`I>RZiB@=mifswiy}3~c&d#z zVp72&BtzrQZ@>XF;Q}U+-1E>ljFeTel<*Lz%?wB0iPTCIC;M^WhsDQX465=(lMMb> zGo3FxI~8ZojDE=AL-spI52xfJ*G!!bDdGJbcsm!Zqwfgz1y13g0VVa<)gR|%!E(ox zP_lQ3Hudn)yTK=06!56uR}Fw)&|r)1^HIf5n>QKu9nZT94zjl!gqq%l+fu-9TMFA_ z5J5E6WqZvJp~#ia)gdVrhRgt1@f2h6dxgtm28(h*WB~G zkvDSE!f)KEIIl^&YR%k{b(JMS-lXg)aNF_*E_o94XTai-l(K3Q#@02F;Ah9ff?nc` zUK+pq1=FL3h6^;Vd;TgP3l63S*$&xO)fdc(NDVJX~8Uo1z6t$`=Xn?NWAzl|s*CeXh2nV}PG5u;eB6O1g+T|i`*TPgODS!r zWVD zrHxOr4-01Y}!b!cUs4QJ*ce|ZKN0w!3j zB`|6@;rIy&C@OqyugOql-iS7_7G)a z;%5Jrr=Tk=7`~NqV=11XObLUeusnlta_L3RG^zUTeFpR0OKei15`7=uu|2&Cc*}=D zaY;cO`FdT%e1Ui_V)YcvLiwEcuQOx;Ucv@Dc~YbTsq3zh#T2yi+~mhk!b-$*c$MnK zjEfEtZe{NK@6ksmpgUfF%g8vIaJQ}dP=OY$XkdB(lNS2m4E1@h)*xDWkP%jyij3D&kVMfH@J zu__;X{r4=^Z>E?MtaKCCG0iN4adek_;^KKj@pJA8?-hcV|pWFrCQ^~W#79wcvC zD_E4PhHr8?^B387mJOrFGNK8mZ`+u+;A{Kh4t|6y3{eD z8G^D8VkWU+b%_wN$ogS@-M8+6K-An!vt%RTo9~h>l`zDfsI0alyXm790#=evebHh?c+Wi&Qt5<^*q3U36%I1z4WEvX zqd)crtyW2vL0)7?Gzq>fIc}LM7>MCxl1wW!k9IdX!&_d(K?5`1O@B;YMnKFO8VI56 zE<5QU+~iJ#roh{uD00J87K*i+M&W7w$q2Cdx1!w@ih=$}Cs39@ScEONdKPi2Jh zlulTJa0Y#oErenu54b6@`JAc-<+3-OpKuG>50J@0H6-&696l5>n-GsZ0nw&b%wROj zN6IMO#IPmEo$gWhoVsjmEmAyaVy@>@jM~^zU~&t{QVGOPEsfW%<~!pN2Rx#6CIV6X z=7n-VAZ`*luyV}D!Y?tFCb%g|nb$Kq?2y{Pk99cuWt#Tsiag=^l>FBrEkD%_fXb`m1lN9AdaD{)d|nEDLcL~^ zs_AiuJ{MS-^>PoK-t*h=1dP>xDM&t&U$J1lB&opzLuE0=2+J^FLMK_uAS4_|im1>^ z({6ADq7&N4{%h1J*8OPH?3{6D43?4n9#f5 zS&B&mOWjS`V9-O|2;K_4yzq|3U2$a-&m*f?%3C#>XCiI8}|=K z1%AyYA@uQyTJ1PElot;p`cVsClL(74Mk2EY&~EtsCLO)?1Nnk5b})yl@zC;#O;8=zBT} z9f1pn4+CY!PL59dv^x^!CY=*7|M?2U`{kPeN~Q!0Eo4k)cXV`6eB zgKD^hUN~^z{*)4$|p#ATnJ>$$DI=#59O26EG~O4fm3kzZU@8Q@>4%p}SIDM*!}AY@ zdDR>d@YsUqo~26-CfrRZ0OX9G=kHt}TDDU(Z(Fkc^S1KlxdmOxip62_s~j@DJKI1$ z1&A<>OtaV%P`Em9;yAaSQy{1d zk>j8x0gUk>h`L2>i>Peg3Atyvh#rD(ts0@cQ8qXlLwTdQ^?tfPCd(eL?G$86s%2aH2}S@NkPu;=dL% zk_18!57S8eTT2~{%inC3Fke|uG% zW`6kCg#ZbOHT%G4s)0dV@44ppF$D7WJ2K?F_$Vl_4n5& zBlwgZn6M(N3UREb{Vw&texCB*x9n{a|2*l?;17D5<$A1uLxUIDP$dJwyJvf+D~Plx!^C1*lnRIjW>+JIt6d}Gbgxx5 zSx@(pZmKk?9Sm8C=HS?%%Y&NaktN)dy2D>;#_SpbG zw(NJyG*~V{q`ly3a!VKVw$X5!Jua#Tg#s4RGNeQf_S0D0{yIye#m-#Xl zI*P~gLjoQxL?TLxO)U**+YqbarpVA%AubD|Mij4cju^A~mbz42s5Cl{IF4}Oo)aM= z)K|M@*Won~?BZJ=G4t+$K5(H!JYwtr$ZRGh3r)(9Zs`SLlUEthRytI`mHO2i{t}lU zSNgRiRAHR-3Pko;^qkR(00xVYkrT2b@&TEKhId~>7KhV@4w1o4@(ZR{)LG-)47&(3#l>byKqLHzD&>UOJB-z%Y;0)InR z*BFbWn}-rX;h`!>QLeI=Ugh}fCx+~p*mq(e>7bTnL+Kzwa>s>LJPO0IQ}8`)**690 zni*=Ke=5<@Bbi58Lhq()44z}Dt^DzqllJ7zF%}|hrSK5~8bK{;>?4s@$tps6S%QxA zts#HT5hFW1YFrj*o`aCh#{T}JF*Q1oJ;l7 z8JouS@wsY_c2VLHk_!gh2ywBPGEBu`B)%L8`>@Kok<9HdUi6Y{UCTG;(U_XFcGVnt zw}&y=Qr+I)3rUHfx9T^?>dOzVS#mJ;@R@O45N1AXk6y4bi|Gcr8L-88VgQ{5rGK@# zH)3e9;L53Na4i8D0cH+~HA%MD-BNp=XUt+;k{TwsS<7Cia5)ZyXdeaRr=QD~1WNM| zvsqirWgIC=lqt-VX_IT##ksuOfq*B!Yq-v$I>-NSBkB5u*6`S8Q&r#576Hj5PBfWU zX48kea@Q9NU^Jl_>uRzwFHy~0MTsP!yr*fC;dbwjecZ3StmHw*_h>1mf#@F>9JIxO ztUX0{Gv+o0STyZ+r-DNzbeUu0qHTxIu&L{)8f&QY{oA^UdrgVWd>m{xBcElgoI;iJ z<5l$^8*fXI^g71%&)y9ey+QHr>6`E)P+` zuc;pR!VHbvK!JOVnzg`5XIW@(UTQ*#Q?%}P^;2Gk28SMQAn!nhbIJO8;W|;CEnO2r zfp?=b6bc&&&tWnxgTTvrvkrt`5tQ~@a-O=pdIoDJtixJ#gPZNtWL`3v8V>=V3c@~A z*)C|bDoz{yBw^UscxSO-H`H5*yCjH*zYr!m)J|9Q3Z^hSNI5VhVaEBTAYqeNAE1R9 zj&@|e&K*WN-T!f}2AKz~7}adgttTN0!h5oN5YZ%%Q^tTC{!DY^_R@=g)gXv75fX6` zMc5Z7IphH14s)Gtz9=6--~JimA_DM%Fk!sDItxl)nuPu_eG=d!Ds<_JJ@OAQ4{g-7W;Hgpmn9YG2Rt;r+~fkl?;5{=@#yV;jWE_y@B&;9up4V^h$Aco1TUQeG!zQ=On^D}YJ} z+AOCA#W;)$kKnj~!ecb!`%`YpMI@0`ee2mPF#_1X z=c{HUP+_Ts+|JA8xvKV{+;(4#G;Nq~7EEbTo^J=pr6kp;UKQE{jbeBs~qcfUmX5 z7(`9`U6gipH0N|RRbe?8Z@ubiM(KVhC3$^j_o8m!O zFpXC+;XU=)DAX!mm~SMS*-e;kyq%%nv3(^{4^K|aX^s`g1|uB%7M+qAqXw-=kbV~= z{?j%CaVyih(E50noVd&SVHhf)& zztxg#Y~H0`Cdm5bOR`Bi)E+u|pC}vkqq14CQd7onO(s|l<3joC@<74DP{qPPg@7pg zX7SJ$0SH@{E_p^#3S3zx*nT0kWjYG=5s_MGO@H$3!6~z#j8s)EiP=e5RkPAA`&#Ve z))SPUP|eaw(`lKI*cGsYU+5OhZ9c{5QRIC!PL+fHZOT&(^@{$`nF3W$Kuc1`kF5oP ztcA#r3^PY=UMe@|SCg8_SD5%O@iby7^$ z9>CT6piQ*;Pi^yJZ|2r;Qieze?#TDs&$%|=rgt--9>;z6VR#V>a-I(>%uNyx-sXqt z?;HYIt)KIQLGoykmPf;_47&x}|3w?(Hd%1Yj0oxVrI0d@D=tN(9 z(j`B}M=-F#O~zbwV^~6&f!y)r`U|v3DEIfIAimH+N*jic6-1}JS{+|T;2BJst81e~ z@}|U%0u`?_>|}~p^ZAa!xEYkZ$V|3_>isVuwbjS?i0wXVt#=u7ckdcaUirK)?6?{AUIm6yY?z2<;*r?%3=fJnYL@c9;xc6mZgOyAsIh+{|M)_UjmM&Ii_5sy z$kHerIZdSy>VobGI9?pz9nZuKdX2Plg#YecBs_5N5i1*8SLHJ;7tLaf9|^4ppc_EW zgBrh=)pKwPt38&CkDS~c`)Xvab%G!faJ_BL;6F3drj<`f4mAo>N>Gw^ROGJ$*`BGh z{^(bmLn|LY-xmORh^pgppLj;lQ%c$bw{#d@AU${|K+`cU7Rh!hR z`$cuch)6OZgexBYgQsr+UC_SEK>W3hvh()}tPt0#Z(HD=&%lZN%@vz! zUHM0m-6XVp98&Z>r)?)G?Ql73;;k`YwhUf6u5;tH32KAT!TMXcH6uUVc2rv;e7Vk5jTc8nD19t?XGXp+CYJ6D11qPgr>=PDFgv7rspa1sUjjFO_FF9(o%7;rG&E+eu zD-A=Eeca&C8KX43*5WxB<)?5XOmGJ1kG;J8yF~OtUV-O!(gF~^2;SI1AQf1%!D^2| z#MsvU@Okxd$ufgpDGc~LV^JVskkz4o83@g{Cs})j>HDN##r=^Pjb4}1yEcZ`<4;3Re<#lLFye;$|--)u- z1CP5cG8Wi#6u0kE<(>emBQG5E9K=U zU8s@QbaXa}?Mh>~mBf~r61i^Iq7B@M!@<^m7X*V5L9)RWN7! zt;Z|Sgp$o@p5da6zdj?cdKrI=SCwY#&Vx3;unf#Vnt4>%v@Ge-6*aE6Fr|+d>(xno z?2ba)HPt}8y~75-`sy57n;s3^hjQ9Uz7?p{aQt+7<@;x!s+TBsl2XZ&FhD5Ls3it` zq-PNQ@1(knDCOCo5;~ww1%H5WD_}o0HDofPFPd^o(I3l2(<|AU?sF@Pj9L((gv~T4 zL8vCSVznb3*PwBC`&sWoo=#ORmG6eefQ?ZRPeOMzw)i(|4dSYVJrc$B=lJlFP3w2t75MqEA? z+)bu9H%8(6BzR>e4>LE3IlrWx8FQ4bJxt-Z2dOc{#c2xUANwjX&T|t6ULA11GmJd^z?;KzT^<>j2;1w~sdjJ6?s>XKzx= zzD>Qx)4p3mKv$My-Vcd2S6|f(to3?1c}KYXcT`R-26|IkNb)cK84de0e@p6i8Eu+O z7>EQ?0MIS=XW}7(fjNA`|1{P9>W_CKV067~uP)Yz#~^r2iaOweSCvMv>%uS3T#JF3 zCL){4T9g(jD_XG({W%JzadYt{dFhuucAJv+0ApWHlC!T8WqT4BYe(5tepfzytro4ErP89O-4xAtB+mMpU7?WI#FKb2 zcPBM>j!~Q{68@xN@cfDiTedD^aFXw;PI|T2$=oKxN`<|U#W*mH2FK^MA#tnsY5L=> z$0u@n4y0+%>flXxjimty`y0Q#Qn*(uEv)@>l_{JY^XkhUqP z348pcz}*n_gHuP3!{ZWHd-%v=$O3D+lxLB2eScGcWasHtSiU*9E8s?OFzCD_R3Fx| zta?g`iwnq?uD(uHN{WXEyJ1WjshLLgl1#a3uGpyJ_1(Q!*!Q<$m9yaGL=(SzN|8@x zudn(Z@G#n;9{J4g7!;NCr3M980`hFA8J$eH`60(b^-p~{IFGBrtOI9MQ(CEP`UX*cdP4?DE*!Wa( z9%+$~pL3W~FzD_{GA>f{lt&-kCOm(Xnx<&^8w7TCCi;2~^9|FqX{EXieKsV>M|@*a z9Chf{3ek4oa=`7&*xQ*&aJjUKAV8;O&;{o%R653htI`x?$czzeM9-A*Qv8>P?pnpR zQU)pNk#E|o&=dbq6m=JMbnr33?wtDR(!z_TN5D5iQ_0hboi!(W=DtDlg&UZ$rv_3kWXmz`jCMarCz#W=;C+NMF6Buk!Jda;QKFR3SYk8Z;b!S;o~oqD znt#&H)`%Z16*O&cswZ!E7TO9L=OngWikssPYMsGBZ#QY4Vkn~{gdS8Wd_P zs6FVUzFKBn)NYJYOOxR^3a%tLHLZh!-DEz&i>UPfL>y5s6XnQ=kHcaJILT9e9!? zlGjXj!nXRBIl>o1!CVq%4j8q8@-Po45Hk`j!jX@ri1n(~!HbtU8b-)5A%IB0UFQ&p zyCEHmR*gs;6AIb}Ea!jy76*zS`7>e67iILTw2y`uvs#S$ z*5td5=o&IbPC%rENf|r^sUj)nx6xb4ga&P}@9?ocdnw*Tj$EW3Kq`kJ59J&dcTSiM z2lIVz0($R1>EL8ABXSSJuOUw~$RjM^(lw_~aP|$O4l+M#@%lb}dVJFY=q0^Ck*Crl z|LllDeaYeQmO=y7M=GP zBNn>_^^Ogo`QLp!fi%jc@K6__XiC9Bju#Xg7{?DB2;7z{i*l|J+%%|F*?_vTKIjjJ zhL1{|3*~_t_@EOpM&J7`Lq+1p)3-)Se#VBQLSJ9d{WlnB)If~*7SX7*fEP2L>E#cZ zc2CQAHpOQTAa*>Lhpv7Ch#mamKcK%?u`fpS?cY)y^UCozEAUk3|8PXtZKlKfTC%G- zF8Y(umkM*jI6GEocXlD0o-T8}Mr{svxus1jMZ<(T1U9o^ zAxW1@2QHEJss9sKl=C#y2hudl;?%F*qC;BD40n=Tx<`e!VHfdFe9-88(`0wE>%`m3 zmo2KBtY2Ev;~y6*}+nXPE zu-!N0jzt^HwA8lL;q@A-w66|)Lb#uqqk)V zpc*ZH{7n(f5y!8fLg5@k*rjE8W~XBf1w-ID;yNfZOK#no@Yf^H01b4`TbD4tlh?<& z*na~&@#;f)-V`sJGFE=+1|g+cv09XcCjCGv+|ZB-v!NGN_oybFkm2QNF(>ck5F&|xVAzV0r(GEPKx zzB6Df8W$C2!kKw!GELPr$BE?yra)-|1oS&~`2JsN(O?xKDP6H#q8cfwGRotmP$*0~ zMI4pMpl}t830ec}vrDfr`IlB-p&JlYtAPZ#ELMbuVns#yZO>mfb96W32EvC$eMez9d;V9Gi7A5UZ zTLt~+N)|yYlpQ4oJAE(69ze1W8peE(_%R4tosDoF2LGCXTahAhgv6`URLu@4G-Y_VN6f9$bntPQOqiK!Dqh+YCk!_=9gz`ekL{QDwD zQcpP^B2rSKf!t{d{Q?)xD@PKDF(lcp@>>oFE7al!6H&5L!{9n=n2|z}5%ZuaNNV4W z3L8QZnvD;6df;;|4lTKipyu9}XSK1)2}z*z{n6hQcms__^F>-zF;|D&{Q~7ah9!=q zhp~@(#bk1A|JaN?ZmTpeKlo*U&qf8hzknfh4V|B<cp)t+|Lq1Ho}Emr7#jKMZfpkDMLgr_egQv7 zf!xUhSCs7hX!q2OY*5_jmt>2n?n&t%8&uO?(QMDg4 z43>f#JT@!U?C%YijjWT9n=x~w#HU_o6f0NZWw>`c*>+?6%N7d;jayYa7uNlbby}+B z{MY1Wj05J&>+}Jdaq{KlKe$anOim64_?4}*PJOVJxgFEUhXjXosPjRK;?Ra0lgwNQ zG-MW|*hn@YLoDX8CA;0>aXr@{1#TWgIM#AGTxo+|$xUKw9wVM4IRDbnwPhmPY)+f) zhW3mINsNRe4=ZZQ42Y4(VLm^C0+-E!;R#rr>g8X?3{RHfQ}_y=H~t?*F=lc9)A67T z)*&Fk(&i{i`~cNU+^mqeUC7MH+g=qB_PIG}gkcPd;_F_4LEUGu&{U$?RkR}xLR7eL#rNWm(wCOEd>2C~&hwK$kw=pHh{`YjGc5zV4 zj5`>&vSfFH7<|A;b8>iPa<&8qU%V5T$$;jdu#IpxYZ(#Vca4VRO2F0ld#XME$ zaV2(5WT??t1B{(py)!J;iUbl8SD?i39TjK|!Y)xX>4@#18{dIW2=6-mf97t?B*CQTeo3$+!h` z%{F9NpJk$bBdg0VQW7=vy?ctk7DU3(KVsHaa6$o(-1 z40_-1ms_FUCLt6(mv#wIsb2PmKmcGj`A;TqrNN3x_0%m!tmYqk!o$UP-!K>+rJBH) zZ&mF-ZWl{&qhS|$^eY@%ARzjvBgaAkZFt~^W5vRaj_pR0l*yYdzA?4D6r_P#@LA}^ z$tMC%ZNxCI8tfu?6H59D?jd(_wMXJwB1Gj}je`u7d4kI#;kd zXlo?128aHjQA(V7ZXGtsB&tL|>N_H3Wp7~(JBC00+8DRo6c_%u1Z#!k5BD7nK|`Gj zPPJg{K9w_DkKcpIQk^30Y20fM+BL{v`GR9_##&FB@x^}_3w6w~LVKzv+C92O7O=;B zFbje-E0U5?>~A&(OvM|lMLvA8%t)YkUS`5?d=RG42iC!&x+E3BKHR$33R2uo7X|(f zLSA2gPn~QxBsb8Y(u@4_x*bCTyfnVcqL?UN8w$TMkQv7c{QDMfZ@jVB&lK+xlw5#C z16-hl2hKhg+^jb<7@1nP4^6$DzpkPq274YWS#s$+T^n90=|?FH%L-}~Nxsx84P897LS)Ao!b8%S zWx8B^8`1B%w;DwI|Fk7YEEo@?Ue$AgrIXkUiz6eR^hHZ2#+<@3zHm0D+|FVnhcZ#C zlJ|%Gg#NEIVBPw^Q(3+vY4T*pnK0>_X%tITBD0Rh3CwN&NJhyX$wO1;iLnEYJPI*e z+VvB!P_QrpCixG^4~S7e&TM`Wav<>p{89%ehm(Ta1bH4Dd=~`)t~>yzJ0`f(Gbo1F zMOQ~dyJ#&-WYR9T;CqrmMy{DJ!Ve1uW7?q!^Flz_S7a4weukvt)Gn0Uq85NRk6(C& z2C}gLez)=Mp1(6VJ@bo^O7)`pRT&Ha<|6mKjQeLzj48*|A&K<+&f6mQ+`(_(l?3Z) zg0-)%b}x+vx_&(Z?zp69PS@nDOM{9f-?%1f=4k8jlvtM=$EVqtY6EIZAVLnAB61e% zwKK@NT?m^Rr@(-0vngFrUUz*bnpH34P>T^M_{-PPwV7w4 zu2b@++AS~;2M#b>finvLl%!)h$H0Z8fX)MEUY9#dBEJXM&iQSEW`#(=X_QDdeOQ)=G2<*i{I?mF6))IElgfUA*StejR<*)epO&MVR>UBtS;nCIp(rN8=EPZMw6>FKp-dBXmCv!oTSSpaG8jsA!etnWak506Wqx3JGGayDjb>tO3~13oelBgj+&{~ znZ~`&Be5<8_2U^ssP&HF9q)xu_LdapdVT8k-5%NS02nEV56%UgYmAZspQ`@}_`q9XV(A-aDQYugcA9K%dG(LjEugU;zIai zF&F9H2{nOd(#kAb;ZTarJ>y`OtUA)b93!j$ff>2&)g+=dUS#_A73ih2E$-GF?)d2G zK%n)uRQWy;Vt)Wl`ZZ6-{ZEW6>x5;bwF5Lg`74OzY;|!`bmcwhxWmm`nH?pv-vVx* z3N`f%uvtio=-2aHKPNnxaf;%;({y8v!Dz=#;JTRrc=wGpF$bS>@nI792q{yt^v|{% zUgL>$UDaT+NRON_Nr?F^XO38;nODCV@k(8Z%4ZTX|5<=tFMhkGF`X0L3RLv$z3WiG z;|yV98*oSxt0MVuN-ae_+ED&HEMh7KQ$oI6>5Q<^<>$*{tcl7ikB6$3i+A2lNk4xZ z8Vi-$&#!M7_lmF4|CKL9HzNp<*S*sB4o?b$EzmD&_}AlNzc1RC?8}>yA<}%WGkI5; zeaq+YOak^4!k7jg(4!#eBCaOA(y4yTR+L5gp&L%V8&amyNXpNLwE6s#=lhQ{=|f%7 z@11BRmO`6{p2E&jH3*kLN<$Lq(b@g2y8fe2p5O5;q%#pa#TIYT#-oTyX z#q5by%I;^xL^kzu-7zh+C2d_ zTFKj&5teoOE3=tT4BQlW8m zj2(WXhuho?vG)f8!ue^4P!6raEsR}4HE0HQo}x}H>zMNA-=u#|lvul88)&e0Z|0E8 z@J{~Fv$`dFo*shL0xej-%g~mL*K}FAJf9VOqmj$OR?UR}>8SNxtwnQa+d4KvL_q4p zGc~LH1m_*crGfqi6o&f>uE7F!fjneN)KTiD$PkpS$)Xz6QSOp&xt4#J^iW4%_b~RM z%U_^2{mnet5rGTW+^U`gP^&dmwVr;UD{j=5*d&Uvmw6}Heka8~3^>o$oQ{3~nhTTX zjVk$G-+obyYn@46gnS2PyTZVyE4e`6e(_e!1^mZnKfT+j@9{I+l!?tKAC5)#KASw#n*B91(w${S<^5KO*oC?Fa3uNNOMP+mJPo%u{QSzw*E^5o zra)wccq(Rj5XCi@Qlb4yZ@*qUQKC$qfOb>>2NHyZrYdavFZCWmjvZl(i6Z_K$~999 z(ac{%ugt1GCf}k>$MU^%8PO~1CDp+GDej}}(KD6;0)B7o)0Ok#?{=f`BN6?zqyc4b zpiCwCISV9F%8XX9XL;}!5DxuBAiE%F#3{EK za-|idR!e%fjAW)q;bOFD4NOns%g6DJzwzjI`J6AcEH4x%J}iJimXo4<{U34Q%16@YVtYQjl0Q14V9R?YT# zWcz2#pB)D8h;jmt*QO2%WnY^@`JwqjZVuJ-2U)9^w9h|U1;&>h2({3MI1q-oEoe+C zcZ$7gtMnaH+t;=H$vpm}VZ;C|k7H6ak15)8W4`#*soZiPs=})b)UV(HV4cb4hGo5n zqd{XvmO5O{dstAvYa)`Zz9CFg)i}ybx~s9q7IU#i%2;9ghcmv@NBIm;&|C7_RD-^| zcTyscH#{2AI`7c7(fK3IauH5HHJKGDJumD_Db-QnQR9g)0<-Q(rmhL&XgoyZ4Gq;{ zrY{1%=kOcD0UD;$c^iMUCV7a`?yq!L-Bd=`{i+H*#7DkWTYa33zMUIN1y#}^$iLdO z2V*`-*L6hSukCLVVRR4wE($ST^!P->_wd6Wk<_mLg1kiJSB~#3o=WVhBo-0l(dzLS zgUX8(EoZ^nb;Xnve{rK=%wE`9FBZEl-2{B!_ueOX0U=tf4>3xr( zy3b_I0)r;H8?tYxCSJasw`I972H-By)_|B^Zr`IGrAF@9O5Bj}g&$^;KEg2Jdi&*| zn}lO1QB(Oeego7>rDxVsAWOn22d^}frmB>pjzYga8Av>faVULhjB%CYXJTixgIbE~4w0Vqekl^@wOVgyJQlywIcH zh3<<{(CnT1GF{ZTy_D>yjE}02JF`tvinqB$NpW39UTh?09%J*#f#n#*lEgz)Zv6d( zSqp;yKvihMLIWqo{_XJ#)FtsV^h2)mWw9IpNJ8-GH>6Vc@%F;0`PuX=%NjF!NO0>4S(~gD!X7w?j9`c-U^(*QhQyaY zx&7`p?ok_jiQ)ZEdRTZ>c7E6gUWSxdMX(}K;zrbSkgEQ`N-1lju~fuF#OU7#fW_=L zuPLntyK3IHS*A8an&2j18XBS{ny4(tK`DJ2>x;s?+aik{V1KF1cHN}^BMyE#o`it- zkAhTe-$}>RJW-*wBlw&CbB+&8uwcoS!9!`NsQb1v>hl$|D<2~CHcyEzq$h$ zN+5s9P8f8z$w41v`45TAfO&y7C6~+xnSz^gIp|U)NZ^Q4UDuQFvm*5`wXijfup;rQ z#r%nfFz@jeK5l<{E107Yag+lOklr*_*!I#L8|j~V?^mjTNjPD$lcipH=D$;T4$bh+ z$t#b0`c(M}<8{fmHCoSJ4!+M4msc=d1M0D_5|*%?O%Ef*^pW3AlO`(;PohPP89vX4XAO5-P`rvoS?DfGP^ zn7uwp*$_FGlf8qYH$U z!i^QZExieR;l%CRzl+fi(irtyVN7?#*9Vw0)ASo&n*v8tqBNXH@YEPPWML;w-R?WEYr`6wxs6K>TL?m0gQ zW?C+h#W}(GHk~_Bs}EQ2=c$pfJa1~K+#V-Lrz0Wo0fT{rQ+6vEp*<15cG1a zQ!0zmC%-cGr8X6jdp0)*+8Kn+Uv*BY*P%%wLhAkO_%jP0%B4+L;%u6SF}KV_DqUTP zH)Wo&dSxj<$z}Y-9t;u2Y=lt7S!Uj-Amx5>S>$1dZN-Mh?&(6{#Uao~I|0E=**uzj z?g4Vln!I__=$~!mlt`^tdm@H_&dlZr)$)VI?U;~d0&VxYt=wP9-70wT1$cEmh7dBFBkdw}lL&J9rsVw1S zQOKp{>_9}bNR0yzj+Z)6r#)a!^6Q9s(ZbiF(C$6_NpkWek?WGUMl7aemias?r9`F+ z8@%~w>v>cBE`@$DuR?*WtzLmMw{ce3wgT;82v&;70LsrkpO_2h-N-BBfg$)VVHz(r z!vwH+?G9NsirGbs)bpf}tkBQ{)tDyfLVG<(Mc#c8LEYi{P&koM10f9mMaXuJpeLn~ zTade}CJ%nkV3_CqK%ivPdE!_i{m2V?Zbpa)uzmqTeO<-63kVb#UbGRIGafSczf5^240fZ6)Vh_!eRe)b(gYb1PQmuPJaf2_ zF@yh7mU>Ie{U1ly93AJ|L^rl=+eTxn$;P(R*tXr+*{EUTG-+(xYV0&-!*74zIrG>4 zv1j)@&pYp(J9p;tUAfNB?>5Z;%B7}*U-wiw-I-$8QaR8YjpT$BdXkXE?Wa!w6pd*> z!QHz5354;#b1@ZL$>dFB4E|H+;r!M%GbK=aUc6c5*DKxuvCLyEyh6R%ndjPW<-R2* zfl8dU&^W$P`f|J*(fdPRnke7zi_M>Psn0`->4K}UNK%CB&S60CjRJsDR}79?43X8D zD(MBmTZJT<{Jz!Qn+mHAn$;bv?N?Ur7!6?v0%A$Y1SD)US=q|$0BL-*@-tx-L;rsEY zM(~ix?+XR-$vWtl5fnW3-j|uleX@;_9J&mL?vh?~CW}4hdfZ}-Z*(#}A8giopUE>K z>{?^&R**0-F7ut|M-JT4Bab2Ow-5pmy(-q@v#9~F+(%vHsCow2gV%BZ4@0B2Tm5Vm z5|tj7NGmfy#Mm10Yhl6e&Yg(N$b$3aE?atnEOF@`3{stNj+_n?eKy#tL$@fi-@m!{ zIOrCU$cfMS%8#gCvA9}#o_{YmRFk|2NyA-*3SAtQzP?;hH*DgdsAdZ>XQKf6mjw)- z(%o4uj(yis|MC}>rgPxzwMi(Qs8I4{lk*b8gGVV#L4T+yJsZ@E_iS(-DcWHquq|#6 z`z-zO=HouwRxE6;51B4$80>Hj?$-Nl$*v#mo2Q#1j{7nHlx_IuiKR7nE9o+a&JQ+_ z9maJLICM|7?HccU>(I|o8osCaA?c`Hd%6zdFVxb7py~~;y z`i{J=tbYk-3i!R>7kuo0Qb5x$!z-X54GD_>HZ58tr(a^J6fsM;ScCr4fz$b6pY=Q8 zoSF#dB=$Rchk;Z;Yt)M)TAG^DOk`FbGCMeJ@DkrCmD}jnA$UjxEW~`sIudB z^|2e+iXfb;J3*-<=5-T;dPHT+tH)b73MU&r*9eS)Tw|NGOMq)536hw8%0_)c$UxO%Nw-{dX!EMbH~n4-&0g^Nv6JLT|^f-IG6 zLuvBVlG!z$*+??vp0eeH72v(Pj5vug&~ z%*!Qgoy4@J*y4 zKsTX%j%|Xk@2e2+wS6eX52x_{m2CQJGH11WKht zH45TZhg^;mVZR*dxsw=z!XE#MR2CWG`bXloZ3)nb#7WIz<1J_OCXi2YyS^DNG!dSn zq8V&0N7Van$lnA*OZVL?^o#K|TMznWG!!;wCvsaLkEP{})-LBH8-&63EBVVRKJ`q| z^X}&{*WZAyD23|yf8HTYnN)e*_)+^$yz)2(L0O*94dfX|D0*$Mwt4C+M8k7ao(~`e7$R8pT@b!I- zPdTp1#ma!j?R?Y*u*$tG>O$$kgigUZ*!OJ69BT>3+A`@JSQg0!^(5Vlal5^HEZ7<3 zuPnBHCnuN363Vu?;_j(v+EyvWhwnC?(Mz@aG7U-SEKTEK6tpQk&~JV5b1j^oU5t$Rlq)*8J7SJ4QmPdhH(Nd4XoYV>x|c4=>hT$xRxCtlAC1P= z^DPF$-=^f1b+La}S2QX4m|<{x^3JYan$`!!H(9@CCDDppO^!lNw$rJX>fXr_3zVgn(WDg^ZG_gbcDTl!U5Qc;=8)Al;N@pP|V}m`9el0FW_VO`;R%N2JzsBp(r4+DYY#{ z+*Q+L-#gdB_ioT0ONv|yus((>`t@yfL)j$`;))g}`^Yl0`nFlEu$IMa$(m*14KfE` zt^?tXKJM}9RN%bN`Ml;P7bhqnct)v}@F9`*+;%rq z1ole-_*w7Z_fA)^R7oU$WrlofyV2u>8;^NGO_ zLI;bpk3FO+@>94 z>%HT{B~kTaV4LCO1B+KaUI2^0&!k3rS8dC$1%ws=>40i54GC9L0@=DjX9BT40!kOfT7zzo;BU$E^j-sGRS1|h@LNuxLH?*Q zehWtWj}5*V5qoLPRemXry!szMo}k=tk4c|Q<03hDY3jm;R}%V<)pvBAeT*(z?^Bmr zYk38v;M&@;8Y9?oml|SjP|O8>7_3C-yuKK|USq$pQ6}R{iXYKQ2pliIG;oX&cW(8v zsCz-W$Y6eJ1&rw6Q+#miPiYGfuAhXjr>T;KI#?Ja9~7cph$8V$yKQW+LSFb(H`A%wCb!lAUbq4d zA3vf+7Y+d;GuLs)VdC_%T98gFG=;nYGXB4ijYVBp`taI0YO~9*qY#?$GZ8p-~yYmG7LTZAv04aToD(HxP8s8llM&LIdQ?{b?WCShhso>)R*nG ztvACFqmk#_D=OgDPK`}g}WsLpqy zRv^6s?cey@F|)Is1<{~O2tJeS%IEj_vdU>~WhT3z$}XPLi?XTcA5g!zRDJM@-)lwT zDAYKmOcUUvx0m}>d*uj(F*KgWveo_n9l!GXaGHh7jrFrmc5RW>pLF1VzG1Tkc|rmz zJQZ!WD@y`4?tt|KNpz=Hlg!S*Y7ls=ni0EA(cvncL#V**>ZcN@ZT3l^yd}lI@O#S> zA?Z&=2q(J$Kb%^c^0=ZJt=(!=bBp=FmJOMfjp2jRq^9H7tUOf8E6Bo6wCBLnKo~)G zG0bsz@H#qMlWz21iQ@QFTy@Zw)6P$N(@1P+w1XS*JWK&vGFu1|@6SAE&n|1PX$dzT z0alslGr@6ylg;W^8N%!wC6LEV{sh#)Ag!|@8a8}jnCOkr$xuf^C^KHX#9@MoYwNOa z$1(u7)W-8LIE0pp$p)l;q|0~;BgrH&08(Qpww$|;l3OC#6(k3b_{=@6+`#6|V}R5J zkDzz#MeIOe%P`+FUYdRCt}cC)G>sI+BaIQdUU>E<;F7%!@RFQ zLqflq{tO!!N_}_0wD0tgJmraWDKIZdMJx6ePqLF}%h}w&}tnKT<4U#@* zVkBp(;fv-#H`$3z9@7{1oa%oVi7GB6NeO>`pP88r5+452bcg)DFUpiHNR=%N z+OzksH3h^cDw~TzPTa*2v6xGXh1#R>5+tLaW8DM}-N@Z_N;$k5=r%xZ`#%a}ty=x7 zo$AEmey|)+O%z|x`Z1?N)|sbXvu<;#mHtkbv|ntCcafR^uFP$P8*&k~XhK z0%W1cfFBTc0Rj_2e8|xLJkHz*^}cbw#~Yn^gxHE?xX*^RG-nfWc>4N_Mp9Iz?y66* zn`xRDY4{#vNG#JBs)OrR7-)JVIy}>>?&cAg#AU1RvmBQHPX z=I?q|5tbv?o>8}Vq&Rl3fXT%(R{BJ;Czt1I zWB`y}|75ea#H}uI>m7I9Yn`6Yj{Fx5k^Nr?xoyLk*N}Sw<9Qx-I-Ur(roN`QseO6! zI_fF+4+@6!G7^2W0F<(9N%`?x0Yer&+0Ga4fE-|@(a=%H6FZrT5c%#7bbN=?;9csd zGu-t*ziWS%n+agXRhhpqo3X30j-`lr4QgQ>0NG*-dR<6vBQt_8O$&g1IEXB-X0MD% zI@t(LMMRCJL&MAtX{O0-=JK{H(pP~bv=%QpMtXS^r!NL8``P|UP&OZJ!gcgyfu00c zVaO%ZT1;PCz%UBd!Ez0X!YG0;T@i&)4l@G=Wwq1WIQ9jRy>E4+H=%s2HjI4>HeFfW zYrt*%bUb;?2wq%pKNkHp7lou|f$;u$mzXV(6PDEy z1Cqodm-~OVyAzf@SvGsh!A z9v!IGJ7%RpY2EubMLNni-pc#@nOKaX=f+R#aYS}fcG08Ds*imt2mS;-JsLJ&UKW3v zk%ei?!V78tW)~Hwr&|6Z0PjQn_uCG{t-szZBIr&-tR`{%n>a3RjH0Z8$gD3-uyK<% z6zyOvZ4Vma2D~|#ltU@Xujuk_EDI2p9-!X3t$dQfsxGbO=fceGEh7~jF@xFqUD3IV z(k&Hs`7jD|48XQHIdTQSo*|yYq9_H2lnNR7IH}2`)Z&+*asv$&iwq>p{h%H$SpS-d zyLo|cj~$SZ4cbspap8NI$K2e2?uicw5>=@S&E3r%7a&e7JC9%SgG4v`Tr(;T=O;^gu{L#mJkz%j?BeF*Ab@~ zP2yNbpQpZ22x(&t)mBIW2Hyar`xCGvbP)>}EFfThU8NpFz3l`&BRhSJvwHTdr?bC6 zTi40@+oU?z1d_3~`bObhwA-!FbBT#1L6_cY$vGwxKLuOhe=W!xh~>_QlGA~!2Uf(m&j42G=}a1cS(ND1WAmYoD&HVC_+7?S zK3TOePOyHcy@7c<(r4|kHvQZ(!XY<7f4J_zJwX1kW+4>u+}9ZK5VGCrZ&83X(mP(% zNsmx)hx2or#1?$93*3%Jn{|#z=XYinNX)EnX>EJ1G9sJ+3Av;MxE=gpZi>WdC?)tS z25gV;+-5s5apI19LTn939UJ>}m$`2NCVzcQ->KHc2!Kw-#KK>ug(Sz#eO4@ejWc^b zGf9}`?2oHLVI^`oogg?2mkQ@~_+YYn9}0IIdVQUjc-ISB#XG)xCxLH_AchlYluXvh z|83j;E1-XFfS@3VUrmZzt~JDUJ})2FfE^OKc?-Sjqjp8e%~A;6-2w-x!qFZ0=L7)2 zO(mc$4^Rp~gE1NfNCATbDne!{?tq}Q21wL%q*!AFP%jdU7%_0CE-E@c+YE{%#4KI&qX9O z^S4B!U2|5UJgj@mujRSyl*!g0}&Rf{$zZ zzqv37W=8PH*X2m+*t$4!mk19M8rFczY-up+NK!^OC3ZING>md|j{)W(-?mDxD7)0JYgrkYS zUq9s}WMil^Sv0gbv@?YLj)#S8G*kc2ktFwuhZR~5j5#gSSYrv9h^&7w(3wAJ!>}i( z8~b<{hx0Z?&~w6}Xq>7M^rLeFg3Zd}UzE`vezMj`TskB#Op#;Fgi*7FpN{JOYjwSM zjd;7JjM)Jfp5+X{4c!_}z7Akv1jzsksyB@Y2ej4&`a z6hm~vo8DF!n_-G}cP7+{MFa;f{S2F*&ax74y*MFS|3%i@~a`J_9R?E4TtVBbnou-9ucna!5jdAL_I2m%1@yI zMI3#l14f&7%e0IaR@{UGFl*d0rpkPA8h0g#gFI!_W!i*s4xWZaLoJpLk~~M$Gwp91 zdQYygt!JJ%$P)hpsS9vh!@D8~X*b;{-a1;@GlX}}MhFt!kSE5& zOcW=I2Wa>PAr}hk83b87#xaOW$@dtW+O-ih5HOZRRlnYNLj#WY+}t#NJjtjB1u*?s z>+M@>;+QT_VHeDD+4p-$DtV(N6LDE^u zYgn|%EvhOwR&k23K*X1uP!o~TV;AX1P9y(7g+6Gnbw`7Gl=A5cd;!=|^(C4AGk2ezlJqtF1Dnq4QNe@}>`b%0Foi7c%j}#*AXo9PSkOXE{?~KEn zx0$-6!WFcml3Xp36_SI_pqTA4F587yIzBQv-s_ z81&F4ZLo?GPJQ#WjPz{dv@5m=GvfSHwJq?O^3wP@ab(7_N43Kj#$&jTRXaf1MBGfD z006~;mMrnR@W-Zf2&_Rq_JwmhM9fG_RUnv+3h*0v;;m1;@=_Ksq*AyQWWP~>pJv&E zm~Ltav$OSCe7~Lmd-{6gFIdNb#8X^$s)`XxA+X{p3F$+2&ILXV6w#nbFycU)cHjVb zL7cT^Jw|0cMD0dS7MCf2l;RjXcDeLW6CNn^cpyGTFrcuhm0Fct*wQd?uEf9CJ&ydV6Xvo1=0ri^A4zd?qkI#SsQH2_d*~MLW9EiAAwrV zhUgm-=GItKT`a4I#V;bXo1(gUmugw5F;U|4zY$I)yefo{l(yLZ=xF^e`stV`4+iwg z+7XNw;LJmNRD$pV&LV>RM`_m_Xn=nDa^?{0%y2rh6&FVR+4P5OUhZIsbD^RKh3K5P zgduDtAabD-6T{;7RkQ^yS}ub#yrm!dxjvh7pl-3bm}a=`6Z_qe+JyXK8=5=$m-&_> z?QF0-I|Rz=J@s`+`|Y>Xga{qzmIPG74dJw$->u@x0yN(l+PmUX%N0-iD>?)14$3vm}IeU{z&tSxsPS7QG&)KOJ+~&l>=6o zIOza(JPQh;qW3UvCK%rqWw{vf7C-k;}!uMAQkV*NVfg7C$%;wk!7kNb>AZgEp6INce*aY{#=)qmQ>7rt*{P0GYMD=~`!#7Kun%kqHe z#ycfo=0M8aizp#KVHQ#D)2OViPD1Z?&oQ{-z};59#ifxs|0@=s`m#f99`Rn3C?438 zz99R<7)5EcjzxHEm@#pVn_9ZN(p6bIB({aq*J)e)^%C`k?TN$R1A4{9Y~3qSTO9%+ z`q`b87*jtp+dQQcv&~7QOgmJL<3ke```XY77Aw@nwhN0oAYzO*I8?{jCd?`7X^&cJ zvv68*+n)<67({M;xp%(?Pp1OzUWwcy3t-B@nP5p=?~RP$U#d7iW%2^)H)1ZzC~tJv zn0sU;2@<}lGq|N3M3i=CcBkeHQ z6k*mELGpIaKP>y!@L$=YhG@1?;jRx38z|>vB&BI^Bh!Z>>1ZEegk2vuh9!qnEQAIZ z7>qwGX>nxh{|U9sC~(F-odrvgP<$=@wgp!N#4`UogdmIJT5oIGp>aERf{oS)q(vZw z8yzAV=7NRMq$nfjr+arF2B?&UkeCFDmMJ&&eoUVQb7J(wdB9Z%sC; zJ`T6ti*U7VTjI(HORF!%(ebB9hQWq_629%L++jfuEc0wy=8~%Zhx(;1Cw^{o;q$9x z|E$d5lZ5k8ZK)_s>a+!!ze{A&=LrGs-Kbt-6El_&x)2Dms5JzhF#1XzENyY~LRw1j z^zL$uh09rPB(K)r_b^&?f*j-3uv!>22Q;{4&~GOi*Bpg}8NWv~xZvy1bP zdcK(vT>L&WI!s%jp{+KMdi`MGzWQr&eFp!I_v&nQmkgI0fp7NDqxeRF9NC8aPJw(A zk+g2+X~qx#qz_s*V9+8;=ZkJHbf8Vl7J_(>QUrh54G`D@_N509ub@bN^`S5bQD};` zYwcXj;~`>ozO{%>1-qjfCs4TzVb3J|F&-zul5l>r58u6-a{kN&ub-*K=gmtnz@-PE zkwUlT@^F|fP({?<-)8(+^j~A_gB*BNgMy9qY>K2!*%+M-_aAsf7K7qoL*NEJ^l2($ z6pu2lRycUht)3R?{ES|59cQ3}iGi&WybcyFKyGp>^}im%R|({X-@pu70Kiov8kV;6 z+1@ShBDkcV4EvEsQzQPV$Kg(OHd76mIvJsvjqi~68HO`?K~WhtbXo=cg(yX2#ht_bXC+f zC*Ww0+qqC1cb39nP0Wg|!UWG&B}ILgV8a6F1KdB^|)e89)A_nRt`cQ`%N$w?55ei71t-_ zlEE$WEJkevpX zV9eo@UF)E$09b96z<#)DVbaDB1XF>cc<%r+-0eE?RmuboG>k{F9c-dn>K{!zH^8g( z2SUf8dqKgRz2_VB5fxZ`lCR$*G{~LNQ#8^htV#q#UQ;ty->u*}jGe5{7NQ{qByRD1 zvTjHI)>XFVbeq*QSKU5=yoQKafcRkz1x_3~NX>nTm= zB`1`?1bjHMe9)E!R*;LubmtZ+m`{p@wx5N77ANt6O?%Lo5q}ZtBr3L{ZV$v6GZ@#r zh#nD-_q#IU48>e+tL?St9+B<>xAfY0xJp%JmP5eg5Y|(PgC1^o_Boa47_4-KD%4 z(@Z7oc;Ye&oq-X+2aP5qO#`vR3dR>UX62+MA8W>)y-j3fx3Qn-@wY*1PlJbeo*3Mq zw0_1_n@fA|SE967gD#Knl|=hrJs3I>s2N*U-PSlLptV9k>&#{%p6rD$R|5LVk{m=bae(026CsRo#riWa>2||++l1x+Y#R=HpTqjTsg5ahO7{Vk97qdX|~i9NqR`!O!L6< zU?4{_eyz|=T)IC*V0pAQfdLkfIp2E+uQ?Zy)=|hml>ugsn^dzx?~lRDrgxU0o!-WA z5iP^MmZ~Qk*7pO`@*40gKgb0#-Z`I~Z42yT&SV0Jeii0ischi#Fy$_>+vbBH_>`sF ztA6%=lRfk(f&&y(6;DrJ{xe*w!~UgH+GH$RTeh5o4; zS7|G}GLfFoH*QvIX7(@$Mhb?($#5vR` zt-WBQdn-&}V(P?=n!L?(3WYH_PXEmw9)$s9^MwAztt3X1+Rv!(h9w7vLLqD+uD4eM za684FpCsgPFY|q=zU33G`R|zs67JIVJfSb_Zra$lBj^;;eM8DjS+=w-kVncf zq?#|Cjhrb_+Ge7?t7&Xo=QYNybTP3M!_Z9=gw;Mw>{44(7nhgIt$T2ZYyhz^NpJQI zEpRS?z3^huGz%IAmCJGMYOIMO@I_SR=BUo0j=NM99X9>d{OEmWle!t&uMkDhDDW@S zrRDLs9tJ*Ejdj->x8RP8>iI7Ae+0t;#6IeJ{=M=Y+i6SL zLjOYt@)Mc)@*+P0XBn{v)#ZZ>a)!#sCXgPuEmuVj#^WujDM>2G)qkB|R(7JibIn>2 z3H6jx+HQsPTIy;WZ}v!J7}IYU{q@sMqRl05vF!~ny}tt?wQ_PoAY+bWNbFq+X&c0$y;>t*(icEnv3`^*ovB z&V=SRw;6jV7(9_1->q^>1YdpBIhAN6I4S!)`o_|c<(WEjr~WE@G@LM_Fn3M(;bT@9 z4*0X7Y2TwM`>h;#f-*~m*#Ctuia$^!BatH^h5j?cwOv_?1aIJ->fV^tM5KUG!hd+d zy|c}Wne_UttR}#cD2MaL6+x)G2$`H_!If}cQ+3OjBcbB>W2d!2b%>~^El#gBerU08 z$0T&0E+9{YEYhTEE-!ZqgW{&${acnL#^%d1>|^(wuKefzK0)BUQ%|yE!DIp4h#6t- zWqyq(3A-bcWo9)jQkp|85j#752`Za~mhvPQ1VxHC)KV5Dda+J5sG%$5KLM@jP;Sg| z+5$=mHHo?!A)745+ur9RF7p>34Bz)P^rXO1BIc^ZuVOGaI-cFAhChb>IzuK=fv_39%Plg&;QV;_scYprn(-qio{m(GSLK1CGmj^?0PmVZ;#}@T=q; zgykb1d5WT`pMY=*qZ_k+Bqu_s_t~?{Xt4wl9D8g)S0|VmD5hK9H<=_O9bZz6fDqi$R!clyy*7uWZxs_kJ3*6xBb)3|2U``l0XZU6I0pjB(4CiL!# zraV__@YjC^;pSdg`}6JB>rXxAJ;+2Kink1~MhM(}Od(G)7T#8gn#o{}LgH%y z8ux<|`}hihAj@e!`6CVTK&LQcq4fZ|Oq!W9|FBBnr(!KhenYx%Z`JwiD5RD_29|LA zka30qn718(0t3bCqq++aP*=IZ4o996ZIo0|a+$e-xh8d+&o(HVhQ)ziw4`1tC7ybg zJU`wPeC&!agn**>@3`Fc_CzgH|1UcUc@EOlmCq^utWz z*W7PgnlI(eCZSmc?)i8wPEM4Ex%7oxYyCeaFZ=tNMUllL)=d6JF~yt>LdQXig{ZOJ z0PAO;m#^YzY1y|h+xp#|2B&W^BV$y4n%LtzDxxDE^1*inaN8m!S z6HVhgK^3Tq`p%CeeGX*mJd;DUnsUC@%5|bZg4IidV`oe>QN4{RR5(3Q^e$+m!rBf2#Dc!V#oJ_UO$Bnw$rs_!&EZ=Hcxn z_sp4B9Iz*#?-*W%WsQ)`^n^&pfBi@P16wwvS~dp#h5B|5DI#A-oJrs~Ek)~2o|4ua zw5U7zT2;9^KDhiygXQ5;r^5jL_V4Tg1nhlmScO~I3IPZmxiCQVkwIk5s`zxDv&Lb( zV#x3fq7YRx%+r>}edXheiSJCQ->3_AVz)tm#}*(0Z?EXUN~@>Y&04}giicT$fE!4_ zK$Pg2_nQ{-k6zoymh0l@kD3c(fC-2(wG@=OymXM=Jfi7UStHMILj6nCap^x%(DHfk zs#fPhp6j?R11HZ`Y6t=+x~ZTZXkBe1ujj=*I!aVX9zgprlF8w&I^U(Ak-?+WG3u9m z72@0{g~-eJ2ba+h4G03aOHIOcgQCaQ(3?Ks6ZcCYu>r>QCS4H3<1OMp+g!I@_BY5! zvfTTlgb+58mL%hw;3bL%$HhM|N?~vS0wpbzqzLOT5=7&kSup}uOJyt*NTLP}*ttt~ zAyCC^<%-8h1ZZ?P*-X(*cnbRopfo0A7C(#EcxM&f(PII>%Urr}lg~MZt)cwYtb)%2 zbkX+#S=Br@E9A8K)z01be;0bVcKRe>k{wth?c&8SiF+NsoMMD6uwS`pg%iq`yxjSI-$COm1VLC{uil6!)v;r0?&*DkFACyI}Y?p zFXK{{i_3P`tjveyq5GdoaC`G^hj;zyk$qJZjKV0ulv590=pa+bp`kp;riogJ{9E!s zDOMjB2PbZ{A9L*f^*9RzExK;#))$Hsx;9!7Ne7|tQV?uXDF?8>kjuW$g=MkoNW;9M zMqf0>Vq+bt9aa{#OHdxY?qkoaai;DPi@v|pwY?(cwml>m?PaBz!wjH>9$%UH5a4{- zyqgf3#ku`hf0+magW{}=ugN?bk@Ah59$~QKp4jF;u<11vDx5sKP`)!+rf%e^D+~OL zao>Xb>vH`?Fu$aWX*=n72|UXAFAVE&q!STcJkxXQ$$=o&PmxtV_WI^n{5M=b4V4*& zhMosFnjN7)*iCbTcWT$dIC*PAq}3Y8e$atMNxJjV!I0=pX|WPj^|lR(IBYeef=x#n zN{r@)LyzZ&1fhtSfC!Rrj7~5v=-TMmV6L}KReeilR*|6R82fK%%V;tCglAl5eBxSfLt*m=j) z6>}Og3Ec)xBK4LkDTqNNr)zJ$>nQ9njLjs(-wt^2e~8`=A4%d>#(zmUcJL@r`^R`n zcToWE93mU^iCE4S>HWImrCL!MCt;6=V6cKP!hmF*22DB(ZN!K36NY+rOeDL?5Z(m+ zoDTx1Lu1psBoB##eE~$&`4wST2UdgPrE3lRwK@i;`)?Gw8c69+Az4AUuJK)U+~yA5 zZ>335qWrt-X%E$cd@$uR`US=wL307dR~{$#<<&p@(ZVN`!$BH##o+jT?vT@-HSI5E z8h*M4xSNCCQ)JkX4Z$qXP2@O&1xe(^&4nS3<5!tvIYR*{=<8nSmAY_NT`VABk-Km) zH%t*@j9^2YsXMG)6MPMmD-;3u&Q8 zp6``xe?qc}2nK65ySz0g4J~psLPH8U{iktFxIjre)7*zK@>5|&Fg$8FY!1Z4VO!{f zT=L}5B#KCh*x0r=JHH?_dUKC18A+`dCXplmLr?PJ0nB7oY>I={cUrE0_e64A8Y$<7v`c70;xP87=LCWl3$M{YbV`s z>x(#VBuI3Z^Y=sw(}pI_+nnn->hIgI3vNsSY&Z8|prY+4XI`Tw1wDNkf8hKG+^1u_ zSKv9XUdO4~2;36`Dz?~b?o0cME%m|D=$4U$aV8`?`f&C-`)Q~c6riFxx5CLwbzcH> z1U|ZBd3ht)D)+UU8}Nd(g2dU5{hxf(n?nO+1z?H<%5uA%JAHb$F+$!bL#w6SXgP4j z$7%HQkniJo!tdCPlXArcA1wmHYJj7{WUQuX7A>)itvwn=2N6A(;uN|p{?wlIlfZUd z;%@f`ew?l00`Ie*qfrn6EpzqZbHvwire+1b7(dt0!z6bhLM!7c?~)q?<7N%>0WB8= zh+6-=KI<;j@u^5WKlc)<0EkrFtBH1?@iMqR&M(Eynryht6#f{mb1L@hFk38-rL{&a z2;%NNogfHa3ZkPefCnGdMeDa!-)h8;tbB%m=P<0!=e9x{7rQ;RydW=uKkmh8;0G-B zqZ&$SM#wY=MG-sMb$<&)zn|V~skpwk5tc>qCI0Cv_Gya0CV}C0%ou}@?b5^TVf3%s zze1`0g{lmoZrXRfazR??wkMTeF30a*OFr^{$Mw9eh(dtCyE}gnc$baSzp8yFp&um- z)I<|uHuZPMFoKonsN(%y-xhl-2mmu@pzbAUhmtt(R&nr4^mK1*kX^oIdh}M%4+mXh zRy50uBQ8o9k0GHZO{Tl;Z=)>kPOpDm#r;Zm;;ZJU@K~*gZbpJ#M}*zRwrq&nBrKH@ zoY82>nhCshcQn=acv;AESC&_liQj4FZ)L+$QjYIzp=jgRvXOp_;qT=ST0sBOzwf~1 zxd1k)&+o&AL)7Nz1(Mz@%D3HErbL`>?1tU)M7j*-Uu>%_ArR9Zw2<+m9iWN+xgfVG zW}|!*yV{fBS~%M_&wa!0$xtBp1pkS3E3|~4to9iVD~LF2I1#o3zbwioro|4@@&~V| z6ouJ-aQBt==&XNho^QT6Vr$_LGT8Yi3x%B2i88LalV9o4jo@ZnOdwdf zJcsIRh3tS9mN)w-H0h)L6GWbFrokaVD@!9-vyR%%f;Ce3GfvOSXfVhAFh#2HP}OXp z4A!*%mctpR=Z76^;Sv-HOD=RcAt^Ts)hH%HD_vUoe(jkO^9Bn(4L4udw0f>e$0 zQD0}5Q}t5}MN%SS#N+%WoQ)&6lJVU&wny|w>WCoiiDj-5vAUKM^nhPAqI{|1c>nCk z8G;d#y;6UDO(|yM-JP8yiavas=Bh^yzZ=0)b#jEr4#n`w3EB@ zqiZCdD2eI3S1EGqUjrT^O}z#?)7N}L-b-zhY$q2G%?eE9h}#%pYEY`cVKXyMHIDn$ zw#@M*OMIdVhkrmcxZ4`c%8c>sU;jqSMORN6jmQomL0`gbn)v*2DAY5GHUWUCoJ$(^ zmO*4Z0l@rlVpF+Pwbd0bWYvyhgJUVnCmlK!#+^n;p6;O?X;elf%in|h+=n-QF9$>0 zxp3~{moC)MYnjILj0p-_FY^{7MDQ2J1TaJ4(!YE?pUb2EW&wdRAIUYJ-e6Azrg0y> z1{sGTA?-I)| z)m*9X(~lD&?y7lIY5FMKV212Lko?ynp0V(JkS6Y3UM-)GR+dJt2m!O!^dYyGqRnN7 z^P5F%rYM=Ih-b?dy!=Y9+HX_y*L=oyukoT{XU%=G{K=hm){0Ly9VfF9G4Om`M*e>< zbpSKy*MIbU!jMI5-#HaB+FKVU{#Mo@i9CKFT)Z}1Sr!{g9^EbAd8UXS^XIWHxUdoyn3d~dg3s?#UFdm>^fJ&-QE+k11~rp5oK-?7;cF5rg@C{Gn@COu z7fYKI5knV7BP6VCLQ)1Kv4KEc-K`nWS03pQfuh1)4-8FJFMjnWx2OFREe8uJ^wOGT zLt{A7Y(;e!GESd5hayr!G3Hi;yI^3dH>-&?W;f}b*4{MU<2 zd(8Y0Ioc6d&dgNW_k{(wV&mGqCQde4cdvJ`>qJQgvQ=r+3%EZNnu0I97{Q~kSMp>c?u`hDYfV%>RH(WT+1L{pR31(3&+ zYPT?OS=(dnhdp%q<@Kb~|K{?hAM{J4`*DkF6>hyoJX`=L)EZL+FN#yXA6M*CAh!ZY zMfbKnGFJ$h`Fs>mHbTDMy4sXfg|~ITm)d-dL=^gAQkim1yc)OwJbSPt0Ewv={;*v^ z*U=$lM}OgjK^|-b%hLh>Ww4FE#ocoenmXT!EdEP_wWV>s-=Qg?kHrLOy!6<21cTvp zZ<~CnsD0XU!_|#55Cwem)Wtk2gZ1hX^y*N0{Wy+8GyBa1<9zT&vHz(7ArNI4apMx$ zsGx9y0Xor{nn)yw^NU3qKPOZfg3Zm9$X9rB^UBtkp8eT2p!5GK*Y3Y8tnk2)V1~eI z7qW4l{N<#|q zC95mTNP+)AgkBwtHSg1t0)M~RnbYL7H+j8~7(7_uDaObivRg*5m9-xSQ=nxHplWi* zTilWGu`SXe6I|AXL-qEkC^a@!VNl(r;681CsrT%>g|3XI29Q(E(D{C=Kju_HP4|LR zghNZZ`Fy>C^S*oaTlHy$o1o?Q;~6{-dc{tYSz(1jvnZH2He+c=-gcUmu^T9<*&1WI zR+xR2SLCtWt@!SDMF$v?*ix{-kFAs_wnJ zKa8$#4O|6td;Y%Dd>$Kr7S;}9ji0I|a~XrRMrMNero7-F?la_{pOxjLj676YS7)@W z!M0S7r}*m0s4+L2;G5lME6<_3BA$I19bY>_>j2KhgX>=r@UymvbcH40|GFYPB%s<6 z83qnC(u^G#wlN}sXFS77DTpuw*LujO8kx~0uBU>2C%C@*GoRq9$G)r3@%sv)95WJR z0WVfpeal0SJUJBkvQGxK{SsV)5=}(Yci8Ytgjj#AWAYT@fvF0C9I&oIkbn5(8oG9$ z$x9av8d|>p12zRq(?=)P{i}Y;0zX<#a$#hv@{|hJo%g`Ht0LsYgdVz;u*x`+aGnEF zL}S95O>N$Uhg;`)`}i;QIZS`QoL`(iilxbTT9EkgF}46G-ffLxxqI%46|g`P)IBF2 z!X?MqQ4Vf}EF6~CXx;w!$IN10W0Mk;O`kCBsQJ>x4fts9{BkRd?y4XSZj6>~NR(|L zF5sqC!qFo`0ZspMBgYO zA-1-EM4CtP2a+yi#L^n zKYKnXTd{=^yc2qn*PG4iG^aNCi`&}H%dE7>xIfwD9YsE#L^4{|Uzp}id@a?Z#^f;( ze|cLmSY3soRP+IE!*+^8Kk|cSZg5(~W(144I*cI8vJ&XPU239ucCv&nl+;WB&HF$zlM0!EGyQLeH5=r5`-`~75XZFvX-Mx1|_nhZEG0n@}Nzbng^uPi25T#Fil5GrpG}i-6ES&iN?r295lmcQN zgFKB;O88!AMXkn0aA*AR?%>^rDB2tlWH>`(CD6{q_+PhWQL8rLiQ>6a!MlvHM}A~7 z#!qfb#pre{*2#wM)X^rZQn}N;7=^1l5$$On68i*3qg#*GtbK?b4 z&nY2f>=Qw4{~A);gj8L;4aToGb5@@P%67+s!uMIxWzj}KV{%;@Ep3(VdQmD8cq-@K z_)_&}WlLo#NNA!pd1%rVx^fl7VFraM=91+8us+w&6|-t&q7vDCnsk-&BJIh7uL#7T z4J~$ERKGHv*Epm|SeI_uYG|`jN5O~t0?C=DU9Ahb&%_G{@7 zU>0GV9a@x-IuFg^lB#*qG@0*6t30^)&T!Y2a|*v*lg8!YuM z(GfY6`3lbWO>`h&xaJLtcsY6kZQ46udd8bl<>cHt#<#I@homeF@CE`(%nyX9@nHu61EHQL z3)WsME{?aS@++%|W=92Ab4rMc!!kM~oOZt)> z7WMhe%uGzin=pj{>RaVcp~_t1Y8gV4fzsb?KBU#{fodem%2M59&rhsiihc;-E1Uis zJoN25k9=}3BHJ%GQ_o4N=8G*crYRw&4KkEyH1c2E@1Lq|{=ie8SG!ziy6rP&uD^c< za0;(W^&Q=DtaArpviFDaL`UvMtW1l@4AbjYC<>SbSJw*B*nPU#>e+XYE6S;^{Bw2e!e}RoE#p) zZbkh*i&pf#Pqe{S4G(z)h2Xp4Tk(V`34CxnBB0J!Ey7?Drz^;cBqj9Q?-DM|NT4oH zh>NNEWYFXEE{0*LJVPFh?|hTvubonm^f0#$JGI%gQ{j3!x>k8-jZQmmS=VP#V-rIX zT9z938Io)$od-1XYd{gG5QKOxgs6ggy2;T0br zt^7i)xh0FKLrS>=ive*0(<~PCnE6g43?bWQ1^D%ePr6>Q z-THZr& zHrP^qMW&GlV;IAxdA;5Ebc!O7kYo-IBKKveL?l@d+ zP(CV-t#Kpf`JpQ`8OaFX%pSmP*oqK$3=!j!A9_9*v-yXnvc=4Fe=y2cOkPI`bYmD( zch>#lE;b;@-Trq;9TSYvwlzxqNF;Jbx6G6^BCDL+@BTHLC0gXQ3uZWAf-50oA!KE( zeF_}2WAo95YDA6%%M1h*zl&hzVSZ^&@glwuW?DkrV$t2S!Wg)dTQ zF0a)HpZGlW@|G2c!BOj{)BUp&xcNhW9%d5Hdu#gp7 zz&>Ip2(y^?pg8JqwFat_N>R;?>tpW;X9Pn;O&y7jUk_L!9&9m5^TKWuR z@42i%Sdmu6v-Sl*IMl4OyFbh}Jo8wx#NeoIEcjD0WP#y*>V?_zp>L=9(?)LM&cIa{ zR%|%5=m9grT+B_gGOM793^!QWd0#*z>uo*kvsAh#bl4^p7zL3n|08Gs!$_Z}QS29W zKmpf#Cvc^*RftTtj-z?VJBcnnMTb$|mtYA)L<@cC0`eDf!W z%FXv2g5Ab%J{VH;oBQCzY%MOW-tEx5+WpA&y0tPsk)y-PR|Z(62o_iU&CQC{6Z~QT z#!S_vN+s@*-ZvNf;Rpd?CbL2-1}zXUN*nw@?Om_5vxWRKQIbE7iX_eCxV7nZU*q%5 zwu!JHVY>%S$Nm->Iqu$x3Hf_dTBz~2#S zOakn(v2fIRMC1OdY<1!xcc;7i(tjR9L#Z6So@&wD|E9x4570lkr_kNGW zW2izR3-%iS&A$1&^ed6RbcvUn8lCI#32@Mi)}V||w4;*Fvk50R4cpln5}75)!G`hw zI#gWN1=~ie$H_*ME&93exoT`)Oi5#v#1onJfiakV?5^BBg_n!jV0Mjy-GXOTIk^8J0Z6{_BJsvS&043V6g4;?kDn_4PgXVtzb+H zjSpnp;BQB&ME5RN-W~S%KcpImS~nbiH12X|S`n=K)rAd7NpAa~tX%qPQuiGt%_?n# zjNSES$B&Ty5~x-}i@srt_N1%ED%Fqu3bf_=u@V{|gz^CTD6GBEG8x-Q;d?w|LQof6 z8$~bOH|)Li(_A^0s`1LY;F=8Jqur+Y7U1%>8Qe;w6>qm;E_(^IqTJ5~?PBK-6BvoTIX+hdW zb($dDxom47>oui-FwLiqJ|7mIwN46#3 zqSLX-T-AuTQLLwR&QY9evA`e&@}@`2o3rv*K6ap2;rf!0j-TBZ|h`i}46)$G-i-QA~;@7qgEmR!|6hAXZ8bYPfb8g!}YGOw8TN(xI7+#20` z8`!Wx)kR@l-r1wyZa18^kJYEPIK;gt;lm8X-xTmh71(>MK(tr5@+2zC$9GNNt4;oZ zl@-KLZFu)JwHLzQSP3~ny_!^+qf9`15c@Es z+XKEOn;nWT)3*uD-UuUX$ESy_nmcNbujjzGD|c2z#V|K?hw&jm)C z|Jo9Y07B)(=%X1)5w)M`6>u!C>@Ep549UL3GQZWkpr0hiA7T8}xq8V~cCPGISF%Sw ztcUW62?+(A_6-Ic&PA+YC)m}a#&4a`T%AL=*~!0A%Vzwu_93ujCF7d;0(%UGqKtsT zizK>B%{9D{a>%l8K|gEXaAgMtqrcQ*`YP9Lh!2!Y4c(1DQshqXt**!9`&(J4VAJk-n56gyt6AJ6 zw*SIxCqtpwMH`sEHCLtBBV=PDWGnUYuZ_7EK_tpK#)>(ffBo0^$4cOhbD2UwcNsLK z`qx-rc@5v7zZP$4!WN=#ulxuqrqT_YQcEQw$1v+x$7bSbGb_ZI;oED5qjYuYm&8KCD8tDh;OXgh!wf?9mstt;Yy76&?Jz zbA9vJ1+`K=St1%s*5@VA0G#xOEwQ#ktSL0Un3^(vbx=}t@JRUl@?PT4UwhyJK~kKO z%Ph2=2{oJvrwL|B-l6U`iN@!Bxi}hndx)=A>&p6L-3V#ay2WnTwx(KR`qzCI(so4J zyp>Et%sOoZZm?F(&H$Wb&C8ghtZDfD2O-QZV&XXi+n-GFOumRRC+Td?Tb`3utN7B3oXBbPHk!5UHvUkc(tE;&ti7OD^YmyVUmOm~im|Ke<;3 z+n{`81bE5t3j)OH;TxE~F6kH`LwEi6&&e@Z@pEsiauH}6i`Dnc-9g&|D!nw5^lt9L z6~*Pg093l(aD@{|)6}!QeC2!1h8Ly0J>tn|x1@TXocuNvcfOqyC=eXrGTrTA?aoP- zeVTeLUA7z__S#&6u)I&kVEJ7T{=E-~Y^oNK*l0Ks(a;qE_s=}JXc!VXejh!am+Ct- zYGw?F?lGdv%k7kIa-6U|u`HDkT;+<+XdPc>C4&a;ki#FWvrd=`@<{vEKi1f5G!OGv zvJTYfzauD4g*~ed5)BPP($f!&{0ZtHR{Q_>gHSMXzg4?a{z<}SFO{BYyk$E87jv;s zesr@56{)B72QfvIr(-Ow$*%@0|3OcAZ)v)){E9SktrtQ$@wu|4w$( z3^{EEk_Uh)S(yw#uo1#U0A>;t8=)a{p%DJuADL6h9SX1EaG6_rNw1|&TSH~m4*d(^ zajs&0&SH&<0{zSQ8X0NL^CuT+wWZ|ApF0q+87;}T8}u?zNP1^D4lDAz;u5%0Fx=uf zWO<3pYWFQ-e{_m=C>)iv$YQB5L)~cnD;EYiiDtv+VjB(uC0SNRbm1x2*Q)73+cPpT zM0E0ip3@I@Z0V>x6wL-Hj?ktkiYEiAl`y zANVQ8UKLd!LS2dNH8~2ehDr~M(%Mr5jBC-H%$LKESN~l| z$%xmc@XW7h%G<-qdcIEVF_t^RmMcPMT6nji{iIeTLm!qbL=A+qfcj@$O9dYR) zQ0aZ$qF(un*#!2im^6rGa$l#dL~!ObDAc$X^gX__O8|+Oib?l!!0O&{j-q^q!{FgL z4TOjgIpRdE zx&7Z(^R&XXrR@Crt4;{m0Rf}=1|K!&-+?9V%zz4G(TKivNRIwmIHb3&cqk3rp%re- z>JX!;v^`1UN`fg8172_2{l`U24X@XS8;fkT?FP4Vtq&m1yS)Mb6!Q^Y;cvtc;#hlR z=ymH2H-^C3h|Nf1>M_8}aum9SFd0^K%8N&_h#_F941a)z=;%|Dq2rkM$ZyB->Bn*2 zYvT95SJH+fQ-e2{bI~~Cie!-EY08F3q@KDGyEaG6f(*Y7qa%#EVHU`Y7iJA(j=>Rg zK@}e}h1js7687gVd}_nd5W}*WsT|ua1@0*W?jg-wT^W;I-E|q}M`g2oSJ(}l+Wu5P zc_45cd{p_Lf8YPaR%HGq=<{N_QkKyffQ4K1$!~hm+f)mYYE)t<(10al1|L;-ec2Bo!B1dmP*sK zvvDjT;Hb{-ezPG#G}bat8d_^7MK|uK?T`#v+==7fXK;`Tt_cHRe!LB94s z8)Q})Z`13WpmTebhedL?g_FPg{$+(T#l8vwuj~gn@agRoFO_J{`ru?oRRj*K&rk-> z=!2Lr914Cevo6xQ&6!#4ug+PqR7l+~4m79ybb#${3F}J*5qQXgsh!GnpFrcqE@~1RuBp;a$2raaYld%U_dZ!)hxo- zBvh27yVn;YrtZB`rAZFr5ZOXN(iC$TQ8Y71lTvTU!%#tESRx1xX$%F`p7u3zoG*Rj zB@=O7aZ1s#utcU%^V&`kf11z=pIAJ4L5~zLZSrH;)F_KYrJc5|`SV%lr9YrfbhM^r zeLVnNgI33X+}BY)nypoNm_u)ArFQtXg8Hq}${-uD#@ZRM%|`%zj{%qcLty^8u^GS@ z&E6C$gqGUbgKq?b4IeUO1|?DIMXvJk?A|UCjoc6@zTZIL*JT5BUOvmpk!#lQS7K@| zs4BDeEC~$o|Efv(^Qw%;y}~LwqxJqz2bHkMBK`)P{oW0 z?RP*|NR2T0-4mcXy6`DKf_1eL)=Xgh-Eb<_XuL?4`ZReBDq6zZ8**Gr_l?sRbWK1J zP}sRIwQJv2^!Ycv3@zN7dUC|KL|xbr+9;6Cef3TaO>oG!7YZ5j9l9IQUFS(eDrYi7 z920))nIW1Z+mkXo-(I@TzI`y`!}A06^B=6u2b)&SN~^g? zi8>#S*Xl(#$mZ=b&1&IdtZ%y766}^L6+cIv8oTrA=`N|{U2#O|;HILXu#FYgzZWei zfRolTXCx#c%Jt~zJMRDh$EgDQwU7vR3$PwNo@MhWPIMBMxKoihJMN z&L?JY^do7w+UH|?l4YgHc%|9+Rc$@3ncAUxRg%l>uu!4Q&^OKFW}vz}BFu#-Kg^z6 ztkReRE7F<&`LinT1GxINB6VdY3{{Sp?T(7|1N?Gr z5WO3&{0CsanVZiJ+nRZ$Q4*kfs+Cx&Fu+iPs5A?eIORkz#Xhx)W!x@ub1%oW=i08h z;!T?gd@0a^&)I!}t{eqH*> zw22gpEChu-&vbN$;^Lvv)kSQ#NQXemxwk+-NbO)f`XmyL7{PmYZYe^<4^`Cvg221n zziSqpAH)jgliiaCubb#}6 zCp!4gclq1$t=RD(0vZXJlmnHxq`tABEUK!DgbvSK+w`P{eZ?nQT?vH<-(r=?IVqV+ zWtim|KnL4BW}HU{iQg*1g0#0Zz9@qQ%6~_#XeH#iZJQ_a1tWFPWm(A)OZS$Iu7C3E z5+|8o6JIH<|8xODmV^~hJFsccfeQ+ZC_=*`jO)@+2+C1uad&>ADuoCxs3hp6b|^)| zvSpY@`${>I30CL2aPA-713eO9(&k8^1$z-ey?J`Jl9|8`-CT?k5Hj8@)xy> zKVMl~siOV9#&&tXUk)q|l8I{FkPMWJkK3=Dx^_T%Rnqy1&o+UKM+1>GHuya%HILnC z#%s%VN?%s%J;sLvQL~`TH=OKAs1VXK=pU^MO=O*62Dey&bqm$+2Gyv@>@gaEL_zgN zNpD3&kVSCh?_Y}iiOX%89=P}OzSHD1Jk%(C=Z92a(B+EJ>XPqt3(f}*J5qZ^1A;n4 z3-Dx8S~0snrblo|Wd`mWE)*^_m8MtP(})--yubSW6H)&E68bgk3R^L|{3L$M=fFTv z07W+%uG2;`v3W0tW}_6!qpg{6*ak*T=HH%0Lo0XSH9^nA{h*1l6u0wWm`1W_J8KHp zF%)-WPq8exdQTukXN3Bm1unkh#2eg=qB0#Joiz9T9B%Oa@FNQBzG=#^0+jXWUU^ zH1*^5Z zfJ2DSqu!2NTBB0gw48LeavZy17-Dn_MszC1f1ua`FqLCe%dD&MO{y9PDpBOA=>VhT ziU-}$onx^7xp%`&LD2oOw8LwRZN@aI07;DqVeL@Cq_`Kpqk=TVR{)4EI}KCi2V<*% zkl0eeBB~ubNjqj1^SLT0JpR^V8j93DGSi#5UPeapKBN#6q&JyIS`44?FT+9KXl1*i=G)dxJ&Jaf z*Np96)M)7_Jo*Uw342=B$c&avL2qoZMRg1{10mX4BcJ2ifyN>ndpLT*{h;n-Fd8US3DDL<8o^u478a!`2 zY>r>WA2Dk&Ro-%~gTt(@W$^uj=*wwM3>~TPoDmP>Xh)Nz^_lR}3lm&*>xrBs$ ztsfO09DJ=+pz;Yn-@nUZTOOU-_w1wMuT*H$=NF^bQC3)MN(lHMDBv?!s3qNCC|zhM z-Jm7kpc`b&!R4vC6ta5(G9mUilLl+N zRN9H3Y2xAwf{-jX&?CE=W;PrU<#F@MH&D||r-DLHRr$Uk8D!d5rL=vquL^DY=VspK zInfqY72X%)u>q6zIuB+nTa`s%Qs;r=%?Hb($YtS;RP%ZoAf8awc;kg|A)>mGG#_Oc z^Z0Jj$)NajzsF&Q+=zw~^pq+5#}iX)*4=_KhyeZ&RH-}&a_#64+7#7QOAU!Yf_kxI z-4n3lH}u7cJid?i^~W>~XhR8_LztR`u2)>8^&DdaXdIaHU?U$2h`)uKbR<~GSS12e z+c0cd&{a9!c7Fkc*6dV%+0W(Z^nZ zT*&6}R5lK=g;+MwDnH$uIGJ2k;zeb0#P~+;kIgnYmyA=`NB%p4hTfnHEF~_o?Ph>b zSS_>1-Bp8CEC8oAfOp@V{lQm>^N571*#j58$93cNe&Z}NnPz1b%XI)CWpQvvo;IA@#6k-Z*EKHOJ^XF zcfbATw(sqT!Gpr6oJm(AU${4o^+O?IGIgv`GX@_5fqNamHfnEf7H~D=iMUoQDo6m4 zM|O58@$eAQhPpSpid0_6+bz=WOUxt~EteX#f4%!M8%eQn?TU!Fgc=k@6e<{<;VT1* z@skO*@&zG5269~lG-}s^%|l$J)@pyU{$hmAbe2#kk#jnW{Wj>|)XHrLk{ZBQFOybA!C?ghvP`;hn z<%we?lk7V)w5V0HIP2gLP>AJNm^bG%6R*xXbQ$zva>XH5CM?RY6!cyL0sV|hXn|$F zy`)NxLxClbphcIJ|F5w_D&w^aC2hh)PgO=lT_|ess)jVK_2T}AdX#Xn#Yv2elkvio zSkk_JdaU3oIy_w58k|TGZ;WK#kA27X<^2+InvjNYsY0HKEU!@+Ds&6}th<%$R3voZ z%qUDD7KsdLbkyu}mnEd^6C;C$qyi{=S>KtVxfP=@OH5T0TYMc!v4DBK@zUkkgRkzE zvkKJM6mk)Sw8L-p&FOI$F>)W9{*P=u=lWw9v)5+DBOTLn_ku;*n%lK+^cukeku*{n zsH*x)L``IC)vCUMHCf zjZOIrh3)X<1Y`tB8mkV!WaJ0cz}SX%do--K9UiRhkRX%CngXE5(ZEKh;cUP$e!wz# zz%qWoBYMCxJnS?G^VNatHskMLT0gdWxJ&?5OsDchxgV^w`Ae zv$V^7i+gMCa$^oS8{vO|gt8o=64P=i{h1cJSH)H?2_{OPx+F_=mJhpg^uYMY`r^zr zU6Bpp#6PxY=n#MTobKCn6p@6>XI3Z@XAgHQKj@3L1^PxvhL$O^db*DQS6oO-^-jJ{ zf5d&PH=j+_8)WVf+8kCo$~qNR?0ZXd-kaX>FWV;QjAabb2O6WtndPVQV$3qoPu!6Z zmE`k*UcP$)OZUD(z$wY-C#Ax7jDFwIca_HP00wtUQ^dygN%SU<)YKI;xSA4wcHV>a z7K%$Wy!wUVj?lLhiQ5515`SHSSe>no?Mtu%(UR7kQ?@_`E zERk;GV$dK2g1*z6B?YPsv%P5IJ%md!(S-k)UNXw)STcP8{?HhVf&vTT)u*NKcZ7=b z;I=-fm~gk0TtF=>icn+_m|_>?MLzo%M)!$k+{qwbh7tih)!<(UsPi$G4tT-dpIag} zax3S_`EmUQct>hCJCNCozFBsG=%s`CqUpSj(U#Z&@0Ok0ILwx};HbAg)^QW`)KBJ^ zrcXavAvZM$Ou9UD8MMA!GyHn&)w{Y@ZJzgX8d_%#yqk==LbSmMve~t(+;3WiWoElE zmJ+p$%ZgmB;ZTS-H4H&@RM_r)l3BAPx+MVr3z9L9U-F&|0}^4{mu=$=)>-Z(c;&c+P zZoI2;;NZR#bP$HjecV~;1Novw(ubNK*RqggS4Uz-*=<-9Y2$rK5yZIF1AR7rZ=KHVis1JSfQ>a*O#T2Pz{4zhn9PDifGx( zVpWTUMlNq-!Q{NgPsTLiqtpvyX~z=@DkV$PBfhTC|x;{$>TVksdK zRtHlJ(tcBx+iah1g2bkvfpN!zt=`vTB+~bC$qQkeb)It(cTXdJzXpTg1|qOifQa%P z6Tp`GOl`U^;tw%JnnVF?@pr&)bR7i9)_V$3%%N*}T9^)E_rn^6txLLuoG&C2%|09Y zc5)px)0M$LPTK_IY?O|iy1$d4uO;05)Nq-*wI0o>QT&xRHuO_540Y3CJErILrMPf# z3r|2g=|#W)eFiUbFqw=?=+Q>~XqgC3qCb80m)XING@6jtt&XP4i#{0jjKR))IH3X? zXzksLiih{{dFkoBav&7NMpd>pYjj_0ov9`bj^Gp}NHH~&(jK7_EAX)=175YeZneK! zZ$mnHI88sj!{8^Gck5bNaC<6zT>vuTV9Y}&tLNNoOz&&#x=GjH z2!y6t`I^D3LYBL~=E-$l2V;+YlXhG)?QqifLeTB~yQc1ccqRHm&uZrccztzWDZgHB zrzx;G&eyBZg*r6FHibm>S3z&8ltP#}Uo)v8(c@N#7+{D*l3{C7BTuIsN+XF*al5c+ z$!nA~!8UPVh?D#c&giFO($C z@SZ6WS7Pdy-{S|ahrpjIk7PN3FYL8xy_yb;V#0-z$L#(V?c(p7Z4c4zrADTUTgeC9 zi+f`J%#P|0kYG`6jf-|XsmUvWg4n>3eNW|}_(ZhuH`J@WaWP|nmKb6-&p@0|&Cl`NpM79ISbp&Q3X$@Oj_dFgznf&XA3yk;>WAQ!2+!=$I!6Sg zw*fBwe?Gcy7o|OB;MQJH>tphB_PZG~nH8*;qs&+56{(a>aCyd*JTI0dc;PE>eUj;q zly&&rz;<4WTOvmwiHUsSiv@+#y^4e)A@Ky2`5)#2AfWL2-?}j5>IVHulAuW76$ku+-S+b1r(prEgu7dU%vT<*t7`BkUkw`3Z1b+Q z?&znd$?o;s@4wu(xm~(H$7K*{8yCcSPi}pHtBuZjHS)}>_~Gj($7v; z#QH7xHfuzw8+Sy#M7)ksilAw-OqtY6$JjuE>_=^7ai;`2e?T_~AaQ0bqEnVbL+U&A z<5dZeQ4Oeoq66Fs(obvYh7TVwC;;p z_|)?OX-peIH%aYjcplnWH)=tjFNl%H1nsPDwDCy`5>-dWgnnOHFSpYQ2ASUPQ=N7F zJUF?$5IT~%q8uS6SOh1CzxfV&WR?Bh;x^L53{`~fZOwTAD1CJ#ngAHA)J?V7QhII z$<&P8#DeJhMGgT~>DhGehUrkTAn29l^yOoyKCpTz_c8>=ONcO#^mURqAo)hIGx#K+ z?J#BA;9c~JZSaa&C}1UK_ZhZg7P;aR42MLf6Kx@2sOU5N7K;=i@gE8+3VVP4t>Mv& zV&4$qYu(V@U7e}9kQ;8OTr@8D@3ZVFJ%w{TzdMlxR8Q39BX-H?C@VuI%t3KeuUN0Q z_46$tzIF$KqN#c_J4U|+9j!IQ5dQ|!!+`g^?4^;AdvWdCKaICKtZeT0zu0`b>M@`= z%oE774Po(=X5(~m-PUf1tRRnIQHk-DH$kl6bY(Py717L`q;T4|lgOM%&@wt%(k+yf z<`5E%`^L@?V^m~%GE0Cn%qHuzP1Rp90@T_Q+b%f5ehe!it(Xf^=4|M-*Bl0p*!CM- zHluDDFO@JQvIQ^9eJ_I4|LV<&SEsYm58F%-DoCaP`giF_|NDz4;(PB3Lf3;BG>weO zPLZW+ITfCZFMTwZ?t8TS%VA37=EEh_QYfzbav*WZgFBH&RQ`Ymy$DxIz$Fg0)pZ3! z7Lq_=q@Da68C$vhV)gEosg+XGlr%onhJ@p36hNr;y*ekhM+j-2M?R{FGHMyZ;m8UK zCoIP%8S~0-6rGGR(u0_#CJcvOadMg!7{3;ULep9D-)URSrzUQEWrF+-R0LV2&JM}S zE2gOHugIq7Go}wOQr?#~fCX=@tXf6B5~hHm^&G*%-Ca?So6GuR84sugPe(ZU@XOs* zQ)F1Z(T2zE&CZUmnYTa5+x_0w`ohB!l(ypSXpk^D4`X3f zPqp^=_i_`BfIR0`?wqPG@$Ey+iMbAj4=?A38CT_&hAV>l;=gvz{u zVm}pZISq5lC*UzBetY@Hy*S9e6$s{x14^beAn^+Z%uhnU+rUYzqY|r^c0WZIR}WQxKEa*r zi$g%+NPr`vhK^v4fp1lMa~ds1q(c`HDsF>DZWlUqh8}x9zF%27atz0L*%K`*mg8_q z-RhO=q%c5;y}sfbrr)()6Xk|O9T-a!90V-h`7vj#bH|AnGmf+u3OOc?QSgz48B@%{ z(AicPQSqIrtq^cG9>Qu`f>05tBx!b7K0jd9R{hn`E{*cFQ#6>+Al659r4=OCx_CD4 zQ0g}AyH2OTo)_vUzgaE!dOgY`+t@5%5?ys6uTLz_Lxo9)+_a6hyZi3t9QfXhC=d{u z1fgH6z0bLx*R;7%1yK-8>i%>HyJfjs!meWe5pQ#m{HM?%G4%n|4KYQJHQ3Sz3!)&W19VPXtT&?4&#D<-`yC`16O zMVxyQH{0;$XjP+yq2r5|s9X}MW5RK60`5;Ql6dcQBXTyw?|!}f?#lwtWMOXPisEtX zW4xPNuONx@yX!+woo4DtC=zAsjFYteLG0x3q&(yZGXC1~t87cK%>2_HXxZhG_Kw~YbX%#U)O6p$gt!POO0Yp|^z&yW9GaJXgdFhA zLqYH3_>0__A>k_I2(netDbgg+ZXINLX<0jV&t<}*_33rxdDSY1OyML7Xx>mR;MCh? z7ucJ`kC-`&V06LFSY6tMFxIvr4?~*9@Xz@3iHx!8h7hii|@VU5+v! znHzUS`^?ZbW{hqB`C$h1A|SC6+UrL-eabzh0Wq|7(w#X<3%yfDqOb3SW{Dp!V^5CVkOLi(nD<{0XwoBmyO&o! z;w>o^rT<4sx^ls2pToyBpnLf>kB2*3xe05p|OLz?~lc z`PYlb$$f&%AzZBa9o#fg9~(z}v%bycupdUoiVNS@^}M_&8P9#^&ZN)6<$OcMC)L=V zuxUxeUxTU0h(xgLwU1R>+qwKjwxNfs%&=GE6ch$<7-dRhWlFI+v_p;Adbx-+A^ttO zg`f^IAivFw2(yA1%ew*O$(l0|93?do8l5pgYBf$czb{3b+X~92X$2)MeHa_#{3Pp8 z@LljL-)FK@8aiY5tm0wOpSJB-@SyQxXzEZc(vxWM(-#E9twog7(?+Yca;HBUYk#0f zx+9;+?1(l{nW@pztUlbTQoO!hqNP%XTxex{Vc9O5AWUh+#pa%VRtdw%hqQfsmM2_J508u9 zt*(bvx&47Q7O4Nxsb<16G3exMoZ7ia>L|Qk=|o2?2;>W-hi0LaMEcg$HS);NE%SiD+EkTs_re#uPNP1wH&l zIeAY}AM$q?yAauh5~HjT!d!n?G<;Z@ zz=gROtU;0aE}XQa)TfY}Sju7>Sa&(9ijfHVufp8Xner>R7AqkOY!K}#i0&xh(A6;E zflpI)AQAP66ik5()8R8ygA4@l4Fm`bIszr~S7?YVzCJ^xbTlB7B{(bbKpM$c?4w%d zH`^3c8detCLcXxOX_xOj$aayqj`$p;9Cu*R=yA#CdXISeLl30mbr@3Vj!e;+%|kjO zIOXc2;JyTkWuMIQH(C|s<1l;h1gAQz+;eTc)2ZFdl-b-Dl!ll7uhCPx7PTMocc;w< zi!Y!<52;8wjcOekOdfz~_=P1SE`VvyoNj!VFeOQWv|#)J-mDz*QJSod2J7QW@4w=J z?p6V?gJH_bI>+`;)6M>==N-YL^)ZWce-NYWFWK$5;HP?3M2*S|MC@6}qsr92rm=(4 zHRnhm`$W+U36M2+Hz>9C>65*#D5+P-IxrNyNnl$E>wzp4*(8?9Va z`5%fV$IMDUL|M8AX=J2Zgf=2xaQxcJo!pWk zVPtqDE-fK|ixBWY+QNRezksNvrjZG^v8yLO>TFd8q3PB7fetj#k@MP-vmUSb$IG2D zvUn3tc;-ny3EsWi*?Zbczph;1!Zk%{4UBnEp#mP&K$if>Tms6fZ^;mN#IpO`))O|F z01~n3NjqW&^!|YojdG&W@DqNKXco#6{!u}O@2byll&ky4oRRrCXn@PyRXa1Rnz;YH zx!R1qPh9PfX^gk6=?Lrz^?7L3ps}%jH!DYdg`B%gaYTF^Pgw;BVWh552aA=g4z4=wdZn zGGO@@tj8KqiiYq(h9Hapf?%3vafHYE>)(T;=w$G)H~U{xZb@<3f&#pZzpz9bmY0hS zMwREYszm;C(VvX%3R_S@(%@aF8QSNR0+9K20P!@yS}ksCiW>aOJX$j-CzXUrxA+K^ zOH;kr%9d*u=JMLXT-t7D34I+Ch8l@8^xq+@OmO_5cqdh*fgJ`0?O@<8l#Yv9I1TKD zInd9kY{^U*zSqPM5FP_2L6=>d&(GvBVL#A@65oa$rQ)(NJ>h(SN99^ouW|_(U zI?K~4$(iO9uIvQrToj+JJRFh-&G0SUD^*|_ z63#S>fTHp|iV#9Ls4*4{F42INu0R1r%uDBPGm82teU%2ZWlpALZYOW*&-mu>bu$pa zT*3VnS6)79FJ2>vB~e|0VkT|lzWUcVn`uD?n?CEQ&g^n zC&21-7Bk#Bp8C=P>Km!Tle{bb>Xt`#c1wkdt?TV{kl84zE%+ALOcxlWQsN@L5(t08 z43|#Ovlp7~ZV6ee`Q;-r8=KPXJlLg^v{v29(fO|i!$r@cxq2*$QI~xwQmwbMx+#OO#=M%kasI0@}9V z2k9rC0^Y6D>px{G3R_V+Ff7TP<;2rV$DT2mf?IoxWxG7@nO}Ekz<&NG2o46+$^be5 z$nI^iPJH}PC`S22o2^uRUk2!n{3J zX+>m^MQ)i8Hy~?LL|6})hSs0}qV!Qfm;URZ6laWuK}MlG6vI|B&&w;2O(%3*N^JXP9+x__+dW@o~N&^z=Vi?wocv5iHI&{z?lAKGA>E{m4~M` z2&t6t?NoS1JTYWx2iAD15pdO7C-bc1_05k z>UTfiwVw5?b^m1ncjn%6&VKLx+INg_3-fH3kMWEyB`EkmU_FwxDn_)W$atXlzFXB+ zigWAApw8f%&X=j`wA2E47e}|dR)Mv1MRi;~cNh(Ak8=QfL(eCkZ*)lzKPI$THA`lC zbeY@%(Wcw9$tK4YRCMx+ujI_%MKLJ}^w)Ifd|?FaHok?GN0tF@#2=GipWwFK{ouW| zkWks;RV8&OjH*lzm)M@7O+Q!(O`3ijm|J?_l(42rzFgl;30BX@#h1AM8Ujwq!=mJN zZ$c5&G{kH0w3(N0@RK$UcO#qZbo%=71Yql$Ul$v&a1hjq*>8j@PxgsyuoRoL`w^^K z3S*m=<+gt3sbOMMWbXlUf%`JHaLB?T6av|!e9LTb$fjd(O#b5%rr_0G5E=q5QAw<% z^mWZeK;~UoviNPlr1sn8%mvm>7?7oDhME84+8E)(Sttawty|Z}KFsjKMrpNwM2R+E zb0kZhLwN~0s^hBuWHXwCelWFc(&-5;sYL_3`1TEcCKnkce6HN1$aSP2Wqb3d!V5Sv z8&!CGW?pa**ayEH8oHxF^OU^%zwvk$tiadDjn90Nb!Fw-+%4Jp6!TCT3*e^XE@2W6kJi2W?Xl zZw}&m&*7nUM2hszRV0sV%aHB|Z}ZLFL(B;jnJ}ZLne&vuo4ZatLGiWx;C>e^-avfX zM-xYGZ&V3OqvLg(oNhohk#6kbJIX^!US2|A z$htVD1Viv=`RR!qp)Z;J3afb>j7?VUZmsI+B33F-1*D(T^!|$+?8!g%bnATUYT18EYF9tbo>*-60Yb|LOG>ceY4lFaA|5xJO+G(>jZ_GdT>zAtVgT}$#HWPuSGLNuu@8MjAp#KF% z?GE=Qyrr`7H@s)6iBLFDTMsJr#yj>FSQqCN7~AHmC1oV5Kqz|eG7&+$=7DG3Z$9_V zg`lu}(ty@i0t13|$0n zk9(#;OqiuMT?2D#llM4;BWh{U5n(M_h0pYSHMSR+ZR?DkU;nGjk-*8u;eh_?!NFEr~Zg!wN|v^dnb%@$JRAX<)T zI{-lwaF7!rU%f{A!7GhgVM26qmH($PNK(QP`_;_XK;! zsd(9`_{T@H7TO=W1;AR|bWk?~tdg;#11bz3xR7d0idYH0qEiQe?)=Q8uZwkM_uNLp zHSuB4ylhI;gR_?@D#z8oe^-lAw`qdU@cu=noSQJPBw^e<);i0$&)VC7y*LTR?8Ty< zkhETQ+VG-|sj|mGKW}<|&Yy@~pKh4>M}1=tkUpmfQSzkNxS#xW+{vG<*%7q8{W~1d zt`OlSqvhLXi#AisO?teob92f>)3lw>ze7Gswo<|=OL!AWo$KYD5M#L=P%mnpX2KlBJ*P|ixhnK8FsgS;X4z=^@H}!YGFBF=YtrBSZ#`JWruD~VL;*tfLF@6kjVuhd zR;_W(8?|?TcoyW0io3osaM+6!{NOBcAPL)yyy4zNCR@j6=>iS(tB;Ji3+P!_#LDR` z!;yivi`SJlTf&cyi7KCqxsHJ<5I)FanWhMMzC!hZ`sIj6B&VKw)V)+y5lHr91`58O z==}jfusVsHK7t@5jXMj5__DxCJhQw4G+9K3A{uu6U<|-*S)bf2$UG?7d8F7)%-Ph?taDk zBMH5nB8F_FNn6PpgRdl#QYCkAl{LH|Cz~Cy9u#WIS?K?Qv+VuXf@E6Z){1;e+3qVM zsQY|HEtR$EK{FTGJ~HHkftYJ`!-La;ARGo0bz$7xKf*T^jDwh|@^P16BtxM> z`++@RlpN}jY!`G|SNSy(h0brQ^{c1ZPA#cLty)8!&e!LIiGS~T!lHNx8Wq1Cz^f?{ zNzc=TF4SB?I;5Lp{1#%*3{RR%G3ZMz0(%JuY+kc`Z#=T-$`~NYE(yN3(fKf6KiiQQ z|NYELm~{I%%lZCwE-RS_Xp5mXZc~7Z)?PMivrb_E<>4G^plm8 zQqln2b>T!0LJBZTfkGfU(h;kHhD@?L%W7ZG*5w_X*{-?UtKDC07+l4cJI(u^ZpTQ( zs@;qKrv}C?>yZFe7IV*_t1Z&raV0#v>riKC=E4&1Yy(rrz@LYsx0+$#_fBaS@f)q- zG%vHS*@@SOri;p@?$kb^5pH(i8}z^on<~!V49wS9sBXX6cgP5Satn&zc@@T6m~{A} zOCFwf;fmX}60Qp}3z|>7c-%5z*3CO&){Bd}PxYM@8ZZ96mYPb`?=~Qbqxq4?jmRxo z`<=Ya{iP_81tc901gb2pK{A<1Q^6!9WF`73@Za&I!pTFc`uxhZtBXSe0HgvJ0I{C4 z;{qOfGGYQV{s#6wmULBXd@bt2FrQ0gn=v@uqK^<-xWd)~0rpd-P=u+f*ezo8JZtebZo3fiJ0HXlSH6Z{DhO-M4SqEf9KYCRX4#i@OFs0i|ANP``1*^xq z#JqRn?g3k2=%xeIY%nAc&lcaKUdlYjsr0VL+j*{Bn|H}?3faSE1%eNVjf~UM5-tuE z%6*tt$G?ga7R_p^;szg8UlL&P9!HdEnVCBv3vtA*qSNJ#v2Aa{02cmmf|7X5J$rSJ`Y>G9`HV>M(w6++NOb!J5z4W=Y21|Zts z?MrmV<34Yq6T82x;oddQ(1%1k8d?0ryQbBy|27DmD&{pLx51tS^L2~2#Y!Y|XtYUB zRX_D}3a{m>&ik!X45}s#(`K(`0Z%{ptvCl|1GazGBjb4DBV=AGlX@j8;Cqy7jREU)%88 zA5mPL0}$6ZZesaz(Cbpy$2L*1yeudQ!zj{r&Ufie=ug`F;Zi^BPJy%H=JvhUTFDL~ zTy*He)$h>9{V@-o5baJDX5JI8%7@p^!i&k=@)1PJ2V@h) zFmu!?2EL{18JY7d?{Jo>n9r*zc$+s3+!+lj2m^A*<*v|_SqIpYSb3c0V)ZD0nuNoD zmzmd{+99!-0rEqT&`?w{5_*slq&}`qehy>3lpR;gN`jOOS92QatW7<7kZH*MDAY1*u6S-D4)E7~_kSEO2$Dx=2`YVzIJKR1>2=QuuZd%_QY zIv^N5g3RioH_ym~$}oZmhkr?3@ZJfKIP%o2&}+Du5Bz%T#W^{iJ%lKAP^gFon0KlY zwC^8l#|=RW)9_*vLd;%-p+07vu{=&97FL>`zd^*6pu_iEz+nY?~)B=`pN<@19hlpbteuB zmt4s(8_AF>OUdLP3u?7H1uxrg)bm7#zivnfKxvP5yJMmy@~_>MRTiiPU09gW$c-s} zce;}N`uI@fd|z6^z7@N;;Wjw5(6u1h>BQujMj!)hU`7pT=AL^nPm;lkXu*~?q$=Ft zoDTAx$l!CdSdeT1{JYF1{27Pzuh;25 z<^b*+H#qy_`;qRZVSHcM4I^47HtBRIl#AIeV|>x)N*vhY&!hC&m3|#Uar}IDJO4oB zc%0jfhg;fn=&*S%#7%*C;$-nat=R%oEStj{j0}E*a~}dyJRf2~C0OkE{kdaj&?BNa zQp4qP*LRiMQ%({bxdnuC(+YJb2Ls58BX&p8_n47^uR`vNNdN7aBx1Bp`~WZGIzse^ z*C5FmT#7zuW(DF5Er3747&br{}6mR1!ju3jNIuO{FH^NmSQO8)PdfP=G%o!x&)Md@DyM{X zl0={|4niAD4!;eq3I8C5ipxt-nt6?ynRBzHYG+1{Nas?3&}2JuwWNA>uzQ0hardNP zFp-PKj>h2n5)6n-34@-o$fQtP+QENBZ0zM#ZRwRhHS7=62IBi z!;8zU0`3zPa_Gc|j4?yJbj%x0N|3lgun$#HfDAQ>eO9;5IcP@QmHm4cxF>h#(|NL2 z5~U^*bnl^H6dn9NW)>u^^I|Zl&(g%O>AE0SyEwt9F7}@-ZnK#Pwbz?+HjSwO_>O{x z%j0jd3I`kpq%qc9c1zYshA}9xOc5(5w>SAvN8Q^hvkpG0z1hRIM%3cmJkaV?O_HaE z+y}ZkhxWimSP5tST}amay(JwTvJ}4ak8RhCP&Wmyc&qsAIP76|=G_xAn{7h=4daME zM=D{l&-l_Kb+gK#|IZUXoS46LH~YHfD$6oR9gLqO7>uYS2~H|evAh~c0fHYU*45Oe zU_Oh=O*byDjx4?cg#Op4{;=UJFDa>AvMRaZ<88Efjee=);D0Df~9oem!dE1zK_K6^2WKP>&NS;$ZthqfB@~LOXo6c_h@xf9&%hJs+V#x(5LpvBdx&z~p z2)SB2e>(9_@GPgG)3o3F`9ARRDfkiplumZHPqPh(!3LzvHne<178{ILSDokb`l)%2 z$8H+gtU8oG`U8C&#vLq`<<{Bq&sX(_#hE%$dM5zw-$JV;IKPfVzytS;ig<+|D8dAL zB++8_z~r6DxWuWmzti54aNN5P0&2`z8)2=)3nE|z50kKzLY!xgF*eSO(4~zk64tu6 z>BDb0Asj26=?7R6a)lGwj{6P^H}q8oJwq$Fz|mv4$uaOT4&8|`2FX<@6h)b}lN}6! zL{s7)7QJu83b}i9VhuLgyZ2iwS;myg$S2$vP#Li>{EQD&Y)>RxBc_@3?6Fyc$0l}! zeWCksXapx%>R{c;vExR*WqL_1Q)N0KHX-sBMs0e$*3b z&M)jd0~6ztjg|D9k=)F>_rQ1Jr1Mf=+%8Pc3&S)kHkNY!o-p;1YAu(XJeT*twID`_ zd>^5n9dpnKdGG^KsnMI|P5d5faRi7XF(G@!aY)6#>(cKJW1oga8O%|QIfMSIewI^J z6^⩔2_z-Qu(L!{>^l)L6yZpm9y1Ru7Jzqu22Gv_>d+4Eu#mk!Y5N(a3X*2g{IY5 z!BDCaZkDT8A5$J3lSQo|losj4gFu3<txNlUpYc2n-o^8r3qb=O)q8`QT5@OQuW75=G5<-E_m6RrN6DFWM*#wkL zO#Kim=d!+DyZr=|+t57r7Q5gTid&JSCc==2bE4)sxFqmrI$yBo>s$)(w~#M*>Buaw zGa_^XjFM`%r#(*az&hk?phGN)xhXT`vw-f%>E}~OEdc_G)?(F&;&2RUca*|c^baSz zi5+MrBuID`n-NFY-HAq6+!mb>OBezsMAn{n zFH^~nRD7aQYvWuL>koxkD*3iJ%Dsu-+YwHp`kPZkkByf2{%Pv%>!ny19LCfP1#nh> zh&*wFs2U*xa@q*3V-7t5*?{8nh)v?pRAgDSABaYBNN}jR{s5Bb%uyavt0c&*nHOG_ zglZCx_vxspwLP@Po0>a&%)DDjcb5t*{!ZA9C%f&S4Y$vxc{W0_H<&ILv9nnEQ7ex&vcT(yvILdK{u?3S& ztp&;q%<2nzcR_st!fRa~J#oTzXDYiN*NAvN<3{{#v zL!th^*SbRieZ%GY90(gngXR8)+oF(_iThC&4C#0t8$Zet1l3cYOG+2S&8k0gXI{V2 z%el7>etp;h4?Vx71O%7XuyqPRaB0ej^f!2o>>hN78LdrQo|XhS%>Y^GuP~sllIG_C z$Gd-5z6D$G+ZxF9HlHje_!S*D{lf&9xi1v2h;(W=SAIo3RE$VHET|b>n3*UeHTuOXCSm^GCP(mGXZ5H z>RZivJqeS897;*4cZU4`MMFHN$6*GuvP|vonxbP9pS#XD7!ZU<3c^zHJgn>yTr0MJ z(Q6pWG}|Ay9t)!Vx|%e1DqO#2_c-e`9vmMg2)Lg`+ZOjp4LncU#cOgm6)}wdtT-ec z+;HHG_upav;FMZDzq@a%e;D|@X?aycqL-Z&z8m6r9Kxpi{Apw~)sM8}?gpHXBhrnL z_QE_i5p;K7fq$|hAjC<6s0l$m2nc>q_y$`1WK*o1qi9Ry>DtOW$G644wB@8W%~9EE zRH9D-aU#xp{;Jg}%JIU$MVx{@#AN0}4?}h?f)JUJKS;DRu62@|Nr>6Npg8UpB&myS zM1CZ^v|M!1-a1vX@w!wq^nlw@#a4>X<(iuR5OC z#BgOZeIM)$kV62%iBWl!{P!@wx;V>0{V2A$u?6Aq>2D`t+7fxk;aRk{D?+A(2u`&q@(W2I?vj2lUrv62;K`c z!7-!u5{>U|_HfS|qg0~1?W98}rSExZTO>h=8L`$7f_wT~d4q)HyV?R($?=G*w6hEr zj`vv93}i&n0f93G%p?`o;PrM%9BLq~`)$J)^JJwx=t{sUDD46ZW3txBV&Qk?Q#&fqcV05jzf-OQhdB-U^NsiXGn*=WuwOhcY*lKIC;3|b}vuU z7f&ybYCl(OY8|29h{tSzzztx0%g?f9UwCu7FZAtZ!Ge=dwF>40GAAkFdKIIXJ@|hN|aeR^HD|@w>XXmhA<`*d_Q<^cyPXYh;jyC{Fwv|M)esYwBi6c#fWA*9T+x71@5svsh7zid1 zLq-Wxd13H_LP^u;<9EJQQZ0-S3~72YS*)fn7!C}&!h7pzY_tRrILCi73S2{9Z}ZO~ zEcD#qoeslPDQhSlG1evwv0KzZlyLGwtEFwKQ)DsV0iil1(B{A}*+rFMxUIX%>Q;VS zc8(<+6NbM{MhaS%K$4#g3QVKsxyW1QZ*yK(cvz>R47?`=w)}mblU5F=k-?{4bGp&l?qvh};bY=5HeWum7rtUXzY3CC+xN9qmWFhAK^ma8K`nxu0anAoK}F zC}*o>W%;R|QlBw^6TgJa(f{3Oy(pUd#8(29FzzjtOYA1MHp{Qdn^4gyFy#Py>UM!7 z6hw-MT1(@4dF7=wAWBZ%!dbO|TFB@-_Cw^YvOa#ufb4E*~fC z-^clrJO`-@>-e>8D%HECyMKet zqkQbO`rAiCx#R0{OW52dJ@cJYhh0=_-}@q1lb;O#J54I5fx8~^Hw>42)WXt#%IP7K zaz{2+=Re)Ji~jq~Z9)&8>A|jfYeS7Lnz>mc@%W6|aDy>xL{!#3>sn|c71$K}ZyyC{ zk}(K>-~A2f@F8kV2<_*h299h5xL6j}sCvYB0I;ufRaQvW?u2~zXcu?MBzK%@e6Eoi zMJbhm{x=NquO`aZF>Az1SBUlqQmqL!0FeUDpeJ=*p>D-p&!|nd%0CXQx5&C~D0B&( ze~Y8hJns?CH4xO^=atjyJ|jd?HFYDD{;~p$nh9M2ae&Ct8Tgr<`M|l7Q|aInD2V!Y zD&_9N+RS7)(r&M}u0J3+4+dvvA>*5wn43T^m84yMjMbf2grFkMKyhScsUp5PiNiJx z+vV(2lSjrn4f>gArnHtmY){?_$o^*C+W}Jt^A7dKvb+^I)K5zf#L|a*WM4&hmwwQZ zXJ`j?+^z}d1q%%p=WC>E|7)UPiOpthvbkRPN?FP8Y%36&9uJ2AWK{Uj6bj(_sef6@ z$ix)`bO%auwdWt$E&<+_(_<-I1iqwqA@-U~l1brheFBuSxiHpwhF zBBKNf7Hl-b*p*jL5noFBEB_A!)PZla{#Q7FlxdU7x8Tm%Jz-70KjO-aQtpm5 z-2tKh?|6Fy_ANv+-v#jZ zS4habxP&Yus`HRd!DnoG{LN~6n8LDKkT*o%NkzRE!<9bQ3>Le_*)(=1^Lcf5MAK7e z=a|J!@JPaSqiQi0!;np{-@HG{FP-k>!n`xv!E?U1=o&tMa>KCap$wNZmvcX>&k&0Ze=>#h?u1h`GA%=8%{OB4w8l7jbiNiytg$x`4-`ka2D zdP&aU-F391;*vnz#_v_rs+|jP>9dm8toVCiRvxH!I2%Tysq^&2nsZ0^{^>Wz0ipX_ zE0^Qd32u2pg9#J)ZkblA3k_5CGJ56P+9f#(<1=x|vv?7~$89|IJaI(NRhMV4_ZCTC zW?m^xwYr@j9KdMGQx}+cM8A29VVJE#JMjE(w4d-T27=B1`bwlMzvi%jRu&^;g(1<; zNLr4NL7Gsdg=Zw~4GAzJ!mRR^8n=db4~z%t0AaGi`R~l?GLQ)gfrOMN!j>#y-d$^w zdNQ;98Njn@&2GB*kvyQlEnI_@>p{(070u;nl9xOeEPlT=vN0)6H7HHa6R6#v#c=2s z{Y9-SsnOiN;+4|NO|IHcZ+>7ccmo8X@nbI*-D&CbL_P!P5SdH~-MmN)1*vGX{sGjD z%v*H)ebJ5gLgj*`2sj{_UTAs-i1JQO6&^Z+&&RS}i3u5(K8P3jE!KkCZV+WIqwc^& z^d92N^dSi0l$4MZn<`F$^Zc`UqB0R162TX2%(S*}<@f@fQKna99Q%ZGJcAy`mcm{< zWUZMA@X?Bdlnl&{=Uo=S!Si&Jk=RRvp^OF#d=}OD@OoaQT3@9DcdB5;dQ?^`!~Halg{Arw zGCQdcsSLXRj!v`wbq)1K{OkeWdH%kl>56D_p?5~ih$v)lF-M8RS;qd+$zSZp%N1(j z#P;h57-`QgE=$%dZ1GD>EfnQUrC|KR_!xLVK}t=zbu%sA8z72Mn7Tv(&lQ0zGVxq} z|Gw0?!^?{%64?akt`AK%%h?Cj@C7C0`fDGq?+p2AP2Hw>w%QaP5Y;E;r>LZ;?r_N(v|b z&Y}}Ffl(UxSS)&OEwISZ4gkni7i3P;Xa=TYC5B`0yip251I`JbMOJ_c^P=Ss5m?Rd zU7KG5VlHNS7lIn1-QHNXsg1gt)aN%wfyXp^txL}R*oA~OLU)C2Zwr1kT|C##UmT)Z zP5Uf4*Tuw)PJ zi=XUihg_lY7Q-y?tcS>%Az-@iO##JwIa4%(=Rt^YD7qkIZ1&D*=MQ_L$@GsV|Cev5 zl&LX|RU`J;)@_CP2J)zMJ^6^b_PrB)*_r^udMd%-)&$%xsafL>lWzW4XhL7{6ManA zt>&QeKF*TAEK{A?mVjjLQ|#dYDwaGarNT*&X~n$Dk+FHdqv+6V1|U^m3+3E{(@*M9 zvA7{Qk&p0hb>#bAglys~&GUsiMXPWZ?AWins^r^XEYkHJQdP7e#?|&}SV(nCT#;wC z@|6Lb_C+?@A34rHcoy_(S9RqkO`rU5RE;pdMmI86NRf3_wA?pzn1xu#b$Sgw3FSSn z-io`d8H~`4{IF76G6-d^XQqrgb-5trv}KC^I5xopS=iC z+#U1GgovoJM^Nx8#;3dK7TLtSnEzypIR@XDrhd>}igL$bY2$h#K<$}yKe>rB6En^D z?F$X!w=`QNK!A9vjmz!>IjqH}WsyZ67m|nkpy=Ll{Q?&PPo+_6bo@Z=dS^1Nt8G?j zZ~LE?PhSk=$Ah`-C+O)rYq_qhMpv2Dv;^?DA^6shJQCrtn=sbgp=Gx7dA`vCsR=cG z6owxEJNCEOz*N}2xS=z8eu_Be8R=sq3|pNqZNzi zZaP%Q)w_R((8Q?Ha?4?(YUg5@pu@so_tDyWM~8SV6x>ECqAX z5Rv@*6p6<>uP8>3KZcK*2x1A0LFD_sR@*gjSWjG3L|nkvmda07&egyn!a+VdKLf7W zL;VHF?O~n&pc~4U(7>e}ktPX`p`j`G2pDnVzf%`za++SBOpTT^a3L9zlkMWg0SFyd))Q@m9js+ODZxVWu(d4___X82~pN3HRZRt8%W!PNDM8gd~2Q1p3d8`_a2S` z7qg`5jjuye>1_J^STNKW&ax*ns3H1tgPpMe1XW6Yc!7*(oERS;w?jnLN|Wg6M1iQl&@ zLv_~D&=g+dy@A0I_i;NQPqVG=_z(o4 zSLBpc3}P9IizMCsYFxuzO6vomC59(Kj5+bIJmnPZaC&-()DPGqF)H8PaPc#iMoDMPu!`SuYz$-gjwAfC(I->r>Yr&`zLp#5y;c#!Ql z_>nAUX(9s`%%;QkdAxTVIzR`lbq+ z>lj}?iaaY}uY}$U{ljqjK>~uAK7)vP2q3Y34172ODh^!E4@rRaKf>L;Km@}_qxD&_ zMg!yE3VPifMzV0R6^$a+*PQ8tI{fF=+OQ2t2-Y-9L&Oi}Dd;4K+u};rLNX%*37C3g z)W)X4QBuKPtzrB7jGx<$fl+>|LH<8~GVfX2D@TzB%=(76eTQ2n4-IDg))%5mHhX1_ zUdD{+md@W{MokGC5}6jFF8vRjGTw7^T}9rK#M;fY1goCws7hvN?1tasC%@AaxZm>Q zNW9>eYnX!y3QKWmsZ)-fQW25;j(K*g{avm1WMrnYc`-xYR1(0zp)8#Jh+1@nsF?Ti0c}-F?=@I6XYozU1uuX$08-I2i^qklj(1NO(;SFv z*7x+~<1#g!K;N-I{mZD`e-5eFtMs74kAy=}+KeT57PR8Q{o8wqF2VdmQ7jmxv>Vy+ zDI$;>vDS$O@i8PyZr_G`LBKpbZL=xGfGbIddG03lK5ADxl^pvx=t)dxIb#507hh}H z^Gh%yLfDdOO}A2Uov_AqxN;R-WFwFUpeh^(|3e0HN>HQ_0WiHK64~)XfT%U)P*$_`1~D2 zi>xaP+BklYer12*&QqhwX?fsqRs`rDe&Fggb@hA1F-$&yVsXIgABRn9~71ZP_5nD$z6 z=i8kN8=7g`V{&RCmDN>~AmpOQcv3@t4N=Ak%HO&9RNATvNPEkJ>N=rTG0zS3$QXKC zfj#RleD)w9Y2i}-fc^hnDw0ouLq0tODu z#HvZ?yw4Q)rr8W(`CyS5+NL1m*-V}E;GzWtW3$_FKeD6(sy`ut`REM+Di$QrJY?4f z{0&4}t#3cj+5fOBt>E{*N|z%#l=A=F@V1OIet|4^^8C&&l3Ei2MyW=XuCHyO%`~mh z(_A_K%L|N7@`%{_4+edw9FH^{?WYjJb2y`pi{jVnf{|;CMSSjOhYjN7VML_dzV5BD z@26}N?V;H%vg8`s+0%Fv{&GCLX zWCI-muQu$Q1b(}p`fo($P<=2mT736(nSjcK1ACR~qMroxX2a~PJ!RIH9lzm9mu&DX zpx1o0)$(v_(_2Ixm4A|B)Y0Ep#+~nZi*t+{5 z5DcUwE2T4%=?@C&IN8by9i8bN`ly5c6Oo^4-lg9WB zY_B>N$Cl2*Z4jMmCUx1O7sWacK~uc5yH#%Eq8ZOR^{NAZVs};H$YG z+3cay1S$Rhgu+X5&9JPI$y^{$=e*KAE0OoS_mUt?*(FKe^Lm9eh1>bHDsK%{r7qEFt4UgM2Og)B-xY1ML!Ax1W<2kWEUG`oT`~Dr-+` z-raq-jA8F>D>;9Uy#hMx!`*LgUV}r+*NCVBCj?Ocn#eeps zjwH4hP`magtYrI|;a0~+jC2Zl=eT_yS)Re1joL2CX+_Sgop5aI?t9RBH34%X&*&K;q%EQl}7vo zqgBn^WEG=8xyd~?ZgKs%`}u6DxIvI8P(xihych>M9Th98e@jhi@8nZXWWY&k}DdnapKH{-tlQ^6}0#v9d#-J-t{+;~r zxbwa%9HGt0ky$e+H|)$@Ty zCZ{d07P{-7lDooYPe6Z%Y^R3Cj`vf)Z7yIx@Yh<8FA3Zrp;`p2EZLYw&i`Ca>17GM z3kd^hP45m*Xe)v~Z9*M;nJPDXgz6{EXSP8uT^$bESRd63FMygC6u35s5qb8lL5DZu zRW=2*&C7mxXFP#H8VPCwo4}!97~+B065HNyT;##ivW0nxFNdGKqm5WG%*oit6gWsSsZ*$5q3mMgp?^{;OH# z&_1Mto!1jsXd=IPu4o>~wJlCZ`hi3)KAko6)FeLXubQBr3ghFYF%@K(u{X$#D~IE* z1~)lbL)tGEBlP%7&~E282J&6SwP>%&yx_^^+MLzOat{HiCMe4i?|#sikc1>DKmW!w zyYxub{ZH$J#Hv9>fy>7kJ{@4EZSRTRz9Av?-3*nlr{w3~suNt}2U^XS7HpxjcNgS& zno?ulklrpwtm&=G$|-lYAbR{_e9D%0W(A<`o*pK42s)0Sxa1fG_q(|gm{MWsoWav? zYFmJZJ?$^oQ@enbJT)9RzD77Nj;5kN9IsX9&@J*keWzr!CmlU!xU7k^N!!uBHRQ(+ zx-abyf&mxgowg~3Y@HVrJcsTyHR@RnxrP0vwCD1sX4+g2Wd;!CWzfG zd~H$!xwHl*;{}cB^1hsdtgI2z3l_gnkHfj@1EWO1&jF}-XWUJVac-p{#oy0dF8@(g zyfZk3QudOw(egXdnK5vDn|>GVJ-kq%u;>3YeZPLXFNGs@rkIe$zpIoTD%*Lr-sPsZteMQ6H&5n*!VQZ}&?2-0{E6tAi*M8y ziLI9}G)4}=yHvaian7i!an823#_ImrG?uGnOcZV%ukb}~XGUK=eYE=O{XlGJ$a|z4 z0s{9mHU`P}SqlQ5)3+Xu-^g(So<=uggl&)1A$$!i6cD^g<|ob z7O(5WB>&n?j;ju~>Y?drf6Kfyb0q8oM{$y9SU_6JEG|aHMv$=SXc2GnRT22q+Xcl7 z3_9E~!{r>Vth8wG1FKrT8fy#d>@Rbi`s#<1Xk}7Eq=9D{WsFT_@qP<^xx5_u?Fa+g zW%!-p;+B}|9NApOxHrT&3q9^lNL9m!E9NjVS5rBe^mvz77&zYju+be?T_p4!d`iJX zx?cEO{!|4B3vO02cd<_D*g?a;SsB9p14h$k5|sXlxAAiqEl1h%q+&yRpDn1E;_SBO zWHD%Yb0YmeN~7)b&-1i*#iZOC3kE$A%YQ#2#WncassW@L+70pNsEh*dvG68mHs4(f zs8Bq-zhqv{j!0bVI>Lz&Y(y+ng~^+-RcKO9T+MZkBB68z$itwN#jg&fq(W?5l6Y1B z4_QH`z6wp#P*|#9W_1~*N(Dssq>Q6($}(34vU1LY_qrx(rv{#WQ$?#`00<6-qKYD)TchV@%YIyTw7yX`m@E&xKB?OqQq_DV} z!u)c|=Nc>V6)A9h&~w&3-9eExN27vk)8J)dL(8PG^7sMfHaD>U^aYwnN46yaKs0ZI zMaQg|H$fvBEG;2st${d}2b5VhW)@qm#;=o}9tYpFv{kH#c&g#I?=rnDO4PLO|*FLf)C8um*VF63`?;tl*=HCA* zkHi-bQ498>e3=^~HlIz4a&;7&&KM-Q=d?>0`}Z{hUAHjGZk7EU_zlQ)MENBfO9!foQ4%bZDyHza`| z4u|P*m<>iyz*=m%e2;Sh%4j@s0s@isg1hh)@Y~I}G@|~vqmj#MN?G|{>-6%2KWDK_ zm<=G6(y;z;7R$FwK}{%`^gtAhVvwZQ4i^gW&*3{yD1>0n5eN|gKue{N&gYRS=5bOd z*!aboug{o#|6zXa9qc(ZjCSMcnxT+HIB*UN4GIb(q>BY?e)$DX-f!XX#Y-55Y4IC6 zap_Q3h|7dw3l#jkhjmTDC@^{DiR<17faL;@;W>PkhYp{>@Ee2h8kdpJNFq1@#KHsr z_{A8`EUJQMkl?flLOfcWXhcYJOKEe5)@Hp}$mnk;5Jr0wu72F{U(z<2;&I3LxSxQ^dXtD= z7A*dY5(48bcRk;@%jAg!2EOu)pnw-9@aaJXQELol%ai}dzxn^r($eDCEDb~p2T>aC zqP#&g%|~UHAC-O)Agg1xe#652@BZgSEUt}jN1bkq;??(EG;557jE%UOkKRiHEuS=a!#o$8^{p^qoQ8n*f68E}`z0a~30< zMI6SefUuJW@iZw}%LBW<-7-!R;RC*+-1m}ypUX(#$3a*V3AikB&SA?)t|by2SW&Tb zI>;0-f-(VP;4lEby$qa197eOqai2#`icG19rTceLsw~E+my0_lUo}L!UXkR-p&>+R zBHDqU+t{?QUphnZ$zSF{&S^j8_fLD+|IiBv3q(Q$HtvKS#IZaO#gmka>Vsj|uJ0e< zr7=^v;2k3dj2`gn zI|I#{5ug`%+9B)*BvbBTao%&gX&5;Du#Mw4Z((%0iD+@*S`wnxjhHkhA#xn&ly(9* zujVO}8whzMSwu3%GS~OD6-i#0z`%_&VR5>|+JF-pfKossma)PA>p%QcS<7zGG`y-D`7K8|**eJ+wNyyuG=7FKmE ztfr97jeb;l`MvE*3m9s`7`kmyinFa99KLu3qu28f`Ya)D5BT1OITI8laFL{fgc#YE z0x@J`!E?9^h%hYBL^h^eoySFiWZ7uP~z3Aif zRY9D^wXiZt6%yEZ;>B6+xA`E0<&E)MA+(zmKm2Peu;>{jKnOeXVNpCReNZMRZolj{ znmBp)0oCmtP{WWiFW$EbGB8#i+_580I?v2F*Z1WG+EXwho8zKrEjApteTw3mrU8sF zVdYjCn;*?1Q()R_Kxr$cu)r4y7Z&)gI|_k;o#xNC%BM0JqzeUP@_D54S>f35^1BWW zceQm3wzEzqxTW7{3IFqmY}F z))fg42q9S7Na5O@tFaG)X#j73>f`j#2&ErH^aK!}M(4vUCNb7f)5O{KE{9rCHYh zA%w>`o<##TAEr>8CHU~X2h)_d2M6gau&|uM!m5sZX?(7@K!N?8wpB0Gao*x|eCnnL zju6%3kWWW|nT<7+)|PSf>J3ibzOyz@qy)qyAUeOn4R4*mPX??85V74$IK(}%7o}lM z2Ar=_0CLwxMBua3j!^Gan2KQw3qAo;ZI_9rOvC^2CTX_|g31Cdq{r65GZ;@w+VxVh=N&r3`=5e#;o!ns055AbCGNa+R#JbmD# ziQ;U!E9hjrDw7z0J8!$FTpMh!oFw@0q8o5sG%DsKDWftBW0RyWri*zjKe~s?{oAPS z?BVp?2XtyRYmPA)rq}j?!80Zz%~g@mL6q)X!wpdC>>cPU!}@1+(<$p_(Rpt#pREDH zWcrwSp`Tsimbc2l`>}Mey_bY@|M|57ZhpFm!fZzV4MdCm(tP)0a^E)MTT}V@(Hd7? zgw57;9qB^ewKO1{)inPmt-6V$y)G#8XHwG__x*9>1WVI7ACQeZe%Ph;h~O9&+9zal z7zsb`s_-TX(~FsFt0+|FvG?Os)DI3}QcI{wG+0iE9Nt3`_Kn|~HLhy*UI-$dL*~{9 zVE7H$o@~S`Pg!;P2tI=sR9Jb_)A!CpZ~(W?yfVDvP%IvSwK#Bi0**g0ZA*mW;sRD4 zKSVm8b2)4U3!?QJK94OER^G{H=Q;lT#+eC!{ z^Mq?b?7S|U!_dV+}LcR#DsC!_mt(=r$Ub(3gtoaGtgnjqA3dsE?iw3VZFi zi1y~_Zubnr4Q<3XKnJTd2LBeT%7yF8qGDp&n9mc9$MUf_I^UNkNq2<<04FrC1-AeS ziwh{PuAscK1TEz$BvDYDpHtrmh!z&YG=3b277=9Ek4l>+Dd2fv5b2!MRDXk~ zYk2)b58ZaqH+CvbFjvtqzoMfwt4Y+^M~mu754}!M&@#!kqUk!8?%l!M^=mkI`U17R zeGq4F142M_kA5bUGC~AEv_TGK!>|W+XBETUJqHej9uOp82tfjSUK~lmMHeOvGGXxx zRBRcxj(rQNjKzUN9Qrm#7HJaM=ebOQRoI?iEc}`l*5AeX;AJFyX-< zf`dWmow$8#&_1Z?I_5UkF}Jaf)@c=|??0fnyJ!18rtYSl6adjE!^GH(1pIp_u!}y< zmI8b_;Ll^z!L}3M9K*C3&3R`pkEsY>d6$85Xj^+6WZR_6kO_nbJo90d)lgo{HbSI6ms?UpE?gPzI6YC{gTUw7sA79fn*;KvA=AT(-B zixP^mWuk)UtVDZ{HWNXf zgV4Fq>wETynEL5c;y(1 zP)<1@$af^+Lnbx96GAYvwu+h671Z_*arE-FM5!l8hl2pn7(uP`uKud^>d7tR(D4w` z-oCmJDD3zW-1p$W$zb&nc3zl}fX|n$*$8RX1OfvmpI7$e)7^2~=bFgN(=-jmN(H5r zC6tybt~*c>QUlj!qKc2ULSQQ)Doyrb!tNiHM#Edj*z0JTz)Sd?G**MoTR}V@Y4EEGABc`krnaJaCw1y}g>K(%f65vY(eACMn5GD*Ps{A?Nd*+*D@a2M5`U7UW{Mz>z~m=#-?0FAf|2dfcfO;d~p z6yVKifLH^{X)m0G0_^fm8DIM{gKaO!=y;Neu(l|(%7y>&tb7m@XS2BVSp}8rrRe&X zF)>lX0SKlcr>zOD;ctJz^W=;A1nFEB>AYoSkjiBv9zu>*-4gaW^cXeGAiXA_-@G}~ zI}$>y^O0Pg|Y?U2b>LR*FG zu{4C|aCsBkHh~q&`uGjXEM(d62>_T#K#&k9?1zlMV8`ZhBk~pl2o8MD&a$zI-4zAX zgUUr7-{HD1GCkKuGpj3Des~{xI(_BVU*bZvPQ&N1WrCX9gi%bOzBE>O!3nS5*0Hcc zuH^SvT-A`tX7K9!4tiZGb{)*o6NJknY#A%je}sGcTR zTSp=UAi94;b80XOGXlDtis)~^Hy%6VySM7dtQT?0CcgR!H$w28=lgzfAt)N)wSol# zL9s$j9HFjQVBq9YLD(5%+4CG>f}Y8sSXo4Qc?pHZ1^=GNA@xH3pa19IoJ*?=TO>%T z$&+NkAx_>XH14JuG9P)v_{Y2ZX@0DmD2JhTOi`R6&?aR+LxHdU*AsNwo^L$BThxhK z*hwV38Ud<1yEuIQ61`5x^*ieNtdJh8L86Fy|m(U;-yo{<6Vq$j+tI zm&UuxOJ%c2<#X0P2Dx1DT?WLjNimJ#wXg3jp1|ri&9b6MgI7x z`{N+IL!3n{-?RGGH-j{U%MS?X|1drw;G)P08-77TI+w%B;|C}%E{?SBE*qleoVav^ zvz{Yb<6)0$x6)X>IX=aii-Jy*;>DjkXf+HWEs?}(SaftSsG`q#q1$NU`0ZQNcJ?gg z3JD#~(%`)7qF8jfUEX6TKPs5dE-M?k2h0R-xqor(5H@bVePn<6j~h};`l`F;Z4udm2fVmv}$)TPHIR7f;@ z#1I$c=Lq1yQ7)_LGhl_-`773zrMCXajhn@6*<~RIfl%ts?`fa`LV75{i>ARaI z@a}oX?wh0E`#=;AtII*^|2&YLnZfeR43-|;L+#)Y)$JWLj*eX`076(7K-Tz1Tt)_V z888eBm(KXL2l3Ah{9+UUP{yL)Gf?o_lrx^P=MznH$6v$b-5U8d16>y!c3wK0!i|p? zvH5rrnl2s#X^aRBAwu{?e!oyaEKHy@m`DZXQ<;n_Fi7WfAlm3%A~b6hCkKW*cbN=r z#)iGQjWTqt_o}`z0}SoU3lo@oi_)Z!&2a&3x`=pGx`M)lP+DF>etr&nPoAN+yJu^x zntoSw4Bw$~aYC#y))@~eygd*^`y;UON0!&%`3wN|2F$!dlYbnLy3!P`OYkWKF-L+J z*Gg764&T5pXeAOHxaatV2i|7LenA4eX|l=gf#&Y7Mlid%fu*~5py|5WG`)e|Gu;(=?K!}6?zh>y-Wq6&Fg|;98B;BMH9;^WIwuTT ze=*l*q}I1W+Bhf-VAJ?AER7w=Gudm2Ef9#Vp|G%kQe_dvN(E`g!gtVCbuKQ9*oujA zL6Ag{5ci&g(BR`V zJ}G0D#tIV%KP!lcC|P|*j2#o{=3(PfWLajTM|J`d%#lG2OOF|wT`XYf`V0=YsyNwg z*sfFHO+bX1v>1lK6AakpSm6Ov-ZV`RJ%!5co0z}85m+n|xoi;S5lML`FKir-%2${^ z1lmQ+xnCws;9bguxiSKYc4t&Q>Y;XGpgcD^rfTn97wtxWB*KF*AwpO^5A%)A8-x(d zEH7bZc?m|ZhwAP=s=K>rot}Zfbj!GNmI0rZl#}o5?*mPw#p*2_0|>Y^;?1TVCQ)X6 z;L^W_z40-#UjrL8PbzPT5tm`6~8zTaEE{d4`MYuToz9M%JPTu{`W^s1Z z#r{@1@>zZN;^O=q7#5d)Pz8kvLrbL9w>jo1_73J%lNNt{U}p3X3;4v3TP; z>PN@co@EDz?h=sC5{zm9amHJZ-ZiP|(%&MNqSUDn_Wpz!qqy-`G#nFzkI`X}?H?xm z6j5WYb-_TIP*?!Cic!J*pes2q-J!<`a^(_=i;E~OE+9WQ>tBNzL0dX^Coo|`1h#^t zl79{&lj1nfFp4r40kXOdOP9Gk^2aA@1u!X43LNcr-Fmoa?L^OHu=?mB7H(`}|Je&P z4i4M_-n5BnO(X8+WmNMtg8~~XJG49XtQFXNcY6dP$mi;hQiteagbA{}5(L5otGpjl zfdY5l*b&i2XpD3GQ7Q-$pLS4C+Zrx$NGXuXr|=j5^#kZB4flVsj<-J^;D>*D2cz$4 zXtG3qz$BT%+U1Ud(Jlg9%LEY2-`K?B?OV`ODds)Ii@afZVeTWG^o!OH72;zMfr6~f z$b<*7ax!ecZsWlh#gV*UzfZCCvVAVXgNrOQkU>wUF@OCU=C5Bvztcf&Zy&Y212j)h z0n=m{8E^~p3}a0lVS$~md5jEih2UTKhmRD}G)7RxgqbZ6tn+Qx0Ic;(!1Beu$J(7) z-23?|az)!^q-gl}K9c0y#rU3agavj2<YI zp^5S~?i8_cr-b%p#1&cwHd+qQnbwiFN=TxxH%9j5fuM5d4i;|R7+676A$)EUrSYze_;e?{u#&>&gUqB~Zz@3uaPv_X*#g1) zXKiNO5LqUP=kS06Q>^<=js^(rqlBPTSwyL_h+eym>h>iY)l!ctPiq^V`}G@mz!u)tSNCNQ8tI-f^jVIIZG zB8m(1)*dorM2b#8v=}8sU@I+dxw!WoWx2}&M;ne$^J&5{X_Wc9YB)RTquVikb-@1t z2!usP2a~ABbRmyxpMQeZ=^6H(K0~Kkbr)HvySbT7&=X?sZRYqa_4~VntHt1GJ-}DS zSzNF*8yxL+;<_gaFwyoivQlH-^q57!79mQY;9fLFy+=(t3C9X)=}>_IuYDN4*lB$) z(-q1%mIZ_m%PIf_3o9jPDdJARn~y4(spRqX|MC*eYR4}q@CgnmV0xNFUcRkSM6C_b zDtB)qoy!H^fym#>zwuEpon4FzGHfB^Ho1CZ`elaD5+%g4QD%)0bYnyUVfP&F?`W@! zW<7^|X?PbbA6~Z6?Rxv^__avH!UX3iX8!rHBzfDvsQpFHW-zz8fw|2M7~LLf`}?Tv zAJ}e=JYfRBEiMti1%ji9>gH=)9M=8?&UJkCW+#}G(RgLF0WB*8!}Txhv_ZZNS2oIc z@XIxn=5zM-BEAX#-2(w;9;Ko@?;2Ec5&jWU*(_4I9MU=K=-Ye^=TF;gzwWuTmq|@w zSW2%^KYivtJCVPK&yoZ{Dns15y7O@vnY@M<-_<0a%Rk2n=Qk3woG4#GVah;s4a<-2 zqrA3)ogaQgui3Qt3V3{l=FGhsK%oWr36|%u&eLG9V>|Kl8kVruKlcM3xv3Fr91m4k z0yDP?te*pZGe#`DcdsE#j3G2&P7W+c@o7vrk>J3NJJ!h}KEcV$&COx$qesXT3WB;< z5J9$1;}d|n3#>Gd$IoLzi$^Qy^HGZ9;ji6>2IOMuJzBdPVir! zw^HD($k;s5^Cf|7%oZut$f81}F4G^E3KaxG1U8(?j?Z&ZoX-n8_bO`(9PM=As#;5b5QIfXNaE**{M-y~{^}Q~ z?H%CY`3v+~E!zT^x`elF5eyK(u=}m15k|uYK5ZaM1sXy&;ZB`N!kJjz1r+OfSS=u5 zx=fhBl(*s#FY1ekekUuef(9HDBF_>s7<@cP{PR{De0fVq;4w(lWF{|1!VOE~%O}YO zFk30$-~ETj_~ZZj3cGL4+$QW!TBa8_?Xbr{0QtE&EI+u1Y`G+OHyW7KWoh>33e*81 z{`{b3de|6#xySaajxdD-xmAw9jS(Y6a5yVI?)u@1A}N8{)i^}{1H4v*09blrs^M=Q#bYZJyo!OMrqSSP>|9+;+| zxgp$K<_bRHk2?!ormK)*VKP$c%Xx2=e9ri(~J2?q|l$wG56Hc!&b--y5dfIPRS}N_YkL!1e$YeD< z`Qw>8FMHQ=K0|O$3!m#`;jlCXh3P?lZU(ph;+Hsj{T4^B-@-6VS2@X`u=Z%9jvGu8 z;4w4}ps;ul*!##(XU_p=s&-r>Idm@5Pc_DsgK3}dC^-RgVFJsE1Y8)&EM%Qy0`1|@ zQb~9RnUFw~7A82bGb|+`Dij=0`+GTp11s*7wS)(np2G6|yO`TpkMpJqLqfFXoU@#mmR^Zfc7@5uumw)2^FYm`W3|xBu#x|dRXI* zsM{#Z17F+>rX>*;_~IbQ6bmTK&mliMi^9TOKuPnw$J5j#$M3jS27LK(KFei-;}T|= zbW4-NOLyJOO#0;}0SX-L_6Cw4h!%sT@d=>3yoAzH1*adjaQN~y%zh7qngKxpD@f2Z zjS~{UTQrfF8oR_jft}$@vhWf3-eRA9y%@p-CznTs@ExR8LjB*7 zdDf`7DD2h;#7vqjUEy*e%0*TzAp#@tbjB(^4gwr}=wS119+~{$RtRsOHevR|-4-5ZBgkl$jeHL__){{FpGlB(I zh~O|f_NH-$;ayYHGLV811>Hu)~krcs_tqh9UdwI&Wk{>8sP!OV_a{Cr$R#vd{;}f*cPVKRf z!UU_lL~S90CrB_TpzBtDknZ*v%Rj%?7QGQ7ruM= zauWy^QS8nrTv$S68rd{~l7=j2fd7Lbf&& zSeKE^OY9mBo5ucu0;RbW?tYqQ_6bplgF;!yC%-P?$sZc1pY^5XSX#rG$7M$#gl|2k z{v%r}jLP?osnk+Fap-Sqny4QfqPn+_`r#2w!?4exuEzus?d=I+f(kv3*98Esa6$Mi zLsX#fJDBo(UI4(RMG6b3wRcc#I0={RSuRKEN;x z&f>sxZDg93B-qWjFT$D`3g;BPc#)Dt35nD_k zJkWFvdM1l>HfswFvLL!1b;qeevr17t>LXgaBaBv)(P;j#h?aKJ(it(GJ-?j6=l^CN z-~G!8dR>a*ObRpeX_V*Fn3+qXIFp9$Nd<>HZJZpmBcH>%uV4Z5oHY~`6ox^jn8%GT zzrg9%HV&S@w3U~rkLPe)98sI!&~?iKjOR5pn>Vq92cAU&(KO$fN;FoGKz%$0VbTFk zn84AgY=U1X$ZVF%L6$CqtY}hYBPWj+47lgS5FChv1CElBX)-hn_2Q{q9@ic}KyhIq z_^=9NLsIL5LRrK8FLKaR!F+{iq_YGc{i=i)-!^a<;i7%>Z`#WP1C%=$9V>IHr zFp~0LAwmRpoFov#lM^-Gb&xidl;U zlX|WoJU^s<%LJd65M_l_Y#D)7LWnTIiNqBsIM+y0?XsaLnIciN7J*D?z?WeOe5an= zMR#}I$QCF#8Vvv(pNa&QC4tk9oqyiMX8l{-DC6J!UmoH2|MN>6@6_ykf^?yP%AH%7 zSy`5jG17OAQ%~&qT$nW*7luV~adj(P0mKy|i1MAj61Lgx&bt<_-_6^;&y(TRPYu_t zNnGIpz|1QVRtRBf3Jf$2`T05I=jX79TcDy0))wfSvzXs|4!O zNlw@=d?5C%GYWwX%J zDO_aiyzM6x9Rz7IKJL-F{;&fP-JHtEt&z%eX?*!#R-owwx-K6tcHg(WIZQIoO@4Vn zL1DC*y|#|h@)Gu+y};S_4oq8!;P4lkrdfNkX#^CObu`OsXqxT5li@X-z1$3wdRo=Q z-mlUTQc$Ol^H0PI6|59$vh;$du)~A3Qh=~bGE0Og{vnKa5V^_-6CAi{BEf+jcZCHe z7iaQ8G%Vh_iN#wt`~n<>L=eq0qV*fu+75_(eaFeq7BoEgc`@LEPazpJ9k~B_5!r%< z4=-E3Iox8Oz45?_yJ0Y0I*h-SrHv3IqRr5l<)hYd2_d%s{&_6jyMtc4gXZxu8n!Zz zwF$HP%5Z3F%2*8hEfjpUD0qg!>r7^0AWEQuvDrz}(`jVOC1lGb7SF?}aG^a>W$>>c72@@BisN-akFV;_X|QU0?GS#$vv|%>OHIZ6FKlDFUN!#|yw}C)VJBQrAW0J)`LHOi5D!En$j{9n zKR4s18T~%mXH~STHFO&dbm|Rs8V$fUo910%X!e4MZ_`ggU>XKAUBlha*YN098_49- z0K&c%jg{=5KHTL4Xz3KvnG8~y4D@UkscZ&%>S7BEM9AfVvjId1er;nu9;{uTut-1` zM=ijlGvV&e+D!xd+b!l9{rC6o$+j6KLQuFI(9*VeH6(-U;+b=Os@3423& z(*P)eVG?LMu>}JJrfGP5tEO4|BTxzg&9!uPUB)@rG+BPuVt8+TvW)rV65jl1~U09((9|3U0($N^gA6iPtVXgJ44H+zm0w$#2&lZd-3D2Tj%^u zk|0z#?8F5;7$HdKa>x`4$P|nAv4C8;1U;RW-E8m=Ga+0EPC_=p3Ks^iIKmc6_>Lwk z&!_R``Bq@kZ+gC{9{17jCU0Mtp-raqIjn#D7~NYpaPZ<4Y6k}(wqIvUVBq|yClp}P z{re<%5@W}!kwd#Xnm+CvMc8zaFu{60kEv(tT_R9$6T|xo%M65K0O3NgNysGE{s3?K z28J03SRoM4brGee`ym{`g3&ia1qU`K0Sb$$`}KeOIX-S=@$&l?OhYD&3<`dQCfpI) z6O>Y<(i$qOIV@evVPPo?O()p<(8LeFKX%_E&b1U2fS?c|jw>U-5JBG7wq7-{aXSx9 zk2fh&O7Z%qdZd6r2@j?RdMbtD;sT0`3tl^-6x~)E-DVTLW)t0J3%yng{Z0qHb{o{N zHjl9uxU9BRn$P2}{+mabTPoU@i20pDEUGj;g;Y8PJ(Yr<$v{u1p=Z+2)9JBV@=g<0 zZlrLu+ea^gMvFUN$m)$hpD!0BAc(@d=k>t;3sKLk=0JtZhWy+tZvOh0IC=j8hc8}P zG^|N2n$}fbqM*=Vn3m?RB^a=+_$n zfaUA6_|N~t4`|jp0GI_S$}lHjHet5GS}FM58V$BTS6u!fOlaUnU!VCtSq}}y;ckDTHRLSsiWJlXm6v}W3MQ=|7h0m zC?-L{dfTnqZ1R@Y5_}vy!TQk#Jr*~W& zu>u1t!xJp9n)rTKN34LE7bw^l@m0QGQZ2*~A4ont#S<6=ui+8+K0n+}L05ahHt~aN zbl{r&Ir0U=a3uJipuk}wTvr#GhJw9;4+ueS7AVf9@$`=kv>JYb$bo2qCGI$#N)s%s zWU;iC#lmtnupql~y?{5*>ZqN1uEd-V7;yI>`E(blzbT9s!wM06E{vG8PRGE`yB5}O z#j`@#dDlj*+Vee!bBrrAh;&;t>B_zeAOz_`9_d0JgjAShyG{xDzsKr-kn&ZlX!lVRxO7rMkH_zfpsBMkw%;BJL-5NQ3 zMJAud-Jh+wG_;n|@aWeY`1qurPerflT!4Nll6 zOaLn^2)ZkB1P7vgXD=oy9`a2o_gzIM`{q@8{GD`_7`Y zR6dVVK97>U_o9<;8U}i;wyVI?@AhEyx-fcun7tlM!vHlYjDFAeEQDy#Qr3Q7S}JAD z5t;@qm4cp5K~H%KIeI38R5pWDHXAJznc(+1FQn8W{KbdN=XjL%WSBOUOd{x>!w@8w z9h3q`dp#esAN6zUD{SFM6u$__6pL8@ceMLo*LX`m?2K=6hT z-ogT5F34`~Y6}Tm{=k*6fP&gW2ZDaP=V}sp&t{!OG~L?BXLi=wY>s*Od_lt}e^J7# zA6hut?FpYdXi6y8q)eqXEUaX(w3fyEQU*Suz0iCx{=#aUxPpQVGGPJ`wB~#Fyn(eF z`A7>{!=QNmq&ASSK<#;Ou|RYUnL@#BBkMPcs9c{P{f*T31^`snb?m(-XgAFO4HiYC ziDKdjFy3nYch3YLb{(`l?eeu%k^YuJDK z9PQInfD)M0;x!y0fY~#wg&&OorfCTgh+}P_0fd4Qiw<_I4=6PNdtaJ6pDaVyiM;`{ zMMp1h&S7>jkF&!j_kf=JBkMUGS9T2D3)t2~V*WhLZJl^tmz*mS7O>@cI-eI3K#=BI z7?{FysGUwJFtfIXrMtJ0%4H+oOW`~aWhDn#0=UP&D4|f+6#Xz}o8Q=VXGJ{K~SQ)s;{V!WAV*Qhk z(Y@Wk;j1^O@9n`+5u&!n4!fzcK}}ac5Oi&1UzAA$Jj(@uNp~0Y;A#f>2zSO!pKo&W zuZ43H8#@6GRv)tYBrws02n>AJVTlOmeYuH~)r9tIEfVg5sAHr60RBzS7{Nh)eh#^r zvL`qYK-UTGeOf?yu8ViiI^4!`kP;g1`6+^hl{6}=8O-rQgAnw(CYsg0P+in90}q&k zbGMH&7gxC4NMeN`I!|_NHTu~5(8lWZTl*AF{d5$?!`0oAsr7+1ub z(8Qo=z{Z^nUVPi~#(8_L;Nt-^QLazP6mHmZ#c~>VKAyqST5jlXOHJUzi-w$D6!rO# zao{4;l?qoAzByOWeskqAZhZN3RCo7q@cbqE?H2Tu2GfMML9*-S$RaA8bk}7OJpRDF zg-MYsWpMMOC6|`~qY-@XXV>uc-@Wkh7cR}~@OsYeO`i7kFQ&y8A~2fD5_&d@cL=jS z2ulkU98hXm9hemP`FSiqxQAT1Ed6E*;|IUK!={O#X#|gdUPgKDLU=I06iB5AKKa!g zo_|}#*6XI21_9#D`-n&z0-|;k)|U9ZKpPZ1V`Vjm^&8eA|H^tE)uS#5H%{_=dKh0X z<>}RUGzi@V;&$^dl;0rM8 z=Dy9DnW`3A>;&8$4T*?_J)t&{M3cx74mbq7X9eU5794(nFxPkbvX|~yXiyl2>6#yF zsT2xxbI8t=6ZyRj44r-uotQ+%Kb8 zR*d_&Fu$auG^e3<(g!s9Jtzr+!sS66|I}=mnbl>?tSsU5!v`F`d<|+CmKz@8ZKw>>+WEksjI{wQe@2dC zlV%GUD1bYkt$^snCpfU~3oLJzFd-^j|cX%By1!dxW{ zh4Td=!2Qo=P$=to{%sWiJGW1X1R?Sr*V1nABaTbb`KA)b?W9i`oEZn|@lXvfN^8N#ie&6!n?CgyIR+!)jrM#mf zM6k;FEf82s1=eDLjr)WRoaI3voxP5@GsS!Hka`LjAB315v*pF^jo@Qg08bOjvqEqm z9@I2l$-Z+<$8w|7HlSrP$j{9pQ!Iv#DPbti>UjLCB3}K_M)lZWtUULeuDw-t#FCJTNrS0!l$$OOq#DOfa+NfhkI>Q zR>f8bFMq0`+xNRDk%b2l9w_UB^8txSleM3oLGkG{?HZ2Xyv51ecc3PP2H2Jc6qFKs@!b+4m?nkh3KEH{SW_rwaQ)E| zEYC%p)~g-9`}1}D?*I4PRp#+M7b`rV>^$tz@PoP?W9fN-5hMr{kbJy@PsrgS4Qke{vZr{S}#x)RoJwRc65bl2<2#b#H&spaIpb_BS=VdIcWYzpIL0r3C zM6RggyRS}Q^l0pT5YA(XoF_>GdmYHa?0O>eTzvfAr?bfBy*)Q_#S}{O8PraDoI+=e zeGMb{d|{fxNTGlE;Q#)&lYR(=_y}A0^(}%(d^NL2_6Jx#>iKlb$D|gB&s9<=&RqUo z#;eB$_CIuEFXD(WX+eSLH|V)>py@ga3k#UvypB{hi*BQdey7V#cH&9FBYv65lv{HT zn(KN;X+DMPf{W$I9YubtEWbHI%pdps{%iWn`rIh^p4HL(@yc0>%EX@sfSSzp8SNnZ ztN9oR_8l#u0#L40u<_|*JpA=0QkhI5s{;{qonUd*(r9TnC>B@J*tnCy&4*bmuB4GK zYD3?n-=}!`r!!&Ru-8_&dJy`B70jQqFn_)iZ`KX0-z)|ehtH0?c>3qFNWpUm>v`P$cqVXjr!WGB|!M|6OAB0JA?Vn4dmx$(C>86Z8QUQK$#VHUaa8OJ6qN*LM_u}>MzSm z5S;wrhDDU6!2ME(VSa$`eZaIE;l_#*_IJ3`#{O0dPJiOvOhnlFa92}{ ztqL^IKK*evUlgW+bbzAUHULm8EM?s*wOa=&s_2xQ~ff=Yn(%4bW1jM_s&s zRY$AYN3NhFn;VTH&D>%Z+ix4Tdr-dbD;orb%Yit8gW%lExiaOhrKeLURTeS7aSfm* zI@OxBXb!*oUIOat%=vN}U;NE&Xzcthd9Fe!gAdP++_=mNkT9me!ns?fjK=aU5&Q#S zD-DJ44%9U5bcZi6edl;(9p1t^nx+ZUu=bHyyt#?>&pttMVgAxC-tlW6Nq^(xes?}9W8-#V(p9a%Ad}Uww3fr(hb9a|M3=BfSz8-j+l$-Y*f77w$fr5+bVkGH zf4L~6aWfeWTdy1LyRh%+ghgq=I#GPlc_XD1*DeO!O=AV=fJb{c<=FCscv=slu;|C< zQ)&%(w?E3EJg@ogd09azaJ<(`x_>^K~}r`s3Te_~F;O2}5CF5ru_Cbem0_ynBzc zon08czD3|@egY1ht0Nx+PyCY#D+psADd_5m3-erpd_R%!_maJOoQ;#IAkA+AZ6!E@ z0Af2+;28;a<09VmQKnG9!sd0%ZCpbtoo4z!8CyFU$94C&+kQ(5*EE|ABXATw&t)IC z6b3__4VIlS;LNcsfp5iM{#Zr*tcTU>1+*Fl-afCP(>4P4lEss|IZzFnhiI{5TIz{y zOP#iX*H7wr`@DgbYk6G1Q$%T2?U67nGC6{k>ltjlZgccmaK7;8h0$xCyFf~T53d^7 zdfmjrQWn?mmQYz8ZZqXfPQ#tYGkEgNX{`B7&2b7B4KhA9ET89PJ>C>PUY8I8J(I=C z!-rVBbqmLD-r{s?8%E#ur>$H4SF;!Si3Z&LVjYB7u4oA*u=Y4$7N_iS0fb^qhL+!FkT} zqV~pz0bp?@hne{dOoPHSEy1QG5FzMyt-UZeZWocsYQpC&lvBu;QfSutOy42k*GZz+ z(FE>_Rhu6x)F#T82yr$=X2VIO>~obAZhw?RDt$S%5JadR_ighsn&5|!FhatEBhDsd zxyKYBh=m3G1Q`@8_Wzu{aui+dB`KCdKhy zhihlS4Kmq{mk43Mfzr+#H-^ASk5=xl{#qRqi_CB;Qx0uDo?E)&RS>WoBTO-G{+ga>wwbASNEf1Wz zft?>@^M#deN)!yN`0+s-#|LfXiz!^cQ^LlrA~b!l54UleQ7q@N`Eb#ryFo48 zCE}>+1abctYxtM{?=v^o?VXec#}CPBUz~l8_`Fd5!K8x4Hda937dQl571@FVJ)OqF zjZMs7zYZ;>``_gf;JoeFB-qX7G#_b|*KPpe;ITYeyZZQK&9XCc)a+I~F$@NdC zoo(JJVQGChZw4#tIlO*a6VhSPJiVL_@}HwHYA98w3jo>TOkAI2;lAp6-DVS~TiZC>-ht8Wy4pcL zD+A&z<}({EgUJKz#TK5iV3-fWOvV8-yQp{L>m*^Fg;*W%M87QGihyz3Wc5>P{{w<_ zK99L;>zKQ?j!Zru+P6)=LiAMQ39Ms3b6TF9RiL_!4i8Kd%}-XV?^L0sX% zRV0qb;YuNC`->xdihDkP-q~>%XUARSiz!^YS-|RMZlwP9uL|@O!Sy?Ny!x>rv=(9^ zvLuQEFA!=s4Ltwu47q}iE9 z7ehd9{RCY|z%ch!BtlBsWz?lNl0)H9!%WL)-EseHrGn;azkxADAbA2>eA2Gbh`3n! z1BI~3O4Vy%_ZuRjZRsh7Cv21DvwTOuYK&UX8*FWDahElB;L3ub`izlDfBru8Wl`FCoRK;&uQc*-s)}&R%fJpJY~f9EqPMulZlDF< zb9d(Pgit6vA&|{wy>aS9WjBIfEp4Q!n^OsQgg@HF{3@ zKdB+d_;#4+FU|=6*g3sXV^hf-+g?+iH$aT0#z-355;piG8MfIh^7)nRD_Q!6{|GOi z1`_%ORJYl^+)p=zFN2s!g3mRTd&YIwRura2G|6WqxTYO|y;B@Uv zU4#|NRvgU837pr<-ric%cI~EHR}{sYBWl4!xlCBJ7axlqRM1py=ciPgz#z<9e!68v zoRI~1LuJ__V6?+-wR=g>@K^NTi6KfOep3bF&)Pb?Gv zG6ZnI<$}#IgHu@JF0QH~f-v0AKGI^QYk2PX2x4^>(45;V1XO7s#cu^dXT?N48A`wJ z`e;)8?74GQlyL_k0oEx7c2=<&XJt>O{tD#x@nW%0XUH;%FrjT^{&G$2IZ}|64v(Fs z>ePCkIlc?#8PXI`+Q>1@uY32^2l?%{!&;MRJNADaeJm(u=28dMT>vT_s)=UX1u`-I z7;%^3KFZ3pv&BaVyV#yHr0iSG6_PONh@ROTwr_Hi`3O|ai55QZwf+fx=+{zYPbSDu z;>b_=(Rx&S<3qgUiC4$}(mWOK3Gvb8|8qQUTgP96Rkilf*~U5f02$TJjju0RL>Xhf z4L5KRc4QCW^PNW^@=kmh*Fa3Iz*SjdFn!Nj6VZM!q2)8Eeob-Bp{wP;EJ<6JSMk*m zUHXg1R1JgZYrvE-u)3UXUZlt1meOIiBZXaI7RL?3ru}jtIJl=rWsZbVP7?jW{Pa| z*BEiNpPS7G!AW~Loa6c99L466K;pZOI0%F^%QYn^B<1}M)zBva2pKUG>{e!fxJ!mf zjmqm0Vqw4715B?euHSBFi3BXdTeZm8*pF>=PLpRMto$PecJ{pX_JC`(0X$;bqm_1U zY;ML}#io$qBh`uZP)SttVLk)Y{2s1RagSN<2?!(viQv(djQhrYE*N3QWg?~i746x( z09SB3(pZBZV8nW^3`n+3K8x|fwl%6l*IvOYSzT&`B2?s$-Day9yNmk z0#J<+pq|2ivE@_JmY2|dV;(zhe%Jmt)U79_f}BZu+q2v#kWK@BRev0ja15d)kFIt& z$NJ2r;ZY0Wcb52Yv?U(2MK7L@g-fun3aAQ^&kMd)75F%YJzrq~ikNxpwtt_HiQC7L zp?}Z$iwMq|wj!o--Wapxq`y4jZ{;xJt5&M3=;;9(9O(4Qwuw_jnReH(N>dlqOG&($ zsDicVi?(WSjzxNC@P>q}mRLyh*)a17<@uLUqMC+*K?$u;5NCK0T>b%h(&ldMC;ULl zf2sRaM|#IGGMw*kKjgTKo*;jN5&H?y-=O?*0a4sjlXtuKAD}yom>Xl69`1<4Z6e3N zEb%?|%b{p@U|TPofDDRNm@JVK7R#v$9hr2--|bOQI9={gL`!fZtpM^V>4lWR(5Hh0 zeR(lsot~JB&z#)ZC=5!y%`VJ{xu?5W7Jt=`Ss0y_GM8@1=02eNHZLn z6?39U&iZbnEc-RJUn16cOXl4ui72#A=+f?>*)e0a=W1U9*(YvuOw*IGzZ=`5b22jn z5}S~0V>}r4i$_ccMM*t`0u|BNtAAc;#zTi3HkrL)_>OqnJsb?gbHEbto-GrSw{n~h z1O@5-t-B5F5`tX_XX_PoxMl2wtYLM&uqVr~buas@h_($>{u7g}g6qp~?k$Z~`!0}m z$>S7mm@LBYjt(a!F>crq*EbdlXQlH3G0>PTgMapBay34~42k!0ktHd5^po_GV3JK( zqd~8s&CHcd@5Wm0DX>>gQ$s>2!UlW;K_EiFot-q%k{if9xc4TMwhy8z%DRzsvYZ ze(nO}9aHg5HRnSL{v+wI8WlEppCV^}?vnSm67SA#@b-1PIdf}6H&JBtRYr9`pGIz_ z;L|W)*i7YiazrgUR!azO3sd;$2YT_%X#uhi7Y6m!cddcLI`Cz@wG7EsjWA(|153Ec zm3!3>d3d>7-2>hczAi2dp2OY(P<&rbjiRwAfqUz9!yd{J>3HLjGb#E*Sv#$oEBIz0 zmv~&$*bC#sMBB9&D=kR;7c>EVP_D{!O>;7mu8A~YU*OL{V8R7x`~UcRfXG)G+9reg zPw>|mewIQH#qykG)xYwXslDqOgi;=n&p`zP^}r9!YlHf}`BN8GRQ`xzoc({UThKpx zu4d#4rf_@2y(|t3n0^Qwy;D9n`zZ0@OwT4%{Lv=Y5BNe~uL(roBXapol$}K;EiPyu1+P z9WDQJ2etThjs6#3Bq2w$QM{BbRZ;FJZ4lz=&!}Nk;d4*D;DI8smF!7=TDKq7qp@e~ zqXXaLy}7k|peXV2AMG$-M66R6SB{N*+ai{Nxiok=0mi}5#R2b^UwIVV6r{ z)*ZdjTUDA}atg~d-?(XmTkMGHTrgXzCX@W>t_pdX6(`Di1DjACVx-R)$7Icp$p4!3=|uu@^6u-BFqmUKasU)2xb9B z?FJXi+D8xSX&84>n96Y2BS{sdd$!(MQz+rNz2>79WojUr_zwsb!;fPHuH|)WRmt8P-9H_@*W~CG)*_zUe`XW7bRON_ zLjX-=mEqGduXGL$c}Il8^Ib8v>KTU@4Y95ius3^iWe=X7V4+c{#rPj{UEOW zLUvT{tWZ?dm<-MBuhA!2V3DiJ&5|cJCfa2=h>eR%!-6?Q1YS@tjtg~OHnI=nma>Ma z%2vNRF8MRGP*@no4}S4>kQPobO49rb2?dX1?TvIQF1q13uQvxF zg4fxPJEZSOQ^@32)+z1g#Nr!q=R12}wgeuqT0Wcf*$UtwV{y2VIPjvidoK^pm$4Le)kHf+hMb|)Zcni1*w9A{~0Bc zRHOgc7+FNu(#)!*p|J3+b>i{ev9~XLVWa9+DDqnoY5UceRVhG_8e+`I0-O!5%ZCjD zhpqyL4;~!P-h}#X<#NX9@L^-}ewYy{lU%@8{?CWC#4`OPsHUx)p&5nZLz}0Fps$P6 z=0loJb*>|d-LL8d;$|TbFn*Pt31e3OTMZKu83OPXbY)A%KNxu|dC)c;hp@4^%HY$T zKNK_KZ$?RI#Y$f0aCxg4t(eqgisx}HPX8zaZ9=M|9X)2VT3yu<#gQ=CD9t338Ajg$ z?lh(|bwE!ltx?L@_)GAf4biFhA!3>b<+@3KGk!}ie%cNq`eQu+8&|zq;OSPKxZUk4 zJZBTAhEWC1n-?7`4G!wRZCm#PFDSR0a^jP;M2@X(qgZ#!-`u*ZCyGBwWb-fa zrQ}hW1Z7LOcaiyP;n08%F1YB5#Z|8@Yv9TUqnI^e)`_g;$I>37+erW)N|Lcm@Gc=Y z8CIyp=1jaeEoGRRM%d3?V?x|BI;l30EF|;itn;=GyIF0rA5|7raxB$J@HZlu!ET>F z00KU=QgSxwLs;TK_d_m`!!8|j9$m`4@3iacHsf2)rKq;GGOwCbxV{!cf9T$aSomHR ze9c<7M?g@it>}H1y66=BEle78-~hQ zt3H`S)7O=`bagyZHv5pQ)N(g8{t`5PsZ z-)2|^$$97PcQ3cTligWQWC-!KMk>3`G&|rnK2q>#NFrs$_wVR^Gv=dB+kn|o%KY+a zQ|ROR7lNaV&?^?(&d8KK(3*M(rJ({<%0e77M^}5atpQ5p&z(QN^;G-V-z?y>l7DHe ziIy3^5_5kT^PqS&a5Fr3lExyl`R*sO;aOESV)HRix9{5g6_UWt4CQy>J`4Wp6@B&P5qf8%ybN!K>nn~e@G`E^5pWs@P{2*g#Gv?e1U?ZxIl)vIqYPd zHu^p!q2vhGm3(6J`OIW8E=Qb!D{J$d9?t8%jhF%LnbjqBc5aY?+j^(miaKj=XMd$XUna*LtSyt4jJ=% zeFtMTG)tpR-9%ldBb)StEc>*&P)$HXCG>dImkW{UA_!@QPe0B=er=x)q#+DKizA+U zsSoD7@9}B4FEybs(=Y>+dnp$%d0QmjY9{76TXh4HKmF4{RcKL2&yF@L{F?w@MZqBBO1t1$EM>t@#eCguUb28Gne!-Cza=F%nS(T zXr=sBr z36&B|_Sv1Ra9sS=1!{Pt$OoCMa03FXFS%&K2gmllrDjoh-I#s$<2cKoT{D-oes)&H z+wMJHe7n$QDEi-~m?+BYyfb1Lw`%s`4dqoP8HKX6lRW8``{|>Uor2E0MsoQG7vL61 z1<87HU>m-H_lmP19XJ)oe-KH#sg&>=*dujW=Oq+JjN952__Z$8&?AO*zxMPaNNZ#N zIZKe|0#B4{Gk?~*%s79ItNVezG3M$u_QDt648S8bacY5=%%J4c3*U}t@xUGhO-KDE zAz_jT;no)xcsx3WfCY)nMuNF<$uqXde^^MUBwRrON@7{j?n=42=uFiQ$8z7c`*DTU#}%K;GJ@>j0oj}n%$8DX{^yBnK(~@ ze2D@{bGGLP`XI`b1@KJ|Dt^jO2HcrRnl)R4NLM9GB#S!6QMTth?kHjBVB|Y%j4>Sy z;4`Xq5B(o8q%UU68T{$kGBfNv;|KBfd?1mZ*3}AzrPnR<276!U5XT17ZwoiQ2w2F! zRo8jYT|J0EYh>uIFM6r?eQ-! zZ@!Fdv>eMw7@tYw(=ABcY$mB(i=6o#>Z7E2M_3f@7a!0wG;lSbW=`rSWyjP0vH?$;9EYcZvS1|z#SfHQU%1b+t;Z$?yDjem`GDXEllbbwI8UZR}#j+wt@+Bs@Y?kkKH<$ z$xU!^bndnHbe%jq^YB#SL718W#+4@lGK(V2l^`#EKqE$XSHf*WSM5Pg;qnZAD9|Q7 zYw-RVI)p31;XB!_b>M^JV^3rvC|jG0nN-5T7gW!if)NICLHH-En3^P?DAjvAIj8DW zaq&HE$FfVO_XWGIn~$uDljo3M%$+Cd+LoAkL9lBpQ72<#Ua4(y#o7d7M-!io=l#qG zLN}Grdt+o$R%g?ktnuhLZ0wmkkrI4=!#yj;5+{jG<&qTd)Dr!A$puexa0Dkj=55J~ z3Q8~WJJcUxrWm!H(URg3_d8o4Q=gyyi$_<`Jqh|odT|d!LWjX5YSagYLr`_E-=L-+Zt$5n&wx zZfG(C$?0KS>KGYcto(U^R?t9k)KSTnH$9Wa0?MP#+tL9KRUrgc?07LGL^gbQ@EZLv z28pTUQ;KnPV(V5%>hAGhv9P@_xj@Rgd4XTr6N}IDOkA#>SBk49*=K%neBeAAY(lCo zhUUGP*!;&!Uvm@!Ug#Q4!XnAO<$TTw-n>(06?}FFmp>#JKj;t93vwuOQc{N5IAiYp zv5IYGVO3f$>~8#G;#pZe&6?M99!huLq;u#Z=(@P!?Hn{yfKKkkPv8hDDhaDrVox9` z9f95JwHl^!m6PyxNB_@M^<0J=zUMTp%P2{iK+_D7!xDMTLtf89_M7DD%D{t)EEE|v zM~G=Ym*%-h1eQ*i{eonbY5oE?aH9@g6mDDR9e^!pE`1X$#a2}ym?3>uPU5B%M^v#| z5%;-5i2aZ;03fu2WFOe!D>p{APfJIvf`*^tP7%T&q~g7rIQdt!zazM`TlB;p8U`B1 zU<_Vz5$c<_TV+sOvs}LDebIBK1>_*3rm44hsNDn+ zzW2ghAW#4MiTP19ytj0ifmVhu3k~)_QdBUe7E?cl8Ou}&CF6kYn)LCs4ae(gfq9ba zqny#Frc`M8iQ%WRvN4;1JjI|D*Dn$3mS%koR&O^Jc*a7 z|GRZuuwugYT^(mV3H*Iyx^N!!JNkuzI@o2k$I@3-taJ0I#wN0C&&@u3_K{bP0VPO2 zs9}s+_-$ajpZ8D8DDYA-a^?)pYGQ@@5dtP;rAm`FCgnRQQkqL0#!R7g+_*Gb)y`lCQTxl)Y)W>a(#~m9 z94vU^+s?KdN2IyK;*;Dt>o&8bhD-_)J%XLr%>=^Z3wtsy#>nQ`&2b^c)-+Y5PY1XP zRe$o$X>Hi^s&8p_@{b&Wx75TJdmDLjhitA-nrUA&iFb`kA`EyL1PRssF!7U%(5U&s z5=0oomY3=rl!H!;^6&ql;|MAUq>AV>!RIq=9^sjQ#}*q_&mrvM&=xH6 zat~$rvKQ}&^}ys0^LQ0AwCJ%Cd#&?fLWY2}X5p*JGOf{-Wyb5MufD&x*v0`ar= zh{;$D4C-{D2|xFXXYlPxwUmddfuF@?nqlh&IeybLCb31#8<&GJ?AwAZ;p63>s1w(a zZ;lE97*#7Aqi(%{s;#W?MFamL5=5*Kb+u%5sq=qO>&RY{ORsbbnjT6%rKcrspEgHCt$alNJCm=XV7#M&l~xYWuSWQA3c%(>6RNP6axPE(#i-9kbG+k5T87;eaVc}5p*Y}9A0 zqvuLKZUX*tXrn&^HB;E=nYP)C<$#LBsoSx89l7AM-183De+m-D-^`qa z+fuVrq5Rc0&+2&Yn_5e=h^)93oZ9FV`aCGSVPc9-U@eF2xU9$FFJ7o&@FrPm!J+7n z5xTzrhM8$4^iBf#u4uW^7A!$#I=6(l$-+LU@+*zR-kGG0HMOdUku#cbba0*TuUL%+ z3zYjrGTapU3PBB@L|B=Io1)XoqnDmgnV7$n>$7h@uYEoj&KBdL&j|u_p=Tb@HzQw0 zM~mXv4=2CDhKLyxW|IiMPoad~Py=O$vT$+OwV4fOZM~ZfL9KJ76+sO7afvrDv1-CR zr?YNH7;x;ZwakD`cLjeo--!W_q0DM6G$vG=Od;j6yVGX(k+oQPz|&38udL!o509hIgJ5+7c(iJr+*ruse7t6NpOcXHS%vpU;J zlPhrqAKQaO-fxH)qIlZA+p!UCTgynd#B9x|fzSZyS>eJ;yY65{i4SWf!-EyLtHSz2 z3W}T_!N1tAShlN7!O2VR6chT(rOTS654>Prb&D#3gBJQ0jNX+JyVK4K*z6@F8{*i1 zHh=d|4cDKJ02iKF?Xp(Tp0@6jv_?!Z2>$p=FmJH_AJwl*%0DbICSRi1->iw)J4JfN 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 diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-slider@2x.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/taiko-slider@2x.png index 05b8c5783f4bde04d74aab93db8c17f5e73c5060..07b2f167e05f2ece819d34d2a98fe091fdfccd38 100644 GIT binary patch literal 96449 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

  • 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 Q5P%;=IaS#TX|vG(1MkGfH~;_u literal 127176 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,o z@E!mDeW%&1T_tI$;n$k@dx(GeS}b)rp2VmrrPL+c5x^WV6)ajf1( zI)&f9ex$VM2*EpEgF4Ls~H>WlBsIV>UK|S-37^6e?tGO9$bk({tadf7EFI7<09w-f5t4#p<^HX zmJ1I72M<%%Z6kXfo2y#=d9|b!r$mngnvcqyyPDzsHk(&LukYq;r%+n?HzP(=ur5@Zaw8Z8=wPR zR#*#h5B|Z4m_>yXBOE9*W%20efrJO|!K4x!Eme+}K-|JhJxEN{d}~$Pw>g!etJz(> zG0`PGCAIg;4a=TwjS2Vr=F}J)dRS*)E*4KpGEhr0qfwit24Wo&@kxYS3y?P1dU#u6 zIoVDY9D+=-o&-QA?cRl{YUSC|J1l~tDG1WV4~LkH=0<$Rr%2M%y)7Cygy^)0LH^ed zuvfAKZPz!uL{1YsP%fvvv2Q>I3A5Jka%GSyc?vY@Q)h(Dvudq{0^m=tNy|*eXVw>l z@!D9qr4$&o7E4{?FI^>qXflU?%BOG-n6d}{-Xc6~VW_hd4bHIYg3<$GzC2I$Kr)=F z(db0LWB_9T?2Wi@l}>(RW@f4`PNod{2pd1F<8U0fIu{-Qw0>pi#PG*x{|f;`y=Pa! zTw+Ucl6ds>$s(4FNh%hugi2j;SONf4_I&9a%>m&paTU-t0sojK{|HT|Q)YfOJG;}? zi5Gk?j=FDH_fdY;9!%se&Ba6r-sHx5aOk{=OKeA|$PuOiASZ6;{ML>795B0In z%^3Hh&R38`|8g+{ivCPFdC;^U)3!KY)ZZCpvi{mGXm5e>@&hOIIiggvkd`O}wyt|O z$AzsDOJPW`6^mcA4fKU}6ek&fhkRO80_&KSS z(Zi&*Va9z64RbZ4^@l&87;n!plCpB~goCSuOq#P_onCa2Nz$o3HW!~L{!~nCr89pc z+rjoooBA=)A8>@*8cxwpHon{ZYM#I#(_rGbg*KFA!{7jj;$gzV?C(49>=?Y~ci-Sz zKEaxJ=rZb%E#=E)*w}B5|4{bVcYh7t`XF^QD-W)ev-nB?wc~N|Hal_S&(zw0q;F=> z$xP>9r#ER+fLTYI*?_PSP0HhjM<-=4T8Y_}fegSbUV@`QO@XL*%71Sf<{7}K8^6Ze zmdC*&jF3&Ux_-Ls^+5QkL%}*93{jLJYYM`7!ZRmtIFzG%RUY!pE!fWU+#*+`TscAr21!?Gh>_$uC{@sDpWX)?f$`4mpfcA(osY5O#ev&aCz9G&c9rH_RZA3Lb zNVu;#E#flsX;2EpuboNuy*|%_!Oc(c;@)S3H%G6AVxP;EpU@AsP>6ZW9~X@YDqcSr zK6rr`F&63C?HaC)ayY>&@?g5r^_hu4W)|#MFwr~n9*#r(g zb3nQ1WL>VG{5{?fpBNKj{^e}DIpKr}={D|u;5+F?VkY`jHv#!-Y}*C!W)PnleCC2% z^|g1tny1Y_{Solk$QB?rSoUi{&a_FlvG~u()hzwu?2^T_nq*dYn8da9o|{73c@3sb zo<9+Y#F|4_2>>$W@h$F}wtvu&5{{~{+-k%93w}4&fDdox(8XZ@|D(CjqfoKFo?hul zamrtdSiW#GX$HZhX%3Cjt%4n7IR3_Y`+mhh28@~m8VT!Kju5C}Uht5makrY-=XQ%M zwU0C6X-mOejzXC$J0fjWkAPmBLd3rn;8-?S$3a3TQ0C6fhMH$*_s@e0BaZKf=8%J? z5jpxt51IOF;eszn_kDMS2>_lFXm>wxF!{wLt_7;S%;v0JIKA(Fy zUDl|COq7JV5ZVhtq?%?HUF>l8j&qNFv50fo?ZMrO7AEElj*u5K_zuc{<>i1n;I{VN z$3gC=qw9%)qqnTJL88_i6T8t3Z%!4ln+2+0rXQ)Wf<`)UnjWOXqZ3|aWl<~}E)lh@ z#V%YrT90B)U}Av~3yI3#%F3c|U*lIUC<$LJbz@3jWH*Igo`rk9A|~WlR|@2|Cm#lT zE<@;+2OQ|kbhXnr{8d>ja*@_OK(RkRpDI#%
    }yM>XgHr~ImQgC~?`F>Gh7oE%p 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 z z`{V)Vt@f8!8NXwf$vAHEy$%DPBviamIHKf=yr597KEq$#M|C-;6mu<;W*IEJ#dk~ay4;0$|`h)*A z=vl=jbaw^jwOY6S-`#aYb$54AqSV*lTMhwXtQfx)9B&*1_)v~btaJ+s!YQIu)hk7X z{@^%2BxNQdd-?sgkTCa#5`IqCxA=_b$IbkYkpL4<^Za!CzPvFWlA@lR$`IHe)!j5$ z92jG1jCgdNbNifCqf%D?2ipB_It1jmz4)=F&6m#a@sVyWP@Unn6CdN)FnjAsErU6Y zWy(J3E|1?k35bN@%pZp&XAV5hUv^ZjXC4p;O}#4oz%#{utB0609OesCs3#<&;YfN# zl>9>m`h95OX}P<1#Pz1ki&BS3xdTv_eTl>&db?23@HTQmjd#${2~jAWA~d_~ zJca6&sO%EkQLkRcR`fQ9LQ#L@YJ18v&z5C3BFiks*x`QsYEt9+_{>q~I!E2XW27)8 zFx_u5q}d~NI!tIqI^#ScX|`~H@Y(mNa0K{i=ot4!*1bn7_tf)|5x}tK(5{1rN9Wo_ zX$3}lSV5S3%?RIlnbH-dHb(WICl4n))D{{bKE0Wxd$N?KikyA0Ltj3~!#Z>Fs*&Mb z`Z3RdPOdhU_RbJt`!vf8sIpQp9*{U3u=y)udiFKs&q)gE@WBFOsw!_%SmV|+^*kDG zkOx=f60|dkaCFOAFOPqpej!qUN1b?L10@U&w$#9s9WI{p)P`J9MG`#@RxQ*YA@`n~~fvxjL*+(`4{y$o9>YZ3JdnaKgx zsE_99U#su@0L~V`=9(`yEaV9Hu?#T&*8ME)KImJR?c&p0C<<=1;b!^EO8!i)4=-JQ zg%|%fF1^H6a6t=l*hCG6#QypfwX1P1Tpim|{Krg`zz+ZbnFQx8yw3+$cVSdBILuMj zv*`6o5idtBI9k*MgxC-V*ayoCHKfaF3`nf4?A+@we2`o{biCcUXWvrGyi?BRY!DG} z6R|0nDeAHDv~<;S^Hz_FZ#|yBcmcTka6G<8o+r;~lh@qy7x6Ljd2IXl#d8DBeiE7O zXQ0Jox~st^&oHBUw=f(@d_9Sie~ra~7Uj@Ju5YpUMVZ^K#`jKT?Th>>U-tZp6gevr z_Z&2fen3QnBn$~|ei79+;O{$w1E4eClUkXjpDYg~S;BuPK{s_jGU373Qx(@2+K?qQ zcXaR6Cd80nViPnLWu;hoKa%k>C{!g`ZV_x`l=@?0iGJ!An_?yDeA3vbsgQV#2gO|h zh%!I(B99ZHD6&;%bl0q#{HL&ku|QmD_2@uyR+%>eRCBL~R>F7#h-$3pIeuWrm_O!e zFS@+W?dba^=4IsX86?;(!<6tmg!67;*$&Mw4-7DMDvIx)qi zL8rEs>H0{V#l?3lV^uD8IB7hM6|rxIX_^E7CK|+gt$rTE=8;(A#LrdhcNF1Fz+xA( zxiS2#=Ak@A)QsFKz}l3BpIlI&vs&!7#!vs<7dIxZ-4R|NHVKVAJMdpHwFOnVrUS9+`lt5$GrqJP5|LK05tDYE z3ar*~W-h`^>~Vo2?*>fFe;_Jjk(xp%vwUi=kF*M4iy3N}KyfuI=d7BNZ5bP@x6%2H zlb)8)T>a?M4C})AH4vh&8%o5t{bV?2-n7Z5gMy!uS3k|kzu+~n$!uu%y@#+S9RXsL zpJLe6o^I~Xt9#9-Wbf`vPW9PM1dG3!-n;j!KQ>Ux12KnMpL01IPD|caM5Yr(BT+kr z7U&1z67?z+2yzH5zT}HHqQD6>{~UXXu+9#0E*RPlg+T}-&cyFGZBxFr z4wYqxUNl0zQnS8&yEZ7gV1g_3ie5fWw$5aZi%XiB2Cx#Yam!*NIX+_AyI$W~;;8&b zS`TX^4S>-tixPM4^dpHM|L5SjRQP21?A%3n@K5>WE9HvgE~i`|Db#i@UyNmz3q;_F zaT>u^Y1g^70=`JhKT$Y|(`m*9;+OtC+#UV}nggcqX=7Gs*ZR5?IyF2WRPHlj`E8w*B`Syvj| zH=;~>UAwW)e6QS9JaIa&x-0pr!WJ(-T~A*3Jp>RAdBrL>^XcNdlZf{Jc8ETGPJ;UU z3Laz;7gD0{U_wr{Lc#>Nokh3;+=LqN%&(Xb4J$-T!Nl&TErk+WW z#bMNr8`S7eY~OJ-gzOD`>&vzCl&V$&HwkP7AD=+GKzVcN`x}H~bbq53hJyol)n=Jo0F?_EFY_3RtF0=1`B<@Rb;u@ouU)B_o#9V>TNZW8NI1hR$Rz_tH5q20Tnj+*O%AY3aTIviJ#mM{ z?Yo8$Nt|W}${H7dF2qmH$^YF>_PLG(^>laFjtR2ir(zQYK0^LrH^@S0bItHy&lVD$ z(9n%Hj*g|RRlSyl-|ry|tiVe_c&*ntr~|5Nr$`W9_4}v~EYZ(vPts5~EdV`;t`vlf z6<$Vc-U(Q0E$W-8bD17X-_D(knZ(1L0~n~<0!Y+@D&q&i?CU=dN@39gpI3+bH6v7x zxN7YA?MzJfMoR-+Y92Tnfe{Gc6aX9-#%<_8%IZY}N)eP57L!!&lIw?3&*253&P*^l zSu$E;{3S{xqht1z5)HEbA1N=m-hJ|}<$A6iv89FM&MzVpR@?@ubTn2wG;C#;ZX2$LO7mI zWAM;fW%kc2IqqW0L>9;Q`RuO`8PVNL*ixEP-CuWuCN#)j&}v1i&lOg}6_gEy9q=Q8 zTON>S(MJzGpUj(mQr_&*X)2K9^gMan9wdoNE;?d&Q^*qL{MAlC`4meq|n;8KdF* z)v$Q@<*V9YK^X*q-Bz!RKq_gU&d`KilA>fO&G1vjPYCeIh^q`?Zg7+T!GQ~U z;c3+>A3(-^^l{%q4`^j7Fec3CY2(E5Xhr4@Tdb?c%9Tvn=8eG^y|MGR|EpSh{pr8N&Kz`mIe4%WcSfPvhf{{F&Tu@oRuHJ>X2!n(`2AU!>R8Z?DBAtm+kj z>$1M3UEAA51aMk-J}^_M;CtL`cU_KUdUH8JqHd}EJ8$(LhTj_^cTaX5R2l3}o^I%$ zu*ALt6FS&KgnkRyE>)z^`(|JwX7G$ohNFKnj%2zb0GxKS8L2e`1j3R|F&-j zr-_ib-57}KS}ybpCiJ3Os-(5n>0Qe!`rf8jKAl~8!h5$+T`)J8Q2RGg9`!n|n9BOeDp z0eJaXSnnrARN5PE^b!uD3V4R2#T+ri2>o+sjM@D);@OvOrAhoEe`&b-0vPt2R)bZI z%QZZG_j{0Lb>P8kS_lmA`|n20Jqwyl9@=@=?L6Y;(wqTuDIde{<cn{i(=Cc1}823T0Y-3?QLFI*adDFmOaXa zp0pm1g6YTaSp~5i;}VKQ5U~^?zPqXVFdeBEISIsDotVzMSzh|a$DRpra2^9x zYjk~`QNp3}4ZAdZvZx`$eOXmq3y_pkhb2-lB1opMX^$@>4?W`8FqDqQzvaZaDK-U1 zrkKw9r<%6x&h+mk0K4qa4>cgUb9{ia4uFUqPOj2juYd}TD$36pt#sSm^#7q}QDI>5 zU6wA-wWWoU7)!^JA*h5uM8HxPKX19MYAM?H5{*svrgp?%O5rNgH{j^$x#rhc0|(A+ zAD6HY(Ias&7by@*A=-WQw*2-VWoeCPhSBnJ(dF|s*NtK zpW<A*(DeP?Y5BnQ*|c8PYwP1V|SiE`-8Jq!b%LjoogCnSot6Y#(1`q z&b47?maA5nRccw>7drR2&oh}84nl_CSNeb0y#cX%FQBr#P&YeTm!hAA^{|dC8u?rn z51zpvImcz6eUN^u4Yco}GkG+EuXUmFe1T!oBcjJ=al>E*Ce;SiXE#XtpU^_(!r@8u z`=4yv{2H*Lrf1Jwtvaxmx~u#ybD>QGj~}^U1?zO>O5W<#$I_BDK+>N|MF53Y7GJ)` zOAS++A~<}S@Oi7SXxVR{Q={@!fuQQnQlG)fr3K#K5BpN?7neZ6$NXt*5Yc)k4!u7g z90svum&XB+%gQDYHGTvB8Uf5 zod1^>vbl#0=!QOhJ2L6S`QihMcyTRM*Og#)1(4SO{v~-Xab))>h+cmDif3;dV)L7~ z*CC}b_1leftkXlbRa2*NWo+SZZf6zBbRd$vyLDFnmUGQsoE-Lq!?TOQv=b-gDO>+8JzEwfAD< zL7G9cj-wzaD`;i#v)XfV_pLnCgXV^hGzMlc!xJ;PRw;b-4fI<;sx`$TgBG^;7y+Ts zTh9+b?9EP_;od@bm&n-xT5JX?I^;6%qjr0!V-`3|GSZ|{ln+J%Gl)c2jcoiKVAM? z(O~W3!FT0HM|lrB6!F8G@4Y>0EK@?qgfAmF0rYb|aai<1FvW>%>>Dc_xC=xgNX1_U z;@$fnbATl`k9{f?N;U*X;&$|D3zKpwrIm{2F_Ty-Nu}hZ<{4exP`kG5|IS1RgWv2U z-o9JPm^No2wn#0)%%{AQ+$#uAz%ZW5Y_nU3`O{t2IsboL2C!fGpO14$QQtydL{J7? z7*hbskXXOYQRpoiNVo7?{L`*ta<88%;ZPf(p#IOyr{NgXQ%^#PwL{kCT!bsj7>h3) zfg}3w`!{C2cZ-bJPkm(av6@JSH$=A#x2N~SQ`30Q`Gy8L^jc#TADa#a(=`b;L|y39 zBnL8y3Ohj>R-HvjpVdrXtp^qMCy(_|MBkzc4{L@qC_#)4N@0oZ^D?eld>|s&h2}$p zCt@RM{2vb*K}*DN;gKBNk?J;g$5(5{hDg-MH}TVV39tej+&!;7b<7+aVEqNQzMw!Y z_yY49HiKfru<3+Sud-XFJHzE%n~)7#2vK|FVhiHytf3#<>t^l}9i9@+`YFWzy)zcvAo@wXU6M6|X5c{Lc^7SwOoMeK}6Q|cgR-0do4?SX4zW&~RQ zw}_|k=TF@Nwq{~eqP5huHm&l7@R4%R9}Eq7T3eMt!X5XC*1`w@M(@TSvm{lv>NBKI zqw{=`Lvqv!OdQ1IzN?8LZA3*CtGSr@#i^cYsLAfDr3c;I@FHujxX~>kq!M#1eJM)w z*_hYx8`5K$$IE#BR*rN1?rr5Echm2NPU77&Sm7BOGwsTv@q1D&RyLC0;k=rP4Rev~ zxvVi|O=g|B@W*n>ur{KGA^nFG5LCGkxpiNIMUCow%aIV4j8G_t@Cn!(9i_xiFO%r- z^1fSK1Xr8CI>Ako*FSXlPfYA*MLw?N^@|YIQVU5*b{KGafayH(tDq7Lq#a-87~p$1 z%df$kI?6kQt-MxNg{b~`d0FLBm=IvmG;pwBcHEFddF|(^z0z=fcD49B07v4IM5vW$ zDTu1}+!{u7cR4NxsSM^7y8XU>Qyr3X$i{@OYsaYeI9KDUK`r7Us9u_X#k-mO%Z?bh zL&9Ma*2lHxK7JjuaE;9Tit4BC5OBuD-4Q+qFwYSDM7tH}Tqxt41PO)QZ)oyVPK=Ia z6L`OaK&CiMMQ(q8Lx2f(jm(*_@gNxcJ0kO~n@kq7d@$AinaJ#hT7Q$Yk4A((H6bw6OBTIv`h%VEz4*pA#iD!i zbA;-nOzqtl$CfMiWRe(S4JCRG>v$}*D%kl=W>Ea2RS3sXS_s&0O9ue$m=7$a(w#9N z+1QA^rft({O&tfdZatcRQ#!bfPSP2leSA+NosjP6gZQ)SxA5Vxg-a)V>>k>*`PDTH z$m{@P8O%r*j+gDe^;2kf3t)et_nsdJ$*z>etCTIhxLrh1tpLsa@hbb4Khc+{=qyQb zOaXBBPutoO|5#}PG&NC^#Uh0}3@H&inBe#isQXJG)i2(N-*KFj8#p!f+db0T8t<0;ndT=`god+3~LbF?L$~8Lno1$rRS89`^CBnp`Id0&I+J6 zH;y^|m3f7w@ox&Xz=UPgjXU?DXW_5QR|`?pq%ruT74kcWuL%1s%Khmco%Qh-<(2q& z-TD@&c^BSN>t{dgFFIdbDv7X26}{C76NMOgQf=OS$r035Vt7l#^0-QtEH6nS=FT15 zI{pF_DCg_d=kaG3jN^5Y3xR)oZx&m)-vS5ugF z>8gqedh_2?MXj{tw+1H63dWguQni~!$trjecJYHUo42mpbr)iVjL-$APt!w_83G`y z3*Y|5P2NM0+~px+6;RnPxa`fyU-EfCm)ls?bLjxcOAO*Aep=Ye26Kz}isRR>0}|{k zsL>Je3LWcPacDSfwY7|OYcv0rg6ed2cN0zdlB zzv2+8d)(;>)|bOTKwfICpPN905ubUe0TCUQq7? z?=6`BXv%#cq$fbOnL?=f__^&8TNu>GV+V8iV{Qd za7jJ8`UEr7jboFGD`>X*b89JciV&4Cd3_&R=U_SQ?TXjU%lTzdywIh>e!>X-I21BT z0K7#(^nx15RT!!$C8%IZlP8-K@m`|(6eY%O2FSm1LhWBWwyfhM>w3Mitx8LLE4^u! zK5(U@^Ew1{mE8lm+f4ZSgVG>DJBT)`#j7y%oN)0peVOlRP49102MC4dm}C+bWFr7PB)UqKOOCfA~mplGOVWjc*c3B(N$sXvfNrk2OY%rPxHYK3KF39OW@ zRLm8y)F7{jV8B~?^g-eGgQf_6Q7LeBz%DqK}M#{mL1pxpEyz++K@@FUZeswBSdiTJk^p2^cUFLljK!cXFI9? z2BD?Ox6Pc}pVlPLBG>mAr`-pz@y!v6y!wpL@-bVR9X&cF7g?fz@AAr9{*ArFg*kv5 z+yHM2u>KMSCesm7@KK@6)P<-G z>=fzhXRvSg;@q%jo{N1q;Qu*+^a;283e%bTCE66Pf)$1>qo4F|XrH6ZIyN?FuRV{k z)*bl2l%fD*nevP8B3XYaB@m}teQKc!5Dz$I}&b`ZBOU6B$+s8LxKfRVb45OCtqLm0?$*IB2 zHal+3Piu)ndyrEQVnAV!^@;YUY&RMXgLh2`udV6OVOlk6f>i=#W1mqhJa!2sI*)et zLQ;9lfsagja(B8@;h6M79WYJ_hD{a!*&gWI@Z14rwfl#s)(>bR0p-hmTHU{7Tb$r{ zFM=C2{))0>xw;LtvHTJ~^=`%k8$V`%BRmoWWx?#h5kDCVx!7zA49-S|5On6!t!I|R zt={Uw2eUQ2Le@fB0Py{R6EBnG4Z1)n-Z5to%_MSUT)*jzjdARLg+&w_xTB3hddDcIe-9Wy{%X%8C`2EfmvO?Cf?rJ$P3`1prLs0gfi8 z=nql|spqXx*Ka2lLwEI43`rSJh~+W}XOJQ?6yI@Oqd;1=?IQmJ;n5HiNbY_Nu_|YZ zl^frNRsh4QW^26#YtNW5GRE4wm>XbXMo0pp|ASm@7eS`q9DtZP6*C9`@wDABj>eZ5 zj!$D{CqcFkfwn-?BI-v#YTdVu3}jAAG%!Vz2GY8bFi?9uJji0Gf}5-WT2D~9;L>@e zfR(4p-I#l5CU?z@an}J@{9CKH#6FX6p8D@e5SLALLEu%Ym=RYVwduq4*JJUT;x7So35<3t0<6DVF0a((P1iWL?_ zFV$N@dmm%Vk$Zju!lb$Kt4?_?n84*8kATa`tDlqafbSPccm73MTQKa@@bgI9Dwr-^&(5N zb=fLZe#Y^FE#u;vAYy%*!?q1TgoFMqRr?;VHHfa&6Ee4~+^!9JBjWxv1tIv}LN`}B z)A7vo6J_4ax6(rFz#y_vdhxLP!J;QU@@A@uK4n4sqYv+|c0 zVf-M3q48pDxjt()en9ng;jxcVB(9`^md!Q-#T$D825c@;9qzPZjU` z?9()NGVR23S5l>kXW`z~D8N0hlS5XlGwE0a1LFPk{Yv;W`6nT@(CpE!(^X@0Z)JUi ztsmpP8o|f#a7TCU&GV?%V-SXK2bQ_8K<(pA2>`tpXdDth9`ED~L~8WPF1;OU7n`k? zN|-B6X6pwOCPc_m3#Cll(1{$u3PKK?kQk=@*c}*tsBz%=WiJ{~c{vp_+0`b~51<$O z_4}+d<^W7;@>*{(Pa3r`j5@HKan$$C51-GPp^OpO(^cQ`svn0t65b_e;W%?ZWN|zd zq<6sg9%Ul`$I)5%HTnI0cmbokyBlfg7$DLO(l7)BB&C~8@(W0(w19xpjS>TtE;m4)Z6LUIMSVe4A2M225wqe_Zfny zlxqH#K907jf}2_7K@w=Yo7OahdZ(b0=-zFRoyEv>Ak&Bu$1(tD=MF3A`!K=VHGIAD z+!H!3ocN#cdzV9vX;eXH>YKa+IW}0?1jn<;K-sy* zu%0PWAq9`;-*O6Rc@@vRk#un66fl#y5gyHw*hybopMNFef3QHsz4y_UJ53T0P|dB zq3}pcqiLZh(cdoF-I4O$_xM{)Z+j)#AT6~ZrUw&b$tt8uwn#qNI_pY=TX}XBUEBA? z@9-U~4ZfAk>Na8bJ+;l#5(nzJ0zt^*6NY=};AkUFp7iU%sPxJ|3%y^a|2$?;G%t5m zzfL{mfWBwnCKq^330yeH{!93=XM1VNXq_LI;iC1Rm>;H{HH{lKqa*(7VI?>z7mklG* z3Hlxw%8}^!L%Bgmij3)(JztxJv`Xm9V-VIxQF`%PZa8uXL;PB^|N18;MU9hR+}Ud@ zBghLc61gW7uTTeGQ~xGSz@T$o%B%ADxIlGwmeK-+^g ztQs#bRHUQP%4d5WdFfp;>rFbukvv}2K7qa#PI|;dZMK30@5>|2Fwd+_XNxrgX1nA} zle`HqmS4%Zw|-r=%=u&dv&|(bvhyJ!P>}ZC@J)g3N+%qYOn0t0y#LUYR0Uks`xg1B zpFlbM{C~Ic^wON{f}h;*K$f&@?0P-S7?QE9n{J+$t|g^8ka-xI6#sp50<3%BuS*+U z)_?6w)@($bIgj^tKYyZSW~u4uG&Xf`MqLeofkW>K%FRkUa6oNsql-$xu2#n7`ku_j z5aA+3eeE`{h)4bV|Kw)-I^GakLgipe=0vJX)Mo2>132ZEVU^89Fn3=;s4T@7Y-+uQ zE?n6MK73DN3l|oE)X1MD`iPFHs=*PMEzne_m-Kl&lY|&f94qQCro*4x-gsm`1fOxW z`PKUr)y;(1L1v)=PwBAl8${(KY0r@oE7+Tryvcf&^-mvlHp#PI6)E5fGdJ`AYg-XU z4XrCjKJZ`xUYdu})TGtPCN!IV9_`%fbINYVykc}G?un%7PNY+73(y~`5$wiaY8wLJ zkl-r5M>Ui2uUIaN)f&D==Wg>e*Uwc32(>DX1=2C`=j@qa&{I4FTHhS#zG#ZnqGI$d zG?&~?{yKL%S$Zp(nMD>Ir1GOGwT99^eiY@M;tP2NAwpBh} z)qsN~;1B!A>TNg`y7J9OjIyqPX~z)_B7D<3WXW~r31=C*A@m7rfCI}-K&NLp^5wYM z$un-Vo1bW*nB%DTjJH)wW=&fHh3k_@>w;09oYd5Z!;a zWL%8^`-;|0BB49`C5ayd5H+WStYQ7SEGs>S5HErIVxPQT0LDN0)BBKOv?P!@Cz3DD z?&4#f9A2JGdH>qF_@lwh$Qv@rmL62SbC6fZLsS|LGyWk+x$?UI2PJ`l>Ah+pRh`gCmpIFjBvKpY7VPDXoDVqv2u+$@30AK}B{2IyjnOe(B!t!V&8AO`@k z{)!;P|C!MVg7UN(Ed#V%Zm^d|?t7lU%bXcEi^&2JsBP+Sq`Z8u-iML?=L6xHh=PZ? z-Y>ev+fN4ag%59w+y=k|%<~c+v^9ygrsL*=H4yG zMxTE;!zwdh+wW)*aq^yv(y7LtrhyA=YyAY9seY5XkgyMgt%LFr5Gt0D z_w?mdvjDJh|K!owFrLF!e&~|WxoX5`ApK4|42}7c(3+x{ag2YvZ!a!RUJr8oGsIId ze+fx=SLq1SmsU*)>@WI7(9#Ot@23@+kQ)CKi&5OTV*< zaq02pAtIMXkNAiqR3oEgXeQ-!3msnD`&VDgTZH_xS=7XsqwqO|D&J7$#W%e-S6dc$&}}AZfFgwB zPtb?zugLAN=Q(_0Fr=Hvoj8Dc1*h`$@EU?x1qYVwDk{3(H55(DtEw4~Tu-X3f7ml- zK1{nHuAo<Sn*ZU5c;SB2*DMZ#jgw)}i4)Scmq@qEKE><$%&wnlwKzm3V^gU# zAVe)IJCU6N3ha0FwDE#g25jcI$ZNOY0EmuQfv_Ewy)h^XT;hz7$4^lBN zVKhCPTNN646ecxAXvHNL~-XCv*TaA926csBss;0?l~zF93%@G!L?i{ZPxDD^Mkf+djMXm5 zqT(M45?5uAGmXgcvo~R1OQ4Y}fz3|d+x$Y_luor^eX5uOG(*#){gds&1BfN@+g$e{ z_ieZ*Y`na_?6|4bNKs?hM>+=$h3_LXz9M^oTA1CKta>PBD_1fUCJnPhxW1tD-t(;Y zSVqJpXfr3tco|SiF?#$RdnI`h>^Fm&3c4BNK5IX8yGC&$$BM{f)l@B0qvnG4qx?xN z!j@FSV5*(%)qZg4D$q(+N_+&g5v;M&*go(2ETa9!>j>Rfr5*7}?L^(hWteX5kW(-Q zkQfT$E$!ny(Nc@qevvrzVe^}Yh{^10g`!9@YU-ZK)T1)TZ6L%?P#`BU7UO|+2w88AyZ7uj(_#k}C-GcfB zOxn_?tu8ER9Rg@mu-Ts*%}k6=XuPg4c@<0H)T0c;EQWha#eIYNGt7PWYsz>^V`WN) z4JTc4L59!%M=Gy);d`jZgwN{%S5R$D5+ zJLSM&6)sOQ^MAg2zbGAMKMymE4VDj6Ny5RoIkV19T4+s$+(%AN=(O7!z|(>f6M!Wa z>Y&0S^Ia8s6TCEZDMUPK+LQ5aY}!X_y0nj$!Y%0j_mlH2kl02){^xGgo96A_8wI>k z%a4A7ynCx4zc5_{IAZZXlO8u%K(fj0K<0(duchI!X3HRQVEF`KzuefggwF(?&yZc< zHn^2*gm`focorbP9I)~tKp5%aA`c#Q{*0|vH?kke3`IyuMGiDkxwk5U9*+Kcgg_Aw zgysAQy~Q7L#DqW?)Op_lb=IAUh546d?m4lO)Vf8gMuC~xs{%;8rtPUV+PUy@&kteJ zp%i5rdcskppqB`#900-s2&k>?^?PHa$aSY^XQM_(7^~=0|4|3P(YJ7$$d_BxA3`() zV)BUj+2(Fb*t^jMFWw&_sH3%*i)QZI0)!Qg7*)S~A$y0wz75|#Lr%Grhx(_L^)Q@e zi=yg$3_8ij}b5msD z?u7dz(ESD)$tu?$5V0%B{lilrMLw8khj!-VK(#8z^6PiF1GaWIn3HCt~;M zH^x$tA_WR*BERd-Ut8hkq6x&IqoWCj212A2cCj*Zzr&YgUv|<>iRBz@^A}VAlsPL{ zg?iLtp3kzl=XuLlZj4MdNSak z?4EFUj=rKduLOiYke)C`@YdtKY+c)+U|3$)RZROTx(P>_&jtBc@>DwoYLhto%tzXOUpn?*&k&0b|7G>a%pm=9156Zhu6gq>D~ef)i=5x{J!B|M zONnyq5*~acmm`88e!SEEAP4n)N(rf~okzYSsZ)7E*~(toZ%uMh7+?|~P? zxH-N+NPo@71d%w#Skl@-2)^#By08UKQ0M3flxU`eSw8PI3oQw>?Zoy`x~nOz_xBzD zvMc+-wg8jHGWwt&jv27F3Th2ADJv`B^MMoSdHq(uY-~LW-F*jpAVBs$AYnuSC$k7T zCgG90Dwp_*X?^^O98sjEAcsbd{V^3N+7Xy4SprGT<9F?686?-61b5kz{1862S2(~* zESA8{xmpyuFl3*Q8Q`tqPv)L~zJ)Qnk-DY76RU4M;34l`ukML>bM)=5i1S^z+>6Sjzt)01yMkQ))C z=j}buI%wRha!rn9**-`%a*HCITs49MCAi1=!TBo2pKV9u|MULy=}xKQCzWb(1>VwC zIs66-G184a2&}D}Jc`vs2n!G^+BmKOQRi8p@>TrNs*et{=mGG~0Q|cRzK7;u>92kC zUS%e;0b^lakx*7Iqlf<5A(cw#+Hhme@7auT;|h)wJW^RV@hU#Cu|{2;@^@MnmHjFX z8vi6!QeyFB{=;fOCBVbdTHL28|1(^zK<<@SLXa6VOI$i;!z+jZv#lECS|(}RnGR}{ z>7zT7_0#6)>yKw>oeW_dG=Rzz#cOBS`jwKa!&9+(-}{1Bh?<$gTnwIUKA9Fcz|&U5 z{NYg3TW38oo1Jf2I8yp$apA~xmJb4V=)y~h_|*AUvLVu7-O3LD`WM3Jz!LA%_DL;y z9ebxn@n;_cB9RmZn?3mzo&v`NZ}d_+Llv|L%K{T=iMd`!F!ajErlf?MbQ$mz z)o`%DFK8tIQ}tZh-U*Ap=@54@8IU(j8`&4{{f&rcz`6d4v%3n#SX&MWytf*?HC zd8CjC?MDZ%cy36@TQWp-F^w+Of z7~g_P@z)t3Mncz~`9Aq4rnow=!bW~>QC=9bOnty~Y(t6w z`sW?p3A0q}a@2(?I?@e@_(M^oO@1q&`6%5%Jauc4Ai@GU3{i3ufh?fEd2GljzEeEr zacc}h`FyH4<*QKVfhgDX-_N;0LX;znQ8V4N+25H04hw+~GVW0^%Q`yQe0-?(bi-v) z?9sttOvxXP~Av<`arUdfM5fRp5Lm%f$BlHNvatoSS)VZT&kBLJoQs+PoC zeiry?|Ect@?z97{wA1KJeHS!%QtcFH=O38$m*WMuTH~&S#px;~z9mwT4>VqASU*?! z+(|hmO9>7lB(594&yuhl3?u1d8lZ`VZWnwY0)1V@_IdSr+MkXI;AbO-AdrdFH#>%2 zxsha}a$DO@;d|Ypj_5h-pAO#x+PY5V0(IAMx%QiQ?&3(UP^f3lA@a>f z_ifS6VDVlRZd(3inl6tN7Bm^3Rn7}5Ls2THFO`KhkPZG8L^yRS;6TpCmVPSE4In7RK=Ts? zcfVhwT<_>K{-bqR2ocgQM-QT+>aIUJw}yF%wT+a=bXW*5emwksz4vbNv|#}a#@jYr zU8Xq}1jPNS?{JM}E6V2b5908=_g7z8beXgxuNwTDxv6WlsS3nX3!^|5?aZ|v2}9h< zh1aKWht%~;tKK!0D}IISbGR${n{iBq%0&Kp)J84b!6O#U%){~8Zb(ows>azln*-^8 zC;+227>6No-7RN`eAsD}z?Vgk%V8L54PAzQe1^Pq>`Q0%mE*J04JI)SX7k(|L;h99 zej~8h`IDQBw*K6^aFM}Lnru%d|foRSfb$|0~8(lj~ zHE##PlDr;&_LqTM&33}^H=SY{p)&?x2OXKFlYFpBJIZP!TDpEtS{sUS48JTH*)OWV zzZ$iic!|Bq;Il{W=?`x6GHy3y+dcaomp*PYIAPHqh%dCwzi}06`@v_?t2g2K)g%wM zBhKbuyXw1bWQ32+*3*G3Vl*DP^j zP0ayYROx!v-Tn{S^F~qOe1B8E9!IRVymGBzp&AV@18HCZu86JVN|^PFTSYi{eWqHMIdBw*MZ) z6o2=PwlGUk|8_-`3lEA?qJBqrz_!SYD+HuXr(hl^GQ!PC3P`o_`)noO2JPsJIBB zV`%3gL**?^iX8TW&?b0q-Wv3VgO~?dJokA}-rPkY!!dMunQieo>63TEn!A5n|`gUq9{f=2Dugxiu^;ysR|99g_@0px_63Bq@ zEOnHc!q*XgcW7K-N6>x~{mG}ZP}Hk#%D-9-Zy+aH1`r)6g;+#ez?NqP*hs04B>oGu z!iGJFdA?<63)7~#D*g3L%*Gs|=DC|x8or9jS^7Xc9DLXBR}(_C}NfSH+ZXhCIaqyp9@Mb|^rVaQ78#hN8`X$95a=e`m zaj0B|9CqqPc?O=SpyddGjHO|2YiV@nH1W(4fwNCO$XObEvO>#DIf&2#El!Pwt~5o5 zj1CGZ{Rco^n)4aKLC8OOC(K0-4@}~RBK=Gt1cj=@0?)o+rfzTP9A1ay4Pw&H;f7^R zt;$c-WPwTAf|>?^`O|Gt>Cc(O<}ls=7iH)ly7&XbYdg8br?2ZIMpp#8B%Y7uvp@k^ zM_{V9bu@r-6o&dMr{UQi)<+o63HKLih);LZ`upb4k=??=C9LR-5;cS`+^q>L&s4AoLI^0C8%v?%{)Q5^7a4&n&ekoZ2+;N zP25azr**^~Vv~YQtYZJ+;?pB=bN7TQywWn11yuL7YX7Iy@VzvF;`(qoQ`b@W(nH;` z1`5Fhw!ZC#FH=<|Az%4Ki@gMRqjwe_7@36_3(vLTbh_ZXR`o{uIDvdF98uS&tg;pU zSAW-J};NQ8F9tpY6yCiAj5Brz9|LKUHqcfZF^^Q7TEEk^X3h#{cb6$rx z_KWhgX}K#%%~BFNnT_N1Y-SJ>d6ALYyu0Ez9k@Xk1%GsAhClBy6dGb-Guc2vaPXL@VNKGP7qj2-JKo^ zOu^m!0no}nkZ~)(c!5d$cdKrQt2>JX*1R=NyAh)3ACU&KWq~cO&?^QMVDrxV6m%i0HnqKte(8YU zI7Yai{79}|Exx6%u@oqz*WAzkd=yTS9sGL1UncDBGhk;t@=$Y#`ZJ=6bu#U|{IX@L zG*I&kzQojEds^PCN@>8!v?GFUmLfShTKv;XzK`V&Gwnksha2){VR@$mgpZ2@!7{)I z0K+@0(ETb^O7|j+X7)bBD8yE`zCPX>kuPy3={+yAt-Qpqp??g9cjE(j#+Y7Xh zKy~*oVK)R{y1wo9-xX7mvEe71?D>>^fX3sY@6ACH?tm;oJ`5?9|Lp8f)GGNt7Buak z4(AO7BTKu1r`n`tFLQooPJhY%5A{RnnuVMjgF5slw>{9Xl9%DAkU8zYdLn8NJjKK9 zHaL04-}Z+<*{-^IIU3}{sH$}&qfsqG_C*si%zJm}fVJeI9QF>TEJM<(5DYI3<6+`> z*Ax3gn_!n_b7T(#YWxn~TuUGRgeJS(A6r0kuE)p=qDUal`RdAq9G989LaB}h2)WmQ z{>q^dVMMM3MO#{Kp^v#2tFgv>h~~Hm?u|%GxuTA#Vwcv(XY_py1h3WN!#~`^O>4ek zbPNMwLv!4$4i2lN|NZeuogqd36vSe@buaSg{bxpQ5LN%`)i}P4;6YaA9#&$_BQBi9 zz=6DJAlLft1NWDmC5Wka=_rzkjp$9V(qW2&OX>H#qhL~}t)A%5s-kYFwV3q#lfUtM zXQ`lAu_sPI6LOew;gf{gBh-3}J5}Ke07slD|4dp2*9glKN*GsVN`OS_c;Cy+N+t_Y zs?V%EQvy*50vsnesc5pg_9{}9yOk-rL27}KO;0cU_+MC0*zJV8hFzU;VL-FN<4?q% z7ps6=riojqYI%C1e5&x;FZ)Z6W8%Tb12OIC>{+%7Ux;okpZq-Tny~_Mw^~G5J~8tV%Rk0BO+Y78InerOh(O}4fBW| zp-U^L5H~6!7WO>MBPIM4YqC(k-k_Q@J-Nm?+d6iF`7LkaPnH~(P$buEX21Q*OOIgI zCUf*=5B$~UGkNBEX;wH6btZr)1lhw~r1&ea1db){8~m2#2Obp~wNE$cJrrQByx`3* zXV<6xfX%t_SauOMkCYTOk&?4}aj7#s`SP58_Kt^4Xr%2KPG3ZF^k4Kad?^jtw+iOB z{&Gp5Y*}(?_!fW+;~@!VJbJgdlmHsDi8Tk52Ou&n|O-0!!kkccF4_{yBA z^hj9LyP%671*Dq^`JaI&6o_1xj8_f5Sb|dI;66f{v)XkM?kNsRnbd2U5`#E-RkpQ( z14A?|{jQl-@&)uS@3TCci+8o zS&)CZtn>e7k}J@G4bc6O68*gYE}|T~7M{`;-aMo3OxDq5GazP|5|bm@@zt#cyOhHh zTQhW=Jp|W&x>@J$WN>@JI!rJIy@1#%PxJ}};0V6?#U$ZqW*$p(x5{rwRR>FsT?P^j4|GA++ivY#Kfft$4 zoIp3u&N8^;UJ&s(#&4yNE*@pVb7k}uVnIE;Gv`^*!m6CsgP9=_1!}IgNKeJza3IQxIY&qS zcY`PeHvbxQHh3@HoW8?NTSbRs4BH#39Frso1|J3sf`}xc?{m)%vOoKoePgCw#)6}n z(EOwdy@2&k2NPibg1dDrl5SYr%_)xN2?QG2H$`_jVCD zIxLX+cEBl2>6ZdmTk?5cvF4BNLQ6^3!YK{Dk&;;M@gX_AW7q`=>O=ul)!Cp_&h!ci z$U)NLVBmzM*F3y^RS3kG%VIw(_?p0%wE019L*#SA*~obK*wXA|Q{^;}F-Q%F6znRJ z9rAjqB~0v`gu%?^PcG0fdR$-Cp*UdqkwuNh#ZoX`6cmt9sfQB*_#Wbh1ZvwM-{%>B zb^HL%Um-=xUGc`xMhAEWO#rGG2g=vi0_@o2K$%@W3;wdUaq#I?9G`K3$)jO^xXTgG zH$U2e>31w#QsCXZyfADv?27|5@56Y%IpSVY;4?ZC#EDBi9UN&Pm$sLLU6i`sDHqcE z^)HMq5p-H$Qfia=GtdDLIc4YLn&AuV*1LX~N(JG&P9i^@1T*hIq61ha6R|JkEB;Cw z%y=CphfFk+#ON(jW=my-;Z+v+bdOqM96o3w|2zR)!*PijV2?Pk6JO*~iJp4{nd|h$ zRqnSb@IA#zc=tZCJY@9uwX}WRZ=qd$=?WcEfV8IX5$VGehrO81%r=$s zXw!F0?9E&5ypUC&WUsMiT^gU%!o7SuLlPM+Swhw?|I|V=fj`hM93Y{)uuEL!6W&SwJ?fs%q|}AAWb&yEc2Q%8xAxyrtzKu_?^uXHpjo$X+_# ztuxaw(MdCK!IKp8%)YPlJaUhZ#B84fEqKBn+)scedf!cVJfT^+&aae~D{Z;keTETL z7?Lw<4Q-w;>1}i#6T-X#nKnB5=)Fo?pYSn_0>aZT#p+HE>N7rPT)aS?qNnV?)=fV& ze&ySv5qySO`=4wmQKX(Hi`u)vbOw%2?xPB_n8e%R>#8lzsTDKS(iP~tF1Yg!_$yyW zh8aDz5W;Hdi==n?Sct_Kti@wv9fY$(lML=Cvs znt+h(MgX#^vvMV4MevqoA#rklN2vy1&P2IzOXa8Mm?Tx&0jKA0eq?bfy-!Cj@q13l zA$#9Ht;7TD3d^=^?+EE|xmM5oLyuHmwW}OOws21kTWFgLYfZP<0K^q=_`AM526vy? z!uE57j*;6@$cz7u+IXKtE03y{A>(=ai{pfzXVD+0Au}N_x1TSv4pY{&hce$y{G5K- zl`yWGOsJO`L-2-#blKEbwZ(b=8b6AyUPAK z?7@Cl5cft0X{xuWhZBzSN|CJVk5EH=m|j2ZKf_~e`zs;wFRU`@$uv{je*uI}?dXT= zLJ8GN!Eq;KVw*<*yB42B88{crClN zg8uB&zns!an^2Pa?reCI9k!S%RWLjTXiU=CUBFp>;GgVHyTJKs&%wkG%K)A!yfnAK ze3gm#YJ%W15t(E*{$#2qY{jHlGFTs3Tts4D@)TG0)bYSqVbxJ6d>QiZa+X({Oj(b8Q!4&^UBs`@i1)T2KHWpS#F@|i;8|u)9s7DDjZO=Gt8 zXQWIY89`hGGb(WWxH_}MFXXR|NcKNOL%X~#+<;1luWkqIDq(~gSQMId>X;ul=CbXm zdpM2V%{H42ED4{%<#KEcS>hviizr=#TxVaogWg)^=YSa>+)-;s80HIb*5knRFxL`! z$2W%>eI$SI(FkF%eiX1Ij(5&G^?``meWj(Wx|q9ZlhnNmcSpv(W_s5lF@B2ft&@3E z13rHjX~0?_S^>fx!G18$#ozwEen*?ip3|=Ffe4v*gb)*Gr+>2z!pH_BMA`E!7C8`q zsRM!Fvc1aQl#OLhM)NEH$-0475hQSVTQ<4f=f{nRFiqh^)mP$<%2`Kc9TB+Dr`5G{ zQL%jY6wh*ftBC~@pv$V{fs8!8m^UO~M-}e)L{oPmCX#HBMyP87TQ~+Wp{sr~j@oOzlotI(t}INx$^)UGIN!<1-pfmZD_ zQ!)AqVLzp|Iw$b6C2wqKS)ewusRQim$3vAb7Uj4Vs#|&`Br(5{t!^6Xe#AAgb8d(1 z^nh63GJ9U25yCKAtHgaJo?-xJj<8&R=T9hl6k-F=DdbsYYRPC9T(VmK&oiWQXSr_V zx}Z}~*Mjhubo&LdxIEZZ0X(W;x+-TKj8*5~OdmjOf$u&mu9TtAzNd^O7HfibkX{NB9$`} z(eCFyMiqZRhvCqlQ(=dJMXOwR+>2glb*rJFruD#^5jyx6iSoYI4{6W@C$$_)E3qUR zRm=Nws~UkXeabnXF~L~hY|8FdFU2PrhoV(M&*9Wxgg#%bmMc+;8)?Zie6Si%6w%+v zfp!w2f7}YFDtx=?=-N|Xj7avn52^VCY&$tjh(U>e#M+oOKKsBHBitMHkcbXGwR^w) z{vBx9P3Y>HT>pnqU-ifWz{<(oa8QQ2$?`mVUDfcKd_|O8wr;vqVLA6rwXPYZ(4?5e z*IpcfXO$I1&(Bb4W?)1r(TgGmV+^r`g=pzvdgBHXX^Xly>$@r87CFJGYW>JDA~rkD zv~W68&ec}w2@N~=z<+vQ2`s;vL`4Lga6F$Yj|lNnNb#+z4s^xHd0ZTJ7ILm)wYZ9y zF-SXM!JjJc9iwt`o!YeS(m5T}0Qv z!!cLxh~R6A5bQuq%xCQIjwgFoGW3la=6Jpd$X?$mwpYGnv)V&2iwIpQJn%k*ndK@G z$8pOv)><%H=}VkL|&5ES>4 zcWwnE+A$&-ZcY+#_$RL_sAR{1Y-B~9RPYZ;jvbU%>qu)nmgOoRUpBL#ApA51oyJHr z_f6quEA*-QE-f)yWSye3#=kHeW4sAnJ%!)YhTn0DgDRs!xXcdSx}+vF2HEfl18YYW zr&<5jWjZoJdrRN6k%8KlTn3va+XnzGu9uGrk`8}(mAe9LV z;{_v0l?28m^x3a$r#lYO`pE*zyMAhvDo2d6di{0@n z1g6-1vVMyb|I8hR>&`eJ{wvz#Y-t2YDjzDD@|l5hgGihl|5R?^i+)c>L5Q8^Y(lG0UR5Ub3qx8t=&-~N1YxWbvK(8@#ph}VQiB* zST*6vwbM0y64vu~OZ9x{LRUgOl@t+}xfQq|oN#VYb??R@k}7quIt4jI-@oAcO0vx- zZ*1h~OV8G`5;955& zNrXWH&O-ZXcJ5xf96b`CP8Z4ccRnJ~M|aQEt|)Y%yt$@jO%H)hN4M9#%5{WY@AFrd z93RgZcevlo`uFfua}oQXefC{U%(_EYCJ z;)8DF*_#$sCD#dKp&Oeqx^hE;#?=OlwhR+cHKXUIoKOeQ>_}}^b}MJKP1X0k{7G~o z_n06csgpF|#-Uk&?zR1X1IfIyMn=Jzv|0y7Og#?kBnyvDzhgd;eMntoGqQmhDnLyW zF@s0FTC2&U);&KJQo*0I`K`sLJOf1N^vxw7jMKaosjQRShYvNk5$qU?=Jy(t^9wVc z0Czr<#a%iJg=Z&*o}`p)&H|I}sgKW}h~~Q0&ne0OrAKDL$XB8V5U{D_)k1R2T|x%8 zTpOcBxh(Rd(Cx&9^EMjIY?t>OS03OlKptZ+RD8z!;IuiX9|b(+UK$hd6g#f)0t54z9c}a?#$^EnGuS&GROWu zd@d*01(A07j7fAx*H=MT@_0Reoa@Xd!Q-_;EcyEhL|b7-#!wRTfD#DLdw3n@Jmktp zQ4_+5T@${}SS_|H^?~Azvbcvn1lcgFH0j(V&jQ~+!W4J_!roK|$rRpA@VaIbkp75t z_Hn!HoH|d#r}zB**1?4nfIO@rbrKhcAGnd;g4CXsx)yU%j05}3#VI18y~HU&_#PdU z1|IT&=&K&IWj*Vj28F1N=8^o65^{v;SQe1IdutJY^z6s$)iUCZD(QoVig@~G@)gj? ztZ<95@+y5JjPn`zArI_Hyw1BUXPQ=eg+1~w-Ruk;O^B`6HTAyQX(oAr;Ws8u(sM2w zJ7p-!+5mpuQVgimJS=klHfF>r+>aZ7^|?6RC%^0it)@NNJTUrBKo){UuUCf@;r_IO zDLz@$6Y?!8hwoS zm)5YxR*|)-NbUM#uztVrG1X06JpI=mI;MOF*bUIVBUJz9ji_1#06adczfd&d#H}lR zDeKp53Wsc~7?$A?z~GgGJl>cI1N5eoZ+YxSqcG9t3;yh4T_Rw!VUOb%BT}`Bi>a8k z$AK3*nwqIKCW$ddlr*=JtglR#{79Y88j&jue^hxXCc8Yu)wOPD9I!t}u=-yCMHphVdY3lgD^eMcaC`4tB~BVB=1WCip8oLz*j)Inyj}lN>CYZ}HO#7Ch%=4`#mOllM{B*~;1P1E6Ej+x$#Ph@T&POuO05BlrD(BT*l5rARO_ znn)A)k#524YAIC&VAvY|aX%A)@kyZB)Qf@LTjpnakQz39`y^)Y1TeR7`0P!M_SswL zqiGt6?(!a&No=tbn)ZA%0jBB#2JA{80+@Dcxa*2qYg!I9X0|N^0TzN^LuXqp&p$}4 zhAOK@z4?ahO9K3F3ScqXr4Jzi(tnQWd0NG zg3g*&FZugNh`&^f-P!=TEWWr$v+!|&alER z-SQK4G&^0AA2e~A&Ob8yF~V0=&~s4Gr+Meq-Cw2SxX0IMVeL~R@?w>v10uqxJY{Ep z2k4s?b-sL*9Kw1AAQwAK<3<8rGG25iZ*Jq*a@P3 zFW1=#ll61Q!B?t4xLez3;e}oAcZP=gGtV3D`b;UiuJLyqS>-^J@oi^RT&_Q9ANBHm z(L9&K4tJ-wekplhxh@~`TE8!pkaVBvuP^h^cjjFMN&PHjU!p|qi?6#woQ$Krt z(+MD^Lge}V^k%+IUvy~x?vW2Kgso*q;%|l|bcnEVPV_SBGF*H=T>qN|cuEQfcU*)6 zYeMmP2Bve3PY9Zy3;Gjvg?GMN!4c5J%lg_jlG;%}=c<-Aa;8FdWj$|=eM?U}YlMwD z^#EpdkvR#bd(4VH>HO?1M*$vmT#t)(avP3{1E2c1}9HW^iBbeITB=}?%EOdbMt!LEuJLv9Ct@CHaNu_l;S_dz# zu1fcEcT0&m8;qm&Oj&+erOjA_kEIEpdT(;tv`*)~hX3ZJu79%*7#5^V3QDT3xA%484 zfZMS<0chm=GTK&<)zb~!4GCUy+QJV%au|9w7xUY>ZO?zL?9=_Dc-=*aXwq{Zk;>&) z@%shRn6hXwSZh3gNQ#lQMbKkeTwqFoH%_iDT5-nOOXc#_4Z2zO`agj6gB-Gb0Fm@W z>bbejiJi>$rJVUdIF8(`JX#Oaj_q=8n2&&d4v(Ge7``R=jBmc)LTKf=nDosSyU7mg zx3Ma8lGdT9Q|v%Sijy6F_lULWd)*#RI0z===Ki=C-@#ZOv!K-Hlv+W3a=sdOZbkx_BIP+GtL&LduMYRzvNvZ#Ojg9qr7n>d&o zzN0**P#}~&AwW`vGg2Sk9SQ!pL&}c>A&tmgE<~dZZeCC`h*+STE@1DdFP02DGLVIy z5`zBIQwWSQnla++&O$=(|JH)Dx}M3pSJ88>V2!>Xlt&c-i9V}VXPU38)Si7>&4>VF za{fE0d$Z3d$z=EC%lu^@F`qf3~ELk+BVE& z;ciNg!qXv8fEDN;2wBz&=sqrokBw~jxasp(wa@khFL+^ZPFukvw0lmK;q@uM-Z*DI z0}=l7B}k}K847oN3B(#1u?thwA#V4Pwro${A%O(cVaQ&VAV({mS2s^eL~S7FI^e?V zJ7kZQHRHvukX66edcl$Ei4eE9`(UWQ^!$Ruw*>K(`DNpfs}<{hZ%2xcA-}mhgl1<+ zzql>e+!p2xi4#!g0Up1HmaflBXf>+8f05(2H&S&OnF3(7J95>X^ks|?|J_D?-DG))2HHwmNR`aE? zR3L5C0-!^9CLjY<*gHdCCTVl4!0W#^!x=iWz*~L@`0szs{GJ3EH-N0XCSz`<>2S~0 zMInmVavs@1GU*&xkd`R`4(R#@_Bsa|P&s!&d8-T2j6ahgUq9P=9hGR#W35@>u^$0? zXQzE9$Cs8K0bci0z{md{u=%{ncTic?rz7I@MTg=0vh|I2yL_R8LTST@vFH_7~4n+`Jx?K)~%RvdSdLj@op&TljpWSZ_l5aeN- z1uD>N7kKb~VE3F_BjNUKq%rDZV)c_QYzFn8E;Ol?*#uNzy%LgR!wZ4_gMePhGfU~N zJ-G!S>7!DJK)?b9zJ2ly+u%pa^xGBzWG7t|5enh{whUz0aUeNJGzo*7n zQGg)mZ%G1p_P+ubzdi#}T{PSSzVIJ2_gkPOcp#Z^gHez)G-eXazBO;P1kj!VL}HC} z>Fa>yd#2s+<4bE_2HbcjFua0vp3s|2j&*vXElJq?_G12B8mI=3Wk zJgq;W-}Hvbi_%|#hn&#-zDJ8Y2d{dRX7VsKx)A>0Sq*M8?}&4%-Un1 zg*3d80Ms}TW&Z-eYyL3M?k067ZMg5tfm06yH=oYhMU7!pf_Xh#&NI9bNS@0Z!8wn+ z00(RSXFY;FV1cefl5sHSVIa*iY1ksDa3dGaE2UbFCvRwU^6(n0u7|wWbZrot)}93( z{}EvB(zI{x_|xoJ;PrnU_{9GX+;}P(3nrEq`fa`~*K{+I8578`8sN+W}E|#N(iHd{N!5mz!!2TG*l7YqA1(s z5GRHaFuQD0^mmzytRK=g$EzUjgpiAz?nWa2{pe198G&+hLAUAOR%Zp_Jh~ z6KuxWp4@Vt2DBW^bB%Ik$?;5mF33JBB+A@t?*ZXDNB2R(Fh9n5B<{L1h!sqds(<#M32L_)=>Rq z$0em{dOzfRArupQ^*zA3uk@eu+%E%rx3cp?JP_#h?EU85J~~tcE;Te7i8gTlJAr5Z znWv1%ABTQJcG;O_;LMZ2!hJO~UIMp2J@GDUsG&ngE_ros7I@@g;PxFQZyfArprBq< zQ!k!uED{e@Mj`NO<2WcKun5ld2QO~8J@l##li=sC0_+JDTADZ?Zog8Y<0k8Qhz-NCRc;T&P zUMLST30bymn4+Q(MqGUh_bBiH0G93t_O7daSNLK3*q7JPM5zG~jxrT}*fw|&vgUyc zG_cQ?gW~ob7rOPK;xZ?B0P7_{!odM>^%`*VrUfL}o(9_cfF8$;R|3d6+&=SfPbNlK z9}75ef(CUwOBWd6I5yHafx|ILrtZ`UxS;zyMgsKFE+W=Zao?~JuLf411eTr%2Ms7)`yg10&`^e=VKf>7c)&nfV}o7ZSBIuA{ukif zSHKBC?%xK!_+NS80LTP$A9&D&E;@Ro-<6kt2YB!g0P`28-J;`5ORohM9s=%uj^}5A zJh+_lw2Z|Z_g!`7fc4h`E03bOv4KUK&jEw&Y4=`34NZ@VlvjHH)Ecn13f#D7 z;awAufb#n~4z@@<6|_ZmDrY3$_v`+cd;OA2ZwAi161euM{1{?;5NI+i`#tyUGX|n* zMe>7Bc5nd*)axqsEMPnoY~Wy~3*2)7SXrt5tQ!iWDlz4~KtlvP{^x<^C#HR8Cyzu6 zcuMyd&X>_y8(R6BIaokA`fjb|8>0+jttC&xxjH|)ex!lpFKp`P}$_~~u9pKVM z;QU$O+6}b;nvORY#foXy*A~{o%7#}$A_&;)Bj^R0(|zB+akJ*Wfg1!cVx#m^sP{oG zjWG85(@uqM)~+ylTPKMuV3FMzGjngUPM1D0M5 ztUL)UJXQ@fpmgp1nRS+d_i*3UG+aL$I`X8wn{pr{1v_DZpULsvF9O#;q89(===1*$ z80=*Aa^44aUe+*1!O(ai96GXrrBo=4f&MP={=Wje>dyja--MIHL`{+eaN)awyMG5P zUzYO43|~gAh7CTKl<)L_2fustxy&Xb>pMQN_%h8jAg#D%=zjD8F|A_1IP2TrX5 z*RCshL&kv6A(^$&Te2>KCcQoaNPP=ufycfJrnQYlPyR{Ztv{O8q32w^o|wzafPT|n z&*4x{L(iP=T<;(@)4gjR5c$=}2hi>)*>3$*UjNY00c_DhsEhNjK}&|3rcZ<%1MGh~q? zJy*_=)=;g~Zk9kef+QFvs3iNYMU$!Zw~mV!BVmH9O)Gejl+g-Ey&!@(A~Z4yIg>Cd8{_RB>5P7c=9;QA!~3J$Up-QM4&>Jgz94* zU8p6p?|5|?xbFeAAW$9DUIgy@v%s}q2X4L#SbCMR1YLX-5H%@U2D`xZk0#$&GOlq8 zmfH(s+K;5+CmI5PG<*TwmoUJ0W9*dK6bhJS=($h-dtm)_WL!$<&gX#Vf71;V_`m~K zInuO-CQO3cC-0A)ueZIsz=!`1uy`Nvz;^%_zEW9bHB=wXJpin{5xDug89%G%jdcFd z0HILx2weh!oiCC(r@s%}_&BikN)?K)wl-e^=I;aUK5JgL!4Dcb&crPQ%Pr1n%d|Q# zY6IslsKu7AU03o(EY(8GaUzICQI?j<#K`_%`^VK0f zBAYl30{|~Lknfd)6XlTef7MRr1P~CDU3_ zS|!R7Nil*E3?MN?jvN3pn4Htg@!sA)>h`_Wb*rkYZ+JKTzH|C?cXf5DuBxu8U#RJ- zRkPGD8-uN#p|>Y6!U|D!OsAxU#Ra(Kr(oBc+WkfX!1O)vx_=KxeifE3*XlZ8tT&=B zYpshN`({1t+SZ?Q;vj6krS={ZhhXU%X)g_biHk8W; zULn1mEaraV4?Wo)7*GQvJHHRMy**q*yPLW2Sy(t(oxwe6T*k$J`VeSpq?Dy{?-#Og&l8)$-7}>Cr$PF zed!2X_=Y1rJ{WaZ#DI4~j48rZIjHF5O?%q|FlCncF@V&a3$f*5+Y&qIutKY?l%=_S5`g zLVsfGL-<_oCi}KyQKSaBr>3gf@s)l?Ic~**zEED51-+Yh!&^UCwgB=zu!{S_XKR9M z`Yz7?k!3Ky5{`CEKV++?jQI&%eFxn_zETI;<~B$K2YOwJN|W8Y;P6ebd2{c!r|WU; z+{!+-`E2^yd|v&SKCWXfxBfUBcz3%~TF@Dpf?cnLb6yMIiZ%76=iOG{kjq;BvuJAW%FZAJ5Zm1x#D^#vRKw= z4`aIdv8#$2+qQi(O&vJj-6ury4PM0QZC%Q)yT?eZ5t2BSZ;_-=xf3up2D^4B;la?5 zRwuN|^sGfc_sb8G5~x%JD~>X!AFpg%zKhVm*ych=w)F|yO_wx4O}(SLMtlA*VD1#` zdowIugkvA8PMmCBCrfZ(J>E(+q#hx3E{lyI$xq3mPxQHZ1CD(ZPJSGAze#oSCvR?B zvh@rj)3EDpaPBiy{tXqYq4F9W{|UW~U0x>xYsvpJUWXe;Vdgm)-=i!NCJxZh|8H;% zcH9FOzX{&f-|3 z^8q!e6_ZhPW7zX5*mD=0e++A%u2PRgM%xsu?NqNzBg5^I`TuvhO|otK<~69XkN^e; zV8;&SB5GhD{an33P1~TscdNNv3)3o(3z=kn_y+{wsfl1@>m6l_P0RI$r<5>dT~+U4^0o?o3KRR_gNm;ED=l#1pOFPk8OS#9QX+soM?ASY#eh(VST>cPmxW#C9*aC z!aaR1(GC7``*9L}-MvzUIkO&X-ITc)UA;enRq|zx+fRHsHP>%q?XL?){ zw^Z$r=s(DmNiyZ~str(0PQuO|uw}|_Vbo1O*3v3meKb9w>zZ&%Xb+9nLG2vebqWzk z=P|1w!Ew#=-%yJyzDsamJC@}ix!(!Rd%k$hN=sW_vBfd-TyYJ~-wzkQ3R~}l1K$tR zucgKnmJrUKAAn0=gW^oBFffX$lY%Duw(p`3u(^a%i|5rcyjgXWx7=R8_1)InVCH${ z*1(38d7+yL)pGS|xQ1wi4-dn^1F&x&T)PIBud4aj^f*)d;IYr$WMJ(~N6v-SG2i5_9YU5VcrWLa58d{iFa4@Kpji#qzpa=gu8`V$F(;u$qdGox- z!S9D#-oG*LE!4-hJF4ZeQ~&S2dVA}gu=!wP*WY{yRv)+byY%~ogylIdG{j&xjUx$V zvgSOn#ZLwnv25#MQK4+kS>7)U!`>f;9dDy$+Qps9jfcaH4Lr)wEj2)JeRE&@yaZRk zIT}t;1Y5VNWn~4fT!U-ZVQsCP9koP@qVm2bOu*<&==ZPbd{{aoW?s%rmz-KFE=jo0 z62a7dOiAHORn4p9m_BzzDN@3j>Fuy%yV@=ks%6?N@1?bn584@Adji&1s90kp9lb=e z*PQdfHxGjR@0x>IUK&nU^Lm{kP|)7jv_{aM(#hk z1KamrnS+bpR!TOTZ>sa5BiQ{$IR0@%3)x(o0!jS)g3xY4<~xWjYx5YMB~hkH+Qvfz zYCg8zdz97lr7Nnx5k>IIpSHLzvIrA&cHag2?uD~QjML)uB}$&bK-U}z`tj5;?Rd$U zT(4A0U(*y0q+A-swa*y?qa#Y$XxldRFM{5xKcaTMQ)eS zXd=6;5we~$>m5Rb1d^{ew$2L4f3`tn*D+r`Woo-8b1W@ghGYL9octTu{XMYnoiMs< zF4u3lOS{(&9Xd5 ztmR#O-elP^tp>{H7hrV^ZhKG01M1~U>qFf0Q*iddYMdI~B5e*b+W>#Xl2}`uR)+xs z0;>?A#dNwhLbEHrkmqT|fJcA6nX@J)j15*C<#Pb13n8$e3pu3Hjqor0}fl#8Ol!ECc`(~VIS zDc3-S;l|##)x_&?^%1+!K-?H@9PJV~ZB*wgONd}TQ$FqYSx^4U za*O4qxveN+u>FJ3~_(hfh=8wnHOQ^1-SGG zTz}4Og=Er63Pom_nR!83A#Ay&@oSImP|MEO!qOF(eGz7l!)ky#aTyQTP(*;x>G-tX zZTq2%DM(262S(t~kHhviwENB5%gkfh4GZ(z*sKm*6y7@J=a1B6 zW>b@@@jtm)?VH+H`nzRmYZVI0%BIt~@hlV<)9)Y+gpH%0w75dF#!caYS?AaD&Y^;X zR= z+u`~%)i7Jz&9rFqwcA`4N}wi8FRj~6`e%;A#RuKj9~@Ugg@k^u1BC9US;7OGP{2H< zufZ-<7~2EW-&Ji_4^XXVvn-Gj?)hmr^MEl(UmmGXSsxe{2eD^K%~fD*n?eL#V335{ z+!H0~Gc~D%29uk+%lfz;Hd?tZk1(MX{TH(hdgwjn32TM65wFAiS(tlKEi>go@U?|% zUDFGSLVDi6_)RtNKQh(4>yJ&VW#_Bm`HyPujf`jLhP3KI1|{LU5L@TP6(=Mt2_HWI zxBRSXVD&TcI^1~NV^&J7)Y#c5#J+D{FBD&8nbq;tpS7-ZiSvawdOw|s3AOCs3rovN zkT5&1%#kZ|B^cU2P+k|oWp$7$6V1}t(0v!%1PP9Ye)2r!eg|VCs=+@s36m45vs3fT zIWRT3y1_Z%m2aoh&sUag;rcN%T=0{pVb*B+n%366)6K3{u{gBme2vvOZQIBw3mg zNoj%$8%J>o=1;@?iL&dR7uCABmaa$4vaZ(w$#HX?=fB@>ifeG@OK|wT5H(ph{rdAt zc;HGCU)pqHTJ;EnT``E%ujqq(wgEr$cw#bH(6%E~oaA^Rsy$waV9(p&;E$HIrusZj zES-eXLN)D@Mc>!c)(nfymYi*0UkBnF^mQGpG&EvIxQ7~3{+!{}3*~#yEXGEZqY2yQ|IOsO)`V49{bm37~MIE~

    5};$)8hHQlgnx)m3IOv=u%u;#}^7!mm6_O`JX-W z2{`u|n7$Wwy+bL22x&GShFx!f3tuDsPcZ)rdI#0)oEmFWn4o{(#S1X5%~!E<9Zr8i zDW@c4mVe83Ry)HPCTuA6DtYu;J#W_Ku6VFI;zn->yPW3HfQf`NmI$O6Ua9Si7!V6tkp!Zoyadp2? zP>M!Vn_M@bwXCVA+!IS5b*!6MD^q&uaJW5vhHe!X4NfczXrQsS5FEGS**>$ z{EM(~s%%|w5|++uLPD)f>w-k5#gW@2uPdR9 zO#w4#cIggGuO2~M7KdpT03W~AAF!55D{f;}`5^P#Du0bYS zSTlYGd8|$9y>7^Jx58ah7D=Oda-#a2i_5UEsDufNOR&764Kia07Ah}Tepbu-<{?Ce z5|`6tbVO+hO^m~)akXztdwt(l&dqR_>mqZz)0ul7)@JgRJ*d@Y6Y|jv7if)Wakbh# zS_rO=nmJf#drqIbo4o1KZ+W;I-ScG$4SWKr5X?CdlI_$r*WmI4aP?a-c^m9}J51f( z9rgT1ap>>E#cx66(Jaog=*`k}OX4kEs)nyGz^VJ7xZ=LAPiwB<>VR&fS>>#edZL?f zSpoanx%$3sPA}7ik!`NOy+4@$IR}!mPkI1w>r3G9+u-<@P;OGoN=R`za$G`LHJ3Xb zrUjE(*QCZ2jbpmU(^$LK#CUbyxJ@0b4Ag3T<*!-CLjMaP$CBfDtRAz33CXq2d>M%0 zwjYCu{dFa$04!gI#Z$0wQV9&^Pr~xmTH%1rnkY+nV6&!A;_Byg3wmC>@g0~v1mg{Q zGFiF^XTK6Q_kqwB21Pj2p+r%2&{5yU6PASQzSCnZ4CC0;?eNl{gOTkbq)QRO{(l5V z|2-6EYZcbwavVK9ldw3h-FJo)p6P%WMoe8b{9e*{A{C2*$_M zqQA@X3aqTa@~T>|l-FzPsZ;G~rx+seD^o8yRQ zmUArSKG{w>Je^$buRcUjIkT2eYRhZhGS`oU<~i4~gv>M!(9qAUGd4hoV7^zLW1-it z_js=nlDD${VY!klD-UMmuaJAWTp3mNv1`?u;W1_R8l`GoA8B=BA+A~{%p6t1*U_D@ z^DVIL^-$>J!KH+e?XdsdaN=*NX$#lN^TfO5TFDhm}V)(4Th&8pWsg#p!P7#~yXu~D_u2qC&Wk2D)^9>d;G zPN+H0Cs6PS3q0+JdOw#-X(8dv{c!fHuxYmvB8+Z@(QPoY6-Ks{*HbXORn5E8*OXxC z94wtyf`UX~u)b6k2AGxwkQNlUgaRRg15>%s3=bY-X#*}rtHDi;UGvIgYDwBlGRuzzq>lx;j{O9V;Jib*DLxu)wF$)-1t7FOrq_^5na^ ztY4y>;l?pvhrS{xh#c{#j!ncRix44#q9!;<2GJs_15_13LIL8UnizEH5}f{PxbSJ% z{(9K{78u^%-0#+H-1LKR=8Ld$BRxoGQ~0Q$X$>b-G>DD;gtcWj{~#<~hNWxiX9%@6 z${HB7U8WDKjt$56ziaJ{%lGY)kDN!zO%D$d9wcqq8sULvd60xB`+MHo{C6DR54V0F zTzRlMm{=?-A;R)MBU4JiFf^sY zho)e-yc9Ztufer%DuKb`X;?S|rS)1tflYYevNUiB4)kqTJpH?ZU4g-j)5#1^ z!O&!V|Hrj?ID0=VUbLi-Yn>rr>4GHdR>Xx-FA?I}WMmuM`3tb+j&?t7=QH(6*zqQ~ z_*J#|mCe0^M$TA0S_c!(l5^V2N z&$Wd#t#Hrh%7d=_=gWH`u8hokxLgk;$IOY9la9XWwkSDfPWEYrtuMi)FT?B;aNDmo z@7r`E17mRW{|wLmVRqasw-K%Z|K#}9r^22oj}?M1OlUpzxGD-cC(ri`ak18E6oVvRr)2-3aCK%lTBRgPhk8ImW_$(Mhp3j$8lJ-Qr3dPH?bfvc3nakj)5-ubHh2bqQI9V1n zOv2D+rI2g<;qD{(v0(e&W|%L3Sod|xT6g=tTg{0vbu(Nk3lL)M8O{CFGY5!$ zv);2`ah3NZUs<^B?;U9o?Qe{dnPv(BCZ9}@erNYDn!HMgi z;;ehXUZ>E1zEpPn}2Ni$(zS4n(4OvM7!M- ze(J|6H`A76GUb9csb}Kus1&CsR<6{ERKpU@)|IPJT86@CQ$JtVvG470>QgXxn##9} zQ%=UK-#ga-v*pFs$#)5aTzFk$)hRrX`lNN_`M+7)*IDP7rv#qUZQopP*V^iGpAmvN zN%HV!HBtH4cIB>ZbZ1#$upLIHH?E!=*$H?3HmqJz6RIZ5;#pWYr?hI5dK~TfOL6UY zhic!_3`@?9jXDa_>DW*0+t9W{aG%(?*8cBN>ofNo*H5B*+6^J?3SYPnvn^q3V*r$j zRjZvCO(19N`xdl#r3Iysuz21bKM~{&jF&ALHp7m$!RA*q=bJamyi&ji?@RfOU-?dEKT%PM>N4wu9oa>Lc-!BQz%9pM@D0#Dfx>1X(mALX( zXj~zjti0Du>la(z3$0H=ED`kcLN$tY3j}jQWuGO2-ac8r2;&WO@Opu$05AOscpSki~_RY)Zx$>7SPdW0L8I&bNun9R+$CJm4wN5(eC7J&q znOr&XpRLRfs>di%sV|o=hyVd#`X1PJ7hHZ!?Z?>vA(j7#@!yES+xf3Yh?*51YPBFV z4_})sZyXoiZxJ{3##%Xgyi(gS`k<34mWp&`P zLP*lMUcrI6Zz?Kuf-56io7HtZVd5YSDJd6*YTH(}aursw^X?! zW8lZKudf9=jSk@2w74!iw76^nN^?I%h#=pzHwYDw>-&SFFuD~ME`;49EC2a~2-bXN zizkdXXW3o%^yJZyV?`z+yC zuff$v?ZN}ojgd=$kfdqWekxypwQU$0xBQ7Z2A?ot;wHI#2`;bsACcPpDWkz)OKZU!0NrVSYBZ6Cg8ipqPKT~i^?e$}3 zT-&j_A2rI}PWJb`**+)6#NPUzvDv2FLK;^-(vR$t3z=f^jXwy9NX|b7R z_O5W7CY?Dkpa>AEY(7+0o-i=KiFwS1@0={M&h7Mp;eMo^c8IUfkmnkCpQ)2crH2eh z04gmu?SZ|ohu8dbxa%KF`?elJ{^zdt>-1ge{1_Er`W5xJ8yfv~S^oytK(wtr5jt2N z$JN1h*P_Qu;=0aRUxibD-rR4Thl^iQmOVD%LDu4GgSpydHj4%Q8~DCi=(pc!GM_Ed z#*Jd-s_op@;pm^iiO<5d@2L4;&fX6fzNv(wCWXQ5`$#*fV~{xzIt`ysJ~PbrGn+gN zul{A2*x&9q={<(Fz%4%o&-{+sH#-A`g8F$T$68ULg?22q-2qdFp|}FY|GPy3~o{yoy*tV?_;`OkP5uHxEiW10oC9b-2RiW_bu&y^2XTzKA3%4`Onnf zQ%?t9Igor9PRReJ-VW06L|W5i>LqZ=(Bks>jP=zoS@n_7orDh7*VG5)rtIWl<&y8r z{Ys0|hMA9sH>>v2_2;0p-u!=>>7vLMX3N{QW48Sy%9Zxb6LL=4)!c zCkPV)ByoK1Bbq_Tb)>#)4S5IfL;G!6akBn#zVNJLozBz$FTC;B)F6^jWt&A2sGGlY zs|al?WNi&E)o0WkFbCeIlyVcIf$(>i`mgN_eCWM9KGg1q5CRC%{NDd&xc67oz^~96 z#`eJK49p#`)$q&usrY^xwy=7%{gbfG`(WGcDld%ffK9vAAj#w*HITRUHrTY+fBltN zSh^hkysjT;C;0R0L|j%Yws^j{QKa0&yyjoQwwJg2tr}-=lkyvT?NPg*N>gJ7{0+VY zSGeG&UM((ArmEvRjqrl<^$Y%bA@8;?Oz2MXuYn{w(knlkNGSTY}#sB+esQ^<@+R^ii@!K`$gL~VQ?HutE%Jc z(i((pw*}24^tw{lwT4E82hGqFZ=|m2)_k#a8D?LEy>FEIN$08m4mVy5vtHAW^)q?; z&gS;?-O2)_=A44knqD}c{co=4Kc08MG;bd~YrxwzW?|mPH^6S5XM4tl7h&gHHnxmk z{svt7R`vOHD+!l1g83dBbwlHANtQ zV^~PjOf9bc-$RX;#L=zFlr?E!LcRu~2q$}&M)KaoJ{aGvpQQFD_NfkkaaDy)?1!DN zf_>ku{*ObORP?oZxbRS80|2ftTbw){{Lp&`rELt;_rg#Q{VN(}>Sj3h3FY1(t3Brv zOgB>ZbX-@8J8?+~?G`S;+*$WE zUC-3%Y1pHP1wyCc^QCVKOP1IDOBmbU?x*h$3Ij0pQn>I{Rq-X{`9VOw#*ObBm-hP7 z<+Ax}of=WYlgd(Ia7;;yi_2ky+gXS&Per|DJz2(gz`g$$9C&+gYHV$J8C-n`*5G!Qm_MY9VPs|@D3Z`L(BB@c$akGUP%wq3fY z25I-a0S1QJ{U+_9RD?%<3r^mr_NCmX$=f%!g$<6v*tAkI*zyw9HrREKvPyH_cRuFa+En)2zODvCH=>L-^6nvo^sQ{$t~$|{cju17&hJvz=B;M-J8=5b`Gg0)#q~~k z6+Sw3(7O@0wxQ>n@4RpxW>3Q0IpuCDPg-_Dn)NAtF5>SYWX^+)W#T5d`A6Zok5t3V zj(gJa^!Yiq(WrmKBT^Pc*zH8`#&l$yRv`INqXRGHJ3)|&UcLVotW3GV)d zwEw_fV_+0+`+0cmL(1e-@>z3fr54xrJKfjYekWXcJlo%VJ#(x`9_cNb(J?Lz!`9pV zac&%|UT>rGkJ$m&2dvFDgMQrX%qC$aO{rmEAgwhwFrb8Y?b0%$Rj++J9siO1qa`u<_5ZPJiD(D zG8d39yj!^T3_SQjc-;qkyNzq3xC-C?b+~Y(8rJRKv(Yh?{Um~puEjO7MRmwVwkWHmvF&Qm)acdX9B z$_>@=TDei3cuVRFA&X{d`>oEvvgJS4ssR8Owl;!oz)(M~Z|iLhy#t(d;FDvEXS1KB z6`c7PT>P?MvyURIoq>fjj*fc)Mz$(p!svDw-J#Ya(@MxNwnGUp^Ci5pK=5_=eJ&YV ziPVFgOe6RrB3KuayPOV&5=9Z#;Ycoi0|xCa0R#$jYq zJj&{FAYgoRjtSx3w=dUBBT^r8oJRJWd-25IGZnA5vzl*GblaRLH<;U{lA3qw%xB=hdz$+#Kq(`g{bIKJqP)U`tagd- zJ9P`Lx;iAU`SMxFed`SiIVLoffe*b$CQTg(ess&74r09E+D)mZW__-ihYo670EG zX;k(b10%5cFr52p?LbsQi1w&ZB}BW@7B+VZrVh1k!qe4Rc=6LpNGbFt_2#{rbwN$o zz2olsw7{3IDdBpj;%f8xw4Ad#_i+jKd^ZekfN$|@kHB+(pe6-1JL_hr-q(pYkB3ft zn4s?$S5A+!ik!aZ+~{Swh& z(^`vydISid%=)Ka(b&D$m*C7-Rh}xY`Qu7Oo=qMBLb5*hfbu&(d8j$Upl)JpryB6N z_Dp^OLc2TDok+cdxTQ6iJ*o0wvpQf2i5Az|Lc3E(XeG@MAPkPfP2UUKZmYk1&z(|B z=yT?MuV$u`t*z%vAM+f2*>)F4&;BkH*X^Bm-{8M3PDp&;09=Trn(sUl zY2)+`p~3^(c3}`+_7kw{9@zR)<=$^}I}D9OVL<&ai8bMYWsZ)*AY6J3qJaDR!a9i% z0fK%!OOQ}pg3AxW^s9Qux_<5?JoKx|N<43ywx2By8WA7>l*&Sc$;0;Yn1{7RyYQfs z(x=`yX1sdZLz%+)b)_`4SZ0$-?6v~ z7axWfJ_fTVRM_~g{t+f@yA!TFsVuE3jlFUj)$F6CpB@(FC~v(?gslE^rgXmL#* z@=utYhqVPb{wbvy4BEGeGU^xOI(_|3yM+ew9>C@U%7=KP?U&K*Fn>-7uE3waLh_y~ ztX`aLy@=ZXz9sD{@wB#M6v5`3V9RYyd{BeA;eT`hSs}1L^I`n%X$c{?0<1i88`GgOPDb)ki}U%Cca1O0~%* z55d;km37pXTVdNB5DmKTfA!h&d}5LR-{)$-w7M?Ji&=M7){*^KMSx(eQ$mClWr?u; z<;s0*H*@1Tc;wfVHB`+2CPqDlqu*%MQRa5flB50L%7^PKFkcoT6zUWrtS>2R+2tAS zUfBN`8`*YE<(DWsju|g?%=`~(@%21eIX6zHePoLlrLQ@E8n(Sm&0EwAu08=LK4no_ zC@N8}V9m~D?q`(SS6mNesjg~K5MUjgw91Q5IU7ctPrxUvo;IYpHcqd z&fgC!S5?DzWP9gpwRAEgTa+pDrAPc7Qjh z(M z-fwKZRcY(boij9}^)(*tXac^cxt*bDv1u<%zpTFZSzl6abG%juIkhY@-?5)?eNWrB zO@fSb;-VVJJ^xKO@ku32AXbz0H9D}{i41L0dHT%P8ZTpgoe!I&#nq?~VSNdXcc2!R z&_Z(Ly<|PS8E$?TY`M9)k5U+d(QR=3h0e1;s9Z0H#VJwxACYJQ>i?C2Vbxi0Hp`dc z@!wFB-B&CR%Ilb_1%cJ_z+qKj*6E4nU`kUrHjW$T)@bu)2oJJc7Dye_V|WH6Q^y@v zQ7f}Bc~G@Y>Sgw{a^(?XabP=^3U};pIA&Wl2{YTcHsv_OdMfFUMg^boLpwNQGa;_U)4NvX$}FepGv{xCjep;o`U8)Mu0{31S%SF=4{y z{jhoiu0Ka(Wvq|$Vw3t{6)mo9x8)Z)$3-~)cV#Uu*SG1!UYO7jx$<7Q&9eWEYLKoK zj7*m6&hyoKg8zS4v+WCOQhM3O)?E8g9+1cGdPvMa|D zl}YbY``VIp;!|lJSI^V8_4-{uu7}yq#rQwy_p97f+m1bTinL#P4{v=G)rLpLu_#i$ z(6=@)__g_u`D>$U%FwUf-};rYLU>qiX%fom=$i*;kj+(>&~W?c!vJjkOr4^sH7Y-t-|^D*Y)zz-uHDts`8V?g?P?(jLs2>u<<}RN;Gm|p z*$T?%C2gqSD7WSrvT3^2Pr-Qv?*%)$bKsH26_G;{aCh)BS3D!vvLwj)*T>*h`*#t- zCd;Dt?o28T&L6&}1-)m>NjsdnE3)YN(g>D@e$U6L()#2>j0=W+YMB9I7S|ZNvTw6` zDgRr0BfaVE?!pH%-D{Ff$1Vn` zM`0y9QH`CQwym#kiuTS&fqr;(zImrB(=G~yRlaf3EUjfz}yv>A> znLi%khG0^A>ADkc5Am~&_NJmcQ`RQXLG8|k`$^7N>y*TjPvL_Pla@sYm_CXzORL}C zK#fzbmkQ%kv)|T-Ssqn%rD4H%rb=$nF{HZR|FgyKLQH&mNGT<+ppr$kx7|d296A<| zzV#~benZ>0E}LHj#Nm1NAtJ@`QVMJd=s+2izW)5wM~GlVm?2K|>Q$aFobJbY1KJUl zdBrI4TYkSA4`%k}%Fv4;t5%6(-i_3un+mSx4d?A;5*Gru3vFJ|9f zYAqImFRh91g6>gx`299|8XkMi`cr&I;%~I`q-c@Fe50}Ww`b{Lkxk#O7nXeRu<_p3 zu!uD(GRC)ip zl!x5$5$-6h{5wnBg+~*#+Kr$h%12+v1CV_4n1cGuRtl{fO+&MlnlUxohH-;R$~mv8 zMq_hnlbkC`z0arKryXjg_9H!Nr*ikWEFV5@8F8oVvuEkRU(Rq3k&09fRv%WTek5^J zFZ7LUE03m|KouQlts|#HN7*+8DsgU&;|Aenyr_XY$C&bBeZxP7JD<&7w||SH_%3|H zG}MmsHz*iR`PD~TOd_7Q3>%T z+1NgkUR}fbs@1U$MoWQ$;xmg<$mgn00%wI5ZcciER9C1Ce=k;p+v+m+>6F!hS~d4! zaUbuq`)9D27_6wNchk|U0}l5wCZ;&vP(Dd}Dpfv&d09LcInk(TmC*c(Tuzxd`$ov+ zH@o-t0ye-pA9vkBKkARbR%@7us0-wxlvwsmh#u zs(9)rKpf9OKaV)z@doBc-1*ti+yt#CIC^5xdZQcGQEngEJXaAzw|X6J?>~y%pZ4Q4 zsv(qbpAm0}6_5C+y^DDRE3JWycI^svcX>nQC-Rle&mH`RIKIi|_K7yO?q<|6ie~N6 z@T&NElWTf$@QG#dE8x!XIU+`X7F1hjL-cl3@ySaH^85(1pr(OB>MWWxPy2O;A(F`Bk!CKfj`GPjSLF45*bHRp2`7#)2)coRlr`eKMdScFmW zr=*{vnoupV^~{uV%c~F^mkTVkO1|Zkr^g$*?3yafVm0|U$c$~Zc6Khy&GtzzRpLS8 ziaP7Ko@Szw?OjBzBUU$K7XaV7BK&s;*doH*%xAFT2K~qaOh88E0LCq0TkylQomM03 z8Y^Vkc$6+$*|wJnb#3yj4onQZdE2j`Q#{2ZFCl~BRc}mQM0zd||D7d&|xv*GHg|6Y?#o&l( zrrun`%Hf{t-(H1!+tr8UH>?JEP_oFTps!msBKV`X~^`S&aw8+jbh4}`;br8 zAI*pZ^HJ|r5AV-W*r+!&jtqL+Ww-i(j-}~}rNYM`Jfs2h8-of`m(I8wi7odR)&o z|HN$>jJ5FpBJ#2hopfzBVqa$k~a{TUP1A=z-M{O0VB82Eb?A_a@uY373 zyrxO@1|vf#xx8XSaAfk>02$b4Y;5hy#~=pj8A*$Hz)NO`jaxX=f_Y6|g(kG6)XVyl zta(Oztk2PF1vl??Yj-$hqsrPEKY6}C4F2>%pW{qEdZ6N?T6OxZts^_QM`-m=A2ypK zk|NTox4PYEx^-K%&@W!ym}A3y52zuw^TKBH4mb#`xsh!@_~#}f`&>hOYqjZhMWNao z$hn#%l|>E$Veaoa$+NKU!V^pC%}%#hNP3u-RWl`4#@6Khl1rfN2VfRWmkb zd8RS7nr`|NZ_0D+m}@ppeX5E|7zgajbbhPS`l_T>;x}QE+!M!+4Q0aVyPjo^Z+T)x z@EtC3FW@O0nO7~evQbB!p1zmtHf8~w829H_t^y|9nI(3%FXKozo03G-&wL(zw3mb* z105VgaaMN@yUq^3z&rtKVaF&VdcDhcE^XHuoR4G}bmqU9xqy1_)9d07|1$H;j$75N zs=?vuAQRUz*mN&;i`?{)r?!en^Bo4{mG#m<;5$Mx-=r)XzPAVARlr^|S1wkDcIkSq zt_oDSsJ52pvy>Hw^N%haF4t!CCtiRW4WeeEKXdXukL~xL z4#t~$uk@>jU%d>YsP=xaP`djtcuW0ggH=~I0PrhUvyS8p!ykdKZp}{YwER*>wVxXATtl*HxB>HQTmq5uPL*K3^ zl~A$t=LW~z6|t7a+NCMWYwWXPBj2f#XU-){=pE{eBV1A9LYG$`Fwk<-edL8<16@TY zb#Pf&nVM%DRG_?TmeamiiQG~8GYM-0WN@g%2TfAuU3b3UfVVzyEeRJZv8M&7!f6?f& z2RVpy=d`o~w!#Cme>#GOo+*w{9hspCO6A@&^K6L5FO+NMWjcGX!;jYh3Q>9GO~R0p>^g7c{V@KO2jhGB!0 zc>I(?=|G_Dwz@Ir4B(;r1oqhgQ=3-13`1?H9kPp>zEA7qa}m@nrk=Hezj*YQJiabE zZ`)kse5Z(NG&lk1v_7P8Ac!^G=4m?4M+H@2%m%Mnx~Ig4Mdb^FV)?YO2ViGt;kAyYb+)@T7fv9>1)f)X0wxy}bWopW(juxbTVORbc{Y+B0%%>DgWVRR7{s%s0o_WRrK-31jg^%teDOlet0TX(9?`fc zicIq-ZXZ#ZYq-jdiWT3HDw)z?0w68YktXN@6h|Gic{a71F(RlVP+H%VW&w{7Caka= zyiZ}iU20~jF6CrL6L%9oC-<(#F>)6n)`&*=6NAG0nE;DwJDt_TvDnn{Q%yyq~*dz0TarkPdZ0d+9 z9Pr>}$9BxAZeN5T&7zGF_l%2sxtD8s0K@(jm?>YY)fyjF;zrQH_T1q{8{M~E{Trg4 zvyL0>RN1sZ7*?amxMi45-nUzY@TBz(pSb0_;_q?+hC8RHmjdI{iBu}_)sO=uzL)9g zWMYpx4-hStt>E)`*n(n*sVCMpgtbv1almIjB_rco3PnTy&-@-Hp|h!(r9$1hw=#-` zJauY@1HYcp@nK%%Iqy(EJ>`q#owhPSULK^;WETc6AeVIxvF7ihIxp24tu;ZDNN0>x z+a=%Nx!OLh7KzX)a!)Zdm z41I1dcU=rHCn@_@D_8*SXJDS=i*=^kE-54uv}1?$Cy;F@D~w1EP)fRsY_7Woqj*nz z!H&6FDy9m;zsXf9hIqXL1`+0)EryjNZ`QfDY4pvP8Bqw+ws#$cj`MWWyBoR=HTCYp z4W=&(HJOIs-qN#m%FT_j`qwCg({(|XS~L*1I|&?c@l&e96b^~b{iN?>+ic>tbQ!gx z-a+PlLrD$ z-~5rUV0S+ot@ze-aN40l-`1=^w`eQturIZ7HB~?Qt7+>JF1*a=_{5PMKBAWgf^xwR zmU!Qb2WvIsK)zS*uSxpcj3x;SspG6r@mmrffuKZ~lCwmjUu#rz&)a=R>njuc98DYV z;_cjqj=2>hZcnh;;k1!Fkji!+V)&I%l94nQs^6-Jch%v^Q zJ0Be7l?6=J8kFR%wF)wugw#+-^;u8d8Zw*|*2TBD-PIi-&FurikiK&tJN2WjL~!^cho$C>(fxEGtQ(&aG+!4Hs;6K2Cfm)vYBHZf-YTA&xAyv z$FJ}CXrtf#Xp5$!@+;Wc%yRb`R^uH4You7PAM_20QLZXjY@l|Upe4TJ)1GkvimMpE zW0UsF(Tn(#`V6Vqgsb6{7k}iz z1u*hbAn~xWQjtebklR;zUhBczxZE1;Wj^5+*=5lYmZzXI|8jEm1Mws4AdYo>WyhubiX;GQ{x2-ubC%x|~1 zZ6vik*N3|hp62dA{EDzK;-(u{cIuY-wb0fjsT1%IDJn!NAsK1Lg(cL6(Ll z#!{mQJR4WKIQkwVR?k5$q`n4_YYr zmHel$?Mekn2w1+X8ee(8UPl;qsR0jFok!13`>tDNt;aB8_sA}<#TS9HPqAelSdIFo z_-onSVW|j>5p}RpW-#z>hxB!`8swK*#V1C?P#3PlbY`?dwjy)wANaa zCd>895G!8i+ukz7T9Yp=pqE<$?JIF2`4$${4crfuW(p7@(-I596f4?^xBT?CvU`R8 zVgnUpEPrj&>QuXSh9}1!26ujApMT@}LB`WsK9EAemLr_w`++Dpc0sLY$m^qF@R#>O z14?{cf)692LAbomrUCR$S6$QXgS|vwN@*rAYSB0f!5w}Az<_z*%5^Du=-3M%(S!P5 zhN7x$$PCX(6R|hmm0DH`Nh?I{6 zFJ>Md1Cq}!`laKj-aWL0*hGL8fl{&jgV45VA}M{CJF93lJJL1I1mlrwlv#N=f*`q+yZ@diNOt0P~@oN}F8nr8G{O z*vhFV6TVd_sHC2{r#`Q$0Y@<-{q~v%$Rkaw0lFC9{8VD*;AY#7Q%hNr0h_&KeYU?~6);dUg^W&K0?vOAgix}%a4zZk0kbj%eyUhYYag6Mrd-{#(7~0lp&#%cD}p0sZ9qG*s$C8D1q} zY(FEB{*h;d62I*v@`>*{TnGquov=nw!yMH~v~I6yUuEZ#=x)W39 zn^GQDPy3}oDvllKABGAZ0i^`KQz>>30f2GmcrkCO%A_;DeE)+{JUSO^+__PeIJ4Vq z(d>X7&eJ$|iUt$(-QIP&^mkctw3myN(O?d%rI>E;H9pM9*WmnPSNJX~D7Z@~Gr|Gk zdAD;&PK$r@%7Mp}aS z^M@Mj$1gGm*q^=43b$a5qLa`6V)7Z80YQj9;0K5F1yh*(F=|5fnJqICpW};r(tjpS zG^TATM$ZmW3^}qV#?o?mSV%4aK+v*SfLZO@fHx(yfSnc));$F{w`)kt(w*SQdHoU} z<4c4Wz@HgqBTr@R`&GVBJ>+ZUFskhIIO54WLj=i2-|YM!{`U@9wPi>y*HLEOL=FnF z*>xD^0TWsS^`vIHoR-P5y$~4pclvU(d@JWqvIg-4zDo&+Y5tuzjx($VCwb7d$BcuV z60wG~K0pFVq1f=F8w;2H!>@I@-44kuzS%@(=VAe9`OaU8M>VhzzHozOOpibw~uSrG_&F$`7NKg*Wpr{Lm*P*qY zcvL^EFPLsR854$&X+8}!E;2J}9p^fIn|%j%Jp{E*)Q>hxcFh2~I#LSCkfGtF=Rtg; zM`MS7h^~yWAvTXs7~m2W!lq4Q{}jXZysiT;_8tU;r{*{*953*2wHaZ>p8(6|^^ohc z*LnuK^)+9(qpOMUe#U$YmfTEx=DT@^2?LS1xz8(DIErXQ*1M@URI-Lw1_K+GiG^-) zG&+U?Z??j9=%MvVr=H*en>Bcm=cK3(O4_tvTLqT;$s^gB3m~9K007B37&`cIXOWHb zBm;B_=sFgeaJxx^^B!1h9jsEZ{7K&MJ-I8lg<`tMD@Yg%N;iLRX2_m-8EG7Hgj$a5 z#j2wulpHe$tmCeq-KK(+V$^MUV!f_wD-bBLT=p#eaKk-|9Q~g)nIjC>_IfvmM5sA_ zI)^Rk7rF+~X4M~!;de)a?!^U@Os6bXe5l!ytCG!gMvo^R$a_p)>{QIdj0*H2n2;OP%5 z`S{L=sFk?j$r<-yrtrLA;@*#!cZYhn2KvuOe9r^z5dE$Swlqm@Op;QNP1Ixy)8VA} zN__`GMChKRP!Nk&(6F&yz(JRi%b0XDHXOt$z_*Bnj;8%tJHz-eec68BDXm{3KRbwu zXpTqYu>RQkRhTMs-kg@KuYB7YYY8eJax`SxO$vjKr<15p-6kL2d!*!o)pOA{EKpJ9 zmClUSid(`57lrZ4gi|&RXUb>Fnv2xv2DVcU-z%DSy<1{X3L7l|6cmt7-*5?ZY6Gb2 zn>n<=(KLva)nz>hqK)V$X4v9s%EPT%G(!}--PlK;urYoxvv9_lPBg`SK2pu>=(Be* zW>)lQAPUmQ+>IX<5g?0s zrLe=zwM+`?qwMYQt^J~rLz~fDcrdNB9M8ec?-D^na#R9~>N%`9G|0X>JnHU)5aMqI z5XnIZviS8MS+}+Ik814@)QFV_$e9gWgmbD*KzyiWE^Vqj>KVkp=dvMvRCrVelGp$s zO!VMmg(8@j#+FlF%0O0H0lQ$-(2?K5y-T=N^gGQW?(*`E`*S?y2nG`%fdbGlx=EO7 zk8^Vl@THY{Ts`wG`#fumb0% z3zfcn-?f(XS)I`lD(>uyR>8F$bM(1xY9ohI?Uzp#78S^IS`vBJ(ik2DnK!=IkN;r?0 z%lTo|XCvrOHh5AG!?7JfDeRdl01EG)uvi*5vi9fguhpjZ0s)h}-^WtNI?whE%;6Hg z1mu6aJcb)h%E>N2{e#8+;g=qD)l}H#!T?#eRi}%yauWGFfm{ry_ zc6b5`RuZs@c#0Lfq=OSVR9Mgi3Olr5REkWZ5a}C+hSXkrF(c$aQJ2k-e_6IEFjg*I$^K_-)zX;SRal?R-rN68*JcK@c0d`FT zWzb(uLtOUVUAAx=2RVv74J?;Tp+sRiF_56}vZIZ+OLw)}`~ERNKIsR-_Z_iiVL`j-QjKUG*@g-WO)SdVDS)s)F-;l2aXcC01Jn60nCWD21_EuGUCITd=v2QigmXV1Lo; zyH#m^!qS9_GV{b`x#jdZcm8;CL`n=^{R{DWWB21`bMim1g|SU4vc$}`iMb0x{jyc( z2LA#~q3pf={ovA{cs4b*%yJowUQ3@>%&UT{J&#WEJ_rM@h9a@^*KM2R`A<|~Y#TXm z@NXF->awtqpn}i0o&mf2kCllpx1Qs?HFkR|AaU-vDwv}%{e(aPpg555Pu63^GjsV_ zg0B-A>aZV-;bS!)0FULdr3xuY03P0}uL)C415`}Wo1y3BG0H-HsoKU^lwxEz3+Gm{ZXT`pWrYchf39A4_z`hKNkMK3I1zP zd!h-$5?JR009svPw0#~1_uODI*sR^Zoc`lt7ssxI$tbK|X->@>)gm?^Dp>>gyT$zH zm5!|n1HGyyru^&z$grICbQ1md3i}^W&G(f8=!s*2BC0imFWC=ek6EbylLGdB?*%^) zx`jpT!hF%|%oX?FaKm)OIi3O2W3&^#vO1q${SV!Lb>{D%YK-C7kQr*C{$J1t6lQ>T ztX%YLC4Hg9pIQGdp8vR{@T#8ypVCqS|FvlUP1b`j0=ozNF9d9WtSG>LHU9^xB5cOU zzuW!K%_JM%fAaa~y*)OF`v0Q&gp8^W6?Er6K>hQ_QXdtRg8r{xhlTz>5nz|z|7MqS z2P@Y8Z+1F8w-f>QT|cVay#(Ok;Bb39f9qjs?qMZm;bw(>!4VODEG8iQSm5#F*TND~ vViHoqA_Brsq=bd_S5M#mR|7|9OFL`d|9^uAZ5kWc1~{tEwUjDAW?}yaz#`rv diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/taiko-slider-fail.png b/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/taiko-slider-fail.png new file mode 100644 index 0000000000000000000000000000000000000000..78c6ef6e21ef6a4830e57aaa218b73bb333d783c GIT binary patch literal 61156 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 literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/taiko-slider.png b/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/taiko-slider.png new file mode 100644 index 0000000000000000000000000000000000000000..b824e4585b4bc678e5a5165b97f647b9ac0d7fa6 GIT binary patch literal 65012 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

    0?Y{WJF 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 literal 0 HcmV?d00001 From afcb45f28b0c650e472fbe9826c88498bc1fad37 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 May 2020 19:48:31 +0900 Subject: [PATCH 1412/2376] Move to playfield --- .../UI/DrawableTaikoRuleset.cs | 19 +++++++++++++++++++ osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 7 ------- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs index a6a00fe242..c0a6c4582c 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using osu.Framework.Allocation; +using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Taiko.Objects; @@ -16,11 +17,15 @@ using osu.Game.Replays; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Skinning; +using osuTK; namespace osu.Game.Rulesets.Taiko.UI { public class DrawableTaikoRuleset : DrawableScrollingRuleset { + private SkinnableDrawable scroller; + protected override ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Overlapping; protected override bool UserScrollSpeedAdjustment => false; @@ -36,6 +41,20 @@ namespace osu.Game.Rulesets.Taiko.UI private void load() { new BarLineGenerator(Beatmap).BarLines.ForEach(bar => Playfield.Add(bar.Major ? new DrawableBarLineMajor(bar) : new DrawableBarLine(bar))); + + AddInternal(scroller = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.TaikoScroller), _ => Empty()) + { + RelativeSizeAxes = Axes.X, + Depth = float.MaxValue + }); + } + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + var playfieldScreen = Playfield.ScreenSpaceDrawQuad; + scroller.Height = ToLocalSpace(playfieldScreen.TopLeft + new Vector2(0, playfieldScreen.Height / 20)).Y; } public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new TaikoPlayfieldAdjustmentContainer(); diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index a5edcc1357..5c763cb332 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -50,13 +50,6 @@ namespace osu.Game.Rulesets.Taiko.UI { InternalChildren = new[] { - new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.TaikoScroller), _ => Drawable.Empty()) - { - Origin = Anchor.BottomLeft, - Anchor = Anchor.TopLeft, - RelativeSizeAxes = Axes.X, - Height = 100, - }, new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.PlayfieldBackgroundRight), _ => new PlayfieldBackgroundRight()), rightArea = new Container { From 510df8b282c97c046ee86a309f25681d30ce2d27 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 May 2020 19:49:01 +0900 Subject: [PATCH 1413/2376] Improve tiling logic --- .../Skinning/LegacyTaikoScroller.cs | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs index 6276eb1e8a..2bcef0223c 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs @@ -21,19 +21,28 @@ namespace osu.Game.Rulesets.Taiko.Skinning { base.Update(); - foreach (var sprite in InternalChildren) + while (true) { - sprite.X -= (float)Time.Elapsed * 0.1f; + float? additiveX = null; - if (sprite.X + sprite.DrawWidth < 0) - sprite.Expire(); - } + foreach (var sprite in InternalChildren) + { + // add the x coordinates and perform re-layout on all sprites as spacing may change with gameplay scale. + sprite.X = additiveX ??= sprite.X - (float)Time.Elapsed * 0.1f; - var last = InternalChildren.LastOrDefault(); + additiveX += sprite.DrawWidth - 1; - if (last == null || last.ScreenSpaceDrawQuad.TopRight.X < ScreenSpaceDrawQuad.TopRight.X) - { - AddInternal(new ScrollerSprite { X = last == null ? 0 : last.X + last.DrawWidth }); + if (sprite.X + sprite.DrawWidth < 0) + sprite.Expire(); + } + + var last = InternalChildren.LastOrDefault(); + + // only break from this loop once we have saturated horizontal space completely. + if (last != null && last.ScreenSpaceDrawQuad.TopRight.X >= ScreenSpaceDrawQuad.TopRight.X) + break; + + AddInternal(new ScrollerSprite()); } } From 6ff31fb7866f030bc6c617dbb58215435955dd67 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 May 2020 19:49:23 +0900 Subject: [PATCH 1414/2376] Fix sizing when gameplay scale is adjusted --- .../Skinning/LegacyTaikoScroller.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs index 2bcef0223c..f0bdfa4e63 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs @@ -79,7 +79,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning [BackgroundDependencyLoader] private void load(ISkinSource skin) { - AutoSizeAxes = Axes.Both; + AutoSizeAxes = Axes.X; + RelativeSizeAxes = Axes.Y; + + FillMode = FillMode.Fit; InternalChildren = new Drawable[] { @@ -87,6 +90,14 @@ namespace osu.Game.Rulesets.Taiko.Skinning failingSprite = new Sprite { Texture = skin.GetTexture("taiko-slider-fail"), Alpha = 0 }, }; } + + protected override void Update() + { + base.Update(); + + foreach (var c in InternalChildren) + c.Scale = new Vector2(DrawHeight / c.Height); + } } } } From 3033ab80ced82d8d73bab1865d3db897809ca8f5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 May 2020 19:49:30 +0900 Subject: [PATCH 1415/2376] Add passing/failing test --- .../Skinning/TestSceneTaikoScroller.cs | 7 +++++-- .../Skinning/LegacyTaikoScroller.cs | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs index e4673430d6..19661bbcbb 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs @@ -1,7 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Graphics; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Testing; +using osu.Game.Rulesets.Taiko.Skinning; using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.Tests.Skinning @@ -10,7 +12,8 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning { public TestSceneTaikoScroller() { - AddStep("Load scroller", () => SetContents(() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.TaikoScroller), _ => Drawable.Empty()))); + AddStep("Load scroller", () => SetContents(() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.TaikoScroller), _ => Empty()))); + AddToggleStep("Toggle passing", passing => this.ChildrenOfType().ForEach(s => s.Passing = !passing)); } } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs index f0bdfa4e63..90fb1934df 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Game.Skinning; +using osuTK; namespace osu.Game.Rulesets.Taiko.Skinning { @@ -17,6 +18,23 @@ namespace osu.Game.Rulesets.Taiko.Skinning RelativeSizeAxes = Axes.Both; } + private bool passing = true; + + public bool Passing + { + get => passing; + set + { + if (value == passing) + return; + + passing = value; + + foreach (var sprite in InternalChildren.OfType()) + sprite.Passing = passing; + } + } + protected override void Update() { base.Update(); From ff1d63060dba823c0ea149f13f9b6b83e795944e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 May 2020 20:05:56 +0900 Subject: [PATCH 1416/2376] Add and consume passing state in GameplayBeatmap --- .../Skinning/TestSceneTaikoScroller.cs | 2 +- .../Skinning/LegacyTaikoScroller.cs | 33 +++++++++++-------- osu.Game/Screens/Play/GameplayBeatmap.cs | 12 +++++++ osu.Game/Screens/Play/Player.cs | 1 + 4 files changed, 33 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs index 19661bbcbb..3d1ccadd6e 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning public TestSceneTaikoScroller() { AddStep("Load scroller", () => SetContents(() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.TaikoScroller), _ => Empty()))); - AddToggleStep("Toggle passing", passing => this.ChildrenOfType().ForEach(s => s.Passing = !passing)); + AddToggleStep("Toggle passing", passing => this.ChildrenOfType().ForEach(s => s.Passing.Value = !passing)); } } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs index 90fb1934df..f61ee0301d 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs @@ -3,9 +3,11 @@ using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; +using osu.Game.Screens.Play; using osu.Game.Skinning; using osuTK; @@ -18,23 +20,26 @@ namespace osu.Game.Rulesets.Taiko.Skinning RelativeSizeAxes = Axes.Both; } - private bool passing = true; - - public bool Passing + [BackgroundDependencyLoader(true)] + private void load(GameplayBeatmap gameplayBeatmap) { - get => passing; - set - { - if (value == passing) - return; - - passing = value; - - foreach (var sprite in InternalChildren.OfType()) - sprite.Passing = passing; - } + if (gameplayBeatmap != null) + ((IBindable)Passing).BindTo(gameplayBeatmap.Passing); } + protected override void LoadComplete() + { + base.LoadComplete(); + + Passing.BindValueChanged(passing => + { + foreach (var sprite in InternalChildren.OfType()) + sprite.Passing = passing.NewValue; + }, true); + } + + public Bindable Passing = new BindableBool(true); + protected override void Update() { base.Update(); diff --git a/osu.Game/Screens/Play/GameplayBeatmap.cs b/osu.Game/Screens/Play/GameplayBeatmap.cs index d7f939a883..0afa189e66 100644 --- a/osu.Game/Screens/Play/GameplayBeatmap.cs +++ b/osu.Game/Screens/Play/GameplayBeatmap.cs @@ -2,11 +2,14 @@ // 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; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Screens.Play { @@ -38,5 +41,14 @@ namespace osu.Game.Screens.Play public IEnumerable GetStatistics() => PlayableBeatmap.GetStatistics(); public IBeatmap Clone() => PlayableBeatmap.Clone(); + + public IBindable Passing => passing; + + private readonly BindableBool passing = new BindableBool(true); + + public void OnNewResult(JudgementResult result) + { + passing.Value = result.Type > HitResult.Miss; + } } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index ece4c6307e..eeb514b4be 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -193,6 +193,7 @@ namespace osu.Game.Screens.Play { HealthProcessor.ApplyResult(r); ScoreProcessor.ApplyResult(r); + gameplayBeatmap.OnNewResult(r); }; DrawableRuleset.OnRevertResult += r => From 811874773288186d4e93620e44024331ebb59615 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 2 May 2020 01:33:33 +0200 Subject: [PATCH 1417/2376] Make PeriodTracker actually immutable --- osu.Game.Tests/NonVisual/PeriodTrackerTest.cs | 54 +++++++------------ osu.Game/Screens/Play/BreakTracker.cs | 8 +-- osu.Game/Utils/PeriodTracker.cs | 35 +++--------- 3 files changed, 29 insertions(+), 68 deletions(-) diff --git a/osu.Game.Tests/NonVisual/PeriodTrackerTest.cs b/osu.Game.Tests/NonVisual/PeriodTrackerTest.cs index f033672576..62c7732b66 100644 --- a/osu.Game.Tests/NonVisual/PeriodTrackerTest.cs +++ b/osu.Game.Tests/NonVisual/PeriodTrackerTest.cs @@ -12,10 +12,9 @@ namespace osu.Game.Tests.NonVisual [TestFixture] public class PeriodTrackerTest { - private static readonly Period[] test_single_period = { new Period(1.0, 2.0) }; + private static readonly Period[] single_period = { new Period(1.0, 2.0) }; - // this is intended to be unordered to test adding periods in unordered way. - private static readonly Period[] test_periods = + private static readonly Period[] unordered_periods = { new Period(-9.1, -8.3), new Period(-3.4, 2.1), @@ -26,43 +25,40 @@ namespace osu.Game.Tests.NonVisual [Test] public void TestCheckValueInsideSinglePeriod() { - var tracker = new PeriodTracker { Periods = test_single_period }; + var tracker = new PeriodTracker(single_period); - var period = test_single_period.Single(); + var period = single_period.Single(); Assert.IsTrue(tracker.IsInAny(period.Start)); - Assert.IsTrue(tracker.IsInAny(getMidTime(period))); + Assert.IsTrue(tracker.IsInAny(getMidpoint(period))); Assert.IsTrue(tracker.IsInAny(period.End)); } [Test] public void TestCheckValuesInsidePeriods() { - var tracker = new PeriodTracker { Periods = test_periods }; + var tracker = new PeriodTracker(unordered_periods); - foreach (var period in test_periods) - Assert.IsTrue(tracker.IsInAny(getMidTime(period))); + foreach (var period in unordered_periods) + Assert.IsTrue(tracker.IsInAny(getMidpoint(period))); } [Test] public void TestCheckValuesInRandomOrder() { - var tracker = new PeriodTracker { Periods = test_periods }; + var tracker = new PeriodTracker(unordered_periods); - foreach (var period in test_periods.OrderBy(_ => RNG.Next())) - Assert.IsTrue(tracker.IsInAny(getMidTime(period))); + foreach (var period in unordered_periods.OrderBy(_ => RNG.Next())) + Assert.IsTrue(tracker.IsInAny(getMidpoint(period))); } [Test] public void TestCheckValuesOutOfPeriods() { - var tracker = new PeriodTracker + var tracker = new PeriodTracker(new[] { - Periods = new[] - { - new Period(1.0, 2.0), - new Period(3.0, 4.0) - } - }; + new Period(1.0, 2.0), + new Period(3.0, 4.0) + }); Assert.IsFalse(tracker.IsInAny(0.9), "Time before first period is being considered inside"); @@ -72,32 +68,18 @@ namespace osu.Game.Tests.NonVisual Assert.IsFalse(tracker.IsInAny(4.1), "Time after last period is being considered inside"); } - [Test] - public void TestNullRemovesExistingPeriods() - { - var tracker = new PeriodTracker { Periods = test_single_period }; - - var period = test_single_period.Single(); - Assert.IsTrue(tracker.IsInAny(getMidTime(period))); - - tracker.Periods = null; - Assert.IsFalse(tracker.IsInAny(getMidTime(period))); - } - [Test] public void TestReversedPeriodHandling() { - var tracker = new PeriodTracker(); - Assert.Throws(() => { - tracker.Periods = new[] + _ = new PeriodTracker(new[] { new Period(2.0, 1.0) - }; + }); }); } - private double getMidTime(Period period) => period.Start + (period.End - period.Start) / 2; + private double getMidpoint(Period period) => period.Start + (period.End - period.Start) / 2; } } diff --git a/osu.Game/Screens/Play/BreakTracker.cs b/osu.Game/Screens/Play/BreakTracker.cs index 79da548336..51e21656e1 100644 --- a/osu.Game/Screens/Play/BreakTracker.cs +++ b/osu.Game/Screens/Play/BreakTracker.cs @@ -16,7 +16,7 @@ namespace osu.Game.Screens.Play private readonly ScoreProcessor scoreProcessor; private readonly double gameplayStartTime; - private readonly PeriodTracker tracker = new PeriodTracker(); + private PeriodTracker breaks; ///

    /// Whether the gameplay is currently in a break. @@ -31,8 +31,8 @@ namespace osu.Game.Screens.Play { isBreakTime.Value = false; - tracker.Periods = value?.Where(b => b.HasEffect) - .Select(b => new Period(b.StartTime, b.EndTime - BreakOverlay.BREAK_FADE_DURATION)); + breaks = new PeriodTracker(value.Where(b => b.HasEffect) + .Select(b => new Period(b.StartTime, b.EndTime - BreakOverlay.BREAK_FADE_DURATION))); } } @@ -48,7 +48,7 @@ namespace osu.Game.Screens.Play var time = Clock.CurrentTime; - isBreakTime.Value = tracker.IsInAny(time) + isBreakTime.Value = breaks?.IsInAny(time) == true || time < gameplayStartTime || scoreProcessor?.HasCompleted.Value == true; } diff --git a/osu.Game/Utils/PeriodTracker.cs b/osu.Game/Utils/PeriodTracker.cs index 49b372bb27..ba77702247 100644 --- a/osu.Game/Utils/PeriodTracker.cs +++ b/osu.Game/Utils/PeriodTracker.cs @@ -8,41 +8,22 @@ using System.Linq; namespace osu.Game.Utils { /// - /// Represents a tracking component used for whether a - /// specific time falls into any of the provided periods. + /// Represents a tracking component used for whether a specific time instant falls into any of the provided periods. /// public class PeriodTracker { - private readonly List periods = new List(); + private readonly List periods; private int nearestIndex; - /// - /// The list of periods to add to the tracker for using the required check methods. - /// - public IEnumerable Periods + public PeriodTracker(IEnumerable periods) { - set - { - var sortedValue = value?.ToList(); - sortedValue?.Sort(); - - if (sortedValue != null && periods.SequenceEqual(sortedValue)) - return; - - periods.Clear(); - nearestIndex = 0; - - if (value?.Any() != true) - return; - - periods.AddRange(sortedValue); - } + this.periods = periods.OrderBy(period => period.Start).ToList(); } /// /// Whether the provided time is in any of the added periods. /// - /// The time value to check for. + /// The time value to check. public bool IsInAny(double time) { if (periods.Count == 0) @@ -64,7 +45,7 @@ namespace osu.Game.Utils } } - public readonly struct Period : IComparable + public readonly struct Period { /// /// The start time of this period. @@ -79,12 +60,10 @@ namespace osu.Game.Utils public Period(double start, double end) { if (start >= end) - throw new ArgumentException($"Invalid period provided, {nameof(start)} must be less than {nameof(end)}", nameof(start)); + throw new ArgumentException($"Invalid period provided, {nameof(start)} must be less than {nameof(end)}"); Start = start; End = end; } - - public int CompareTo(Period other) => Start.CompareTo(other.Start); } } From deb87517d01019bca4b9a121118d52172600e74f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 2 May 2020 14:35:12 +0900 Subject: [PATCH 1418/2376] Add local beatmap lookup cache --- osu.Game/Beatmaps/BeatmapManager.cs | 68 +------ .../Beatmaps/BeatmapManager_UpdateQueue.cs | 180 ++++++++++++++++++ osu.Game/Database/ArchiveModelManager.cs | 2 +- osu.Game/osu.Game.csproj | 1 + 4 files changed, 183 insertions(+), 68 deletions(-) create mode 100644 osu.Game/Beatmaps/BeatmapManager_UpdateQueue.cs diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index b8dfac0342..22451382a3 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -17,7 +17,6 @@ using osu.Framework.Graphics.Textures; using osu.Framework.Lists; using osu.Framework.Logging; using osu.Framework.Platform; -using osu.Framework.Threading; using osu.Game.Beatmaps.Formats; using osu.Game.Database; using osu.Game.IO; @@ -78,7 +77,7 @@ namespace osu.Game.Beatmaps beatmaps.BeatmapHidden += b => BeatmapHidden?.Invoke(b); beatmaps.BeatmapRestored += b => BeatmapRestored?.Invoke(b); - updateQueue = new BeatmapUpdateQueue(api); + updateQueue = new BeatmapUpdateQueue(api, storage); exportStorage = storage.GetStorageForDirectory("exports"); } @@ -446,71 +445,6 @@ namespace osu.Game.Beatmaps protected override Texture GetBackground() => null; protected override Track GetTrack() => null; } - - private class BeatmapUpdateQueue - { - private readonly IAPIProvider api; - - private const int update_queue_request_concurrency = 4; - - private readonly ThreadedTaskScheduler updateScheduler = new ThreadedTaskScheduler(update_queue_request_concurrency, nameof(BeatmapUpdateQueue)); - - public BeatmapUpdateQueue(IAPIProvider api) - { - this.api = api; - } - - public Task UpdateAsync(BeatmapSetInfo beatmapSet, CancellationToken cancellationToken) - { - if (api?.State != APIState.Online) - return Task.CompletedTask; - - LogForModel(beatmapSet, "Performing online lookups..."); - 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(() => update(beatmapSet, beatmap), cancellationToken, TaskCreationOptions.HideScheduler, updateScheduler); - - private void update(BeatmapSetInfo set, BeatmapInfo beatmap) - { - if (api?.State != 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; - - 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})"); - } - } - } } /// diff --git a/osu.Game/Beatmaps/BeatmapManager_UpdateQueue.cs b/osu.Game/Beatmaps/BeatmapManager_UpdateQueue.cs new file mode 100644 index 0000000000..aa8be823f7 --- /dev/null +++ b/osu.Game/Beatmaps/BeatmapManager_UpdateQueue.cs @@ -0,0 +1,180 @@ +// 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 System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Dapper; +using Microsoft.Data.Sqlite; +using osu.Framework.IO.Network; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Framework.Threading; +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 + { + private class BeatmapUpdateQueue + { + 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(BeatmapUpdateQueue)); + + private FileWebRequest cacheDownloadRequest; + + private const string cache_database_name = "online.db"; + + public BeatmapUpdateQueue(IAPIProvider api, Storage storage) + { + this.api = api; + this.storage = storage; + + if (!storage.Exists(cache_database_name)) + prepareLocalCache(); + } + + public Task UpdateAsync(BeatmapSetInfo beatmapSet, CancellationToken cancellationToken) + { + if (api?.State != APIState.Online) + return Task.CompletedTask; + + LogForModel(beatmapSet, "Performing online lookups..."); + 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(() => update(beatmapSet, beatmap), cancellationToken, TaskCreationOptions.HideScheduler, updateScheduler); + + private void update(BeatmapSetInfo set, BeatmapInfo beatmap) + { + if (cacheDownloadRequest == null && storage.Exists(cache_database_name)) + { + try + { + using (var db = new SqliteConnection(storage.GetDatabaseConnectionString("online"))) + { + var found = db.QueryFirstOrDefault( + "SELECT * FROM osu_beatmaps WHERE checksum = @MD5Hash OR beatmap_id = @OnlineBeatmapID OR filename = @Path", beatmap); + + if (found != null) + { + var status = (BeatmapSetOnlineStatus)found.approved; + + beatmap.Status = status; + beatmap.BeatmapSet.Status = status; + beatmap.BeatmapSet.OnlineBeatmapSetID = found.beatmapset_id; + beatmap.OnlineBeatmapID = found.beatmap_id; + + LogForModel(set, $"Cached local retrieval for {beatmap}."); + return; + } + } + } + catch (Exception ex) + { + LogForModel(set, $"Cached local retrieval for {beatmap} failed with {ex}."); + } + } + + if (api?.State != 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; + + 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"); + + cacheDownloadRequest.Failed += ex => + { + File.Delete(compressedCacheFilePath); + File.Delete(cacheFilePath); + + Logger.Log($"{nameof(BeatmapUpdateQueue)}'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(BeatmapUpdateQueue)}'s online cache extraction failed: {ex}", LoggingTarget.Database); + } + finally + { + File.Delete(compressedCacheFilePath); + File.Delete(cacheFilePath); + } + }; + + cacheDownloadRequest.PerformAsync(); + } + + [Serializable] + [SuppressMessage("ReSharper", "InconsistentNaming")] + private class CachedOnlineBeatmapLookup + { + public int approved { get; set; } + + public int? beatmapset_id { get; set; } + + public int? beatmap_id { get; set; } + } + } + } +} diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 5e237d2ecb..839f9075e5 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -245,7 +245,7 @@ namespace osu.Game.Database /// protected abstract string[] HashableFileTypes { get; } - protected static void LogForModel(TModel model, string message, Exception e = null) + internal static void LogForModel(TModel model, string message, Exception e = null) { string prefix = $"[{(model?.Hash ?? "?????").Substring(0, 5)}]"; diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index acb7fe5fbe..81818360a4 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -18,6 +18,7 @@ + From 917393697cf36c61c3356719bd39fbaac2208947 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 2 May 2020 14:38:46 +0900 Subject: [PATCH 1419/2376] 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 336479c40a..8214fa2f2c 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index acb7fe5fbe..eae763c412 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -23,7 +23,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 6662e57dcd..9ff7e3fc02 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From b9b57792514d43a92dc5d2de7b84b248a487ad28 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 3 May 2020 09:31:56 +0900 Subject: [PATCH 1420/2376] Move deletion to catch instead of finally --- osu.Game/Beatmaps/BeatmapManager_UpdateQueue.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapManager_UpdateQueue.cs b/osu.Game/Beatmaps/BeatmapManager_UpdateQueue.cs index aa8be823f7..be4bd0d30d 100644 --- a/osu.Game/Beatmaps/BeatmapManager_UpdateQueue.cs +++ b/osu.Game/Beatmaps/BeatmapManager_UpdateQueue.cs @@ -154,11 +154,11 @@ namespace osu.Game.Beatmaps catch (Exception ex) { Logger.Log($"{nameof(BeatmapUpdateQueue)}'s online cache extraction failed: {ex}", LoggingTarget.Database); + File.Delete(cacheFilePath); } finally { File.Delete(compressedCacheFilePath); - File.Delete(cacheFilePath); } }; From 035b513b68347b6144fa06c9727a2bb404c46b29 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 3 May 2020 09:32:33 +0900 Subject: [PATCH 1421/2376] Use QuerySingle instead of QueryFirst --- osu.Game/Beatmaps/BeatmapManager_UpdateQueue.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapManager_UpdateQueue.cs b/osu.Game/Beatmaps/BeatmapManager_UpdateQueue.cs index be4bd0d30d..84901f9b50 100644 --- a/osu.Game/Beatmaps/BeatmapManager_UpdateQueue.cs +++ b/osu.Game/Beatmaps/BeatmapManager_UpdateQueue.cs @@ -65,7 +65,7 @@ namespace osu.Game.Beatmaps { using (var db = new SqliteConnection(storage.GetDatabaseConnectionString("online"))) { - var found = db.QueryFirstOrDefault( + var found = db.QuerySingleOrDefault( "SELECT * FROM osu_beatmaps WHERE checksum = @MD5Hash OR beatmap_id = @OnlineBeatmapID OR filename = @Path", beatmap); if (found != null) From 6fef4eeb8f9fd1e46e3b8553b03496c201d911b3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 3 May 2020 09:35:48 +0900 Subject: [PATCH 1422/2376] Rename class and extract out lookup method --- osu.Game/Beatmaps/BeatmapManager.cs | 6 +- ...eatmapManager_BeatmapOnlineLookupQueue.cs} | 83 +++++++++++-------- 2 files changed, 51 insertions(+), 38 deletions(-) rename osu.Game/Beatmaps/{BeatmapManager_UpdateQueue.cs => BeatmapManager_BeatmapOnlineLookupQueue.cs} (71%) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 22451382a3..19d1162d23 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -60,7 +60,7 @@ namespace osu.Game.Beatmaps private readonly BeatmapStore beatmaps; private readonly AudioManager audioManager; private readonly GameHost host; - private readonly BeatmapUpdateQueue updateQueue; + private readonly BeatmapOnlineLookupQueue onlineLookupQueue; private readonly Storage exportStorage; public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, AudioManager audioManager, GameHost host = null, @@ -77,7 +77,7 @@ namespace osu.Game.Beatmaps beatmaps.BeatmapHidden += b => BeatmapHidden?.Invoke(b); beatmaps.BeatmapRestored += b => BeatmapRestored?.Invoke(b); - updateQueue = new BeatmapUpdateQueue(api, storage); + onlineLookupQueue = new BeatmapOnlineLookupQueue(api, storage); exportStorage = storage.GetStorageForDirectory("exports"); } @@ -104,7 +104,7 @@ namespace osu.Game.Beatmaps bool hadOnlineBeatmapIDs = beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0); - await updateQueue.UpdateAsync(beatmapSet, cancellationToken); + await onlineLookupQueue.UpdateAsync(beatmapSet, cancellationToken); // 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/Beatmaps/BeatmapManager_UpdateQueue.cs b/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs similarity index 71% rename from osu.Game/Beatmaps/BeatmapManager_UpdateQueue.cs rename to osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs index 84901f9b50..2bd7529ab0 100644 --- a/osu.Game/Beatmaps/BeatmapManager_UpdateQueue.cs +++ b/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs @@ -22,20 +22,20 @@ namespace osu.Game.Beatmaps { public partial class BeatmapManager { - private class BeatmapUpdateQueue + private class BeatmapOnlineLookupQueue { 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(BeatmapUpdateQueue)); + private readonly ThreadedTaskScheduler updateScheduler = new ThreadedTaskScheduler(update_queue_request_concurrency, nameof(BeatmapOnlineLookupQueue)); private FileWebRequest cacheDownloadRequest; private const string cache_database_name = "online.db"; - public BeatmapUpdateQueue(IAPIProvider api, Storage storage) + public BeatmapOnlineLookupQueue(IAPIProvider api, Storage storage) { this.api = api; this.storage = storage; @@ -55,38 +55,12 @@ 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(() => update(beatmapSet, beatmap), cancellationToken, TaskCreationOptions.HideScheduler, updateScheduler); + => Task.Factory.StartNew(() => lookup(beatmapSet, beatmap), cancellationToken, TaskCreationOptions.HideScheduler, updateScheduler); - private void update(BeatmapSetInfo set, BeatmapInfo beatmap) + private void lookup(BeatmapSetInfo set, BeatmapInfo beatmap) { - if (cacheDownloadRequest == null && storage.Exists(cache_database_name)) - { - try - { - using (var db = new SqliteConnection(storage.GetDatabaseConnectionString("online"))) - { - var found = db.QuerySingleOrDefault( - "SELECT * FROM osu_beatmaps WHERE checksum = @MD5Hash OR beatmap_id = @OnlineBeatmapID OR filename = @Path", beatmap); - - if (found != null) - { - var status = (BeatmapSetOnlineStatus)found.approved; - - beatmap.Status = status; - beatmap.BeatmapSet.Status = status; - beatmap.BeatmapSet.OnlineBeatmapSetID = found.beatmapset_id; - beatmap.OnlineBeatmapID = found.beatmap_id; - - LogForModel(set, $"Cached local retrieval for {beatmap}."); - return; - } - } - } - catch (Exception ex) - { - LogForModel(set, $"Cached local retrieval for {beatmap} failed with {ex}."); - } - } + if (checkLocalCache(set, beatmap)) + return; if (api?.State != APIState.Online) return; @@ -136,7 +110,7 @@ namespace osu.Game.Beatmaps File.Delete(compressedCacheFilePath); File.Delete(cacheFilePath); - Logger.Log($"{nameof(BeatmapUpdateQueue)}'s online cache download failed: {ex}", LoggingTarget.Database); + Logger.Log($"{nameof(BeatmapOnlineLookupQueue)}'s online cache download failed: {ex}", LoggingTarget.Database); }; cacheDownloadRequest.Finished += () => @@ -153,7 +127,7 @@ namespace osu.Game.Beatmaps } catch (Exception ex) { - Logger.Log($"{nameof(BeatmapUpdateQueue)}'s online cache extraction failed: {ex}", LoggingTarget.Database); + Logger.Log($"{nameof(BeatmapOnlineLookupQueue)}'s online cache extraction failed: {ex}", LoggingTarget.Database); File.Delete(cacheFilePath); } finally @@ -165,6 +139,45 @@ namespace osu.Game.Beatmaps 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; + + try + { + using (var db = new SqliteConnection(storage.GetDatabaseConnectionString("online"))) + { + var found = db.QuerySingleOrDefault( + "SELECT * FROM osu_beatmaps WHERE checksum = @MD5Hash OR beatmap_id = @OnlineBeatmapID OR filename = @Path", beatmap); + + if (found != null) + { + var status = (BeatmapSetOnlineStatus)found.approved; + + beatmap.Status = status; + beatmap.BeatmapSet.Status = status; + beatmap.BeatmapSet.OnlineBeatmapSetID = found.beatmapset_id; + beatmap.OnlineBeatmapID = found.beatmap_id; + + LogForModel(set, $"Cached local retrieval for {beatmap}."); + return true; + } + } + } + catch (Exception ex) + { + LogForModel(set, $"Cached local retrieval for {beatmap} failed with {ex}."); + } + + return false; + } + [Serializable] [SuppressMessage("ReSharper", "InconsistentNaming")] private class CachedOnlineBeatmapLookup From 68d40cf79064efc39f89631a80873f0886df99f9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 3 May 2020 13:25:57 +0900 Subject: [PATCH 1423/2376] Fix test failures due to online cache download --- osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs b/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs index 2bd7529ab0..2c79a664c5 100644 --- a/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs +++ b/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs @@ -9,6 +9,7 @@ using System.Threading; using System.Threading.Tasks; using Dapper; using Microsoft.Data.Sqlite; +using osu.Framework.Development; using osu.Framework.IO.Network; using osu.Framework.Logging; using osu.Framework.Platform; @@ -40,7 +41,8 @@ namespace osu.Game.Beatmaps this.api = api; this.storage = storage; - if (!storage.Exists(cache_database_name)) + // avoid downloading / using cache for unit tests. + if (!DebugUtils.IsNUnitRunning && !storage.Exists(cache_database_name)) prepareLocalCache(); } From cea6be5e52324d706b6120ffdaf340609b923447 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 3 May 2020 23:55:44 +0900 Subject: [PATCH 1424/2376] Expose as JudgementResult instead of "passing" state --- .../Skinning/TestSceneTaikoScroller.cs | 5 ++++- .../Skinning/LegacyTaikoScroller.cs | 10 ++++++---- osu.Game/Screens/Play/GameplayBeatmap.cs | 10 +++------- osu.Game/Screens/Play/Player.cs | 2 +- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs index 3d1ccadd6e..e26f410b71 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs @@ -3,6 +3,8 @@ using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Testing; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Skinning; using osu.Game.Skinning; @@ -13,7 +15,8 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning public TestSceneTaikoScroller() { AddStep("Load scroller", () => SetContents(() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.TaikoScroller), _ => Empty()))); - AddToggleStep("Toggle passing", passing => this.ChildrenOfType().ForEach(s => s.Passing.Value = !passing)); + AddToggleStep("Toggle passing", passing => this.ChildrenOfType().ForEach(s => s.LastResult.Value = + new JudgementResult(null, new Judgement()) { Type = passing ? HitResult.Perfect : HitResult.Miss })); } } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs index f61ee0301d..027fe1f302 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs @@ -7,6 +7,8 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play; using osu.Game.Skinning; using osuTK; @@ -24,21 +26,21 @@ namespace osu.Game.Rulesets.Taiko.Skinning private void load(GameplayBeatmap gameplayBeatmap) { if (gameplayBeatmap != null) - ((IBindable)Passing).BindTo(gameplayBeatmap.Passing); + ((IBindable)LastResult).BindTo(gameplayBeatmap.LastJudgementResult); } protected override void LoadComplete() { base.LoadComplete(); - Passing.BindValueChanged(passing => + LastResult.BindValueChanged(result => { foreach (var sprite in InternalChildren.OfType()) - sprite.Passing = passing.NewValue; + sprite.Passing = result.NewValue == null || result.NewValue.Type > HitResult.Miss; }, true); } - public Bindable Passing = new BindableBool(true); + public Bindable LastResult = new Bindable(); protected override void Update() { diff --git a/osu.Game/Screens/Play/GameplayBeatmap.cs b/osu.Game/Screens/Play/GameplayBeatmap.cs index 0afa189e66..64894544f4 100644 --- a/osu.Game/Screens/Play/GameplayBeatmap.cs +++ b/osu.Game/Screens/Play/GameplayBeatmap.cs @@ -9,7 +9,6 @@ using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Scoring; namespace osu.Game.Screens.Play { @@ -42,13 +41,10 @@ namespace osu.Game.Screens.Play public IBeatmap Clone() => PlayableBeatmap.Clone(); - public IBindable Passing => passing; + private readonly Bindable lastJudgementResult = new Bindable(); - private readonly BindableBool passing = new BindableBool(true); + public IBindable LastJudgementResult => lastJudgementResult; - public void OnNewResult(JudgementResult result) - { - passing.Value = result.Type > HitResult.Miss; - } + 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 eeb514b4be..f20d2504f7 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -193,7 +193,7 @@ namespace osu.Game.Screens.Play { HealthProcessor.ApplyResult(r); ScoreProcessor.ApplyResult(r); - gameplayBeatmap.OnNewResult(r); + gameplayBeatmap.ApplyResult(r); }; DrawableRuleset.OnRevertResult += r => From a1cd007cadfdb120f3a977e258676fedad854081 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 May 2020 14:43:47 +0900 Subject: [PATCH 1425/2376] Fix song select tests potentially failing due to difficulty panels not yet displayed --- .../Visual/SongSelect/TestScenePlaySongSelect.cs | 12 +++++++++--- osu.Game/Beatmaps/BeatmapManager.cs | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index aed8e19fb2..802c324c90 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -110,7 +110,7 @@ namespace osu.Game.Tests.Visual.SongSelect createSongSelect(); - AddUntilStep("wait for initial selection", () => !Beatmap.IsDefault); + waitForInitialSelection(); WorkingBeatmap selected = null; @@ -135,7 +135,7 @@ namespace osu.Game.Tests.Visual.SongSelect createSongSelect(); - AddUntilStep("wait for initial selection", () => !Beatmap.IsDefault); + waitForInitialSelection(); WorkingBeatmap selected = null; @@ -189,7 +189,7 @@ namespace osu.Game.Tests.Visual.SongSelect createSongSelect(); - AddUntilStep("wait for initial selection", () => !Beatmap.IsDefault); + waitForInitialSelection(); WorkingBeatmap selected = null; @@ -769,6 +769,12 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert("Check first item in group selected", () => Beatmap.Value.BeatmapInfo == groupIcon.Items.First().Beatmap); } + private void waitForInitialSelection() + { + AddUntilStep("wait for initial selection", () => !Beatmap.IsDefault); + AddUntilStep("wait for difficulty panels visible", () => songSelect.Carousel.ChildrenOfType().Any()); + } + private int getBeatmapIndex(BeatmapSetInfo set, BeatmapInfo info) => set.Beatmaps.FindIndex(b => b == info); private int getCurrentBeatmapIndex() => getBeatmapIndex(songSelect.Carousel.SelectedBeatmapSet, songSelect.Carousel.SelectedBeatmap); diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 19d1162d23..1b29f14c9b 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -140,7 +140,7 @@ namespace osu.Game.Beatmaps { var beatmapIds = beatmapSet.Beatmaps.Where(b => b.OnlineBeatmapID.HasValue).Select(b => b.OnlineBeatmapID).ToList(); - LogForModel(beatmapSet, "Validating online IDs..."); + LogForModel(beatmapSet, $"Validating online IDs for {beatmapSet.Beatmaps.Count} beatmaps..."); // ensure all IDs are unique if (beatmapIds.GroupBy(b => b).Any(g => g.Count() > 1)) From 02b9f51bdd4c2e0c2b07cefea40b54b4f0bbc0b2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 May 2020 13:31:49 +0900 Subject: [PATCH 1426/2376] Add failing test --- .../SongSelect/TestScenePlaySongSelect.cs | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 802c324c90..851801d38a 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -24,10 +24,12 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Taiko; +using osu.Game.Scoring; using osu.Game.Screens.Play; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Carousel; using osu.Game.Screens.Select.Filter; +using osu.Game.Users; using osuTK.Input; namespace osu.Game.Tests.Visual.SongSelect @@ -769,6 +771,70 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert("Check first item in group selected", () => Beatmap.Value.BeatmapInfo == groupIcon.Items.First().Beatmap); } + [Test] + public void TestChangeRulesetWhilePresentingScore() + { + changeRuleset(0); + + createSongSelect(); + + addRulesetImportStep(0); + addRulesetImportStep(1); + + AddStep("present score", () => + { + var presentBeatmap = manager.QueryBeatmap(b => b.RulesetID == 0); + var switchBeatmap = manager.QueryBeatmap(b => b.RulesetID == 1); + + // this ruleset change should be overridden by the present. + Ruleset.Value = switchBeatmap.Ruleset; + + songSelect.PresentScore(new ScoreInfo + { + User = new User { Username = "woo" }, + Beatmap = presentBeatmap, + Ruleset = presentBeatmap.Ruleset + }); + }); + + AddUntilStep("wait for results screen presented", () => !songSelect.IsCurrentScreen()); + + AddAssert("check beatmap is correct for score", () => Beatmap.Value.BeatmapInfo.Equals(manager.QueryBeatmap(b => b.RulesetID == 0))); + AddAssert("check ruleset is correct for score", () => Ruleset.Value.ID == 0); + } + + [Test] + public void TestChangeBeatmapWhilePresentingScore() + { + changeRuleset(0); + + createSongSelect(); + + addRulesetImportStep(0); + addRulesetImportStep(1); + + AddStep("present score", () => + { + var presentBeatmap = manager.QueryBeatmap(b => b.RulesetID == 0); + var switchBeatmap = manager.QueryBeatmap(b => b.RulesetID == 1); + + // this beatmap change should be overridden by the present. + Beatmap.Value = manager.GetWorkingBeatmap(switchBeatmap); + + songSelect.PresentScore(new ScoreInfo + { + User = new User { Username = "woo" }, + Beatmap = presentBeatmap, + Ruleset = presentBeatmap.Ruleset + }); + }); + + AddUntilStep("wait for results screen presented", () => !songSelect.IsCurrentScreen()); + + AddAssert("check beatmap is correct for score", () => Beatmap.Value.BeatmapInfo.Equals(manager.QueryBeatmap(b => b.RulesetID == 0))); + AddAssert("check ruleset is correct for score", () => Ruleset.Value.ID == 0); + } + private void waitForInitialSelection() { AddUntilStep("wait for initial selection", () => !Beatmap.IsDefault); @@ -882,6 +948,8 @@ namespace osu.Game.Tests.Visual.SongSelect public WorkingBeatmap CurrentBeatmapDetailsBeatmap => BeatmapDetails.Beatmap; public new BeatmapCarousel Carousel => base.Carousel; + public new void PresentScore(ScoreInfo score) => base.PresentScore(score); + protected override bool OnStart() { StartRequested?.Invoke(); From 06f58dd3e35a6d233845dbd73482e2638d22156f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 May 2020 14:04:30 +0900 Subject: [PATCH 1427/2376] Ensure correct beatmap and ruleset when presenting a score from song select --- osu.Game/Screens/Select/PlaySongSelect.cs | 6 +++++- osu.Game/Screens/Select/SongSelect.cs | 15 ++++++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index 179aab54a3..21ddc5685d 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Framework.Screens; using osu.Game.Graphics; +using osu.Game.Scoring; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; using osu.Game.Users; @@ -32,9 +33,12 @@ namespace osu.Game.Screens.Select Edit(); }, Key.Number4); - ((PlayBeatmapDetailArea)BeatmapDetails).Leaderboard.ScoreSelected += score => this.Push(new ResultsScreen(score)); + ((PlayBeatmapDetailArea)BeatmapDetails).Leaderboard.ScoreSelected += PresentScore; } + protected void PresentScore(ScoreInfo score) => + FinaliseSelection(score.Beatmap, score.Ruleset, () => this.Push(new ResultsScreen(score))); + protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); public override void OnResuming(IScreen last) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index a7e27c27ba..2b373ab7e0 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -342,13 +342,17 @@ namespace osu.Game.Screens.Select /// Call to make a selection and perform the default action for this SongSelect. /// /// An optional beatmap to override the current carousel selection. - /// Whether to trigger . - public void FinaliseSelection(BeatmapInfo beatmap = null, bool performStartAction = true) + /// 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) { // This is very important as we have not yet bound to screen-level bindables before the carousel load is completed. if (!Carousel.BeatmapSetsLoaded) return; + if (ruleset != null) + Ruleset.Value = ruleset; + transferRulesetValue(); // while transferRulesetValue will flush, it only does so if the ruleset changes. @@ -369,7 +373,12 @@ namespace osu.Game.Screens.Select selectionChangedDebounce = null; } - if (performStartAction && OnStart()) + if (customStartAction != null) + { + customStartAction(); + Carousel.AllowSelection = false; + } + else if (OnStart()) Carousel.AllowSelection = false; } From 81889e0034115e5a22f4882b1f6e98703d9acf50 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 May 2020 15:19:36 +0900 Subject: [PATCH 1428/2376] Fix tests potentially selecting a deleted beatmap --- .../SongSelect/TestScenePlaySongSelect.cs | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 851801d38a..81fd1b66e5 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -774,6 +774,9 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestChangeRulesetWhilePresentingScore() { + BeatmapInfo getPresentBeatmap() => manager.QueryBeatmap(b => !b.BeatmapSet.DeletePending && b.RulesetID == 0); + BeatmapInfo getSwitchBeatmap() => manager.QueryBeatmap(b => !b.BeatmapSet.DeletePending && b.RulesetID == 1); + changeRuleset(0); createSongSelect(); @@ -783,55 +786,52 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("present score", () => { - var presentBeatmap = manager.QueryBeatmap(b => b.RulesetID == 0); - var switchBeatmap = manager.QueryBeatmap(b => b.RulesetID == 1); - // this ruleset change should be overridden by the present. - Ruleset.Value = switchBeatmap.Ruleset; + Ruleset.Value = getSwitchBeatmap().Ruleset; songSelect.PresentScore(new ScoreInfo { User = new User { Username = "woo" }, - Beatmap = presentBeatmap, - Ruleset = presentBeatmap.Ruleset + Beatmap = getPresentBeatmap(), + Ruleset = getPresentBeatmap().Ruleset }); }); AddUntilStep("wait for results screen presented", () => !songSelect.IsCurrentScreen()); - AddAssert("check beatmap is correct for score", () => Beatmap.Value.BeatmapInfo.Equals(manager.QueryBeatmap(b => b.RulesetID == 0))); + AddAssert("check beatmap is correct for score", () => Beatmap.Value.BeatmapInfo.Equals(getPresentBeatmap())); AddAssert("check ruleset is correct for score", () => Ruleset.Value.ID == 0); } [Test] public void TestChangeBeatmapWhilePresentingScore() { - changeRuleset(0); + BeatmapInfo getPresentBeatmap() => manager.QueryBeatmap(b => !b.BeatmapSet.DeletePending && b.RulesetID == 0); + BeatmapInfo getSwitchBeatmap() => manager.QueryBeatmap(b => !b.BeatmapSet.DeletePending && b.RulesetID == 1); - createSongSelect(); + changeRuleset(0); addRulesetImportStep(0); addRulesetImportStep(1); + createSongSelect(); + AddStep("present score", () => { - var presentBeatmap = manager.QueryBeatmap(b => b.RulesetID == 0); - var switchBeatmap = manager.QueryBeatmap(b => b.RulesetID == 1); - // this beatmap change should be overridden by the present. - Beatmap.Value = manager.GetWorkingBeatmap(switchBeatmap); + Beatmap.Value = manager.GetWorkingBeatmap(getSwitchBeatmap()); songSelect.PresentScore(new ScoreInfo { User = new User { Username = "woo" }, - Beatmap = presentBeatmap, - Ruleset = presentBeatmap.Ruleset + Beatmap = getPresentBeatmap(), + Ruleset = getPresentBeatmap().Ruleset }); }); AddUntilStep("wait for results screen presented", () => !songSelect.IsCurrentScreen()); - AddAssert("check beatmap is correct for score", () => Beatmap.Value.BeatmapInfo.Equals(manager.QueryBeatmap(b => b.RulesetID == 0))); + AddAssert("check beatmap is correct for score", () => Beatmap.Value.BeatmapInfo.Equals(getPresentBeatmap())); AddAssert("check ruleset is correct for score", () => Ruleset.Value.ID == 0); } From 46b0526db7afd25e1a962420ee8235df1f109801 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 4 May 2020 15:27:04 +0900 Subject: [PATCH 1429/2376] Remove hack limiting max number of ticks --- osu.Game.Rulesets.Catch/Objects/JuiceStream.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index 19219cc1ba..01011645bd 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -58,8 +58,6 @@ namespace osu.Game.Rulesets.Catch.Objects SliderEventDescriptor? lastEvent = null; - int ticksGenerated = 0; - foreach (var e in SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset)) { // generate tiny droplets since the last point @@ -75,9 +73,6 @@ namespace osu.Game.Rulesets.Catch.Objects for (double t = timeBetweenTiny; t < sinceLastTick; t += timeBetweenTiny) { - if (ticksGenerated++ >= 10000) - break; - AddNested(new TinyDroplet { StartTime = t + lastEvent.Value.Time, From 6d3a24ff01cdeea9bf52d164087279989b277b6d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 4 May 2020 15:55:42 +0900 Subject: [PATCH 1430/2376] Reorder tick hit results --- osu.Game/Rulesets/Scoring/HitResult.cs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/HitResult.cs b/osu.Game/Rulesets/Scoring/HitResult.cs index 0c895bd086..b057af2a50 100644 --- a/osu.Game/Rulesets/Scoring/HitResult.cs +++ b/osu.Game/Rulesets/Scoring/HitResult.cs @@ -44,9 +44,24 @@ namespace osu.Game.Rulesets.Scoring [Description(@"Perfect")] Perfect, - SmallTickHit, + /// + /// Indicates small tick miss. + /// SmallTickMiss, - LargeTickHit, + + /// + /// Indicates a small tick hit. + /// + SmallTickHit, + + /// + /// Indicates a large tick miss. + /// LargeTickMiss, + + /// + /// Indicates a large tick hit. + /// + LargeTickHit } } From 6621d363da05951e3208f785f472190802638789 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 May 2020 17:01:05 +0900 Subject: [PATCH 1431/2376] Add basic custom data directory support --- .../NonVisual/CustomDataDirectoryTest.cs | 88 +++++++++++++++++++ .../Configuration/StorageConfigManager.cs | 30 +++++++ osu.Game/OsuGameBase.cs | 23 ++++- 3 files changed, 139 insertions(+), 2 deletions(-) create mode 100644 osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs create mode 100644 osu.Game/Configuration/StorageConfigManager.cs diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs new file mode 100644 index 0000000000..2d5f1f238f --- /dev/null +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.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.IO; +using System.Threading; +using System.Threading.Tasks; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Platform; +using osu.Game.Configuration; + +namespace osu.Game.Tests.NonVisual +{ + [TestFixture] + public class CustomDataDirectoryTest + { + [Test] + public void TestDefaultDirectory() + { + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestDefaultDirectory))) + { + try + { + var osu = loadOsu(host); + var storage = osu.Dependencies.Get(); + + string defaultStorageLocation = Path.Combine(Environment.CurrentDirectory, $"headless-{nameof(TestDefaultDirectory)}"); + + Assert.That(storage.GetFullPath("."), Is.EqualTo(defaultStorageLocation)); + } + finally + { + host.Exit(); + } + } + } + + [Test] + public void TestCustomDirectory() + { + using (var host = new HeadlessGameHost(nameof(TestCustomDirectory))) + { + string headlessPrefix = $"headless-{nameof(TestCustomDirectory)}"; + + // need access before the game has constructed its own storage yet. + Storage storage = new DesktopStorage(headlessPrefix, host); + // manual cleaning so we can prepare a config file. + storage.DeleteDirectory(string.Empty); + + using (var storageConfig = new StorageConfigManager(storage)) + storageConfig.Set(StorageConfig.FullPath, Path.Combine(Environment.CurrentDirectory, "custom-path")); + + try + { + var osu = loadOsu(host); + + // switch to DI'd storage + storage = osu.Dependencies.Get(); + + Assert.That(storage.GetFullPath("."), Is.EqualTo(Path.Combine(Environment.CurrentDirectory, "custom-path"))); + } + finally + { + host.Exit(); + } + } + } + + private OsuGameBase loadOsu(GameHost host) + { + var osu = new OsuGameBase(); + Task.Run(() => host.Run(osu)); + waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time"); + return osu; + } + + private static void waitForOrAssert(Func result, string failureMessage, int timeout = 60000) + { + Task task = Task.Run(() => + { + while (!result()) Thread.Sleep(200); + }); + + Assert.IsTrue(task.Wait(timeout), failureMessage); + } + } +} diff --git a/osu.Game/Configuration/StorageConfigManager.cs b/osu.Game/Configuration/StorageConfigManager.cs new file mode 100644 index 0000000000..929f8f22ad --- /dev/null +++ b/osu.Game/Configuration/StorageConfigManager.cs @@ -0,0 +1,30 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Configuration; +using osu.Framework.Platform; + +namespace osu.Game.Configuration +{ + public class StorageConfigManager : IniConfigManager + { + protected override string Filename => "storage.ini"; + + public StorageConfigManager(Storage storage) + : base(storage) + { + } + + protected override void InitialiseDefaults() + { + base.InitialiseDefaults(); + + Set(StorageConfig.FullPath, string.Empty); + } + } + + public enum StorageConfig + { + FullPath, + } +} diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 609b6ce98e..fe25197294 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -132,6 +132,8 @@ namespace osu.Game dependencies.Cache(contextFactory = new DatabaseContextFactory(Storage)); + dependencies.CacheAs(Storage); + var largeStore = new LargeTextureStore(Host.CreateTextureLoaderStore(new NamespacedResourceStore(Resources, @"Textures"))); largeStore.AddStore(Host.CreateTextureLoaderStore(new OnlineStore())); dependencies.Cache(largeStore); @@ -300,8 +302,13 @@ namespace osu.Game { base.SetHost(host); - if (Storage == null) - Storage = host.Storage; + var storageConfig = new StorageConfigManager(host.Storage); + + var customStoragePath = storageConfig.Get(StorageConfig.FullPath); + + Storage = !string.IsNullOrEmpty(customStoragePath) + ? new CustomStorage(customStoragePath, host) + : host.Storage; if (LocalConfig == null) LocalConfig = new OsuConfigManager(Storage); @@ -353,5 +360,17 @@ namespace osu.Game public override bool ChangeFocusOnClick => false; } } + + /// + /// A storage pointing to an absolute location specified by the user to store game data files. + /// + private class CustomStorage : NativeStorage + { + public CustomStorage(string fullPath, GameHost host) + : base(string.Empty, host) + { + BasePath = fullPath; + } + } } } From 62d433c9c57b2de866d579f4bfbad543eadc9649 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 4 May 2020 17:01:07 +0900 Subject: [PATCH 1432/2376] Adjust diffcalc test value --- osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs index 51fe0b035d..ee416e5a38 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Catch.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Catch"; - [TestCase(4.2058561036909863d, "diffcalc-test")] + [TestCase(4.050601681491468d, "diffcalc-test")] public void Test(double expected, string name) => base.Test(expected, name); From 5edabbdee25e38e3e0d1ab08296e5a4f6119e79b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 May 2020 17:35:35 +0900 Subject: [PATCH 1433/2376] Redirect log output to custom data directory --- osu.Game/OsuGameBase.cs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index fe25197294..f92db4e111 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -71,6 +71,8 @@ namespace osu.Game protected MenuCursorContainer MenuCursorContainer; + protected StorageConfigManager StorageConfig; + private Container content; protected override Container Content => content; @@ -302,13 +304,17 @@ namespace osu.Game { base.SetHost(host); - var storageConfig = new StorageConfigManager(host.Storage); + StorageConfig = new StorageConfigManager(host.Storage); - var customStoragePath = storageConfig.Get(StorageConfig.FullPath); + var customStoragePath = StorageConfig.Get(Configuration.StorageConfig.FullPath); - Storage = !string.IsNullOrEmpty(customStoragePath) - ? new CustomStorage(customStoragePath, host) - : host.Storage; + if (!string.IsNullOrEmpty(customStoragePath)) + { + Storage = new CustomStorage(customStoragePath, host); + Logger.Storage = Storage.GetStorageForDirectory("logs"); + } + else + Storage = host.Storage; if (LocalConfig == null) LocalConfig = new OsuConfigManager(Storage); From e2593ac3e77e82d14c776ef4cf6c19c60ac697b3 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 4 May 2020 08:56:18 +0000 Subject: [PATCH 1434/2376] Bump Microsoft.CodeAnalysis.FxCopAnalyzers from 2.9.8 to 3.0.0 Bumps [Microsoft.CodeAnalysis.FxCopAnalyzers](https://github.com/dotnet/roslyn-analyzers) from 2.9.8 to 3.0.0. - [Release notes](https://github.com/dotnet/roslyn-analyzers/releases) - [Changelog](https://github.com/dotnet/roslyn-analyzers/blob/master/PostReleaseActivities.md) - [Commits](https://github.com/dotnet/roslyn-analyzers/compare/v2.9.8...v3.0.0) Signed-off-by: dependabot-preview[bot] --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 21b8b402e0..fbe300458e 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -18,7 +18,7 @@ - + $(MSBuildThisFileDirectory)CodeAnalysis\osu.ruleset From 4ee2e6cd47add8c964d61445cca16a271b6236dc Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 4 May 2020 08:57:09 +0000 Subject: [PATCH 1435/2376] Bump Humanizer from 2.8.2 to 2.8.11 Bumps [Humanizer](https://github.com/Humanizr/Humanizer) from 2.8.2 to 2.8.11. - [Release notes](https://github.com/Humanizr/Humanizer/releases) - [Changelog](https://github.com/Humanizr/Humanizer/blob/master/release_notes.md) - [Commits](https://github.com/Humanizr/Humanizer/compare/v2.8.2...v2.8.11) Signed-off-by: dependabot-preview[bot] --- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 91c89cbc20..9db5fe562c 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -20,7 +20,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 9ff7e3fc02..82253a0418 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -76,7 +76,7 @@ - + From fe31bac505bb59891325fc59afb4ba597df3e052 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 4 May 2020 18:20:20 +0900 Subject: [PATCH 1436/2376] Fix build error --- osu.Game/Overlays/SearchableList/DisplayStyleControl.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Overlays/SearchableList/DisplayStyleControl.cs b/osu.Game/Overlays/SearchableList/DisplayStyleControl.cs index a33f4eb30d..5ecb477a2f 100644 --- a/osu.Game/Overlays/SearchableList/DisplayStyleControl.cs +++ b/osu.Game/Overlays/SearchableList/DisplayStyleControl.cs @@ -91,6 +91,8 @@ namespace osu.Game.Overlays.SearchableList protected override void Dispose(bool isDisposing) { + base.Dispose(isDisposing); + bindable.ValueChanged -= Bindable_ValueChanged; } } From 969412a4265aa3c94f0a6112552b5ad9287f4908 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 4 May 2020 10:28:42 +0000 Subject: [PATCH 1437/2376] Bump Microsoft.CodeAnalysis.BannedApiAnalyzers from 2.9.8 to 3.0.0 Bumps [Microsoft.CodeAnalysis.BannedApiAnalyzers](https://github.com/dotnet/roslyn-analyzers) from 2.9.8 to 3.0.0. - [Release notes](https://github.com/dotnet/roslyn-analyzers/releases) - [Changelog](https://github.com/dotnet/roslyn-analyzers/blob/master/PostReleaseActivities.md) - [Commits](https://github.com/dotnet/roslyn-analyzers/compare/v2.9.8...v3.0.0) Signed-off-by: dependabot-preview[bot] --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 21b8b402e0..5d011dfdc5 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -16,7 +16,7 @@ - + From c987af988c98745a8039372563b87717a7691e46 Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 4 May 2020 18:26:12 -0700 Subject: [PATCH 1438/2376] Fix typo --- osu.Game/Screens/OsuScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs index 2124a66a75..35bb4fa34f 100644 --- a/osu.Game/Screens/OsuScreen.cs +++ b/osu.Game/Screens/OsuScreen.cs @@ -23,7 +23,7 @@ namespace osu.Game.Screens { /// /// The amount of negative padding that should be applied to game background content which touches both the left and right sides of the screen. - /// This allows for the game content to be pushed byt he options/notification overlays without causing black areas to appear. + /// This allows for the game content to be pushed by the options/notification overlays without causing black areas to appear. /// public const float HORIZONTAL_OVERFLOW_PADDING = 50; From 0e2ccac33b916abc63e9aa4e8cb474e6761fb22e Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 4 May 2020 18:31:11 -0700 Subject: [PATCH 1439/2376] Add spaces to comments --- osu.Desktop/Updater/SquirrelUpdateManager.cs | 10 ++++----- osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs | 2 +- .../Replays/CatchAutoGenerator.cs | 6 ++--- .../Beatmaps/OsuBeatmapProcessor.cs | 18 +++++++-------- osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs | 2 +- .../Drawables/Pieces/MainCirclePiece.cs | 2 +- .../Beatmaps/IO/ImportBeatmapTest.cs | 22 +++++++++---------- .../NonVisual/ControlPointInfoTest.cs | 2 +- .../NonVisual/FramedReplayInputHandlerTest.cs | 6 ++--- osu.Game/Beatmaps/BeatmapManager.cs | 2 +- osu.Game/Graphics/Backgrounds/Triangles.cs | 4 ++-- .../Graphics/Containers/OsuScrollContainer.cs | 2 +- osu.Game/IPC/ArchiveImportIPCChannel.cs | 2 +- osu.Game/Online/API/APIAccess.cs | 8 +++---- osu.Game/Online/API/APIRequest.cs | 2 +- osu.Game/Online/Chat/Channel.cs | 2 +- osu.Game/Online/Chat/MessageFormatter.cs | 4 ++-- osu.Game/OsuGame.cs | 4 ++-- .../BeatmapListing/Panels/GridBeatmapPanel.cs | 2 +- osu.Game/Overlays/BeatmapSet/Header.cs | 2 +- osu.Game/Overlays/ChatOverlay.cs | 2 +- osu.Game/Overlays/DialogOverlay.cs | 2 +- osu.Game/Overlays/Music/PlaylistItem.cs | 2 +- osu.Game/Overlays/MusicController.cs | 2 +- osu.Game/Overlays/News/NewsArticleCover.cs | 2 +- .../Notifications/ProgressNotification.cs | 2 +- osu.Game/Overlays/NowPlayingOverlay.cs | 2 +- osu.Game/Overlays/OSD/Toast.cs | 2 +- .../Components/OverlinedInfoContainer.cs | 2 +- .../Profile/Header/MedalHeaderContainer.cs | 2 +- .../SearchableListFilterControl.cs | 2 +- osu.Game/Overlays/Toolbar/ToolbarButton.cs | 2 +- osu.Game/Overlays/VolumeOverlay.cs | 2 +- .../Replays/FramedReplayInputHandler.cs | 6 ++--- osu.Game/Rulesets/RulesetStore.cs | 6 ++--- osu.Game/Screens/BackgroundScreen.cs | 2 +- osu.Game/Screens/Menu/IntroScreen.cs | 2 +- osu.Game/Screens/Menu/LogoVisualisation.cs | 10 ++++----- osu.Game/Screens/Menu/MainMenu.cs | 2 +- osu.Game/Screens/Play/BreakOverlay.cs | 2 +- .../Screens/Play/HUD/PlayerSettingsOverlay.cs | 2 +- osu.Game/Screens/Play/KeyCounter.cs | 4 ++-- osu.Game/Screens/Play/PlayerLoader.cs | 6 ++--- osu.Game/Screens/Select/BeatmapCarousel.cs | 2 +- .../Select/BeatmapDetailAreaTabControl.cs | 2 +- osu.Game/Screens/Select/BeatmapDetails.cs | 4 ++-- .../Screens/Select/Details/AdvancedStats.cs | 2 +- osu.Game/Screens/Select/SongSelect.cs | 2 +- 48 files changed, 92 insertions(+), 92 deletions(-) diff --git a/osu.Desktop/Updater/SquirrelUpdateManager.cs b/osu.Desktop/Updater/SquirrelUpdateManager.cs index 60b47a8b3a..ade8460dd7 100644 --- a/osu.Desktop/Updater/SquirrelUpdateManager.cs +++ b/osu.Desktop/Updater/SquirrelUpdateManager.cs @@ -43,7 +43,7 @@ namespace osu.Desktop.Updater private async void checkForUpdateAsync(bool useDeltaPatching = true, UpdateProgressNotification notification = null) { - //should we schedule a retry on completion of this check? + // should we schedule a retry on completion of this check? bool scheduleRecheck = true; try @@ -52,7 +52,7 @@ namespace osu.Desktop.Updater var info = await updateManager.CheckForUpdate(!useDeltaPatching); if (info.ReleasesToApply.Count == 0) - //no updates available. bail and retry later. + // no updates available. bail and retry later. return; if (notification == null) @@ -81,8 +81,8 @@ namespace osu.Desktop.Updater { logger.Add(@"delta patching failed; will attempt full download!"); - //could fail if deltas are unavailable for full update path (https://github.com/Squirrel/Squirrel.Windows/issues/959) - //try again without deltas. + // could fail if deltas are unavailable for full update path (https://github.com/Squirrel/Squirrel.Windows/issues/959) + // try again without deltas. checkForUpdateAsync(false, notification); scheduleRecheck = false; } @@ -101,7 +101,7 @@ namespace osu.Desktop.Updater { if (scheduleRecheck) { - //check again in 30 minutes. + // check again in 30 minutes. Scheduler.AddDelayed(() => checkForUpdateAsync(), 60000 * 30); } } diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs b/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs index 16414261a5..c1d24395e4 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs @@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Catch.Mods RelativeSizeAxes = Axes.Both; } - //disable keyboard controls + // disable keyboard controls public bool OnPressed(CatchAction action) => true; public void OnReleased(CatchAction action) diff --git a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs index b90b5812a6..7a33cb0577 100644 --- a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs +++ b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs @@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Catch.Replays if (lastPosition - catcher_width_half < h.X && lastPosition + catcher_width_half > h.X) { - //we are already in the correct range. + // we are already in the correct range. lastTime = h.StartTime; addFrame(h.StartTime, lastPosition); return; @@ -72,14 +72,14 @@ namespace osu.Game.Rulesets.Catch.Replays } else if (dashRequired) { - //we do a movement in two parts - the dash part then the normal part... + // we do a movement in two parts - the dash part then the normal part... double timeAtNormalSpeed = positionChange / movement_speed; double timeWeNeedToSave = timeAtNormalSpeed - timeAvailable; double timeAtDashSpeed = timeWeNeedToSave / 2; float midPosition = (float)Interpolation.Lerp(lastPosition, h.X, (float)timeAtDashSpeed / timeAvailable); - //dash movement + // dash movement addFrame(h.StartTime - timeAvailable + 1, lastPosition, true); addFrame(h.StartTime - timeAvailable + timeAtDashSpeed, midPosition); addFrame(h.StartTime, h.X); diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs index 3a829f72fa..f51f04bf87 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs @@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps double stackThreshold = objectN.TimePreempt * beatmap.BeatmapInfo.StackLeniency; if (objectN.StartTime - endTime > stackThreshold) - //We are no longer within stacking range of the next object. + // We are no longer within stacking range of the next object. break; if (Vector2Extensions.Distance(stackBaseObject.Position, objectN.Position) < stack_distance @@ -88,7 +88,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps } } - //Reverse pass for stack calculation. + // Reverse pass for stack calculation. int extendedStartIndex = startIndex; for (int i = extendedEndIndex; i > startIndex; i--) @@ -124,7 +124,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps double endTime = objectN.GetEndTime(); if (objectI.StartTime - endTime > stackThreshold) - //We are no longer within stacking range of the previous object. + // We are no longer within stacking range of the previous object. break; // HitObjects before the specified update range haven't been reset yet @@ -145,20 +145,20 @@ namespace osu.Game.Rulesets.Osu.Beatmaps for (int j = n + 1; j <= i; j++) { - //For each object which was declared under this slider, we will offset it to appear *below* the slider end (rather than above). + // For each object which was declared under this slider, we will offset it to appear *below* the slider end (rather than above). OsuHitObject objectJ = beatmap.HitObjects[j]; if (Vector2Extensions.Distance(objectN.EndPosition, objectJ.Position) < stack_distance) objectJ.StackHeight -= offset; } - //We have hit a slider. We should restart calculation using this as the new base. - //Breaking here will mean that the slider still has StackCount of 0, so will be handled in the i-outer-loop. + // We have hit a slider. We should restart calculation using this as the new base. + // Breaking here will mean that the slider still has StackCount of 0, so will be handled in the i-outer-loop. break; } if (Vector2Extensions.Distance(objectN.Position, objectI.Position) < stack_distance) { - //Keep processing as if there are no sliders. If we come across a slider, this gets cancelled out. + // Keep processing as if there are no sliders. If we come across a slider, this gets cancelled out. //NOTE: Sliders with start positions stacking are a special case that is also handled here. objectN.StackHeight = objectI.StackHeight + 1; @@ -177,7 +177,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps if (objectN is Spinner) continue; if (objectI.StartTime - objectN.StartTime > stackThreshold) - //We are no longer within stacking range of the previous object. + // We are no longer within stacking range of the previous object. break; if (Vector2Extensions.Distance(objectN.EndPosition, objectI.Position) < stack_distance) @@ -221,7 +221,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps } else if (Vector2Extensions.Distance(beatmap.HitObjects[j].Position, position2) < stack_distance) { - //Case for sliders - bump notes down and right, rather than up and left. + // Case for sliders - bump notes down and right, rather than up and left. sliderStack++; beatmap.HitObjects[j].StackHeight -= sliderStack; startTime = beatmap.HitObjects[j].GetEndTime(); diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs index 44dba7715a..5e80d08667 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs @@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Osu.Mods Vector2 originalPosition = drawable.Position; Vector2 appearOffset = new Vector2(MathF.Cos(theta), MathF.Sin(theta)) * appearDistance; - //the - 1 and + 1 prevents the hit objects to appear in the wrong position. + // the - 1 and + 1 prevents the hit objects to appear in the wrong position. double appearTime = hitObject.StartTime - hitObject.TimePreempt - 1; double moveDuration = hitObject.TimePreempt + 1; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs index e364c96426..cb3787a493 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs @@ -81,7 +81,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces using (BeginDelayedSequence(flash_in, true)) { - //after the flash, we can hide some elements that were behind it + // after the flash, we can hide some elements that were behind it ring.FadeOut(); circle.FadeOut(); number.FadeOut(); diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index c6095ae404..ba6f5fc85c 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -31,7 +31,7 @@ namespace osu.Game.Tests.Beatmaps.IO [Test] public async Task TestImportWhenClosed() { - //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. + // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportWhenClosed))) { try @@ -48,7 +48,7 @@ namespace osu.Game.Tests.Beatmaps.IO [Test] public async Task TestImportThenDelete() { - //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. + // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportThenDelete))) { try @@ -69,7 +69,7 @@ namespace osu.Game.Tests.Beatmaps.IO [Test] public async Task TestImportThenImport() { - //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. + // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportThenImport))) { try @@ -96,7 +96,7 @@ namespace osu.Game.Tests.Beatmaps.IO [Test] public async Task TestImportCorruptThenImport() { - //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. + // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportCorruptThenImport))) { try @@ -138,7 +138,7 @@ namespace osu.Game.Tests.Beatmaps.IO [Test] public async Task TestRollbackOnFailure() { - //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. + // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestRollbackOnFailure))) { try @@ -215,7 +215,7 @@ namespace osu.Game.Tests.Beatmaps.IO [Test] public async Task TestImportThenImportDifferentHash() { - //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. + // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportThenImportDifferentHash))) { try @@ -246,7 +246,7 @@ namespace osu.Game.Tests.Beatmaps.IO [Test] public async Task TestImportThenDeleteThenImport() { - //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. + // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportThenDeleteThenImport))) { try @@ -274,7 +274,7 @@ namespace osu.Game.Tests.Beatmaps.IO [TestCase(false)] public async Task TestImportThenDeleteThenImportWithOnlineIDMismatch(bool set) { - //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. + // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. using (HeadlessGameHost host = new CleanRunHeadlessGameHost($"{nameof(TestImportThenDeleteThenImportWithOnlineIDMismatch)}-{set}")) { try @@ -308,7 +308,7 @@ namespace osu.Game.Tests.Beatmaps.IO [Test] public async Task TestImportWithDuplicateBeatmapIDs() { - //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. + // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportWithDuplicateBeatmapIDs))) { try @@ -695,12 +695,12 @@ namespace osu.Game.Tests.Beatmaps.IO waitForOrAssert(() => (resultSets = store.QueryBeatmapSets(s => s.OnlineBeatmapSetID == 241526)).Any(), @"BeatmapSet did not import to the database in allocated time.", timeout); - //ensure we were stored to beatmap database backing... + // ensure we were stored to beatmap database backing... Assert.IsTrue(resultSets.Count() == 1, $@"Incorrect result count found ({resultSets.Count()} but should be 1)."); IEnumerable queryBeatmaps() => store.QueryBeatmaps(s => s.BeatmapSet.OnlineBeatmapSetID == 241526 && s.BaseDifficultyID > 0); IEnumerable queryBeatmapSets() => store.QueryBeatmapSets(s => s.OnlineBeatmapSetID == 241526); - //if we don't re-check here, the set will be inserted but the beatmaps won't be present yet. + // if we don't re-check here, the set will be inserted but the beatmaps won't be present yet. waitForOrAssert(() => queryBeatmaps().Count() == 12, @"Beatmaps did not import to the database in allocated time", timeout); waitForOrAssert(() => queryBeatmapSets().Count() == 1, diff --git a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs index 158954106d..830e4bc603 100644 --- a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs +++ b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs @@ -35,7 +35,7 @@ namespace osu.Game.Tests.NonVisual Assert.That(cpi.TimingPoints.Count, Is.EqualTo(2)); Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(2)); - cpi.Add(1000, new TimingControlPoint()); //is redundant + cpi.Add(1000, new TimingControlPoint()); // is redundant Assert.That(cpi.Groups.Count, Is.EqualTo(2)); Assert.That(cpi.TimingPoints.Count, Is.EqualTo(2)); diff --git a/osu.Game.Tests/NonVisual/FramedReplayInputHandlerTest.cs b/osu.Game.Tests/NonVisual/FramedReplayInputHandlerTest.cs index 7df7df22ea..92a60663de 100644 --- a/osu.Game.Tests/NonVisual/FramedReplayInputHandlerTest.cs +++ b/osu.Game.Tests/NonVisual/FramedReplayInputHandlerTest.cs @@ -46,12 +46,12 @@ namespace osu.Game.Tests.NonVisual confirmCurrentFrame(0); confirmNextFrame(1); - //if we hit the first frame perfectly, time should progress to it. + // if we hit the first frame perfectly, time should progress to it. setTime(1000, 1000); confirmCurrentFrame(1); confirmNextFrame(2); - //in between non-important frames should progress based on input. + // in between non-important frames should progress based on input. setTime(1200, 1200); confirmCurrentFrame(1); @@ -144,7 +144,7 @@ namespace osu.Game.Tests.NonVisual confirmCurrentFrame(2); confirmNextFrame(1); - //ensure each frame plays out until start + // ensure each frame plays out until start setTime(-500, 1000); confirmCurrentFrame(1); confirmNextFrame(0); diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 19d1162d23..d7c30dc9ff 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -381,7 +381,7 @@ namespace osu.Game.Beatmaps foreach (var file in files.Where(f => f.Filename.EndsWith(".osu"))) { 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 ms = new MemoryStream()) // we need a memory stream so we can seek using (var sr = new LineBufferedReader(ms)) { raw.CopyTo(ms); diff --git a/osu.Game/Graphics/Backgrounds/Triangles.cs b/osu.Game/Graphics/Backgrounds/Triangles.cs index 590e4b2a5c..27027202ce 100644 --- a/osu.Game/Graphics/Backgrounds/Triangles.cs +++ b/osu.Game/Graphics/Backgrounds/Triangles.cs @@ -193,8 +193,8 @@ namespace osu.Game.Graphics.Backgrounds float u1 = 1 - RNG.NextSingle(); //uniform(0,1] random floats float u2 = 1 - RNG.NextSingle(); - float randStdNormal = (float)(Math.Sqrt(-2.0 * Math.Log(u1)) * Math.Sin(2.0 * Math.PI * u2)); //random normal(0,1) - var scale = Math.Max(triangleScale * (mean + std_dev * randStdNormal), 0.1f); //random normal(mean,stdDev^2) + float randStdNormal = (float)(Math.Sqrt(-2.0 * Math.Log(u1)) * Math.Sin(2.0 * Math.PI * u2)); // random normal(0,1) + var scale = Math.Max(triangleScale * (mean + std_dev * randStdNormal), 0.1f); // random normal(mean,stdDev^2) return new TriangleParticle { Scale = scale }; } diff --git a/osu.Game/Graphics/Containers/OsuScrollContainer.cs b/osu.Game/Graphics/Containers/OsuScrollContainer.cs index 1824fcd878..d504a11b22 100644 --- a/osu.Game/Graphics/Containers/OsuScrollContainer.cs +++ b/osu.Game/Graphics/Containers/OsuScrollContainer.cs @@ -158,7 +158,7 @@ namespace osu.Game.Graphics.Containers { if (!base.OnMouseDown(e)) return false; - //note that we are changing the colour of the box here as to not interfere with the hover effect. + // note that we are changing the colour of the box here as to not interfere with the hover effect. box.FadeColour(highlightColour, 100); return true; } diff --git a/osu.Game/IPC/ArchiveImportIPCChannel.cs b/osu.Game/IPC/ArchiveImportIPCChannel.cs index 484db932f8..029908ec9d 100644 --- a/osu.Game/IPC/ArchiveImportIPCChannel.cs +++ b/osu.Game/IPC/ArchiveImportIPCChannel.cs @@ -32,7 +32,7 @@ namespace osu.Game.IPC { if (importer == null) { - //we want to contact a remote osu! to handle the import. + // we want to contact a remote osu! to handle the import. await SendMessageAsync(new ArchiveImportMessage { Path = path }); return; } diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index adfef1d11f..4945f7f185 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -127,7 +127,7 @@ namespace osu.Game.Online.API case APIState.Offline: case APIState.Connecting: - //work to restore a connection... + // work to restore a connection... if (!HasLogin) { State = APIState.Offline; @@ -180,7 +180,7 @@ namespace osu.Game.Online.API break; } - //hard bail if we can't get a valid access token. + // hard bail if we can't get a valid access token. if (authentication.RequestAccessToken() == null) { Logout(); @@ -274,7 +274,7 @@ namespace osu.Game.Online.API { req.Perform(this); - //we could still be in initialisation, at which point we don't want to say we're Online yet. + // we could still be in initialisation, at which point we don't want to say we're Online yet. if (IsLoggedIn) State = APIState.Online; failureCount = 0; @@ -339,7 +339,7 @@ namespace osu.Game.Online.API log.Add($@"API failure count is now {failureCount}"); if (failureCount < 3) - //we might try again at an api level. + // we might try again at an api level. return false; if (State == APIState.Online) diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs index 0bba04cac3..0f8acbb7af 100644 --- a/osu.Game/Online/API/APIRequest.cs +++ b/osu.Game/Online/API/APIRequest.cs @@ -98,7 +98,7 @@ namespace osu.Game.Online.API if (checkAndScheduleFailure()) return; - if (!WebRequest.Aborted) //could have been aborted by a Cancel() call + if (!WebRequest.Aborted) // could have been aborted by a Cancel() call { Logger.Log($@"Performing request {this}", LoggingTarget.Network); WebRequest.Perform(); diff --git a/osu.Game/Online/Chat/Channel.cs b/osu.Game/Online/Chat/Channel.cs index 6f67a95f53..dbb2da5c03 100644 --- a/osu.Game/Online/Chat/Channel.cs +++ b/osu.Game/Online/Chat/Channel.cs @@ -61,7 +61,7 @@ namespace osu.Game.Online.Chat /// public event Action MessageRemoved; - public bool ReadOnly => false; //todo not yet used. + public bool ReadOnly => false; // todo: not yet used. public override string ToString() => Name; diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index 717de18c14..6af2561c89 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -78,13 +78,13 @@ namespace osu.Game.Online.Chat { result.Text = result.Text.Remove(index, m.Length).Insert(index, displayText); - //since we just changed the line display text, offset any already processed links. + // since we just changed the line display text, offset any already processed links. result.Links.ForEach(l => l.Index -= l.Index > index ? m.Length - displayText.Length : 0); var details = GetLinkDetails(linkText); result.Links.Add(new Link(linkText, index, displayText.Length, linkActionOverride ?? details.Action, details.Argument)); - //adjust the offset for processing the current matches group. + // adjust the offset for processing the current matches group. captureOffset += m.Length - displayText.Length; } } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 8e62819c95..fdc8d94352 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -609,7 +609,7 @@ namespace osu.Game loadComponentSingleFile(screenshotManager, Add); - //overlay elements + // overlay elements loadComponentSingleFile(beatmapListing = new BeatmapListingOverlay(), overlayContent.Add, true); loadComponentSingleFile(dashboard = new DashboardOverlay(), overlayContent.Add, true); var rankingsOverlay = loadComponentSingleFile(new RankingsOverlay(), overlayContent.Add, true); @@ -781,7 +781,7 @@ namespace osu.Game { var previousLoadStream = asyncLoadStream; - //chain with existing load stream + // chain with existing load stream asyncLoadStream = Task.Run(async () => { if (previousLoadStream != null) diff --git a/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs b/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs index 84d35da096..28c36e6c56 100644 --- a/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs +++ b/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs @@ -35,7 +35,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels : base(beatmap) { Width = 380; - Height = 140 + vertical_padding; //full height of all the elements plus vertical padding (autosize uses the image) + Height = 140 + vertical_padding; // full height of all the elements plus vertical padding (autosize uses the image) } protected override void LoadComplete() diff --git a/osu.Game/Overlays/BeatmapSet/Header.cs b/osu.Game/Overlays/BeatmapSet/Header.cs index 17fa689cd2..1ff08aab2c 100644 --- a/osu.Game/Overlays/BeatmapSet/Header.cs +++ b/osu.Game/Overlays/BeatmapSet/Header.cs @@ -140,7 +140,7 @@ namespace osu.Game.Overlays.BeatmapSet { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - Margin = new MarginPadding { Left = 3, Bottom = 4 }, //To better lineup with the font + Margin = new MarginPadding { Left = 3, Bottom = 4 }, // To better lineup with the font }, } }, diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index 34afc3c431..5ba55f6d45 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -358,7 +358,7 @@ namespace osu.Game.Overlays protected override void OnFocus(FocusEvent e) { - //this is necessary as textbox is masked away and therefore can't get focus :( + // this is necessary as textbox is masked away and therefore can't get focus :( textbox.TakeFocus(); base.OnFocus(e); } diff --git a/osu.Game/Overlays/DialogOverlay.cs b/osu.Game/Overlays/DialogOverlay.cs index 59d748bc5d..9f9dbdbaf1 100644 --- a/osu.Game/Overlays/DialogOverlay.cs +++ b/osu.Game/Overlays/DialogOverlay.cs @@ -50,7 +50,7 @@ namespace osu.Game.Overlays { if (v != Visibility.Hidden) return; - //handle the dialog being dismissed. + // handle the dialog being dismissed. dialog.Delay(PopupDialog.EXIT_DURATION).Expire(); if (dialog == CurrentDialog) diff --git a/osu.Game/Overlays/Music/PlaylistItem.cs b/osu.Game/Overlays/Music/PlaylistItem.cs index de2f916946..840fa51b4f 100644 --- a/osu.Game/Overlays/Music/PlaylistItem.cs +++ b/osu.Game/Overlays/Music/PlaylistItem.cs @@ -78,7 +78,7 @@ namespace osu.Game.Overlays.Music { text.Clear(); - //space after the title to put a space between the title and artist + // space after the title to put a space between the title and artist titleSprites = text.AddText(title.Value + @" ", sprite => sprite.Font = OsuFont.GetFont(weight: FontWeight.Regular)).OfType(); text.AddText(artist.Value, sprite => diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index c872f82b32..ded641b262 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -250,7 +250,7 @@ namespace osu.Game.Overlays } else { - //figure out the best direction based on order in playlist. + // figure out the best direction based on order in playlist. var last = BeatmapSets.TakeWhile(b => b.ID != current.BeatmapSetInfo?.ID).Count(); var next = beatmap.NewValue == null ? -1 : BeatmapSets.TakeWhile(b => b.ID != beatmap.NewValue.BeatmapSetInfo?.ID).Count(); diff --git a/osu.Game/Overlays/News/NewsArticleCover.cs b/osu.Game/Overlays/News/NewsArticleCover.cs index cca0cfb4a0..e3f5a8cea3 100644 --- a/osu.Game/Overlays/News/NewsArticleCover.cs +++ b/osu.Game/Overlays/News/NewsArticleCover.cs @@ -162,7 +162,7 @@ namespace osu.Game.Overlays.News public string TooltipText => date.ToString("dddd dd MMMM yyyy hh:mm:ss UTCz").ToUpper(); } - //fake API data struct to use for now as a skeleton for data, as there is no API struct for news article info for now + // fake API data struct to use for now as a skeleton for data, as there is no API struct for news article info for now public class ArticleInfo { public string Title { get; set; } diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs index 99836705c4..3105ecd742 100644 --- a/osu.Game/Overlays/Notifications/ProgressNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs @@ -39,7 +39,7 @@ namespace osu.Game.Overlays.Notifications { base.LoadComplete(); - //we may have received changes before we were displayed. + // we may have received changes before we were displayed. updateState(); } diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index 118cb037cb..ebb4a96d14 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -261,7 +261,7 @@ namespace osu.Game.Overlays // todo: this can likely be replaced with WorkingBeatmap.GetBeatmapAsync() Task.Run(() => { - if (beatmap?.Beatmap == null) //this is not needed if a placeholder exists + if (beatmap?.Beatmap == null) // this is not needed if a placeholder exists { title.Text = @"Nothing to play"; artist.Text = @"Nothing to play"; diff --git a/osu.Game/Overlays/OSD/Toast.cs b/osu.Game/Overlays/OSD/Toast.cs index 5d36cac20e..1497ca8fa8 100644 --- a/osu.Game/Overlays/OSD/Toast.cs +++ b/osu.Game/Overlays/OSD/Toast.cs @@ -31,7 +31,7 @@ namespace osu.Game.Overlays.OSD InternalChildren = new Drawable[] { - new Container //this container exists just to set a minimum width for the toast + new Container // this container exists just to set a minimum width for the toast { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Overlays/Profile/Header/Components/OverlinedInfoContainer.cs b/osu.Game/Overlays/Profile/Header/Components/OverlinedInfoContainer.cs index b11e41f90f..9f56a34aa6 100644 --- a/osu.Game/Overlays/Profile/Header/Components/OverlinedInfoContainer.cs +++ b/osu.Game/Overlays/Profile/Header/Components/OverlinedInfoContainer.cs @@ -54,7 +54,7 @@ namespace osu.Game.Overlays.Profile.Header.Components { Font = OsuFont.GetFont(size: big ? 40 : 18, weight: FontWeight.Light) }, - new Container //Add a minimum size to the FillFlowContainer + new Container // Add a minimum size to the FillFlowContainer { Width = minimumWidth, } diff --git a/osu.Game/Overlays/Profile/Header/MedalHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/MedalHeaderContainer.cs index a5938a3fe7..e7df4eb5eb 100644 --- a/osu.Game/Overlays/Profile/Header/MedalHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/MedalHeaderContainer.cs @@ -35,7 +35,7 @@ namespace osu.Game.Overlays.Profile.Header RelativeSizeAxes = Axes.Both, Colour = colourProvider.Background5, }, - new Container //artificial shadow + new Container // artificial shadow { RelativeSizeAxes = Axes.X, Height = 3, diff --git a/osu.Game/Overlays/SearchableList/SearchableListFilterControl.cs b/osu.Game/Overlays/SearchableList/SearchableListFilterControl.cs index 117f905de4..d31470e685 100644 --- a/osu.Game/Overlays/SearchableList/SearchableListFilterControl.cs +++ b/osu.Game/Overlays/SearchableList/SearchableListFilterControl.cs @@ -94,7 +94,7 @@ namespace osu.Game.Overlays.SearchableList RelativeSizeAxes = Axes.X, }, }, - new Box //keep the tab strip part of autosize, but don't put it in the flow container + new Box // keep the tab strip part of autosize, but don't put it in the flow container { RelativeSizeAxes = Axes.X, Height = 1, diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index d6b810366d..3d66d3c28e 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -110,7 +110,7 @@ namespace osu.Game.Overlays.Toolbar tooltipContainer = new FillFlowContainer { Direction = FillDirection.Vertical, - RelativeSizeAxes = Axes.Both, //stops us being considered in parent's autosize + RelativeSizeAxes = Axes.Both, // stops us being considered in parent's autosize Anchor = TooltipAnchor.HasFlag(Anchor.x0) ? Anchor.BottomLeft : Anchor.BottomRight, Origin = TooltipAnchor, Position = new Vector2(TooltipAnchor.HasFlag(Anchor.x0) ? 5 : -5, 5), diff --git a/osu.Game/Overlays/VolumeOverlay.cs b/osu.Game/Overlays/VolumeOverlay.cs index b484921cce..676d2c941a 100644 --- a/osu.Game/Overlays/VolumeOverlay.cs +++ b/osu.Game/Overlays/VolumeOverlay.cs @@ -58,7 +58,7 @@ namespace osu.Game.Overlays { volumeMeterEffect = new VolumeMeter("EFFECTS", 125, colours.BlueDarker) { - Margin = new MarginPadding { Top = 100 + MuteButton.HEIGHT } //to counter the mute button and re-center the volume meters + Margin = new MarginPadding { Top = 100 + MuteButton.HEIGHT } // to counter the mute button and re-center the volume meters }, volumeMeterMaster = new VolumeMeter("MASTER", 150, colours.PinkDarker), volumeMeterMusic = new VolumeMeter("MUSIC", 125, colours.BlueDarker), diff --git a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs index 7e17396fde..55d82c4083 100644 --- a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs +++ b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs @@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Replays { int newFrame = nextFrameIndex; - //ensure we aren't at an extent. + // ensure we aren't at an extent. if (newFrame == currentFrameIndex) return false; currentFrameIndex = newFrame; @@ -99,8 +99,8 @@ namespace osu.Game.Rulesets.Replays if (frame == null) return false; - return IsImportant(frame) && //a button is in a pressed state - Math.Abs(CurrentTime - NextFrame?.Time ?? 0) <= AllowedImportantTimeSpan; //the next frame is within an allowable time span + return IsImportant(frame) && // a button is in a pressed state + Math.Abs(CurrentTime - NextFrame?.Time ?? 0) <= AllowedImportantTimeSpan; // the next frame is within an allowable time span } } diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index 543134cfb4..f302f8700f 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -81,7 +81,7 @@ namespace osu.Game.Rulesets var instances = loadedAssemblies.Values.Select(r => (Ruleset)Activator.CreateInstance(r)).ToList(); - //add all legacy rulesets first to ensure they have exclusive choice of primary key. + // add all legacy rulesets first to ensure they have exclusive choice of primary key. foreach (var r in instances.Where(r => r is ILegacyRuleset)) { if (context.RulesetInfo.SingleOrDefault(dbRuleset => dbRuleset.ID == r.RulesetInfo.ID) == null) @@ -90,7 +90,7 @@ namespace osu.Game.Rulesets context.SaveChanges(); - //add any other modes + // add any other modes foreach (var r in instances.Where(r => !(r is ILegacyRuleset))) { if (context.RulesetInfo.FirstOrDefault(ri => ri.InstantiationInfo == r.RulesetInfo.InstantiationInfo) == null) @@ -99,7 +99,7 @@ namespace osu.Game.Rulesets context.SaveChanges(); - //perform a consistency check + // perform a consistency check foreach (var r in context.RulesetInfo) { try diff --git a/osu.Game/Screens/BackgroundScreen.cs b/osu.Game/Screens/BackgroundScreen.cs index 5dfaceccf5..0f3615b7a9 100644 --- a/osu.Game/Screens/BackgroundScreen.cs +++ b/osu.Game/Screens/BackgroundScreen.cs @@ -30,7 +30,7 @@ namespace osu.Game.Screens protected override bool OnKeyDown(KeyDownEvent e) { - //we don't want to handle escape key. + // we don't want to handle escape key. return false; } diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index 736202ee52..0d5f3d1142 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -101,7 +101,7 @@ namespace osu.Game.Screens.Menu this.FadeIn(300); double fadeOutTime = exit_delay; - //we also handle the exit transition. + // we also handle the exit transition. if (MenuVoice.Value) seeya.Play(); else diff --git a/osu.Game/Screens/Menu/LogoVisualisation.cs b/osu.Game/Screens/Menu/LogoVisualisation.cs index 67537fa9df..0db7f2a2dc 100644 --- a/osu.Game/Screens/Menu/LogoVisualisation.cs +++ b/osu.Game/Screens/Menu/LogoVisualisation.cs @@ -162,7 +162,7 @@ namespace osu.Game.Screens.Menu private IShader shader; private Texture texture; - //Assuming the logo is a circle, we don't need a second dimension. + // Assuming the logo is a circle, we don't need a second dimension. private float size; private Color4 colour; @@ -209,13 +209,13 @@ namespace osu.Game.Screens.Menu float rotation = MathUtils.DegreesToRadians(i / (float)bars_per_visualiser * 360 + j * 360 / visualiser_rounds); float rotationCos = MathF.Cos(rotation); float rotationSin = MathF.Sin(rotation); - //taking the cos and sin to the 0..1 range + // taking the cos and sin to the 0..1 range var barPosition = new Vector2(rotationCos / 2 + 0.5f, rotationSin / 2 + 0.5f) * size; var barSize = new Vector2(size * MathF.Sqrt(2 * (1 - MathF.Cos(MathUtils.DegreesToRadians(360f / bars_per_visualiser)))) / 2f, bar_length * audioData[i]); - //The distance between the position and the sides of the bar. + // The distance between the position and the sides of the bar. var bottomOffset = new Vector2(-rotationSin * barSize.X / 2, rotationCos * barSize.X / 2); - //The distance between the bottom side of the bar and the top side. + // The distance between the bottom side of the bar and the top side. var amplitudeOffset = new Vector2(rotationCos * barSize.Y, rotationSin * barSize.Y); var rectangle = new Quad( @@ -231,7 +231,7 @@ namespace osu.Game.Screens.Menu colourInfo, null, vertexBatch.AddAction, - //barSize by itself will make it smooth more in the X axis than in the Y axis, this reverts that. + // barSize by itself will make it smooth more in the X axis than in the Y axis, this reverts that. Vector2.Divide(inflation, barSize.Yx)); } } diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 0589e4d12b..f0da2482d6 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -250,7 +250,7 @@ namespace osu.Game.Screens.Menu (Background as BackgroundScreenDefault)?.Next(); - //we may have consumed our preloaded instance, so let's make another. + // we may have consumed our preloaded instance, so let's make another. preloadSongSelect(); if (Beatmap.Value.Track != null && music?.IsUserPaused != true) diff --git a/osu.Game/Screens/Play/BreakOverlay.cs b/osu.Game/Screens/Play/BreakOverlay.cs index c978f4e96d..36f825b8f6 100644 --- a/osu.Game/Screens/Play/BreakOverlay.cs +++ b/osu.Game/Screens/Play/BreakOverlay.cs @@ -119,7 +119,7 @@ namespace osu.Game.Screens.Play FinishTransforms(true); Scheduler.CancelDelayedTasks(); - if (breaks == null) return; //we need breaks. + if (breaks == null) return; // we need breaks. foreach (var b in breaks) { diff --git a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs index d201b5d30e..fc80983834 100644 --- a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs +++ b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs @@ -50,7 +50,7 @@ namespace osu.Game.Screens.Play.HUD protected override void PopIn() => this.FadeIn(fade_duration); protected override void PopOut() => this.FadeOut(fade_duration); - //We want to handle keyboard inputs all the time in order to trigger ToggleVisibility() when not visible + // We want to handle keyboard inputs all the time in order to trigger ToggleVisibility() when not visible public override bool PropagateNonPositionalInputSubTree => true; protected override bool OnKeyDown(KeyDownEvent e) diff --git a/osu.Game/Screens/Play/KeyCounter.cs b/osu.Game/Screens/Play/KeyCounter.cs index f4109a63d0..98df73a5e6 100644 --- a/osu.Game/Screens/Play/KeyCounter.cs +++ b/osu.Game/Screens/Play/KeyCounter.cs @@ -124,8 +124,8 @@ namespace osu.Game.Screens.Play } } }; - //Set this manually because an element with Alpha=0 won't take it size to AutoSizeContainer, - //so the size can be changing between buttonSprite and glowSprite. + // Set this manually because an element with Alpha=0 won't take it size to AutoSizeContainer, + // so the size can be changing between buttonSprite and glowSprite. Height = buttonSprite.DrawHeight; Width = buttonSprite.DrawWidth; } diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index c0d88feda2..93a734589c 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -314,8 +314,8 @@ namespace osu.Game.Screens.Play LoadTask = null; - //By default, we want to load the player and never be returned to. - //Note that this may change if the player we load requested a re-run. + // By default, we want to load the player and never be returned to. + // Note that this may change if the player we load requested a re-run. ValidForResume = false; if (player.LoadedBeatmapSuccessfully) @@ -360,7 +360,7 @@ namespace osu.Game.Screens.Play { if (!muteWarningShownOnce.Value) { - //Checks if the notification has not been shown yet and also if master volume is muted, track/music volume is muted or if the whole game is muted. + // Checks if the notification has not been shown yet and also if master volume is muted, track/music volume is muted or if the whole game is muted. if (volumeOverlay?.IsMuted.Value == true || audioManager.Volume.Value <= audioManager.Volume.MinValue || audioManager.VolumeTrack.Value <= audioManager.VolumeTrack.MinValue) { notificationOverlay?.Post(new MutedNotification()); diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 5a4a03662a..96b779cd20 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -208,7 +208,7 @@ namespace osu.Game.Screens.Select // without this, during a large beatmap import it is impossible to navigate the carousel. applyActiveCriteria(false, alwaysResetScrollPosition: false); - //check if we can/need to maintain our current selection. + // check if we can/need to maintain our current selection. if (previouslySelectedID != null) select((CarouselItem)newSet.Beatmaps.FirstOrDefault(b => b.Beatmap.ID == previouslySelectedID) ?? newSet); diff --git a/osu.Game/Screens/Select/BeatmapDetailAreaTabControl.cs b/osu.Game/Screens/Select/BeatmapDetailAreaTabControl.cs index f4bf1ab059..63711e3e50 100644 --- a/osu.Game/Screens/Select/BeatmapDetailAreaTabControl.cs +++ b/osu.Game/Screens/Select/BeatmapDetailAreaTabControl.cs @@ -26,7 +26,7 @@ namespace osu.Game.Screens.Select set => tabs.Current = value; } - public Action OnFilter; //passed the selected tab and if mods is checked + public Action OnFilter; // passed the selected tab and if mods is checked public IReadOnlyList TabItems { diff --git a/osu.Game/Screens/Select/BeatmapDetails.cs b/osu.Game/Screens/Select/BeatmapDetails.cs index aebb8e9d87..9669a1391c 100644 --- a/osu.Game/Screens/Select/BeatmapDetails.cs +++ b/osu.Game/Screens/Select/BeatmapDetails.cs @@ -201,7 +201,7 @@ namespace osu.Game.Screens.Select Schedule(() => { if (beatmap != requestedBeatmap) - //the beatmap has been changed since we started the lookup. + // the beatmap has been changed since we started the lookup. return; var b = res.ToBeatmap(rulesets); @@ -222,7 +222,7 @@ namespace osu.Game.Screens.Select Schedule(() => { if (beatmap != requestedBeatmap) - //the beatmap has been changed since we started the lookup. + // the beatmap has been changed since we started the lookup. return; updateMetrics(); diff --git a/osu.Game/Screens/Select/Details/AdvancedStats.cs b/osu.Game/Screens/Select/Details/AdvancedStats.cs index af0d36ea9a..02822ea608 100644 --- a/osu.Game/Screens/Select/Details/AdvancedStats.cs +++ b/osu.Game/Screens/Select/Details/AdvancedStats.cs @@ -52,7 +52,7 @@ namespace osu.Game.Screens.Select.Details AutoSizeAxes = Axes.Y, Children = new[] { - FirstValue = new StatisticRow(), //circle size/key amount + FirstValue = new StatisticRow(), // circle size/key amount HpDrain = new StatisticRow { Title = "HP Drain" }, Accuracy = new StatisticRow { Title = "Accuracy" }, ApproachRate = new StatisticRow { Title = "Approach Rate" }, diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index a7e27c27ba..5b7c9082f3 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -798,7 +798,7 @@ namespace osu.Game.Screens.Select Masking = true; Anchor = Anchor.Centre; Origin = Anchor.Centre; - Width = panel_overflow; //avoid horizontal masking so the panels don't clip when screen stack is pushed. + Width = panel_overflow; // avoid horizontal masking so the panels don't clip when screen stack is pushed. InternalChild = Content = new Container { RelativeSizeAxes = Axes.Both, From aff74db80da54a35a438a4e6443569107e5403a1 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Tue, 5 May 2020 10:40:10 +0200 Subject: [PATCH 1440/2376] Publicly expose HUDOverlay in Player. --- 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 ece4c6307e..af724d97a2 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -89,7 +89,7 @@ namespace osu.Game.Screens.Play protected DrawableRuleset DrawableRuleset { get; private set; } - protected HUDOverlay HUDOverlay { get; private set; } + public HUDOverlay HUDOverlay { get; private set; } public bool LoadedBeatmapSuccessfully => DrawableRuleset?.Objects.Any() == true; From 7781408643a682358c463077bf76a6d35a4bc1c7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 May 2020 18:27:10 +0900 Subject: [PATCH 1441/2376] Update in line with framework storage changes --- osu.Desktop/OsuGameDesktop.cs | 78 +++++++++++------------ osu.Game.Tournament/IPC/FileBasedIPC.cs | 84 +++++++++++-------------- osu.Game/IO/OsuStorage.cs | 26 ++++++++ osu.Game/IO/WrappedStorage.cs | 80 +++++++++++++++++++++++ osu.Game/OsuGameBase.cs | 26 +------- 5 files changed, 179 insertions(+), 115 deletions(-) create mode 100644 osu.Game/IO/OsuStorage.cs create mode 100644 osu.Game/IO/WrappedStorage.cs diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index f05ee48914..9351e17419 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -6,15 +6,14 @@ using System.IO; using System.Linq; using System.Reflection; using System.Threading.Tasks; +using Microsoft.Win32; using osu.Desktop.Overlays; using osu.Framework.Platform; using osu.Game; using osuTK.Input; -using Microsoft.Win32; using osu.Desktop.Updater; using osu.Framework; using osu.Framework.Logging; -using osu.Framework.Platform.Windows; using osu.Framework.Screens; using osu.Game.Screens.Menu; using osu.Game.Updater; @@ -37,7 +36,11 @@ namespace osu.Desktop try { if (Host is DesktopGameHost desktopHost) - return new StableStorage(desktopHost); + { + string stablePath = getStableInstallPath(); + if (!string.IsNullOrEmpty(stablePath)) + return new DesktopStorage(stablePath, desktopHost); + } } catch (Exception) { @@ -47,6 +50,35 @@ namespace osu.Desktop return null; } + private string getStableInstallPath() + { + static bool checkExists(string p) => Directory.Exists(Path.Combine(p, "Songs")); + + string stableInstallPath; + + try + { + using (RegistryKey key = Registry.ClassesRoot.OpenSubKey("osu")) + stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty).ToString().Split('"')[1].Replace("osu!.exe", ""); + + if (checkExists(stableInstallPath)) + return stableInstallPath; + } + catch + { + } + + stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"osu!"); + if (checkExists(stableInstallPath)) + return stableInstallPath; + + stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".osu"); + if (checkExists(stableInstallPath)) + return stableInstallPath; + + return null; + } + protected override UpdateManager CreateUpdateManager() { switch (RuntimeInfo.OS) @@ -111,45 +143,5 @@ namespace osu.Desktop Task.Factory.StartNew(() => Import(filePaths), TaskCreationOptions.LongRunning); } - - /// - /// A method of accessing an osu-stable install in a controlled fashion. - /// - private class StableStorage : WindowsStorage - { - protected override string LocateBasePath() - { - static bool checkExists(string p) => Directory.Exists(Path.Combine(p, "Songs")); - - string stableInstallPath; - - try - { - using (RegistryKey key = Registry.ClassesRoot.OpenSubKey("osu")) - stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty).ToString().Split('"')[1].Replace("osu!.exe", ""); - - if (checkExists(stableInstallPath)) - return stableInstallPath; - } - catch - { - } - - stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"osu!"); - if (checkExists(stableInstallPath)) - return stableInstallPath; - - stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".osu"); - if (checkExists(stableInstallPath)) - return stableInstallPath; - - return null; - } - - public StableStorage(DesktopGameHost host) - : base(string.Empty, host) - { - } - } } } diff --git a/osu.Game.Tournament/IPC/FileBasedIPC.cs b/osu.Game.Tournament/IPC/FileBasedIPC.cs index eefa9fcfe6..53ba597a7e 100644 --- a/osu.Game.Tournament/IPC/FileBasedIPC.cs +++ b/osu.Game.Tournament/IPC/FileBasedIPC.cs @@ -8,7 +8,6 @@ using Microsoft.Win32; using osu.Framework.Allocation; using osu.Framework.Logging; using osu.Framework.Platform; -using osu.Framework.Platform.Windows; using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Legacy; @@ -52,7 +51,12 @@ namespace osu.Game.Tournament.IPC try { - Storage = new StableStorage(host as DesktopGameHost); + var path = findStablePath(); + + if (string.IsNullOrEmpty(path)) + return null; + + Storage = new DesktopStorage(path, host as DesktopGameHost); const string file_ipc_filename = "ipc.txt"; const string file_ipc_state_filename = "ipc-state.txt"; @@ -145,64 +149,50 @@ namespace osu.Game.Tournament.IPC return Storage; } - /// - /// A method of accessing an osu-stable install in a controlled fashion. - /// - private class StableStorage : WindowsStorage + private string findStablePath() { - protected override string LocateBasePath() - { - static bool checkExists(string p) - { - return File.Exists(Path.Combine(p, "ipc.txt")); - } + static bool checkExists(string p) => File.Exists(Path.Combine(p, "ipc.txt")); - string stableInstallPath = string.Empty; + string stableInstallPath = string.Empty; + + try + { + try + { + stableInstallPath = Environment.GetEnvironmentVariable("OSU_STABLE_PATH"); + + if (checkExists(stableInstallPath)) + return stableInstallPath; + } + catch + { + } try { - try - { - stableInstallPath = Environment.GetEnvironmentVariable("OSU_STABLE_PATH"); + using (RegistryKey key = Registry.ClassesRoot.OpenSubKey("osu")) + stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty).ToString().Split('"')[1].Replace("osu!.exe", ""); - if (checkExists(stableInstallPath)) - return stableInstallPath; - } - catch - { - } - - try - { - using (RegistryKey key = Registry.ClassesRoot.OpenSubKey("osu")) - stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty).ToString().Split('"')[1].Replace("osu!.exe", ""); - - if (checkExists(stableInstallPath)) - return stableInstallPath; - } - catch - { - } - - stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"osu!"); if (checkExists(stableInstallPath)) return stableInstallPath; - - stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".osu"); - if (checkExists(stableInstallPath)) - return stableInstallPath; - - return null; } - finally + catch { - Logger.Log($"Stable path for tourney usage: {stableInstallPath}"); } - } - public StableStorage(DesktopGameHost host) - : base(string.Empty, host) + stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"osu!"); + if (checkExists(stableInstallPath)) + return stableInstallPath; + + stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".osu"); + if (checkExists(stableInstallPath)) + return stableInstallPath; + + return null; + } + finally { + Logger.Log($"Stable path for tourney usage: {stableInstallPath}"); } } } diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs new file mode 100644 index 0000000000..ee42c491d1 --- /dev/null +++ b/osu.Game/IO/OsuStorage.cs @@ -0,0 +1,26 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Game.Configuration; + +namespace osu.Game.IO +{ + public class OsuStorage : WrappedStorage + { + public OsuStorage(GameHost host) + : base(host.Storage, string.Empty) + { + var storageConfig = new StorageConfigManager(host.Storage); + + var customStoragePath = storageConfig.Get(StorageConfig.FullPath); + + if (!string.IsNullOrEmpty(customStoragePath)) + { + ChangeTargetStorage(host.GetStorage(customStoragePath)); + Logger.Storage = UnderlyingStorage.GetStorageForDirectory("logs"); + } + } + } +} diff --git a/osu.Game/IO/WrappedStorage.cs b/osu.Game/IO/WrappedStorage.cs new file mode 100644 index 0000000000..705bbf6840 --- /dev/null +++ b/osu.Game/IO/WrappedStorage.cs @@ -0,0 +1,80 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.IO; +using osu.Framework.Platform; + +namespace osu.Game.IO +{ + /// + /// A storage which wraps another storage and delegates implementation, potentially mutating the lookup path. + /// + public class WrappedStorage : Storage + { + protected Storage UnderlyingStorage { get; private set; } + + private readonly string subPath; + + public WrappedStorage(Storage underlyingStorage, string subPath = null) + : base(string.Empty) + { + ChangeTargetStorage(underlyingStorage); + + this.subPath = subPath; + } + + protected virtual string MutatePath(string path) => !string.IsNullOrEmpty(subPath) ? Path.Combine(subPath, path) : path; + + protected void ChangeTargetStorage(Storage newStorage) + { + UnderlyingStorage = newStorage; + } + + public override string GetFullPath(string path, bool createIfNotExisting = false) => + UnderlyingStorage.GetFullPath(MutatePath(path), createIfNotExisting); + + public override bool Exists(string path) => + UnderlyingStorage.Exists(MutatePath(path)); + + public override bool ExistsDirectory(string path) => + UnderlyingStorage.ExistsDirectory(MutatePath(path)); + + public override void DeleteDirectory(string path) => + UnderlyingStorage.DeleteDirectory(MutatePath(path)); + + public override void Delete(string path) => + UnderlyingStorage.Delete(MutatePath(path)); + + public override IEnumerable GetDirectories(string path) => + UnderlyingStorage.GetDirectories(MutatePath(path)); + + public override IEnumerable GetFiles(string path, string pattern = "*") => + UnderlyingStorage.GetFiles(MutatePath(path), pattern); + + 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 OpenInNativeExplorer() => UnderlyingStorage.OpenInNativeExplorer(); + + public override Storage GetStorageForDirectory(string path) + { + if (string.IsNullOrEmpty(path)) + throw new ArgumentException("Must be non-null and not empty string", nameof(path)); + + if (!path.EndsWith(Path.DirectorySeparatorChar)) + path += Path.DirectorySeparatorChar; + + // create non-existing path. + GetFullPath(path, true); + + return new WrappedStorage(this, path); + } + } +} diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index f92db4e111..d9f9e2de42 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -71,8 +71,6 @@ namespace osu.Game protected MenuCursorContainer MenuCursorContainer; - protected StorageConfigManager StorageConfig; - private Container content; protected override Container Content => content; @@ -304,17 +302,7 @@ namespace osu.Game { base.SetHost(host); - StorageConfig = new StorageConfigManager(host.Storage); - - var customStoragePath = StorageConfig.Get(Configuration.StorageConfig.FullPath); - - if (!string.IsNullOrEmpty(customStoragePath)) - { - Storage = new CustomStorage(customStoragePath, host); - Logger.Storage = Storage.GetStorageForDirectory("logs"); - } - else - Storage = host.Storage; + Storage = new OsuStorage(host); if (LocalConfig == null) LocalConfig = new OsuConfigManager(Storage); @@ -366,17 +354,5 @@ namespace osu.Game public override bool ChangeFocusOnClick => false; } } - - /// - /// A storage pointing to an absolute location specified by the user to store game data files. - /// - private class CustomStorage : NativeStorage - { - public CustomStorage(string fullPath, GameHost host) - : base(string.Empty, host) - { - BasePath = fullPath; - } - } } } From ed83ac188e20c2b9e6e00de7083b90e1974dc9ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 6 May 2020 23:25:25 +0200 Subject: [PATCH 1442/2376] Remove special case for moving catcher sprite --- .../TestSceneHyperDashColouring.cs | 12 +----------- osu.Game.Rulesets.Catch/UI/Catcher.cs | 17 +---------------- 2 files changed, 2 insertions(+), 27 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs index 589bafe400..1e708cce4b 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs @@ -128,17 +128,7 @@ namespace osu.Game.Rulesets.Catch.Tests catcherArea.MovableCatcher.SetHyperDashState(2); }); - AddUntilStep("catcher colour is correct", () => - { - var expected = expectedCatcherColour; - - if (expected == Catcher.DEFAULT_HYPER_DASH_COLOUR) - // The expected colour for Catcher.Colour is another colour - // for the default skin, assert with that instead. - expected = Catcher.DEFAULT_CATCHER_HYPER_DASH_COLOUR; - - return catcherArea.MovableCatcher.Colour == expected; - }); + AddUntilStep("catcher colour is correct", () => catcherArea.MovableCatcher.Colour == expectedCatcherColour); AddAssert("catcher trails colours are correct", () => trails.HyperDashTrailsColour == expectedCatcherColour); AddAssert("catcher end-glow colours are correct", () => trails.EndGlowSpritesColour == (expectedEndGlowColour ?? expectedCatcherColour)); diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 2022cffb40..520cfeee70 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -28,15 +28,6 @@ namespace osu.Game.Rulesets.Catch.UI ///
  • public static readonly Color4 DEFAULT_HYPER_DASH_COLOUR = Color4.Red; - /// - /// The default hyper-dash colour used directly for this - /// 's . - /// - /// - /// This colour is only used when no skin overrides . - /// - public static readonly Color4 DEFAULT_CATCHER_HYPER_DASH_COLOUR = Color4.OrangeRed; - /// /// The duration between transitioning to hyper-dash state. /// @@ -288,13 +279,7 @@ namespace osu.Game.Rulesets.Catch.UI { if (hyperDashing) { - // special behaviour for catcher colour if no skin overrides. - var catcherColour = - hyperDashColour == DEFAULT_HYPER_DASH_COLOUR - ? DEFAULT_CATCHER_HYPER_DASH_COLOUR - : hyperDashColour; - - this.FadeColour(catcherColour, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint); + this.FadeColour(hyperDashColour, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint); this.FadeTo(0.2f, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint); } else From 52d1e2b5f889dd193aad2c10b0f3b40f87e24f8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 6 May 2020 23:27:01 +0200 Subject: [PATCH 1443/2376] Improve xmldoc --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 520cfeee70..40c7d6a9b5 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -24,7 +24,8 @@ namespace osu.Game.Rulesets.Catch.UI public class Catcher : SkinReloadableDrawable, IKeyBindingHandler { /// - /// The default colour used for all hyper-dashing components. (catcher drawables and fruit) + /// The default colour used to tint hyper-dash fruit, along with the moving catcher, its trail + /// and end glow/after-image during a hyper-dash. /// public static readonly Color4 DEFAULT_HYPER_DASH_COLOUR = Color4.Red; From 25f73c0b9f30d2c3dc129d02f0bd8a61e738374c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 6 May 2020 23:40:36 +0200 Subject: [PATCH 1444/2376] Add [NotNull] annotation --- osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs b/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs index afbfac9a51..64fb4b2196 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using JetBrains.Annotations; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Containers; @@ -73,7 +74,7 @@ namespace osu.Game.Rulesets.Catch.UI } } - public CatcherTrailDisplay(Catcher catcher) + public CatcherTrailDisplay([NotNull] Catcher catcher) { this.catcher = catcher ?? throw new ArgumentNullException(nameof(catcher)); From b44a70ef9ab7240d3a9a285654996889e8e67910 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 7 May 2020 01:46:37 +0300 Subject: [PATCH 1445/2376] Let the catcher be responsible for stopping the trails --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 2 +- osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 40c7d6a9b5..558555af96 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -74,7 +74,7 @@ namespace osu.Game.Rulesets.Catch.UI dashing = value; - trails.DisplayTrail |= dashing; + trails.DisplayTrail = value || HyperDashing; } } diff --git a/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs b/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs index 64fb4b2196..bab3cb748b 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs @@ -103,11 +103,8 @@ namespace osu.Game.Rulesets.Catch.UI private void displayTrail() { - if (!catcher.Dashing && !catcher.HyperDashing) - { - DisplayTrail = false; + if (!DisplayTrail) return; - } var sprite = createTrailSprite(catcher.HyperDashing ? hyperDashTrails : dashTrails); From 5186da8412ded39b4216cab60c0858fe05b42f71 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 7 May 2020 11:37:04 +0900 Subject: [PATCH 1446/2376] Fix potential song select nullref --- osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index 6d760df065..1e4f6aeda1 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -61,7 +61,7 @@ namespace osu.Game.Screens.Select.Carousel terms.Add(Beatmap.Version); foreach (var criteriaTerm in criteria.SearchTerms) - match &= terms.Any(term => term.IndexOf(criteriaTerm, StringComparison.InvariantCultureIgnoreCase) >= 0); + match &= terms.Any(term => term?.IndexOf(criteriaTerm, StringComparison.InvariantCultureIgnoreCase) >= 0); } Filtered.Value = !match; From e91e4a73af89e719e05c1f349836433079a2523e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 7 May 2020 12:22:07 +0900 Subject: [PATCH 1447/2376] Fix catch crashing when finishing maps --- .../Scoring/Legacy/ScoreInfoExtensions.cs | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs b/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs index 9745d1abef..6f73a284a2 100644 --- a/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs +++ b/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs @@ -12,7 +12,7 @@ namespace osu.Game.Scoring.Legacy switch (scoreInfo.Ruleset?.ID ?? scoreInfo.RulesetID) { case 3: - return scoreInfo.Statistics[HitResult.Perfect]; + return getCount(scoreInfo, HitResult.Perfect); } return null; @@ -35,10 +35,10 @@ namespace osu.Game.Scoring.Legacy case 0: case 1: case 3: - return scoreInfo.Statistics[HitResult.Great]; + return getCount(scoreInfo, HitResult.Great); case 2: - return scoreInfo.Statistics[HitResult.Perfect]; + return getCount(scoreInfo, HitResult.Perfect); } return null; @@ -65,10 +65,10 @@ namespace osu.Game.Scoring.Legacy switch (scoreInfo.Ruleset?.ID ?? scoreInfo.RulesetID) { case 3: - return scoreInfo.Statistics[HitResult.Good]; + return getCount(scoreInfo, HitResult.Good); case 2: - return scoreInfo.Statistics[HitResult.SmallTickMiss]; + return getCount(scoreInfo, HitResult.SmallTickMiss); } return null; @@ -94,13 +94,13 @@ namespace osu.Game.Scoring.Legacy { case 0: case 1: - return scoreInfo.Statistics[HitResult.Good]; + return getCount(scoreInfo, HitResult.Good); case 3: - return scoreInfo.Statistics[HitResult.Ok]; + return getCount(scoreInfo, HitResult.Ok); case 2: - return scoreInfo.Statistics[HitResult.LargeTickHit]; + return getCount(scoreInfo, HitResult.LargeTickHit); } return null; @@ -131,10 +131,10 @@ namespace osu.Game.Scoring.Legacy { case 0: case 3: - return scoreInfo.Statistics[HitResult.Meh]; + return getCount(scoreInfo, HitResult.Meh); case 2: - return scoreInfo.Statistics[HitResult.SmallTickHit]; + return getCount(scoreInfo, HitResult.SmallTickHit); } return null; @@ -156,9 +156,17 @@ namespace osu.Game.Scoring.Legacy } public static int? GetCountMiss(this ScoreInfo scoreInfo) => - scoreInfo.Statistics[HitResult.Miss]; + getCount(scoreInfo, HitResult.Miss); public static void SetCountMiss(this ScoreInfo scoreInfo, int value) => scoreInfo.Statistics[HitResult.Miss] = value; + + private static int? getCount(ScoreInfo scoreInfo, HitResult result) + { + if (scoreInfo.Statistics.TryGetValue(result, out var existing)) + return existing; + + return null; + } } } From 401c516503239f341c73396e5539e7b78f2b1078 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 7 May 2020 13:04:08 +0900 Subject: [PATCH 1448/2376] Expose searchable terms from beatmap info instead --- osu.Game/Beatmaps/BeatmapInfo.cs | 5 +++++ osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs | 8 ++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 90c100db05..3860f12baa 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -149,6 +149,11 @@ namespace osu.Game.Beatmaps } } + public string[] SearchableTerms => new[] + { + Version + }.Concat(Metadata?.SearchableTerms ?? Enumerable.Empty()).Where(s => !string.IsNullOrEmpty(s)).ToArray(); + public override string ToString() { string version = string.IsNullOrEmpty(Version) ? string.Empty : $"[{Version}]"; diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index 1e4f6aeda1..ed54c158db 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; using osu.Game.Screens.Select.Filter; @@ -55,13 +54,10 @@ namespace osu.Game.Screens.Select.Carousel if (match) { - var terms = new List(); - - terms.AddRange(Beatmap.Metadata.SearchableTerms); - terms.Add(Beatmap.Version); + var terms = Beatmap.SearchableTerms; foreach (var criteriaTerm in criteria.SearchTerms) - match &= terms.Any(term => term?.IndexOf(criteriaTerm, StringComparison.InvariantCultureIgnoreCase) >= 0); + match &= terms.Any(term => term.IndexOf(criteriaTerm, StringComparison.InvariantCultureIgnoreCase) >= 0); } Filtered.Value = !match; From 259ef688110bfbc6e9eed2f7b8779af8b65bbb3e Mon Sep 17 00:00:00 2001 From: Joehu Date: Wed, 6 May 2020 22:29:37 -0700 Subject: [PATCH 1449/2376] Fix date tooltip not showing in 24-hour format --- osu.Game/Graphics/DrawableDate.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/DrawableDate.cs b/osu.Game/Graphics/DrawableDate.cs index 8c520f4e10..8b6df4a834 100644 --- a/osu.Game/Graphics/DrawableDate.cs +++ b/osu.Game/Graphics/DrawableDate.cs @@ -139,7 +139,7 @@ namespace osu.Game.Graphics return false; dateText.Text = $"{date:d MMMM yyyy} "; - timeText.Text = $"{date:hh:mm:ss \"UTC\"z}"; + timeText.Text = $"{date:HH:mm:ss \"UTC\"z}"; return true; } From 09759565faee8f9f96fbc48aacaa7a8ad7842eb1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 May 2020 14:49:58 +0900 Subject: [PATCH 1450/2376] Add support for 3v3 tournament chroma key layout --- osu.Game.Tournament/Models/LadderInfo.cs | 6 ++ .../Screens/Gameplay/GameplayScreen.cs | 83 +++++++++++++++++-- 2 files changed, 82 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tournament/Models/LadderInfo.cs b/osu.Game.Tournament/Models/LadderInfo.cs index c2e6da9ca5..7794019437 100644 --- a/osu.Game.Tournament/Models/LadderInfo.cs +++ b/osu.Game.Tournament/Models/LadderInfo.cs @@ -32,5 +32,11 @@ namespace osu.Game.Tournament.Models MinValue = 640, MaxValue = 1366, }; + + public Bindable PlayersPerTeam = new BindableInt(4) + { + MinValue = 3, + MaxValue = 4, + }; } } diff --git a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs index 64a5cd6dec..e4e3842369 100644 --- a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs +++ b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs @@ -36,7 +36,7 @@ namespace osu.Game.Tournament.Screens.Gameplay [Resolved] private TournamentMatchChatDisplay chat { get; set; } - private Box chroma; + private Drawable chroma; [BackgroundDependencyLoader] private void load(LadderInfo ladder, MatchIPCInfo ipc, Storage storage) @@ -61,16 +61,30 @@ namespace osu.Game.Tournament.Screens.Gameplay Y = 110, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Children = new Drawable[] + Children = new[] { - chroma = new Box + chroma = new Container { - // chroma key area for stable gameplay - Name = "chroma", Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Height = 512, - Colour = new Color4(0, 255, 0, 255), + Children = new Drawable[] + { + new ChromaArea + { + Name = "Left chroma", + RelativeSizeAxes = Axes.Both, + Width = 0.5f, + }, + new ChromaArea + { + Name = "Right chroma", + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Width = 0.5f, + } + } }, } }, @@ -98,9 +112,15 @@ namespace osu.Game.Tournament.Screens.Gameplay }, new SettingsSlider { - LabelText = "Chroma Width", + LabelText = "Chroma width", Bindable = LadderInfo.ChromaKeyWidth, KeyboardStep = 1, + }, + new SettingsSlider + { + LabelText = "Players per team", + Bindable = LadderInfo.PlayersPerTeam, + KeyboardStep = 1, } } } @@ -201,5 +221,54 @@ namespace osu.Game.Tournament.Screens.Gameplay lastState = state.NewValue; } } + + private class ChromaArea : CompositeDrawable + { + [Resolved] + private LadderInfo ladder { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + // chroma key area for stable gameplay + Colour = new Color4(0, 255, 0, 255); + + ladder.PlayersPerTeam.BindValueChanged(performLayout, true); + } + + private void performLayout(ValueChangedEvent playerCount) + { + switch (playerCount.NewValue) + { + case 3: + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Width = 0.5f, + Height = 0.5f, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + new Box + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Height = 0.5f, + }, + }; + break; + + default: + InternalChild = new Box + { + RelativeSizeAxes = Axes.Both, + }; + break; + } + } + } } } From 836efe3f7c697448b82a1f3b053ef0dd85f3efc0 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Thu, 7 May 2020 08:07:22 +0200 Subject: [PATCH 1451/2376] Initial commit --- osu.Desktop/Updater/SquirrelUpdateManager.cs | 4 +++- .../Settings/Sections/General/UpdateSettings.cs | 13 ++++++++++++- osu.Game/Updater/SimpleUpdateManager.cs | 4 +++- osu.Game/Updater/UpdateManager.cs | 6 ++++++ 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/osu.Desktop/Updater/SquirrelUpdateManager.cs b/osu.Desktop/Updater/SquirrelUpdateManager.cs index ade8460dd7..b287dd6527 100644 --- a/osu.Desktop/Updater/SquirrelUpdateManager.cs +++ b/osu.Desktop/Updater/SquirrelUpdateManager.cs @@ -37,10 +37,12 @@ namespace osu.Desktop.Updater if (game.IsDeployedBuild) { Splat.Locator.CurrentMutable.Register(() => new SquirrelLogger(), typeof(Splat.ILogger)); - Schedule(() => Task.Run(() => checkForUpdateAsync())); + CheckForUpdate(); } } + public override void CheckForUpdate() => Schedule(() => Task.Run(() => checkForUpdateAsync())); + private async void checkForUpdateAsync(bool useDeltaPatching = true, UpdateProgressNotification notification = null) { // should we schedule a retry on completion of this check? diff --git a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs index 188c9c05ef..71deeee693 100644 --- a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs @@ -5,15 +5,19 @@ using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Platform; using osu.Game.Configuration; +using osu.Game.Updater; namespace osu.Game.Overlays.Settings.Sections.General { public class UpdateSettings : SettingsSubsection { + [Resolved(CanBeNull = true)] + private UpdateManager updateManager { get; set; } + protected override string Header => "Updates"; [BackgroundDependencyLoader] - private void load(Storage storage, OsuConfigManager config) + private void load(Storage storage, OsuConfigManager config, OsuGameBase game) { Add(new SettingsEnumDropdown { @@ -21,6 +25,13 @@ namespace osu.Game.Overlays.Settings.Sections.General Bindable = config.GetBindable(OsuSetting.ReleaseStream), }); + Add(new SettingsButton + { + Text = "Check for updates", + Action = () => updateManager?.CheckForUpdate(), + Enabled = { Value = game.IsDeployedBuild } + }); + if (RuntimeInfo.IsDesktop) { Add(new SettingsButton diff --git a/osu.Game/Updater/SimpleUpdateManager.cs b/osu.Game/Updater/SimpleUpdateManager.cs index 1e8a96444f..41248ed796 100644 --- a/osu.Game/Updater/SimpleUpdateManager.cs +++ b/osu.Game/Updater/SimpleUpdateManager.cs @@ -30,9 +30,11 @@ namespace osu.Game.Updater version = game.Version; if (game.IsDeployedBuild) - Schedule(() => Task.Run(checkForUpdateAsync)); + CheckForUpdate(); } + public override void CheckForUpdate() => Schedule(() => Task.Run(checkForUpdateAsync)); + private async void checkForUpdateAsync() { try diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs index 28a295215f..f628bde324 100644 --- a/osu.Game/Updater/UpdateManager.cs +++ b/osu.Game/Updater/UpdateManager.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; +using osu.Framework.Logging; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Overlays; @@ -44,6 +45,11 @@ namespace osu.Game.Updater config.Set(OsuSetting.Version, version); } + public virtual void CheckForUpdate() + { + Logger.Log("CheckForUpdate was called on the base class (UpdateManager)", LoggingTarget.Information); + } + private class UpdateCompleteNotification : SimpleNotification { private readonly string version; From 83be5455d3529707cbac2192e538984fc351eea4 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Thu, 7 May 2020 08:52:36 +0200 Subject: [PATCH 1452/2376] Disable the display of HUD through DisplayHud property. --- osu.Game/Rulesets/UI/DrawableRuleset.cs | 9 +++++++++ osu.Game/Screens/Play/Player.cs | 9 ++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 5062c92afe..ff1f67783e 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -487,6 +487,15 @@ namespace osu.Game.Rulesets.UI protected virtual ResumeOverlay CreateResumeOverlay() => null; + /// + /// Whether to display the HUD with this ruleset. + /// Override to false to completely disable the display of the HUD with this ruleset. + /// + /// + /// HUD refers here to in player as well as . + /// + public virtual bool DisplayHud => true; + /// /// Sets a replay to be used, overriding local input. /// diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index af724d97a2..375976ea6c 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -89,7 +89,7 @@ namespace osu.Game.Screens.Play protected DrawableRuleset DrawableRuleset { get; private set; } - public HUDOverlay HUDOverlay { get; private set; } + protected HUDOverlay HUDOverlay { get; private set; } public bool LoadedBeatmapSuccessfully => DrawableRuleset?.Objects.Any() == true; @@ -184,6 +184,13 @@ namespace osu.Game.Screens.Play addGameplayComponents(GameplayClockContainer, Beatmap.Value, playableBeatmap); addOverlayComponents(GameplayClockContainer, Beatmap.Value); + if (!DrawableRuleset.DisplayHud) + { + HUDOverlay.ShowHud.Value = false; + HUDOverlay.ShowHud.Disabled = true; + BreakOverlay.Hide(); + } + DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updatePauseOnFocusLostState(), true); // bind clock into components that require it From 83998d5ba5a661a284bb19b54d711f8bc583f6ad Mon Sep 17 00:00:00 2001 From: Lucas A Date: Thu, 7 May 2020 09:39:14 +0200 Subject: [PATCH 1453/2376] Trim whitespace. --- osu.Game/Rulesets/UI/DrawableRuleset.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index ff1f67783e..57fbb7f1a5 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -489,7 +489,7 @@ namespace osu.Game.Rulesets.UI /// /// Whether to display the HUD with this ruleset. - /// Override to false to completely disable the display of the HUD with this ruleset. + /// Override to false to completely disable the display of the HUD with this ruleset. /// /// /// HUD refers here to in player as well as . From 90e17853a80af4b970a1cae6481c287c124d6fef Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 May 2020 18:12:11 +0900 Subject: [PATCH 1454/2376] Update tests in line with framework changes --- osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs index 2d5f1f238f..b82339281e 100644 --- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs @@ -25,7 +25,7 @@ namespace osu.Game.Tests.NonVisual var osu = loadOsu(host); var storage = osu.Dependencies.Get(); - string defaultStorageLocation = Path.Combine(Environment.CurrentDirectory, $"headless-{nameof(TestDefaultDirectory)}"); + string defaultStorageLocation = Path.Combine(Environment.CurrentDirectory, $"headless", nameof(TestDefaultDirectory)); Assert.That(storage.GetFullPath("."), Is.EqualTo(defaultStorageLocation)); } @@ -41,7 +41,7 @@ namespace osu.Game.Tests.NonVisual { using (var host = new HeadlessGameHost(nameof(TestCustomDirectory))) { - string headlessPrefix = $"headless-{nameof(TestCustomDirectory)}"; + string headlessPrefix = Path.Combine("headless", nameof(TestCustomDirectory)); // need access before the game has constructed its own storage yet. Storage storage = new DesktopStorage(headlessPrefix, host); From d21c42a222ffe2f569b66fa7a5deb177374e247a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 May 2020 20:59:29 +0900 Subject: [PATCH 1455/2376] 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 8214fa2f2c..af699af1ba 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 91c89cbc20..397d48f7b8 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -24,7 +24,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 9ff7e3fc02..036f87541f 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From 4ac5ed71f4f82b1f9a4b47677e30e00ac3e230d7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 May 2020 21:48:57 +0900 Subject: [PATCH 1456/2376] Remove redundant string interpolation --- osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs index b82339281e..d741bc5de1 100644 --- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs @@ -25,7 +25,7 @@ namespace osu.Game.Tests.NonVisual var osu = loadOsu(host); var storage = osu.Dependencies.Get(); - string defaultStorageLocation = Path.Combine(Environment.CurrentDirectory, $"headless", nameof(TestDefaultDirectory)); + string defaultStorageLocation = Path.Combine(Environment.CurrentDirectory, "headless", nameof(TestDefaultDirectory)); Assert.That(storage.GetFullPath("."), Is.EqualTo(defaultStorageLocation)); } From 754afb9c0bbd5d049352c455ec1122de69a9a9fa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 May 2020 18:31:17 +0900 Subject: [PATCH 1457/2376] Expose ContextFactory to allow for connection flushing --- osu.Game/OsuGameBase.cs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index d9f9e2de42..d0c06df8ea 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -121,7 +121,7 @@ namespace osu.Game protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - private DatabaseContextFactory contextFactory; + protected DatabaseContextFactory ContextFactory; protected override UserInputManager CreateUserInputManager() => new OsuUserInputManager(); @@ -130,7 +130,7 @@ namespace osu.Game { Resources.AddStore(new DllResourceStore(OsuResources.ResourceAssembly)); - dependencies.Cache(contextFactory = new DatabaseContextFactory(Storage)); + dependencies.Cache(ContextFactory = new DatabaseContextFactory(Storage)); dependencies.CacheAs(Storage); @@ -161,7 +161,7 @@ namespace osu.Game runMigrations(); - dependencies.Cache(SkinManager = new SkinManager(Storage, contextFactory, Host, Audio, new NamespacedResourceStore(Resources, "Skins/Legacy"))); + dependencies.Cache(SkinManager = new SkinManager(Storage, ContextFactory, Host, Audio, new NamespacedResourceStore(Resources, "Skins/Legacy"))); dependencies.CacheAs(SkinManager); if (API == null) API = new APIAccess(LocalConfig); @@ -170,12 +170,12 @@ namespace osu.Game var defaultBeatmap = new DummyWorkingBeatmap(Audio, Textures); - dependencies.Cache(RulesetStore = new RulesetStore(contextFactory, Storage)); - dependencies.Cache(FileStore = new FileStore(contextFactory, Storage)); + dependencies.Cache(RulesetStore = new RulesetStore(ContextFactory, Storage)); + dependencies.Cache(FileStore = new FileStore(ContextFactory, Storage)); // 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, Host)); - dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, contextFactory, RulesetStore, API, Audio, Host, defaultBeatmap)); + dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, API, ContextFactory, Host)); + dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, ContextFactory, RulesetStore, API, Audio, Host, defaultBeatmap)); // this should likely be moved to ArchiveModelManager when another case appers where it is necessary // to have inter-dependent model managers. this could be obtained with an IHasForeign interface to @@ -189,8 +189,8 @@ namespace osu.Game BeatmapManager.ItemRemoved += i => ScoreManager.Delete(getBeatmapScores(i), true); BeatmapManager.ItemAdded += i => ScoreManager.Undelete(getBeatmapScores(i), true); - dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore)); - dependencies.Cache(SettingsStore = new SettingsStore(contextFactory)); + dependencies.Cache(KeyBindingStore = new KeyBindingStore(ContextFactory, RulesetStore)); + dependencies.Cache(SettingsStore = new SettingsStore(ContextFactory)); dependencies.Cache(RulesetConfigCache = new RulesetConfigCache(SettingsStore)); dependencies.Cache(new SessionStatics()); dependencies.Cache(new OsuColour()); @@ -279,7 +279,7 @@ namespace osu.Game { try { - using (var db = contextFactory.GetForWrite(false)) + using (var db = ContextFactory.GetForWrite(false)) db.Context.Migrate(); } catch (Exception e) @@ -288,12 +288,12 @@ namespace osu.Game // if we failed, let's delete the database and start fresh. // todo: we probably want a better (non-destructive) migrations/recovery process at a later point than this. - contextFactory.ResetDatabase(); + ContextFactory.ResetDatabase(); Logger.Log("Database purged successfully.", LoggingTarget.Database); // only run once more, then hard bail. - using (var db = contextFactory.GetForWrite(false)) + using (var db = ContextFactory.GetForWrite(false)) db.Context.Migrate(); } } From 49a03f1c06ca6824fc8e131ddd1e8f845c4ea1f2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 May 2020 18:31:36 +0900 Subject: [PATCH 1458/2376] Add basic blocking migration, move not copy --- osu.Game/IO/OsuStorage.cs | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index ee42c491d1..f6cac2f4f1 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.IO; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Configuration; @@ -9,10 +11,15 @@ namespace osu.Game.IO { public class OsuStorage : WrappedStorage { + private readonly GameHost host; + private readonly StorageConfigManager storageConfig; + public OsuStorage(GameHost host) : base(host.Storage, string.Empty) { - var storageConfig = new StorageConfigManager(host.Storage); + this.host = host; + + storageConfig = new StorageConfigManager(host.Storage); var customStoragePath = storageConfig.Get(StorageConfig.FullPath); @@ -22,5 +29,34 @@ namespace osu.Game.IO Logger.Storage = UnderlyingStorage.GetStorageForDirectory("logs"); } } + + public void Migrate(string newLocation) + { + string oldLocation = GetFullPath("."); + + // ensure the new location has no files present, else hard abort + if (Directory.Exists(newLocation)) + { + if (Directory.GetFiles(newLocation).Length > 0) + throw new InvalidOperationException("Migration destination already has files present"); + + Directory.Delete(newLocation, true); + } + + Directory.Move(oldLocation, newLocation); + + Directory.CreateDirectory(newLocation); + // temporary + Directory.CreateDirectory(oldLocation); + + // move back exceptions for now + Directory.Move(Path.Combine(newLocation, "cache"), Path.Combine(oldLocation, "cache")); + File.Move(Path.Combine(newLocation, "framework.ini"), Path.Combine(oldLocation, "framework.ini")); + + ChangeTargetStorage(host.GetStorage(newLocation)); + + storageConfig.Set(StorageConfig.FullPath, newLocation); + storageConfig.Save(); + } } } From 7a2020fd4561e461ef7700a5178d7b05a8c23b6b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 May 2020 19:00:59 +0900 Subject: [PATCH 1459/2376] User copy operation instead of move --- osu.Game/IO/OsuStorage.cs | 63 ++++++++++++++++++++++++++++++++++----- 1 file changed, 55 insertions(+), 8 deletions(-) diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index f6cac2f4f1..955aae7b68 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.cs @@ -3,6 +3,7 @@ using System; using System.IO; +using System.Linq; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Configuration; @@ -14,6 +15,14 @@ namespace osu.Game.IO private readonly GameHost host; private readonly StorageConfigManager storageConfig; + internal static readonly string[] IGNORE_DIRECTORIES = { "cache" }; + + internal static readonly string[] IGNORE_FILES = + { + "framework.ini", + "storage.ini" + }; + public OsuStorage(GameHost host) : base(host.Storage, string.Empty) { @@ -43,20 +52,58 @@ namespace osu.Game.IO Directory.Delete(newLocation, true); } - Directory.Move(oldLocation, newLocation); + var source = new DirectoryInfo(oldLocation); + var destination = new DirectoryInfo(newLocation); - Directory.CreateDirectory(newLocation); - // temporary - Directory.CreateDirectory(oldLocation); - - // move back exceptions for now - Directory.Move(Path.Combine(newLocation, "cache"), Path.Combine(oldLocation, "cache")); - File.Move(Path.Combine(newLocation, "framework.ini"), Path.Combine(oldLocation, "framework.ini")); + copyRecursive(source, destination); ChangeTargetStorage(host.GetStorage(newLocation)); storageConfig.Set(StorageConfig.FullPath, newLocation); storageConfig.Save(); + + deleteRecursive(source); + } + + private static void deleteRecursive(DirectoryInfo target, bool topLevelExcludes = true) + { + foreach (System.IO.FileInfo fi in target.GetFiles()) + { + if (IGNORE_FILES.Contains(fi.Name)) + continue; + + fi.Delete(); + } + + foreach (DirectoryInfo dir in target.GetDirectories()) + { + if (IGNORE_DIRECTORIES.Contains(dir.Name)) + continue; + + dir.Delete(true); + } + } + + private static void copyRecursive(DirectoryInfo source, DirectoryInfo destination, bool topLevelExcludes = true) + { + // based off example code https://docs.microsoft.com/en-us/dotnet/api/system.io.directoryinfo + Directory.CreateDirectory(destination.FullName); + + foreach (System.IO.FileInfo fi in source.GetFiles()) + { + if (IGNORE_FILES.Contains(fi.Name)) + continue; + + fi.CopyTo(Path.Combine(destination.FullName, fi.Name), true); + } + + foreach (DirectoryInfo dir in source.GetDirectories()) + { + if (IGNORE_DIRECTORIES.Contains(dir.Name)) + continue; + + copyRecursive(dir, destination.CreateSubdirectory(dir.Name), false); + } } } } From 5e65bda8d8ba49b5213fcf163861bdee4ebddb6c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 May 2020 19:01:19 +0900 Subject: [PATCH 1460/2376] Add test coverage --- .../NonVisual/CustomDataDirectoryTest.cs | 60 ++++++++++++++++++- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs index d741bc5de1..7f08fad5be 100644 --- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs @@ -7,14 +7,23 @@ using System.Threading; using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Configuration; using osu.Framework.Platform; using osu.Game.Configuration; +using osu.Game.IO; namespace osu.Game.Tests.NonVisual { [TestFixture] public class CustomDataDirectoryTest { + [SetUp] + public void SetUp() + { + if (Directory.Exists(customPath)) + Directory.Delete(customPath, true); + } + [Test] public void TestDefaultDirectory() { @@ -36,6 +45,8 @@ namespace osu.Game.Tests.NonVisual } } + private string customPath => Path.Combine(Environment.CurrentDirectory, "custom-path"); + [Test] public void TestCustomDirectory() { @@ -49,7 +60,7 @@ namespace osu.Game.Tests.NonVisual storage.DeleteDirectory(string.Empty); using (var storageConfig = new StorageConfigManager(storage)) - storageConfig.Set(StorageConfig.FullPath, Path.Combine(Environment.CurrentDirectory, "custom-path")); + storageConfig.Set(StorageConfig.FullPath, customPath); try { @@ -58,7 +69,52 @@ namespace osu.Game.Tests.NonVisual // switch to DI'd storage storage = osu.Dependencies.Get(); - Assert.That(storage.GetFullPath("."), Is.EqualTo(Path.Combine(Environment.CurrentDirectory, "custom-path"))); + Assert.That(storage.GetFullPath("."), Is.EqualTo(customPath)); + } + finally + { + host.Exit(); + } + } + } + + [Test] + public void TestMigration() + { + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestMigration))) + { + try + { + var osu = loadOsu(host); + var storage = osu.Dependencies.Get(); + + // ensure we perform a save + host.Dependencies.Get().Save(); + + // ensure we "use" cache + host.Storage.GetStorageForDirectory("cache"); + + string defaultStorageLocation = Path.Combine(Environment.CurrentDirectory, "headless", nameof(TestMigration)); + + Assert.That(storage.GetFullPath("."), Is.EqualTo(defaultStorageLocation)); + + (storage as OsuStorage)?.Migrate(customPath); + + Assert.That(storage.GetFullPath("."), Is.EqualTo(customPath)); + + foreach (var file in OsuStorage.IGNORE_FILES) + { + Assert.That(host.Storage.Exists(file), Is.True); + Assert.That(storage.Exists(file), Is.False); + } + + foreach (var dir in OsuStorage.IGNORE_DIRECTORIES) + { + Assert.That(host.Storage.ExistsDirectory(dir), Is.True); + Assert.That(storage.ExistsDirectory(dir), Is.False); + } + + Assert.That(new StreamReader(host.Storage.GetStream("storage.ini")).ReadToEnd().Contains($"FullPath = {customPath}")); } finally { From c025814f403dc5fcf4f07342791387986c526809 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Thu, 7 May 2020 23:04:18 +0200 Subject: [PATCH 1461/2376] Finalize changes --- osu.Game/OsuGame.cs | 14 +++++++++++--- .../Settings/Sections/General/UpdateSettings.cs | 7 ++----- osu.Game/Updater/SimpleUpdateManager.cs | 2 +- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index fdc8d94352..00b967c243 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -90,7 +90,7 @@ namespace osu.Game protected BackButton BackButton; - protected SettingsPanel Settings; + protected SettingsOverlay Settings; private VolumeOverlay volume; private OsuLogo osuLogo; @@ -609,6 +609,9 @@ namespace osu.Game loadComponentSingleFile(screenshotManager, Add); + // dependency on notification overlay + loadComponentSingleFile(CreateUpdateManager(), Add, true); + // overlay elements loadComponentSingleFile(beatmapListing = new BeatmapListingOverlay(), overlayContent.Add, true); loadComponentSingleFile(dashboard = new DashboardOverlay(), overlayContent.Add, true); @@ -641,7 +644,6 @@ namespace osu.Game chatOverlay.State.ValueChanged += state => channelManager.HighPollRate.Value = state.NewValue == Visibility.Visible; Add(externalLinkOpener = new ExternalLinkOpener()); - Add(CreateUpdateManager()); // dependency on notification overlay // side overlays which cancel each other. var singleDisplaySideOverlays = new OverlayContainer[] { Settings, notifications }; @@ -765,11 +767,17 @@ namespace osu.Game private Task asyncLoadStream; + /// + /// Schedules loading the provided in a single file. + /// + /// The component to load. + /// The method to invoke for adding the component. + /// Whether to cache the component as type into the game dependencies before any scheduling. private T loadComponentSingleFile(T d, Action add, bool cache = false) where T : Drawable { if (cache) - dependencies.Cache(d); + dependencies.CacheAs(d); if (d is OverlayContainer overlay) overlays.Add(overlay); diff --git a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs index 71deeee693..233a382b54 100644 --- a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs @@ -11,13 +11,10 @@ namespace osu.Game.Overlays.Settings.Sections.General { public class UpdateSettings : SettingsSubsection { - [Resolved(CanBeNull = true)] - private UpdateManager updateManager { get; set; } - protected override string Header => "Updates"; [BackgroundDependencyLoader] - private void load(Storage storage, OsuConfigManager config, OsuGameBase game) + private void load(Storage storage, OsuConfigManager config, OsuGameBase game, UpdateManager updateManager) { Add(new SettingsEnumDropdown { @@ -28,7 +25,7 @@ namespace osu.Game.Overlays.Settings.Sections.General Add(new SettingsButton { Text = "Check for updates", - Action = () => updateManager?.CheckForUpdate(), + Action = () => updateManager.CheckForUpdate(), Enabled = { Value = game.IsDeployedBuild } }); diff --git a/osu.Game/Updater/SimpleUpdateManager.cs b/osu.Game/Updater/SimpleUpdateManager.cs index 41248ed796..234fe8be8b 100644 --- a/osu.Game/Updater/SimpleUpdateManager.cs +++ b/osu.Game/Updater/SimpleUpdateManager.cs @@ -33,7 +33,7 @@ namespace osu.Game.Updater CheckForUpdate(); } - public override void CheckForUpdate() => Schedule(() => Task.Run(checkForUpdateAsync)); + public override void CheckForUpdate() => Schedule(() => Task.Run(() => checkForUpdateAsync())); private async void checkForUpdateAsync() { From 92872496b86db2681da81cc151223e5707464940 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Thu, 7 May 2020 23:27:28 +0200 Subject: [PATCH 1462/2376] Convert to method groups because Inspector said so. --- osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs | 2 +- osu.Game/Updater/SimpleUpdateManager.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs index 233a382b54..5ddd12f667 100644 --- a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs @@ -25,7 +25,7 @@ namespace osu.Game.Overlays.Settings.Sections.General Add(new SettingsButton { Text = "Check for updates", - Action = () => updateManager.CheckForUpdate(), + Action = updateManager.CheckForUpdate, Enabled = { Value = game.IsDeployedBuild } }); diff --git a/osu.Game/Updater/SimpleUpdateManager.cs b/osu.Game/Updater/SimpleUpdateManager.cs index 234fe8be8b..41248ed796 100644 --- a/osu.Game/Updater/SimpleUpdateManager.cs +++ b/osu.Game/Updater/SimpleUpdateManager.cs @@ -33,7 +33,7 @@ namespace osu.Game.Updater CheckForUpdate(); } - public override void CheckForUpdate() => Schedule(() => Task.Run(() => checkForUpdateAsync())); + public override void CheckForUpdate() => Schedule(() => Task.Run(checkForUpdateAsync)); private async void checkForUpdateAsync() { From 72b6bb25a5c1125f038d4b079e2e57fd31fdbcd1 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Fri, 8 May 2020 00:33:33 +0200 Subject: [PATCH 1463/2376] Allow nulls and hide if missing dependencies --- .../Settings/Sections/General/UpdateSettings.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs index 5ddd12f667..23ca752f6e 100644 --- a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs @@ -13,7 +13,7 @@ namespace osu.Game.Overlays.Settings.Sections.General { protected override string Header => "Updates"; - [BackgroundDependencyLoader] + [BackgroundDependencyLoader(true)] private void load(Storage storage, OsuConfigManager config, OsuGameBase game, UpdateManager updateManager) { Add(new SettingsEnumDropdown @@ -22,12 +22,15 @@ namespace osu.Game.Overlays.Settings.Sections.General Bindable = config.GetBindable(OsuSetting.ReleaseStream), }); - Add(new SettingsButton + if (game != null && updateManager != null) { - Text = "Check for updates", - Action = updateManager.CheckForUpdate, - Enabled = { Value = game.IsDeployedBuild } - }); + Add(new SettingsButton + { + Text = "Check for updates", + Action = updateManager.CheckForUpdate, + Enabled = { Value = game.IsDeployedBuild } + }); + } if (RuntimeInfo.IsDesktop) { From 477bd7fa613c75a6b535324cf59e42bdb7dce669 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Fri, 8 May 2020 00:35:27 +0200 Subject: [PATCH 1464/2376] Change to Resolved attribute --- .../Overlays/Settings/Sections/General/UpdateSettings.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs index 23ca752f6e..5af6a060ee 100644 --- a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs @@ -11,10 +11,16 @@ namespace osu.Game.Overlays.Settings.Sections.General { public class UpdateSettings : SettingsSubsection { + [Resolved(CanBeNull = true)] + private OsuGameBase game { get; set; } + + [Resolved(CanBeNull = true)] + private UpdateManager updateManager { get; set; } + protected override string Header => "Updates"; [BackgroundDependencyLoader(true)] - private void load(Storage storage, OsuConfigManager config, OsuGameBase game, UpdateManager updateManager) + private void load(Storage storage, OsuConfigManager config) { Add(new SettingsEnumDropdown { From a7792070bc01f4108515f665f5ff17dc750c25e4 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Fri, 8 May 2020 01:08:17 +0200 Subject: [PATCH 1465/2376] Final changes to DI fields and values --- .../Settings/Sections/General/UpdateSettings.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs index 5af6a060ee..58966e8a4c 100644 --- a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs @@ -11,16 +11,13 @@ namespace osu.Game.Overlays.Settings.Sections.General { public class UpdateSettings : SettingsSubsection { - [Resolved(CanBeNull = true)] - private OsuGameBase game { get; set; } - [Resolved(CanBeNull = true)] private UpdateManager updateManager { get; set; } protected override string Header => "Updates"; - [BackgroundDependencyLoader(true)] - private void load(Storage storage, OsuConfigManager config) + [BackgroundDependencyLoader] + private void load(Storage storage, OsuConfigManager config, OsuGameBase game) { Add(new SettingsEnumDropdown { @@ -28,7 +25,8 @@ namespace osu.Game.Overlays.Settings.Sections.General Bindable = config.GetBindable(OsuSetting.ReleaseStream), }); - if (game != null && updateManager != null) + // We shouldn't display the button for the base UpdateManager (without updating logic) + if (updateManager != null && updateManager.GetType() != typeof(UpdateManager)) { Add(new SettingsButton { From 75e65766ffcf0e3e1820407534bcd0104d469adb Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Fri, 8 May 2020 01:09:16 +0200 Subject: [PATCH 1466/2376] Annotate dependency --- osu.Game/OsuGame.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 00b967c243..899056e179 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -609,7 +609,7 @@ namespace osu.Game loadComponentSingleFile(screenshotManager, Add); - // dependency on notification overlay + // dependency on notification overlay, dependent by settings overlay loadComponentSingleFile(CreateUpdateManager(), Add, true); // overlay elements From e6ad28a1cbb66359faa430446ae1d7b1fbc75b64 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Fri, 8 May 2020 02:09:37 +0200 Subject: [PATCH 1467/2376] Use property instead of type checking --- osu.Desktop/Updater/SquirrelUpdateManager.cs | 2 ++ osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs | 4 ++-- osu.Game/Updater/SimpleUpdateManager.cs | 2 ++ osu.Game/Updater/UpdateManager.cs | 2 ++ 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Desktop/Updater/SquirrelUpdateManager.cs b/osu.Desktop/Updater/SquirrelUpdateManager.cs index b287dd6527..2834f1f71d 100644 --- a/osu.Desktop/Updater/SquirrelUpdateManager.cs +++ b/osu.Desktop/Updater/SquirrelUpdateManager.cs @@ -22,6 +22,8 @@ namespace osu.Desktop.Updater { public class SquirrelUpdateManager : osu.Game.Updater.UpdateManager { + public override bool CanPerformUpdate => true; + private UpdateManager updateManager; private NotificationOverlay notificationOverlay; diff --git a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs index 58966e8a4c..b832e8930a 100644 --- a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs @@ -25,8 +25,8 @@ namespace osu.Game.Overlays.Settings.Sections.General Bindable = config.GetBindable(OsuSetting.ReleaseStream), }); - // We shouldn't display the button for the base UpdateManager (without updating logic) - if (updateManager != null && updateManager.GetType() != typeof(UpdateManager)) + // We should only display the button for UpdateManagers that do update the client + if (updateManager != null && updateManager.CanPerformUpdate) { Add(new SettingsButton { diff --git a/osu.Game/Updater/SimpleUpdateManager.cs b/osu.Game/Updater/SimpleUpdateManager.cs index 41248ed796..5cc42090f4 100644 --- a/osu.Game/Updater/SimpleUpdateManager.cs +++ b/osu.Game/Updater/SimpleUpdateManager.cs @@ -19,6 +19,8 @@ namespace osu.Game.Updater ///
    public class UpdateManager : CompositeDrawable { + public virtual bool CanPerformUpdate => false; + [Resolved] private OsuConfigManager config { get; set; } From 7f61f27be1e3031266110c0f64a812bc2a787829 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Fri, 8 May 2020 02:33:12 +0200 Subject: [PATCH 1468/2376] Use null-conditional operator when checking against UpdateManager Co-authored-by: Dean Herbert --- osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs index b832e8930a..6ea9c975de 100644 --- a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs @@ -26,7 +26,7 @@ namespace osu.Game.Overlays.Settings.Sections.General }); // We should only display the button for UpdateManagers that do update the client - if (updateManager != null && updateManager.CanPerformUpdate) + if (updateManager?.CanPerformUpdate == true) { Add(new SettingsButton { From 3c24ca08d042782166b0e1a7e1ce7297062e309e Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Fri, 8 May 2020 02:48:27 +0200 Subject: [PATCH 1469/2376] Check whether the build is deployed within the public check updates method --- osu.Desktop/Updater/SquirrelUpdateManager.cs | 15 +++++++++------ osu.Game/Updater/SimpleUpdateManager.cs | 12 +++++++++--- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/osu.Desktop/Updater/SquirrelUpdateManager.cs b/osu.Desktop/Updater/SquirrelUpdateManager.cs index 2834f1f71d..a3b21b4bd9 100644 --- a/osu.Desktop/Updater/SquirrelUpdateManager.cs +++ b/osu.Desktop/Updater/SquirrelUpdateManager.cs @@ -26,6 +26,7 @@ namespace osu.Desktop.Updater private UpdateManager updateManager; private NotificationOverlay notificationOverlay; + private OsuGameBase gameBase; public Task PrepareUpdateAsync() => UpdateManager.RestartAppWhenExited(); @@ -34,16 +35,18 @@ namespace osu.Desktop.Updater [BackgroundDependencyLoader] private void load(NotificationOverlay notification, OsuGameBase game) { + gameBase = game; notificationOverlay = notification; - if (game.IsDeployedBuild) - { - Splat.Locator.CurrentMutable.Register(() => new SquirrelLogger(), typeof(Splat.ILogger)); - CheckForUpdate(); - } + Splat.Locator.CurrentMutable.Register(() => new SquirrelLogger(), typeof(Splat.ILogger)); + CheckForUpdate(); } - public override void CheckForUpdate() => Schedule(() => Task.Run(() => checkForUpdateAsync())); + public override void CheckForUpdate() + { + if (gameBase.IsDeployedBuild) + Schedule(() => Task.Run(() => checkForUpdateAsync())); + } private async void checkForUpdateAsync(bool useDeltaPatching = true, UpdateProgressNotification notification = null) { diff --git a/osu.Game/Updater/SimpleUpdateManager.cs b/osu.Game/Updater/SimpleUpdateManager.cs index 5cc42090f4..8513ea94b4 100644 --- a/osu.Game/Updater/SimpleUpdateManager.cs +++ b/osu.Game/Updater/SimpleUpdateManager.cs @@ -26,16 +26,22 @@ namespace osu.Game.Updater [Resolved] private GameHost host { get; set; } + private OsuGameBase gameBase; + [BackgroundDependencyLoader] private void load(OsuGameBase game) { + gameBase = game; version = game.Version; - if (game.IsDeployedBuild) - CheckForUpdate(); + CheckForUpdate(); } - public override void CheckForUpdate() => Schedule(() => Task.Run(checkForUpdateAsync)); + public override void CheckForUpdate() + { + if (gameBase.IsDeployedBuild) + Schedule(() => Task.Run(checkForUpdateAsync)); + } private async void checkForUpdateAsync() { From ebd1df8c2822a76c683b1b0f01e6e7677c3a8f70 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Fri, 8 May 2020 02:50:58 +0200 Subject: [PATCH 1470/2376] Change property name to CanCheckForUpdate --- osu.Desktop/Updater/SquirrelUpdateManager.cs | 2 +- .../Overlays/Settings/Sections/General/UpdateSettings.cs | 4 ++-- osu.Game/Updater/SimpleUpdateManager.cs | 2 +- osu.Game/Updater/UpdateManager.cs | 5 ++++- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/osu.Desktop/Updater/SquirrelUpdateManager.cs b/osu.Desktop/Updater/SquirrelUpdateManager.cs index a3b21b4bd9..5c553f18f4 100644 --- a/osu.Desktop/Updater/SquirrelUpdateManager.cs +++ b/osu.Desktop/Updater/SquirrelUpdateManager.cs @@ -22,7 +22,7 @@ namespace osu.Desktop.Updater { public class SquirrelUpdateManager : osu.Game.Updater.UpdateManager { - public override bool CanPerformUpdate => true; + public override bool CanCheckForUpdate => true; private UpdateManager updateManager; private NotificationOverlay notificationOverlay; diff --git a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs index b832e8930a..cadffd9d86 100644 --- a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs @@ -25,8 +25,8 @@ namespace osu.Game.Overlays.Settings.Sections.General Bindable = config.GetBindable(OsuSetting.ReleaseStream), }); - // We should only display the button for UpdateManagers that do update the client - if (updateManager != null && updateManager.CanPerformUpdate) + // We should only display the button for UpdateManagers that do check for updates + if (updateManager != null && updateManager.CanCheckForUpdate) { Add(new SettingsButton { diff --git a/osu.Game/Updater/SimpleUpdateManager.cs b/osu.Game/Updater/SimpleUpdateManager.cs index 8513ea94b4..d4e8aed5ae 100644 --- a/osu.Game/Updater/SimpleUpdateManager.cs +++ b/osu.Game/Updater/SimpleUpdateManager.cs @@ -19,7 +19,7 @@ namespace osu.Game.Updater ///
    public class SimpleUpdateManager : UpdateManager { - public override bool CanPerformUpdate => true; + public override bool CanCheckForUpdate => true; private string version; diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs index f8c8bfe967..41bbfb76a5 100644 --- a/osu.Game/Updater/UpdateManager.cs +++ b/osu.Game/Updater/UpdateManager.cs @@ -17,7 +17,10 @@ namespace osu.Game.Updater ///
    public class UpdateManager : CompositeDrawable { - public virtual bool CanPerformUpdate => false; + /// + /// Whether this UpdateManager is capable of checking for updates. + /// + public virtual bool CanCheckForUpdate => false; [Resolved] private OsuConfigManager config { get; set; } From b6f232e39428ce2e056c821cf229c15717c1d134 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 8 May 2020 10:27:48 +0900 Subject: [PATCH 1471/2376] 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 8214fa2f2c..af699af1ba 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@
    - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 9db5fe562c..47804ed06e 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -24,7 +24,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 82253a0418..86cffa9fba 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@
    - + @@ -80,7 +80,7 @@ - + From d6840d880a0df25e310c7ee32f7427f0d4baf675 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 8 May 2020 10:38:31 +0900 Subject: [PATCH 1472/2376] Update StableStorage implementation in line with framework changes --- osu.Desktop/OsuGameDesktop.cs | 78 +++++++++++------------ osu.Game.Tournament/IPC/FileBasedIPC.cs | 84 +++++++++++-------------- 2 files changed, 72 insertions(+), 90 deletions(-) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index f05ee48914..9351e17419 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -6,15 +6,14 @@ using System.IO; using System.Linq; using System.Reflection; using System.Threading.Tasks; +using Microsoft.Win32; using osu.Desktop.Overlays; using osu.Framework.Platform; using osu.Game; using osuTK.Input; -using Microsoft.Win32; using osu.Desktop.Updater; using osu.Framework; using osu.Framework.Logging; -using osu.Framework.Platform.Windows; using osu.Framework.Screens; using osu.Game.Screens.Menu; using osu.Game.Updater; @@ -37,7 +36,11 @@ namespace osu.Desktop try { if (Host is DesktopGameHost desktopHost) - return new StableStorage(desktopHost); + { + string stablePath = getStableInstallPath(); + if (!string.IsNullOrEmpty(stablePath)) + return new DesktopStorage(stablePath, desktopHost); + } } catch (Exception) { @@ -47,6 +50,35 @@ namespace osu.Desktop return null; } + private string getStableInstallPath() + { + static bool checkExists(string p) => Directory.Exists(Path.Combine(p, "Songs")); + + string stableInstallPath; + + try + { + using (RegistryKey key = Registry.ClassesRoot.OpenSubKey("osu")) + stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty).ToString().Split('"')[1].Replace("osu!.exe", ""); + + if (checkExists(stableInstallPath)) + return stableInstallPath; + } + catch + { + } + + stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"osu!"); + if (checkExists(stableInstallPath)) + return stableInstallPath; + + stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".osu"); + if (checkExists(stableInstallPath)) + return stableInstallPath; + + return null; + } + protected override UpdateManager CreateUpdateManager() { switch (RuntimeInfo.OS) @@ -111,45 +143,5 @@ namespace osu.Desktop Task.Factory.StartNew(() => Import(filePaths), TaskCreationOptions.LongRunning); } - - /// - /// A method of accessing an osu-stable install in a controlled fashion. - /// - private class StableStorage : WindowsStorage - { - protected override string LocateBasePath() - { - static bool checkExists(string p) => Directory.Exists(Path.Combine(p, "Songs")); - - string stableInstallPath; - - try - { - using (RegistryKey key = Registry.ClassesRoot.OpenSubKey("osu")) - stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty).ToString().Split('"')[1].Replace("osu!.exe", ""); - - if (checkExists(stableInstallPath)) - return stableInstallPath; - } - catch - { - } - - stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"osu!"); - if (checkExists(stableInstallPath)) - return stableInstallPath; - - stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".osu"); - if (checkExists(stableInstallPath)) - return stableInstallPath; - - return null; - } - - public StableStorage(DesktopGameHost host) - : base(string.Empty, host) - { - } - } } } diff --git a/osu.Game.Tournament/IPC/FileBasedIPC.cs b/osu.Game.Tournament/IPC/FileBasedIPC.cs index eefa9fcfe6..53ba597a7e 100644 --- a/osu.Game.Tournament/IPC/FileBasedIPC.cs +++ b/osu.Game.Tournament/IPC/FileBasedIPC.cs @@ -8,7 +8,6 @@ using Microsoft.Win32; using osu.Framework.Allocation; using osu.Framework.Logging; using osu.Framework.Platform; -using osu.Framework.Platform.Windows; using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Legacy; @@ -52,7 +51,12 @@ namespace osu.Game.Tournament.IPC try { - Storage = new StableStorage(host as DesktopGameHost); + var path = findStablePath(); + + if (string.IsNullOrEmpty(path)) + return null; + + Storage = new DesktopStorage(path, host as DesktopGameHost); const string file_ipc_filename = "ipc.txt"; const string file_ipc_state_filename = "ipc-state.txt"; @@ -145,64 +149,50 @@ namespace osu.Game.Tournament.IPC return Storage; } - /// - /// A method of accessing an osu-stable install in a controlled fashion. - /// - private class StableStorage : WindowsStorage + private string findStablePath() { - protected override string LocateBasePath() - { - static bool checkExists(string p) - { - return File.Exists(Path.Combine(p, "ipc.txt")); - } + static bool checkExists(string p) => File.Exists(Path.Combine(p, "ipc.txt")); - string stableInstallPath = string.Empty; + string stableInstallPath = string.Empty; + + try + { + try + { + stableInstallPath = Environment.GetEnvironmentVariable("OSU_STABLE_PATH"); + + if (checkExists(stableInstallPath)) + return stableInstallPath; + } + catch + { + } try { - try - { - stableInstallPath = Environment.GetEnvironmentVariable("OSU_STABLE_PATH"); + using (RegistryKey key = Registry.ClassesRoot.OpenSubKey("osu")) + stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty).ToString().Split('"')[1].Replace("osu!.exe", ""); - if (checkExists(stableInstallPath)) - return stableInstallPath; - } - catch - { - } - - try - { - using (RegistryKey key = Registry.ClassesRoot.OpenSubKey("osu")) - stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty).ToString().Split('"')[1].Replace("osu!.exe", ""); - - if (checkExists(stableInstallPath)) - return stableInstallPath; - } - catch - { - } - - stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"osu!"); if (checkExists(stableInstallPath)) return stableInstallPath; - - stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".osu"); - if (checkExists(stableInstallPath)) - return stableInstallPath; - - return null; } - finally + catch { - Logger.Log($"Stable path for tourney usage: {stableInstallPath}"); } - } - public StableStorage(DesktopGameHost host) - : base(string.Empty, host) + stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"osu!"); + if (checkExists(stableInstallPath)) + return stableInstallPath; + + stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".osu"); + if (checkExists(stableInstallPath)) + return stableInstallPath; + + return null; + } + finally { + Logger.Log($"Stable path for tourney usage: {stableInstallPath}"); } } } From af5e1f8298ad47fdbd783e3dedcb5ba0038aa898 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 8 May 2020 14:22:07 +0900 Subject: [PATCH 1473/2376] Commit autogenerated rider VCS settings update --- .idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml b/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml index 4bb9f4d2a0..7515e76054 100644 --- a/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml +++ b/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml @@ -1,6 +1,6 @@ - \ No newline at end of file From 9f64882f371aeae723e646871cbdfe5bb250f168 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 8 May 2020 14:48:38 +0900 Subject: [PATCH 1474/2376] 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 af699af1ba..a406cdf08a 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 47804ed06e..5ccfaaac9e 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -24,7 +24,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 86cffa9fba..dc83d937f7 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From 8b5de7403f317cd87be9315976632d08873b021a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 8 May 2020 15:12:29 +0900 Subject: [PATCH 1475/2376] Fix android usage of obsoleted VersionCode --- osu.Android/OsuGameAndroid.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index 84f215f930..19ed7ffcf5 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.cs @@ -18,7 +18,8 @@ namespace osu.Android try { - string versionName = packageInfo.VersionCode.ToString(); + // todo: needs checking before play store redeploy. + string versionName = packageInfo.VersionName; // undo play store version garbling return new Version(int.Parse(versionName.Substring(0, 4)), int.Parse(versionName.Substring(4, 4)), int.Parse(versionName.Substring(8, 1))); } From c5589d278b17f30196272e26ac506dbfb2a3e46d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 8 May 2020 15:35:25 +0900 Subject: [PATCH 1476/2376] Revert "Commit autogenerated rider VCS settings update" This reverts commit af5e1f8298ad47fdbd783e3dedcb5ba0038aa898. --- .idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml b/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml index 7515e76054..4bb9f4d2a0 100644 --- a/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml +++ b/.idea/.idea.osu.Desktop/.idea/projectSettingsUpdater.xml @@ -1,6 +1,6 @@ - \ No newline at end of file From 30dd158c33ef1c3968dd7432ffa3fc78f2aff460 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Fri, 8 May 2020 09:37:50 +0200 Subject: [PATCH 1477/2376] Rename property to AllowGameplayOverlays and update XMLDoc accordingly. --- osu.Game/Rulesets/UI/DrawableRuleset.cs | 8 ++++---- osu.Game/Screens/Play/Player.cs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 57fbb7f1a5..e1c21209c2 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -488,13 +488,13 @@ namespace osu.Game.Rulesets.UI protected virtual ResumeOverlay CreateResumeOverlay() => null; /// - /// Whether to display the HUD with this ruleset. - /// Override to false to completely disable the display of the HUD with this ruleset. + /// Whether to display gameplay overlays with this ruleset. + /// Override to false to completely disable the display of gameplay overlays. /// /// - /// HUD refers here to in player as well as . + /// Gameplay overlays refer here to in player as well as . /// - public virtual bool DisplayHud => true; + public virtual bool AllowGameplayOverlays => true; /// public class DimmableStoryboard : UserDimContainer { - public Container OverlayLayerContainer; + public Container OverlayLayerContainer { get; private set; } private readonly Storyboard storyboard; private DrawableStoryboard drawableStoryboard; From c2697d39070d73ffbb2b7894a57088a85df2e127 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 20 May 2020 20:49:01 +0900 Subject: [PATCH 1676/2376] Use DrawableSample in SkinnableSound class --- .../Objects/Drawables/DrawableHitObject.cs | 11 ++- osu.Game/Skinning/SkinnableSound.cs | 72 +++++++------------ 2 files changed, 32 insertions(+), 51 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 33ea02c22f..c32d4e441e 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -11,7 +11,6 @@ using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; using osu.Framework.Threading; -using osu.Framework.Audio; using osu.Game.Audio; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Types; @@ -96,8 +95,6 @@ namespace osu.Game.Rulesets.Objects.Drawables /// protected virtual float SamplePlaybackPosition => 0.5f; - private readonly BindableDouble balanceAdjust = new BindableDouble(); - private BindableList samplesBindable; private Bindable startTimeBindable; private Bindable userPositionalHitSounds; @@ -173,7 +170,6 @@ namespace osu.Game.Rulesets.Objects.Drawables } Samples = new SkinnableSound(samples.Select(s => HitObject.SampleControlPoint.ApplyTo(s))); - Samples.AddAdjustment(AdjustableProperty.Balance, balanceAdjust); AddInternal(Samples); } @@ -360,8 +356,11 @@ namespace osu.Game.Rulesets.Objects.Drawables { const float balance_adjust_amount = 0.4f; - balanceAdjust.Value = balance_adjust_amount * (userPositionalHitSounds.Value ? SamplePlaybackPosition - 0.5f : 0); - Samples?.Play(); + if (Samples != null) + { + Samples.Balance.Value = balance_adjust_amount * (userPositionalHitSounds.Value ? SamplePlaybackPosition - 0.5f : 0); + Samples.Play(); + } } protected override void Update() diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index a78c04ecd4..30320c89a6 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -4,11 +4,11 @@ using System.Collections.Generic; using System.Linq; 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.Extensions.IEnumerableExtensions; +using osu.Framework.Graphics.Audio; +using osu.Framework.Graphics.Containers; using osu.Game.Audio; namespace osu.Game.Skinning @@ -17,25 +17,32 @@ namespace osu.Game.Skinning { private readonly ISampleInfo[] hitSamples; - private List<(AdjustableProperty property, BindableDouble bindable)> adjustments; - - private SampleChannel[] channels; - [Resolved] private ISampleStore samples { get; set; } + public SkinnableSound(ISampleInfo hitSamples) + : this(new[] { hitSamples }) + { + } + public SkinnableSound(IEnumerable hitSamples) { this.hitSamples = hitSamples.ToArray(); - } - - public SkinnableSound(ISampleInfo hitSamples) - { - this.hitSamples = new[] { hitSamples }; + InternalChild = samplesContainer = new AudioContainer(); } private bool looping; + private readonly AudioContainer samplesContainer; + + public BindableNumber Volume => samplesContainer.Volume; + + public BindableNumber Balance => samplesContainer.Balance; + + public BindableNumber Frequency => samplesContainer.Frequency; + + public BindableNumber Tempo => samplesContainer.Tempo; + public bool Looping { get => looping; @@ -45,33 +52,23 @@ namespace osu.Game.Skinning looping = value; - channels?.ForEach(c => c.Looping = looping); + samplesContainer.ForEach(c => c.Looping = looping); } } - public void Play() => channels?.ForEach(c => c.Play()); - - public void Stop() => channels?.ForEach(c => c.Stop()); - - public void AddAdjustment(AdjustableProperty type, BindableDouble adjustBindable) + public void Play() => samplesContainer.ForEach(c => { - if (adjustments == null) adjustments = new List<(AdjustableProperty, BindableDouble)>(); + if (c.AggregateVolume.Value > 0) + c.Play(); + }); - adjustments.Add((type, adjustBindable)); - channels?.ForEach(c => c.AddAdjustment(type, adjustBindable)); - } - - public void RemoveAdjustment(AdjustableProperty type, BindableDouble adjustBindable) - { - adjustments?.Remove((type, adjustBindable)); - channels?.ForEach(c => c.RemoveAdjustment(type, adjustBindable)); - } + public void Stop() => samplesContainer.ForEach(c => c.Stop()); public override bool IsPresent => Scheduler.HasPendingTasks; protected override void SkinChanged(ISkinSource skin, bool allowFallback) { - channels = hitSamples.Select(s => + var channels = hitSamples.Select(s => { var ch = skin.GetSample(s); @@ -88,27 +85,12 @@ namespace osu.Game.Skinning { ch.Looping = looping; ch.Volume.Value = s.Volume / 100.0; - - if (adjustments != null) - { - foreach (var (property, bindable) in adjustments) - ch.AddAdjustment(property, bindable); - } } return ch; - }).Where(c => c != null).ToArray(); - } + }).Where(c => c != null); - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - if (channels != null) - { - foreach (var c in channels) - c.Dispose(); - } + samplesContainer.ChildrenEnumerable = channels.Select(c => new DrawableSample(c)); } } } From 3354d48a38c90314caa376481b06a1d0e9f77fc3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 20 May 2020 17:48:43 +0900 Subject: [PATCH 1677/2376] Change snapping to be screen space coordinate based --- .../Blueprints/HoldNoteSelectionBlueprint.cs | 2 +- .../Edit/ManiaHitObjectComposer.cs | 16 +++++++++++----- .../TestSceneOsuDistanceSnapGrid.cs | 8 +++++--- .../Sliders/Components/PathControlPointPiece.cs | 4 ++-- .../Sliders/SliderSelectionBlueprint.cs | 2 +- .../Visual/Editing/TestSceneDistanceSnapGrid.cs | 8 +++++--- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 17 +++++++++++------ ...SnapProvider.cs => IPositionSnapProvider.cs} | 11 +++++++++-- .../Rulesets/Edit/OverlaySelectionBlueprint.cs | 2 +- osu.Game/Rulesets/Edit/PlacementBlueprint.cs | 3 +++ osu.Game/Rulesets/Edit/SelectionBlueprint.cs | 2 +- .../Compose/Components/BlueprintContainer.cs | 8 ++++---- .../Components/ComposeBlueprintContainer.cs | 5 ++--- .../Edit/Compose/Components/DistanceSnapGrid.cs | 2 +- .../Compose/Components/Timeline/Timeline.cs | 12 ++++++------ .../Timeline/TimelineHitObjectBlueprint.cs | 4 ++-- 16 files changed, 65 insertions(+), 41 deletions(-) rename osu.Game/Rulesets/Edit/{IDistanceSnapProvider.cs => IPositionSnapProvider.cs} (84%) diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs index 43d43ef252..1737c4d2e5 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs @@ -77,6 +77,6 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints public override Quad SelectionQuad => ScreenSpaceDrawQuad; - public override Vector2 SelectionPoint => DrawableObject.Head.ScreenSpaceDrawQuad.Centre; + public override Vector2 ScreenSpaceSelectionPoint => DrawableObject.Head.ScreenSpaceDrawQuad.Centre; } } diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index dfa933baad..9ba2cdeaec 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.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.Beatmaps; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; @@ -44,26 +45,31 @@ namespace osu.Game.Rulesets.Mania.Edit public int TotalColumns => Playfield.TotalColumns; - public override (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time) + public override (Vector2 position, double time) SnapPositionToValidTime(Vector2 position) + { + throw new NotImplementedException(); + } + + public override (Vector2 position, double time) SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) { var hoc = Playfield.GetColumn(0).HitObjectContainer; - float targetPosition = hoc.ToLocalSpace(ToScreenSpace(position)).Y; + Vector2 targetPosition = hoc.ToLocalSpace(screenSpacePosition); if (drawableRuleset.ScrollingInfo.Direction.Value == ScrollingDirection.Down) { // We're dealing with screen coordinates in which the position decreases towards the centre of the screen resulting in an increase in start time. // The scrolling algorithm instead assumes a top anchor meaning an increase in time corresponds to an increase in position, // so when scrolling downwards the coordinates need to be flipped. - targetPosition = hoc.DrawHeight - targetPosition; + targetPosition.Y = hoc.DrawHeight - targetPosition.Y; } - double targetTime = drawableRuleset.ScrollingInfo.Algorithm.TimeAt(targetPosition, + double targetTime = drawableRuleset.ScrollingInfo.Algorithm.TimeAt(targetPosition.Y, EditorClock.CurrentTime, drawableRuleset.ScrollingInfo.TimeRange.Value, hoc.DrawHeight); - return base.GetSnappedPosition(position, targetTime); + return (targetPosition, targetTime); } protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs index c182aa5d63..f95f76b405 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Tests [Cached] private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor(); - [Cached(typeof(IDistanceSnapProvider))] + [Cached(typeof(IPositionSnapProvider))] private readonly SnapProvider snapProvider = new SnapProvider(); private TestOsuDistanceSnapGrid grid; @@ -172,9 +172,11 @@ namespace osu.Game.Rulesets.Osu.Tests } } - private class SnapProvider : IDistanceSnapProvider + private class SnapProvider : IPositionSnapProvider { - public (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time) => (position, time); + public (Vector2 position, double time) SnapPositionToValidTime(Vector2 position) => (position, 0); + + public (Vector2 position, double time) SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => (screenSpacePosition, 0); public float GetBeatSnapDistanceAt(double referenceTime) => (float)beat_length; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs index d0c1eb5317..abbef0772f 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components private IEditorChangeHandler changeHandler { get; set; } [Resolved(CanBeNull = true)] - private IDistanceSnapProvider snapProvider { get; set; } + private IPositionSnapProvider snapProvider { get; set; } [Resolved] private OsuColour colours { get; set; } @@ -162,7 +162,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components if (ControlPoint == slider.Path.ControlPoints[0]) { // Special handling for the head control point - the position of the slider changes which means the snapped position and time have to be taken into account - (Vector2 snappedPosition, double snappedTime) = snapProvider?.GetSnappedPosition(e.MousePosition, slider.StartTime) ?? (e.MousePosition, slider.StartTime); + (Vector2 snappedPosition, double snappedTime) = snapProvider?.SnapScreenSpacePositionToValidTime(e.MousePosition) ?? (e.MousePosition, slider.StartTime); Vector2 movementDelta = snappedPosition - slider.Position; slider.Position += movementDelta; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index b7074b7ee5..6633136673 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -190,7 +190,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders new OsuMenuItem("Add control point", MenuItemType.Standard, () => addControlPoint(rightClickPosition)), }; - public override Vector2 SelectionPoint => ((DrawableSlider)DrawableObject).HeadCircle.ScreenSpaceDrawQuad.Centre; + public override Vector2 ScreenSpaceSelectionPoint => ((DrawableSlider)DrawableObject).HeadCircle.ScreenSpaceDrawQuad.Centre; public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => BodyPiece.ReceivePositionalInputAt(screenSpacePos); diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs index 417d16fdb0..0e5e88c47a 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs @@ -23,7 +23,7 @@ namespace osu.Game.Tests.Visual.Editing [Cached(typeof(EditorBeatmap))] private readonly EditorBeatmap editorBeatmap; - [Cached(typeof(IDistanceSnapProvider))] + [Cached(typeof(IPositionSnapProvider))] private readonly SnapProvider snapProvider = new SnapProvider(); public TestSceneDistanceSnapGrid() @@ -151,9 +151,11 @@ namespace osu.Game.Tests.Visual.Editing => (Vector2.Zero, 0); } - private class SnapProvider : IDistanceSnapProvider + private class SnapProvider : IPositionSnapProvider { - public (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time) => (position, time); + public (Vector2 position, double time) SnapPositionToValidTime(Vector2 position) => (position, 0); + + public (Vector2 position, double time) SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => (screenSpacePosition, 0); public float GetBeatSnapDistanceAt(double referenceTime) => 10; diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 883288d6d7..82e8fc8b10 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -245,8 +245,7 @@ namespace osu.Game.Rulesets.Edit { EditorBeatmap.PlacementObject.Value = hitObject; - if (distanceSnapGrid != null) - hitObject.StartTime = GetSnappedPosition(distanceSnapGrid.ToLocalSpace(inputManager.CurrentState.Mouse.Position), hitObject.StartTime).time; + hitObject.StartTime = SnapScreenSpacePositionToValidTime(inputManager.CurrentState.Mouse.Position).time; } public void EndPlacement(HitObject hitObject, bool commit) @@ -265,7 +264,11 @@ namespace osu.Game.Rulesets.Edit public void Delete(HitObject hitObject) => EditorBeatmap.Remove(hitObject); - public override (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time) => distanceSnapGrid?.GetSnappedPosition(position) ?? (position, time); + public override (Vector2 position, double time) SnapPositionToValidTime(Vector2 position) => + distanceSnapGrid?.GetSnappedPosition(position) ?? (position, 0); + + public override (Vector2 position, double time) SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) + => SnapPositionToValidTime(drawableRulesetWrapper.Playfield.ToLocalSpace(screenSpacePosition)); public override float GetBeatSnapDistanceAt(double referenceTime) { @@ -297,8 +300,8 @@ namespace osu.Game.Rulesets.Edit } [Cached(typeof(HitObjectComposer))] - [Cached(typeof(IDistanceSnapProvider))] - public abstract class HitObjectComposer : CompositeDrawable, IDistanceSnapProvider + [Cached(typeof(IPositionSnapProvider))] + public abstract class HitObjectComposer : CompositeDrawable, IPositionSnapProvider { internal HitObjectComposer() { @@ -323,7 +326,9 @@ namespace osu.Game.Rulesets.Edit [CanBeNull] protected virtual DistanceSnapGrid CreateDistanceSnapGrid([NotNull] IEnumerable selectedHitObjects) => null; - public abstract (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time); + public abstract (Vector2 position, double time) SnapPositionToValidTime(Vector2 position); + + public abstract (Vector2 position, double time) SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition); public abstract float GetBeatSnapDistanceAt(double referenceTime); diff --git a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs similarity index 84% rename from osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs rename to osu.Game/Rulesets/Edit/IPositionSnapProvider.cs index c6e61f68da..93cb605132 100644 --- a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs @@ -5,9 +5,16 @@ using osuTK; namespace osu.Game.Rulesets.Edit { - public interface IDistanceSnapProvider + public interface IPositionSnapProvider { - (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time); + /// + /// Given a position (local to the provider), find a valid time snap + /// + /// The local position to be snapped. + /// The time and position post-snapping. + (Vector2 position, double time) SnapPositionToValidTime(Vector2 position); + + (Vector2 position, double time) SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition); /// /// Retrieves the distance between two points within a timing point that are one beat length apart. diff --git a/osu.Game/Rulesets/Edit/OverlaySelectionBlueprint.cs b/osu.Game/Rulesets/Edit/OverlaySelectionBlueprint.cs index b4ae3f3fba..8202d3a1d1 100644 --- a/osu.Game/Rulesets/Edit/OverlaySelectionBlueprint.cs +++ b/osu.Game/Rulesets/Edit/OverlaySelectionBlueprint.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Edit public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => DrawableObject.ReceivePositionalInputAt(screenSpacePos); - public override Vector2 SelectionPoint => DrawableObject.ScreenSpaceDrawQuad.Centre; + public override Vector2 ScreenSpaceSelectionPoint => DrawableObject.ScreenSpaceDrawQuad.Centre; public override Quad SelectionQuad => DrawableObject.ScreenSpaceDrawQuad; diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index fb1eb7adbf..c06e50950c 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -66,7 +66,10 @@ namespace osu.Game.Rulesets.Edit protected void BeginPlacement(double? startTime = null, bool commitStart = false) { HitObject.StartTime = startTime ?? EditorClock.CurrentTime; + + // applies snapping to above time placementHandler.BeginPlacement(HitObject); + PlacementActive |= commitStart; } diff --git a/osu.Game/Rulesets/Edit/SelectionBlueprint.cs b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs index e6a63eae4f..71256093d5 100644 --- a/osu.Game/Rulesets/Edit/SelectionBlueprint.cs +++ b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs @@ -116,7 +116,7 @@ namespace osu.Game.Rulesets.Edit /// /// The screen-space point that causes this to be selected. /// - public virtual Vector2 SelectionPoint => ScreenSpaceDrawQuad.Centre; + public virtual Vector2 ScreenSpaceSelectionPoint => ScreenSpaceDrawQuad.Centre; /// /// The screen-space quad that outlines this for selections. diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 8910684463..d1cae6b3cd 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -49,7 +49,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private readonly BindableList selectedHitObjects = new BindableList(); [Resolved(canBeNull: true)] - private IDistanceSnapProvider snapProvider { get; set; } + private IPositionSnapProvider snapProvider { get; set; } protected BlueprintContainer() { @@ -326,7 +326,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { foreach (var blueprint in SelectionBlueprints) { - if (blueprint.IsAlive && blueprint.IsPresent && rect.Contains(blueprint.SelectionPoint)) + if (blueprint.IsAlive && blueprint.IsPresent && rect.Contains(blueprint.ScreenSpaceSelectionPoint)) blueprint.Select(); else blueprint.Deselect(); @@ -384,7 +384,7 @@ namespace osu.Game.Screens.Edit.Compose.Components // Movement is tracked from the blueprint of the earliest hitobject, since it only makes sense to distance snap from that hitobject movementBlueprint = selectionHandler.SelectedBlueprints.OrderBy(b => b.HitObject.StartTime).First(); - movementBlueprintOriginalPosition = movementBlueprint.SelectionPoint; // todo: unsure if correct + movementBlueprintOriginalPosition = movementBlueprint.ScreenSpaceSelectionPoint; // todo: unsure if correct } /// @@ -405,7 +405,7 @@ namespace osu.Game.Screens.Edit.Compose.Components Vector2 movePosition = movementBlueprintOriginalPosition.Value + e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition; // Retrieve a snapped position. - (Vector2 snappedPosition, double snappedTime) = snapProvider.GetSnappedPosition(ToLocalSpace(movePosition), draggedObject.StartTime); + (Vector2 snappedPosition, double snappedTime) = snapProvider.SnapScreenSpacePositionToValidTime(movePosition); // Move the hitobjects. if (!selectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprint, ToScreenSpace(snappedPosition)))) diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 0eb77a8561..bb6094ebe8 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -67,10 +67,9 @@ namespace osu.Game.Screens.Edit.Compose.Components private void updatePlacementPosition(Vector2 screenSpacePosition) { - Vector2 snappedGridPosition = composer.GetSnappedPosition(ToLocalSpace(screenSpacePosition), 0).position; - Vector2 snappedScreenSpacePosition = ToScreenSpace(snappedGridPosition); + Vector2 snappedPlayfieldPosition = composer.SnapScreenSpacePositionToValidTime(screenSpacePosition).position; - currentPlacement.UpdatePosition(snappedScreenSpacePosition); + currentPlacement.UpdatePosition(ToScreenSpace(snappedPlayfieldPosition)); } #endregion diff --git a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs index 3a42938fc1..8a92a2011d 100644 --- a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs @@ -43,7 +43,7 @@ namespace osu.Game.Screens.Edit.Compose.Components protected OsuColour Colours { get; private set; } [Resolved] - protected IDistanceSnapProvider SnapProvider { get; private set; } + protected IPositionSnapProvider SnapProvider { get; private set; } [Resolved] private EditorBeatmap beatmap { get; set; } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index 25f3cfc285..1006da28df 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -17,9 +17,9 @@ using osuTK; namespace osu.Game.Screens.Edit.Compose.Components.Timeline { - [Cached(typeof(IDistanceSnapProvider))] + [Cached(typeof(IPositionSnapProvider))] [Cached] - public class Timeline : ZoomableScrollContainer, IDistanceSnapProvider + public class Timeline : ZoomableScrollContainer, IPositionSnapProvider { public readonly Bindable WaveformVisible = new Bindable(); public readonly IBindable Beatmap = new Bindable(); @@ -181,12 +181,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline [Resolved] private IBeatSnapProvider beatSnapProvider { get; set; } - public double GetTimeFromScreenSpacePosition(Vector2 position) - => getTimeFromPosition(Content.ToLocalSpace(position)); - - public (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time) => + public (Vector2 position, double time) SnapPositionToValidTime(Vector2 position) => (position, beatSnapProvider.SnapTime(getTimeFromPosition(position))); + public (Vector2 position, double time) SnapScreenSpacePositionToValidTime(Vector2 position) => + (position, beatSnapProvider.SnapTime(getTimeFromPosition(Content.ToLocalSpace(position)))); + private double getTimeFromPosition(Vector2 localPosition) => (localPosition.X / Content.DrawWidth) * track.Length; diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index 16ba3ba89a..b5eae26f98 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -186,7 +186,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } } - public override Vector2 SelectionPoint => ScreenSpaceDrawQuad.TopLeft; + public override Vector2 ScreenSpaceSelectionPoint => ScreenSpaceDrawQuad.TopLeft; public class DragBar : Container { @@ -275,7 +275,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline OnDragHandled?.Invoke(e); - var time = timeline.GetTimeFromScreenSpacePosition(e.ScreenSpaceMousePosition); + var time = timeline.SnapScreenSpacePositionToValidTime(e.ScreenSpaceMousePosition).time; switch (hitObject) { From c46bfc2532d139838ad6a94e11571f2c81430421 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 20 May 2020 18:19:21 +0900 Subject: [PATCH 1678/2376] Create SnapResult class to hold various snapping results --- .../Edit/ManiaHitObjectComposer.cs | 10 +---- .../TestSceneOsuDistanceSnapGrid.cs | 4 +- .../Components/PathControlPointPiece.cs | 6 +-- .../Editing/TestSceneDistanceSnapGrid.cs | 4 +- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 17 ++++---- .../Rulesets/Edit/IPositionSnapProvider.cs | 27 ++++++++++--- .../Compose/Components/BlueprintContainer.cs | 15 ++++--- .../Components/ComposeBlueprintContainer.cs | 2 +- .../Compose/Components/Timeline/Timeline.cs | 7 +--- .../Timeline/TimelineHitObjectBlueprint.cs | 39 ++++++++++--------- 10 files changed, 70 insertions(+), 61 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index 9ba2cdeaec..f7951fcc5d 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.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.Game.Beatmaps; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; @@ -45,12 +44,7 @@ namespace osu.Game.Rulesets.Mania.Edit public int TotalColumns => Playfield.TotalColumns; - public override (Vector2 position, double time) SnapPositionToValidTime(Vector2 position) - { - throw new NotImplementedException(); - } - - public override (Vector2 position, double time) SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) + public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) { var hoc = Playfield.GetColumn(0).HitObjectContainer; @@ -69,7 +63,7 @@ namespace osu.Game.Rulesets.Mania.Edit drawableRuleset.ScrollingInfo.TimeRange.Value, hoc.DrawHeight); - return (targetPosition, targetTime); + return new SnapResult(targetPosition, targetTime); } protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs index f95f76b405..0d0be2953b 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs @@ -174,9 +174,7 @@ namespace osu.Game.Rulesets.Osu.Tests private class SnapProvider : IPositionSnapProvider { - public (Vector2 position, double time) SnapPositionToValidTime(Vector2 position) => (position, 0); - - public (Vector2 position, double time) SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => (screenSpacePosition, 0); + public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0); public float GetBeatSnapDistanceAt(double referenceTime) => (float)beat_length; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs index abbef0772f..834bf1892f 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs @@ -162,11 +162,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components if (ControlPoint == slider.Path.ControlPoints[0]) { // Special handling for the head control point - the position of the slider changes which means the snapped position and time have to be taken into account - (Vector2 snappedPosition, double snappedTime) = snapProvider?.SnapScreenSpacePositionToValidTime(e.MousePosition) ?? (e.MousePosition, slider.StartTime); - Vector2 movementDelta = snappedPosition - slider.Position; + var result = snapProvider?.SnapScreenSpacePositionToValidTime(e.MousePosition); + Vector2 movementDelta = (result?.ScreenSpacePosition ?? e.MousePosition) - slider.Position; slider.Position += movementDelta; - slider.StartTime = snappedTime; + slider.StartTime = result?.Time ?? slider.StartTime; // Since control points are relative to the position of the slider, they all need to be offset backwards by the delta for (int i = 1; i < slider.Path.ControlPoints.Count; i++) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs index 0e5e88c47a..8190cf5f89 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs @@ -153,9 +153,7 @@ namespace osu.Game.Tests.Visual.Editing private class SnapProvider : IPositionSnapProvider { - public (Vector2 position, double time) SnapPositionToValidTime(Vector2 position) => (position, 0); - - public (Vector2 position, double time) SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => (screenSpacePosition, 0); + public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0); public float GetBeatSnapDistanceAt(double referenceTime) => 10; diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 82e8fc8b10..7e9bb850af 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -245,7 +245,8 @@ namespace osu.Game.Rulesets.Edit { EditorBeatmap.PlacementObject.Value = hitObject; - hitObject.StartTime = SnapScreenSpacePositionToValidTime(inputManager.CurrentState.Mouse.Position).time; + if (SnapScreenSpacePositionToValidTime(inputManager.CurrentState.Mouse.Position).Time is double time) + hitObject.StartTime = time; } public void EndPlacement(HitObject hitObject, bool commit) @@ -264,11 +265,13 @@ namespace osu.Game.Rulesets.Edit public void Delete(HitObject hitObject) => EditorBeatmap.Remove(hitObject); - public override (Vector2 position, double time) SnapPositionToValidTime(Vector2 position) => - distanceSnapGrid?.GetSnappedPosition(position) ?? (position, 0); + public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) + { + if (distanceSnapGrid == null) return new SnapResult(screenSpacePosition, null); - public override (Vector2 position, double time) SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) - => SnapPositionToValidTime(drawableRulesetWrapper.Playfield.ToLocalSpace(screenSpacePosition)); + (Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition)); + return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time); + } public override float GetBeatSnapDistanceAt(double referenceTime) { @@ -326,9 +329,7 @@ namespace osu.Game.Rulesets.Edit [CanBeNull] protected virtual DistanceSnapGrid CreateDistanceSnapGrid([NotNull] IEnumerable selectedHitObjects) => null; - public abstract (Vector2 position, double time) SnapPositionToValidTime(Vector2 position); - - public abstract (Vector2 position, double time) SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition); + public abstract SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition); public abstract float GetBeatSnapDistanceAt(double referenceTime); diff --git a/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs b/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs index 93cb605132..d95800f403 100644 --- a/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs @@ -8,13 +8,11 @@ namespace osu.Game.Rulesets.Edit public interface IPositionSnapProvider { /// - /// Given a position (local to the provider), find a valid time snap + /// Given a position, find a valid time snap. /// - /// The local position to be snapped. + /// The screen-space position to be snapped. /// The time and position post-snapping. - (Vector2 position, double time) SnapPositionToValidTime(Vector2 position); - - (Vector2 position, double time) SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition); + SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition); /// /// Retrieves the distance between two points within a timing point that are one beat length apart. @@ -55,4 +53,23 @@ namespace osu.Game.Rulesets.Edit /// A value that represents snapped to the closest beat of the timing point. float GetSnappedDistanceFromDistance(double referenceTime, float distance); } + + public class SnapResult + { + /// + /// The screen space position, potentially altered for snapping. + /// + public Vector2 ScreenSpacePosition; + + /// + /// The resultant time for snapping, if a value could be attained. + /// + public double? Time; + + public SnapResult(Vector2 screenSpacePosition, double? time) + { + ScreenSpacePosition = screenSpacePosition; + Time = time; + } + } } diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index d1cae6b3cd..e38df3d812 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -405,16 +405,19 @@ namespace osu.Game.Screens.Edit.Compose.Components Vector2 movePosition = movementBlueprintOriginalPosition.Value + e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition; // Retrieve a snapped position. - (Vector2 snappedPosition, double snappedTime) = snapProvider.SnapScreenSpacePositionToValidTime(movePosition); + var result = snapProvider.SnapScreenSpacePositionToValidTime(movePosition); // Move the hitobjects. - if (!selectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprint, ToScreenSpace(snappedPosition)))) + if (!selectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprint, result.ScreenSpacePosition))) return true; - // Apply the start time at the newly snapped-to position - double offset = snappedTime - draggedObject.StartTime; - foreach (HitObject obj in selectionHandler.SelectedHitObjects) - obj.StartTime += offset; + if (result.Time.HasValue) + { + // Apply the start time at the newly snapped-to position + double offset = result.Time.Value - draggedObject.StartTime; + foreach (HitObject obj in selectionHandler.SelectedHitObjects) + obj.StartTime += offset; + } return true; } diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index bb6094ebe8..e1a4bca1d6 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -67,7 +67,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private void updatePlacementPosition(Vector2 screenSpacePosition) { - Vector2 snappedPlayfieldPosition = composer.SnapScreenSpacePositionToValidTime(screenSpacePosition).position; + Vector2 snappedPlayfieldPosition = composer.SnapScreenSpacePositionToValidTime(screenSpacePosition).ScreenSpacePosition; currentPlacement.UpdatePosition(ToScreenSpace(snappedPlayfieldPosition)); } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index 1006da28df..ec2b11c0cf 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -181,11 +181,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline [Resolved] private IBeatSnapProvider beatSnapProvider { get; set; } - public (Vector2 position, double time) SnapPositionToValidTime(Vector2 position) => - (position, beatSnapProvider.SnapTime(getTimeFromPosition(position))); - - public (Vector2 position, double time) SnapScreenSpacePositionToValidTime(Vector2 position) => - (position, beatSnapProvider.SnapTime(getTimeFromPosition(Content.ToLocalSpace(position)))); + public SnapResult SnapScreenSpacePositionToValidTime(Vector2 position) => + new SnapResult(position, beatSnapProvider.SnapTime(getTimeFromPosition(Content.ToLocalSpace(position)))); private double getTimeFromPosition(Vector2 localPosition) => (localPosition.X / Content.DrawWidth) * track.Length; diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index b5eae26f98..03e05b75c5 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -275,32 +275,33 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline OnDragHandled?.Invoke(e); - var time = timeline.SnapScreenSpacePositionToValidTime(e.ScreenSpaceMousePosition).time; - - switch (hitObject) + if (timeline.SnapScreenSpacePositionToValidTime(e.ScreenSpaceMousePosition).Time is double time) { - case IHasRepeats repeatHitObject: - // find the number of repeats which can fit in the requested time. - var lengthOfOneRepeat = repeatHitObject.Duration / (repeatHitObject.RepeatCount + 1); - var proposedCount = Math.Max(0, (int)((time - hitObject.StartTime) / lengthOfOneRepeat) - 1); + switch (hitObject) + { + case IHasRepeats repeatHitObject: + // find the number of repeats which can fit in the requested time. + var lengthOfOneRepeat = repeatHitObject.Duration / (repeatHitObject.RepeatCount + 1); + var proposedCount = Math.Max(0, (int)((time - hitObject.StartTime) / lengthOfOneRepeat) - 1); - if (proposedCount == repeatHitObject.RepeatCount) - return; + if (proposedCount == repeatHitObject.RepeatCount) + return; - repeatHitObject.RepeatCount = proposedCount; - break; + repeatHitObject.RepeatCount = proposedCount; + break; - case IHasEndTime endTimeHitObject: - var snappedTime = Math.Max(hitObject.StartTime, beatSnapProvider.SnapTime(time)); + case IHasEndTime endTimeHitObject: + var snappedTime = Math.Max(hitObject.StartTime, beatSnapProvider.SnapTime(time)); - if (endTimeHitObject.EndTime == snappedTime) - return; + if (endTimeHitObject.EndTime == snappedTime) + return; - endTimeHitObject.EndTime = snappedTime; - break; + endTimeHitObject.EndTime = snappedTime; + break; + } + + beatmap.UpdateHitObject(hitObject); } - - beatmap.UpdateHitObject(hitObject); } protected override void OnDragEnd(DragEndEvent e) From ffb8d48fc30874ff81a7c4c6be3dad7fe70fc83b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 20 May 2020 18:32:36 +0900 Subject: [PATCH 1679/2376] Fix osu!mania editor placement regressions --- osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs | 2 +- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 2 ++ .../Edit/Compose/Components/ComposeBlueprintContainer.cs | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index f7951fcc5d..3e1d4f2f3a 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -63,7 +63,7 @@ namespace osu.Game.Rulesets.Mania.Edit drawableRuleset.ScrollingInfo.TimeRange.Value, hoc.DrawHeight); - return new SnapResult(targetPosition, targetTime); + return new SnapResult(screenSpacePosition, targetTime); } protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 7e9bb850af..5018b7bcb6 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -269,7 +269,9 @@ namespace osu.Game.Rulesets.Edit { if (distanceSnapGrid == null) return new SnapResult(screenSpacePosition, null); + // TODO: move distance snap grid to OsuHitObjectComposer. (Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition)); + return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time); } diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index e1a4bca1d6..95e2f41f1f 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -67,9 +67,9 @@ namespace osu.Game.Screens.Edit.Compose.Components private void updatePlacementPosition(Vector2 screenSpacePosition) { - Vector2 snappedPlayfieldPosition = composer.SnapScreenSpacePositionToValidTime(screenSpacePosition).ScreenSpacePosition; + var snapResult = composer.SnapScreenSpacePositionToValidTime(screenSpacePosition); - currentPlacement.UpdatePosition(ToScreenSpace(snappedPlayfieldPosition)); + currentPlacement.UpdatePosition(snapResult.ScreenSpacePosition); } #endregion From 23bf0d000e1013594129a57e24020d4c0e5312b4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 20 May 2020 18:40:55 +0900 Subject: [PATCH 1680/2376] Implement mania beat snapping support --- .../Edit/ManiaHitObjectComposer.cs | 9 ++++++++- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 12 ++++++------ .../Compose/Components/ComposeBlueprintContainer.cs | 9 ++++----- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index 3e1d4f2f3a..053dcd0832 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -63,7 +63,14 @@ namespace osu.Game.Rulesets.Mania.Edit drawableRuleset.ScrollingInfo.TimeRange.Value, hoc.DrawHeight); - return new SnapResult(screenSpacePosition, targetTime); + targetTime = BeatSnapProvider.SnapTime(targetTime); + + screenSpacePosition.Y = hoc.ToScreenSpace( + new Vector2(0, drawableRuleset.ScrollingInfo.Algorithm.PositionAt(targetTime, EditorClock.CurrentTime, drawableRuleset.ScrollingInfo.TimeRange.Value, + hoc.DrawHeight)) + ).Y; + + return new SnapResult(screenSpacePosition, BeatSnapProvider.SnapTime(targetTime)); } protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 5018b7bcb6..b45cdea751 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Edit private IAdjustableClock adjustableClock { get; set; } [Resolved] - private IBeatSnapProvider beatSnapProvider { get; set; } + protected IBeatSnapProvider BeatSnapProvider { get; private set; } protected ComposeBlueprintContainer BlueprintContainer { get; private set; } @@ -278,27 +278,27 @@ 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.BeatmapInfo.BaseDifficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier / BeatSnapProvider.BeatDivisor); } public override float DurationToDistance(double referenceTime, double duration) { - double beatLength = beatSnapProvider.GetBeatLengthAtTime(referenceTime); + double beatLength = BeatSnapProvider.GetBeatLengthAtTime(referenceTime); return (float)(duration / beatLength * GetBeatSnapDistanceAt(referenceTime)); } public override double DistanceToDuration(double referenceTime, float distance) { - double beatLength = beatSnapProvider.GetBeatLengthAtTime(referenceTime); + double beatLength = BeatSnapProvider.GetBeatLengthAtTime(referenceTime); return distance / GetBeatSnapDistanceAt(referenceTime) * beatLength; } public override double GetSnappedDurationFromDistance(double referenceTime, float distance) - => beatSnapProvider.SnapTime(referenceTime + DistanceToDuration(referenceTime, distance), referenceTime) - referenceTime; + => BeatSnapProvider.SnapTime(referenceTime + DistanceToDuration(referenceTime, distance), referenceTime) - referenceTime; public override float GetSnappedDistanceFromDistance(double referenceTime, float distance) { - var snappedEndTime = beatSnapProvider.SnapTime(referenceTime + DistanceToDuration(referenceTime, distance), referenceTime); + var snappedEndTime = BeatSnapProvider.SnapTime(referenceTime + DistanceToDuration(referenceTime, distance), referenceTime); return DurationToDistance(referenceTime, snappedEndTime - referenceTime); } diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 95e2f41f1f..7982cba4e3 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -11,7 +11,6 @@ using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; -using osuTK; namespace osu.Game.Screens.Edit.Compose.Components { @@ -65,9 +64,9 @@ namespace osu.Game.Screens.Edit.Compose.Components createPlacement(); } - private void updatePlacementPosition(Vector2 screenSpacePosition) + private void updatePlacementPosition() { - var snapResult = composer.SnapScreenSpacePositionToValidTime(screenSpacePosition); + var snapResult = composer.SnapScreenSpacePositionToValidTime(inputManager.CurrentState.Mouse.Position); currentPlacement.UpdatePosition(snapResult.ScreenSpacePosition); } @@ -84,7 +83,7 @@ namespace osu.Game.Screens.Edit.Compose.Components removePlacement(); if (currentPlacement != null) - updatePlacementPosition(inputManager.CurrentState.Mouse.Position); + updatePlacementPosition(); } protected sealed override SelectionBlueprint CreateBlueprintFor(HitObject hitObject) @@ -116,7 +115,7 @@ namespace osu.Game.Screens.Edit.Compose.Components placementBlueprintContainer.Child = currentPlacement = blueprint; // Fixes a 1-frame position discrepancy due to the first mouse move event happening in the next frame - updatePlacementPosition(inputManager.CurrentState.Mouse.Position); + updatePlacementPosition(); } } From 970bd86d2e1df8b109046ff0265d01b850b9f435 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 20 May 2020 18:46:15 +0900 Subject: [PATCH 1681/2376] Remove local TimeAt usage in mania placement --- .../Edit/Blueprints/ManiaPlacementBlueprint.cs | 2 +- .../Edit/ManiaHitObjectComposer.cs | 11 +++++++---- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 3 +-- osu.Game/Rulesets/Edit/PlacementBlueprint.cs | 5 +---- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs index 3fb03d642f..4ebc4dae1a 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs @@ -54,7 +54,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints return base.OnMouseDown(e); HitObject.Column = Column.Index; - BeginPlacement(TimeAt(e.ScreenSpaceMousePosition), true); + BeginPlacement(true); return true; } diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index 053dcd0832..724786a1c0 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -46,19 +46,22 @@ namespace osu.Game.Rulesets.Mania.Edit public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) { - var hoc = Playfield.GetColumn(0).HitObjectContainer; + var hoc = ColumnAt(screenSpacePosition)?.HitObjectContainer; - Vector2 targetPosition = hoc.ToLocalSpace(screenSpacePosition); + if (hoc == null) + return new SnapResult(screenSpacePosition, null); + + Vector2 localPosition = hoc.ToLocalSpace(screenSpacePosition); if (drawableRuleset.ScrollingInfo.Direction.Value == ScrollingDirection.Down) { // We're dealing with screen coordinates in which the position decreases towards the centre of the screen resulting in an increase in start time. // The scrolling algorithm instead assumes a top anchor meaning an increase in time corresponds to an increase in position, // so when scrolling downwards the coordinates need to be flipped. - targetPosition.Y = hoc.DrawHeight - targetPosition.Y; + localPosition.Y = hoc.DrawHeight - localPosition.Y; } - double targetTime = drawableRuleset.ScrollingInfo.Algorithm.TimeAt(targetPosition.Y, + double targetTime = drawableRuleset.ScrollingInfo.Algorithm.TimeAt(localPosition.Y, EditorClock.CurrentTime, drawableRuleset.ScrollingInfo.TimeRange.Value, hoc.DrawHeight); diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index b45cdea751..1e328e6b6b 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -245,8 +245,7 @@ namespace osu.Game.Rulesets.Edit { EditorBeatmap.PlacementObject.Value = hitObject; - if (SnapScreenSpacePositionToValidTime(inputManager.CurrentState.Mouse.Position).Time is double time) - hitObject.StartTime = time; + hitObject.StartTime = SnapScreenSpacePositionToValidTime(inputManager.CurrentState.Mouse.Position).Time ?? EditorClock.CurrentTime; } public void EndPlacement(HitObject hitObject, bool commit) diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index c06e50950c..5c506926b8 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -61,12 +61,9 @@ namespace osu.Game.Rulesets.Edit /// /// Signals that the placement of has started. /// - /// The start time of at the placement point. If null, the current clock time is used. /// Whether this call is committing a value for HitObject.StartTime and continuing with further adjustments. - protected void BeginPlacement(double? startTime = null, bool commitStart = false) + protected void BeginPlacement(bool commitStart = false) { - HitObject.StartTime = startTime ?? EditorClock.CurrentTime; - // applies snapping to above time placementHandler.BeginPlacement(HitObject); From 82d6549161052871355aab9327e7a676e728081c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 20 May 2020 19:05:03 +0900 Subject: [PATCH 1682/2376] Pass down snap result and remove local TimeAt usage --- .../Blueprints/HoldNotePlacementBlueprint.cs | 17 +++++++------ .../Blueprints/ManiaPlacementBlueprint.cs | 25 +++---------------- .../HitCircles/HitCirclePlacementBlueprint.cs | 3 +-- .../Sliders/SliderPlacementBlueprint.cs | 4 +-- .../Spinners/SpinnerPlacementBlueprint.cs | 3 +-- osu.Game/Rulesets/Edit/PlacementBlueprint.cs | 4 +-- .../Components/ComposeBlueprintContainer.cs | 2 +- .../Visual/PlacementBlueprintTestScene.cs | 4 +-- 8 files changed, 22 insertions(+), 40 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs index c63e30e98a..5dbd84e370 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs @@ -4,6 +4,7 @@ using System; using osu.Framework.Graphics; using osu.Framework.Input.Events; +using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Edit.Blueprints.Components; using osu.Game.Rulesets.Mania.Objects; using osuTK; @@ -59,23 +60,25 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints private double originalStartTime; - public override void UpdatePosition(Vector2 screenSpacePosition) + public override void UpdatePosition(SnapResult result) { - base.UpdatePosition(screenSpacePosition); + base.UpdatePosition(result); if (PlacementActive) { - var endTime = TimeAt(screenSpacePosition); - - HitObject.StartTime = endTime < originalStartTime ? endTime : originalStartTime; - HitObject.Duration = Math.Abs(endTime - originalStartTime); + if (result.Time is double endTime) + { + HitObject.StartTime = endTime < originalStartTime ? endTime : originalStartTime; + HitObject.Duration = Math.Abs(endTime - originalStartTime); + } } else { headPiece.Width = tailPiece.Width = SnappedWidth; headPiece.X = tailPiece.X = SnappedMousePosition.X; - originalStartTime = HitObject.StartTime = TimeAt(screenSpacePosition); + if (result.Time is double startTime) + originalStartTime = HitObject.StartTime = startTime; } } } diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs index 4ebc4dae1a..4d10afd289 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs @@ -58,10 +58,10 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints return true; } - public override void UpdatePosition(Vector2 screenSpacePosition) + public override void UpdatePosition(SnapResult result) { if (!PlacementActive) - Column = ColumnAt(screenSpacePosition); + Column = ColumnAt(result.ScreenSpacePosition); if (Column == null) return; @@ -69,26 +69,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints // Snap to the column var parentPos = Parent.ToLocalSpace(Column.ToScreenSpace(new Vector2(Column.DrawWidth / 2, 0))); - SnappedMousePosition = new Vector2(parentPos.X, Parent.ToLocalSpace(screenSpacePosition).Y); - } - - protected double TimeAt(Vector2 screenSpacePosition) - { - if (Column == null) - return 0; - - var hitObjectContainer = Column.HitObjectContainer; - - // If we're scrolling downwards, a position of 0 is actually further away from the hit target - // so we need to flip the vertical coordinate in the hitobject container's space - var hitObjectPos = mouseToHitObjectPosition(Column.HitObjectContainer.ToLocalSpace(screenSpacePosition)).Y; - if (scrollingInfo.Direction.Value == ScrollingDirection.Down) - hitObjectPos = hitObjectContainer.DrawHeight - hitObjectPos; - - return scrollingInfo.Algorithm.TimeAt(hitObjectPos, - EditorClock.CurrentTime, - scrollingInfo.TimeRange.Value, - hitObjectContainer.DrawHeight); + SnappedMousePosition = new Vector2(parentPos.X, Parent.ToLocalSpace(result.ScreenSpacePosition).Y); } protected float PositionAt(double time) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs index dad199715e..e12dec2668 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs @@ -5,7 +5,6 @@ using osu.Framework.Input.Events; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; using osu.Game.Rulesets.Osu.Objects; -using osuTK; using osuTK.Input; namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles @@ -40,6 +39,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles return base.OnMouseDown(e); } - public override void UpdatePosition(Vector2 screenSpacePosition) => HitObject.Position = ToLocalSpace(screenSpacePosition); + public override void UpdatePosition(SnapResult result) => HitObject.Position = ToLocalSpace(result.ScreenSpacePosition); } } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index ac30f5a762..59ec92c79e 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -67,13 +67,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders inputManager = GetContainingInputManager(); } - public override void UpdatePosition(Vector2 screenSpacePosition) + public override void UpdatePosition(SnapResult result) { switch (state) { case PlacementState.Initial: BeginPlacement(); - HitObject.Position = ToLocalSpace(screenSpacePosition); + HitObject.Position = ToLocalSpace(result.ScreenSpacePosition); break; case PlacementState.Body: diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs index 74b563d922..546f0e5981 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs @@ -8,7 +8,6 @@ using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners.Components; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.UI; -using osuTK; using osuTK.Input; namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners @@ -61,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners return true; } - public override void UpdatePosition(Vector2 screenSpacePosition) + public override void UpdatePosition(SnapResult result) { } } diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index 5c506926b8..bab9bf71ef 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -86,8 +86,8 @@ namespace osu.Game.Rulesets.Edit /// /// Updates the position of this to a new screen-space position. /// - /// The screen-space position. - public abstract void UpdatePosition(Vector2 screenSpacePosition); + /// The snap result information. + public abstract void UpdatePosition(SnapResult snapResult); /// /// Invokes , diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 7982cba4e3..0b5d8262fd 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -68,7 +68,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { var snapResult = composer.SnapScreenSpacePositionToValidTime(inputManager.CurrentState.Mouse.Position); - currentPlacement.UpdatePosition(snapResult.ScreenSpacePosition); + currentPlacement.UpdatePosition(snapResult); } #endregion diff --git a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs index dc67d28f63..a4e629b6f5 100644 --- a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs +++ b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs @@ -71,7 +71,7 @@ namespace osu.Game.Tests.Visual { base.Update(); - currentBlueprint.UpdatePosition(InputManager.CurrentState.Mouse.Position); + currentBlueprint.UpdatePosition(new SnapResult(InputManager.CurrentState.Mouse.Position, null)); } public override void Add(Drawable drawable) @@ -81,7 +81,7 @@ namespace osu.Game.Tests.Visual if (drawable is PlacementBlueprint blueprint) { blueprint.Show(); - blueprint.UpdatePosition(InputManager.CurrentState.Mouse.Position); + blueprint.UpdatePosition(new SnapResult(InputManager.CurrentState.Mouse.Position, null)); } } From 62092e3f5b4c60b18505a502a311b5db3dc1aa6a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 20 May 2020 19:09:04 +0900 Subject: [PATCH 1683/2376] Propagate mania column in SnapResult --- .../Blueprints/ManiaPlacementBlueprint.cs | 8 +------- .../Edit/ManiaHitObjectComposer.cs | 19 ++++++++++++++++--- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs index 4d10afd289..2f1b38d564 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs @@ -33,9 +33,6 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints /// protected float SnappedWidth { get; private set; } - [Resolved] - private IManiaHitObjectComposer composer { get; set; } - [Resolved] private IScrollingInfo scrollingInfo { get; set; } @@ -61,7 +58,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints public override void UpdatePosition(SnapResult result) { if (!PlacementActive) - Column = ColumnAt(result.ScreenSpacePosition); + Column = (result as ManiaSnapResult)?.Column; if (Column == null) return; @@ -85,9 +82,6 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints return hitObjectToMousePosition(Column.HitObjectContainer.ToSpaceOfOtherDrawable(new Vector2(0, pos), Parent)).Y; } - protected Column ColumnAt(Vector2 screenSpacePosition) - => composer.ColumnAt(screenSpacePosition); - /// /// Converts a mouse position to a hitobject position. /// diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index 724786a1c0..89ccf0019a 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -46,11 +46,13 @@ namespace osu.Game.Rulesets.Mania.Edit public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) { - var hoc = ColumnAt(screenSpacePosition)?.HitObjectContainer; + var column = ColumnAt(screenSpacePosition); - if (hoc == null) + if (column == null) return new SnapResult(screenSpacePosition, null); + var hoc = column.HitObjectContainer; + Vector2 localPosition = hoc.ToLocalSpace(screenSpacePosition); if (drawableRuleset.ScrollingInfo.Direction.Value == ScrollingDirection.Down) @@ -73,7 +75,7 @@ namespace osu.Game.Rulesets.Mania.Edit hoc.DrawHeight)) ).Y; - return new SnapResult(screenSpacePosition, BeatSnapProvider.SnapTime(targetTime)); + return new ManiaSnapResult(screenSpacePosition, BeatSnapProvider.SnapTime(targetTime), column); } protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) @@ -94,4 +96,15 @@ namespace osu.Game.Rulesets.Mania.Edit new HoldNoteCompositionTool() }; } + + public class ManiaSnapResult : SnapResult + { + public readonly Column Column; + + public ManiaSnapResult(Vector2 screenSpacePosition, double time, Column column) + : base(screenSpacePosition, time) + { + Column = column; + } + } } From 2f78866dfb452c091cca286ec1d7744a3c4deab3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 20 May 2020 19:23:17 +0900 Subject: [PATCH 1684/2376] Move positioning out of mania blueprints --- .../Blueprints/HoldNotePlacementBlueprint.cs | 7 +++- .../Blueprints/ManiaPlacementBlueprint.cs | 42 ------------------- .../Edit/Blueprints/NotePlacementBlueprint.cs | 20 +++++---- .../Edit/ManiaHitObjectComposer.cs | 17 ++++---- 4 files changed, 26 insertions(+), 60 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs index 5dbd84e370..055b92b39d 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs @@ -74,8 +74,11 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints } else { - headPiece.Width = tailPiece.Width = SnappedWidth; - headPiece.X = tailPiece.X = SnappedMousePosition.X; + if (result is ManiaSnapResult maniaResult) + { + headPiece.Width = tailPiece.Width = maniaResult.Column.DrawWidth; + headPiece.X = tailPiece.X = ToLocalSpace(result.ScreenSpacePosition).X; + } if (result.Time is double startTime) originalStartTime = HitObject.StartTime = startTime; diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs index 2f1b38d564..e8c7aea814 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs @@ -23,16 +23,6 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints protected Column Column; - /// - /// The current mouse position, snapped to the closest column. - /// - protected Vector2 SnappedMousePosition { get; private set; } - - /// - /// The width of the closest column to the current mouse position. - /// - protected float SnappedWidth { get; private set; } - [Resolved] private IScrollingInfo scrollingInfo { get; set; } @@ -59,14 +49,6 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints { if (!PlacementActive) Column = (result as ManiaSnapResult)?.Column; - - if (Column == null) return; - - SnappedWidth = Column.DrawWidth; - - // Snap to the column - var parentPos = Parent.ToLocalSpace(Column.ToScreenSpace(new Vector2(Column.DrawWidth / 2, 0))); - SnappedMousePosition = new Vector2(parentPos.X, Parent.ToLocalSpace(result.ScreenSpacePosition).Y); } protected float PositionAt(double time) @@ -82,30 +64,6 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints return hitObjectToMousePosition(Column.HitObjectContainer.ToSpaceOfOtherDrawable(new Vector2(0, pos), Parent)).Y; } - /// - /// Converts a mouse position to a hitobject position. - /// - /// - /// Blueprints are centred on the mouse position, such that the hitobject position is anchored at the top or bottom of the blueprint depending on the scroll direction. - /// - /// The mouse position. - /// The resulting hitobject position, acnhored at the top or bottom of the blueprint depending on the scroll direction. - private Vector2 mouseToHitObjectPosition(Vector2 mousePosition) - { - switch (scrollingInfo.Direction.Value) - { - case ScrollingDirection.Up: - mousePosition.Y -= DefaultNotePiece.NOTE_HEIGHT / 2; - break; - - case ScrollingDirection.Down: - mousePosition.Y += DefaultNotePiece.NOTE_HEIGHT / 2; - break; - } - - return mousePosition; - } - /// /// Converts a hitobject position to a mouse position. /// diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs index a4c0791253..5f6db2e6dd 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs @@ -3,6 +3,7 @@ using osu.Framework.Graphics; using osu.Framework.Input.Events; +using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Edit.Blueprints.Components; using osu.Game.Rulesets.Mania.Objects; using osuTK.Input; @@ -11,22 +12,25 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints { public class NotePlacementBlueprint : ManiaPlacementBlueprint { + private readonly EditNotePiece piece; + public NotePlacementBlueprint() : base(new Note()) { - Origin = Anchor.Centre; + RelativeSizeAxes = Axes.Both; - AutoSizeAxes = Axes.Y; - - InternalChild = new EditNotePiece { RelativeSizeAxes = Axes.X }; + InternalChild = piece = new EditNotePiece { Origin = Anchor.Centre }; } - protected override void Update() + public override void UpdatePosition(SnapResult result) { - base.Update(); + base.UpdatePosition(result); - Width = SnappedWidth; - Position = SnappedMousePosition; + if (result is ManiaSnapResult maniaResult) + { + piece.Width = maniaResult.Column.DrawWidth; + piece.Position = ToLocalSpace(result.ScreenSpacePosition); + } } protected override bool OnMouseDown(MouseDownEvent e) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index 89ccf0019a..c38952fd29 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -55,7 +55,9 @@ namespace osu.Game.Rulesets.Mania.Edit Vector2 localPosition = hoc.ToLocalSpace(screenSpacePosition); - if (drawableRuleset.ScrollingInfo.Direction.Value == ScrollingDirection.Down) + var scrollInfo = drawableRuleset.ScrollingInfo; + + if (scrollInfo.Direction.Value == ScrollingDirection.Down) { // We're dealing with screen coordinates in which the position decreases towards the centre of the screen resulting in an increase in start time. // The scrolling algorithm instead assumes a top anchor meaning an increase in time corresponds to an increase in position, @@ -63,19 +65,18 @@ namespace osu.Game.Rulesets.Mania.Edit localPosition.Y = hoc.DrawHeight - localPosition.Y; } - double targetTime = drawableRuleset.ScrollingInfo.Algorithm.TimeAt(localPosition.Y, + double targetTime = scrollInfo.Algorithm.TimeAt(localPosition.Y, EditorClock.CurrentTime, - drawableRuleset.ScrollingInfo.TimeRange.Value, + scrollInfo.TimeRange.Value, hoc.DrawHeight); targetTime = BeatSnapProvider.SnapTime(targetTime); - screenSpacePosition.Y = hoc.ToScreenSpace( - new Vector2(0, drawableRuleset.ScrollingInfo.Algorithm.PositionAt(targetTime, EditorClock.CurrentTime, drawableRuleset.ScrollingInfo.TimeRange.Value, - hoc.DrawHeight)) - ).Y; + var localPos = new Vector2( + hoc.DrawWidth / 2, + scrollInfo.Algorithm.PositionAt(targetTime, EditorClock.CurrentTime, scrollInfo.TimeRange.Value, hoc.DrawHeight)); - return new ManiaSnapResult(screenSpacePosition, BeatSnapProvider.SnapTime(targetTime), column); + return new ManiaSnapResult(hoc.ToScreenSpace(localPos), BeatSnapProvider.SnapTime(targetTime), column); } protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) From 26fb779f4d7b6929d90ecc434070d88401046387 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 20 May 2020 19:52:57 +0900 Subject: [PATCH 1685/2376] Move remaining positioning logic local to hold note blueprint --- .../Blueprints/HoldNotePlacementBlueprint.cs | 22 +++++++++- .../Blueprints/ManiaPlacementBlueprint.cs | 41 ------------------- 2 files changed, 20 insertions(+), 43 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs index 055b92b39d..38a12ed2ed 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs @@ -2,11 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Input.Events; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Edit.Blueprints.Components; using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.UI.Scrolling; using osuTK; using osuTK.Input; @@ -37,8 +39,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints if (Column != null) { - headPiece.Y = PositionAt(HitObject.StartTime); - tailPiece.Y = PositionAt(HitObject.EndTime); + headPiece.Y = positionAt(HitObject.StartTime); + tailPiece.Y = positionAt(HitObject.EndTime); } var topPosition = new Vector2(headPiece.DrawPosition.X, Math.Min(headPiece.DrawPosition.Y, tailPiece.DrawPosition.Y)); @@ -84,5 +86,21 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints originalStartTime = HitObject.StartTime = startTime; } } + + [Resolved] + private IScrollingInfo scrollingInfo { get; set; } + + private float positionAt(double time) + { + var pos = scrollingInfo.Algorithm.PositionAt(time, + EditorClock.CurrentTime, + scrollingInfo.TimeRange.Value, + Column.HitObjectContainer.DrawHeight); + + if (scrollingInfo.Direction.Value == ScrollingDirection.Down) + pos = Column.HitObjectContainer.DrawHeight - pos; + + return Column.HitObjectContainer.ToSpaceOfOtherDrawable(new Vector2(0, pos), Parent).Y; + } } } diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs index e8c7aea814..e83495cec4 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs @@ -1,16 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Objects; -using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; using osu.Game.Rulesets.Mania.UI; -using osu.Game.Rulesets.UI.Scrolling; -using osuTK; using osuTK.Input; namespace osu.Game.Rulesets.Mania.Edit.Blueprints @@ -23,9 +19,6 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints protected Column Column; - [Resolved] - private IScrollingInfo scrollingInfo { get; set; } - protected ManiaPlacementBlueprint(T hitObject) : base(hitObject) { @@ -50,39 +43,5 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints if (!PlacementActive) Column = (result as ManiaSnapResult)?.Column; } - - protected float PositionAt(double time) - { - var pos = scrollingInfo.Algorithm.PositionAt(time, - EditorClock.CurrentTime, - scrollingInfo.TimeRange.Value, - Column.HitObjectContainer.DrawHeight); - - if (scrollingInfo.Direction.Value == ScrollingDirection.Down) - pos = Column.HitObjectContainer.DrawHeight - pos; - - return hitObjectToMousePosition(Column.HitObjectContainer.ToSpaceOfOtherDrawable(new Vector2(0, pos), Parent)).Y; - } - - /// - /// Converts a hitobject position to a mouse position. - /// - /// The hitobject position. - /// The resulting mouse position, anchored at the centre of the hitobject. - private Vector2 hitObjectToMousePosition(Vector2 hitObjectPosition) - { - switch (scrollingInfo.Direction.Value) - { - case ScrollingDirection.Up: - hitObjectPosition.Y += DefaultNotePiece.NOTE_HEIGHT / 2; - break; - - case ScrollingDirection.Down: - hitObjectPosition.Y -= DefaultNotePiece.NOTE_HEIGHT / 2; - break; - } - - return hitObjectPosition; - } } } From 19e2da9c73e03b674ad2525bc59bf8b6d8e16571 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 20 May 2020 19:59:36 +0900 Subject: [PATCH 1686/2376] Fix down scrolling giving incorrect positioning data --- osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index c38952fd29..7ad2ce4699 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -72,11 +72,17 @@ namespace osu.Game.Rulesets.Mania.Edit targetTime = BeatSnapProvider.SnapTime(targetTime); - var localPos = new Vector2( + localPosition = new Vector2( hoc.DrawWidth / 2, scrollInfo.Algorithm.PositionAt(targetTime, EditorClock.CurrentTime, scrollInfo.TimeRange.Value, hoc.DrawHeight)); - return new ManiaSnapResult(hoc.ToScreenSpace(localPos), BeatSnapProvider.SnapTime(targetTime), column); + if (scrollInfo.Direction.Value == ScrollingDirection.Down) + { + // reapply the above. + localPosition.Y = hoc.DrawHeight - localPosition.Y; + } + + return new ManiaSnapResult(hoc.ToScreenSpace(localPosition), BeatSnapProvider.SnapTime(targetTime), column); } protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) From 7c9fbb6fcfb6a6fdfe6706bb1b320080a76ffc1b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 20 May 2020 21:03:03 +0900 Subject: [PATCH 1687/2376] Split out classes --- .../Edit/ManiaHitObjectComposer.cs | 11 ------- .../Edit/ManiaSnapResult.cs | 20 +++++++++++++ .../Rulesets/Edit/IPositionSnapProvider.cs | 19 ------------ osu.Game/Rulesets/Edit/SnapResult.cs | 29 +++++++++++++++++++ 4 files changed, 49 insertions(+), 30 deletions(-) create mode 100644 osu.Game.Rulesets.Mania/Edit/ManiaSnapResult.cs create mode 100644 osu.Game/Rulesets/Edit/SnapResult.cs diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index 7ad2ce4699..3287c10531 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -103,15 +103,4 @@ namespace osu.Game.Rulesets.Mania.Edit new HoldNoteCompositionTool() }; } - - public class ManiaSnapResult : SnapResult - { - public readonly Column Column; - - public ManiaSnapResult(Vector2 screenSpacePosition, double time, Column column) - : base(screenSpacePosition, time) - { - Column = column; - } - } } diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaSnapResult.cs b/osu.Game.Rulesets.Mania/Edit/ManiaSnapResult.cs new file mode 100644 index 0000000000..b94f5e51c4 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Edit/ManiaSnapResult.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.Rulesets.Edit; +using osu.Game.Rulesets.Mania.UI; +using osuTK; + +namespace osu.Game.Rulesets.Mania.Edit +{ + public class ManiaSnapResult : SnapResult + { + public readonly Column Column; + + public ManiaSnapResult(Vector2 screenSpacePosition, double time, Column column) + : base(screenSpacePosition, time) + { + Column = column; + } + } +} diff --git a/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs b/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs index d95800f403..c854c06031 100644 --- a/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs @@ -53,23 +53,4 @@ namespace osu.Game.Rulesets.Edit /// A value that represents snapped to the closest beat of the timing point. float GetSnappedDistanceFromDistance(double referenceTime, float distance); } - - public class SnapResult - { - /// - /// The screen space position, potentially altered for snapping. - /// - public Vector2 ScreenSpacePosition; - - /// - /// The resultant time for snapping, if a value could be attained. - /// - public double? Time; - - public SnapResult(Vector2 screenSpacePosition, double? time) - { - ScreenSpacePosition = screenSpacePosition; - Time = time; - } - } } diff --git a/osu.Game/Rulesets/Edit/SnapResult.cs b/osu.Game/Rulesets/Edit/SnapResult.cs new file mode 100644 index 0000000000..5d07d7b233 --- /dev/null +++ b/osu.Game/Rulesets/Edit/SnapResult.cs @@ -0,0 +1,29 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osuTK; + +namespace osu.Game.Rulesets.Edit +{ + /// + /// The result of a position/time snapping process. + /// + public class SnapResult + { + /// + /// The screen space position, potentially altered for snapping. + /// + public Vector2 ScreenSpacePosition; + + /// + /// The resultant time for snapping, if a value could be attained. + /// + public double? Time; + + public SnapResult(Vector2 screenSpacePosition, double? time) + { + ScreenSpacePosition = screenSpacePosition; + Time = time; + } + } +} From e3cec9cf6c7251c16d075bf0b27a8546f7ca0692 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 20 May 2020 21:13:08 +0900 Subject: [PATCH 1688/2376] Simplify column assignment --- .../Edit/Blueprints/ManiaPlacementBlueprint.cs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs index e83495cec4..af57b4fa07 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs @@ -17,7 +17,20 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints { protected new T HitObject => (T)base.HitObject; - protected Column Column; + private Column column; + + public Column Column + { + get => column; + set + { + if (value == column) + return; + + column = value; + HitObject.Column = column.Index; + } + } protected ManiaPlacementBlueprint(T hitObject) : base(hitObject) @@ -31,9 +44,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints return false; if (Column == null) - return base.OnMouseDown(e); + return false; - HitObject.Column = Column.Index; BeginPlacement(true); return true; } From 63b5f1a376c1f6d3dce6c6040653297839acf04c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 20 May 2020 21:14:20 +0900 Subject: [PATCH 1689/2376] Remove unnecessary IRequireHighFrequencyMousePosition --- .../Edit/Blueprints/ManiaPlacementBlueprint.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs index af57b4fa07..8d3b3ea583 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; -using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Objects; @@ -11,8 +10,7 @@ using osuTK.Input; namespace osu.Game.Rulesets.Mania.Edit.Blueprints { - public abstract class ManiaPlacementBlueprint : PlacementBlueprint, - IRequireHighFrequencyMousePosition // the playfield could be moving behind us + public abstract class ManiaPlacementBlueprint : PlacementBlueprint where T : ManiaHitObject { protected new T HitObject => (T)base.HitObject; From 69db62b78a0d996843a07afc152b8f2aeea1c7e4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 20 May 2020 21:42:21 +0900 Subject: [PATCH 1690/2376] Combine implementation of time-to-position lookup --- .../ManiaPlacementBlueprintTestScene.cs | 2 ++ .../ManiaSelectionBlueprintTestScene.cs | 2 ++ .../Blueprints/HoldNotePlacementBlueprint.cs | 23 ++++----------- .../Edit/IManiaHitObjectComposer.cs | 2 ++ .../Edit/ManiaHitObjectComposer.cs | 28 ++++++++++++------- 5 files changed, 29 insertions(+), 28 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs index aac77c9c1c..be3e205f36 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs @@ -49,6 +49,8 @@ namespace osu.Game.Rulesets.Mania.Tests public Column ColumnAt(Vector2 screenSpacePosition) => column; + public Vector2 ScreenSpacePositionAtTime(double time, Column column = null) => Vector2.Zero; + public int TotalColumns => 1; } } diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaSelectionBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/ManiaSelectionBlueprintTestScene.cs index b598893e8c..3d654466ed 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaSelectionBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaSelectionBlueprintTestScene.cs @@ -33,6 +33,8 @@ namespace osu.Game.Rulesets.Mania.Tests public Column ColumnAt(Vector2 screenSpacePosition) => column; + public Vector2 ScreenSpacePositionAtTime(double time, Column column = null) => Vector2.Zero; + public int TotalColumns => 1; } } diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs index 38a12ed2ed..31bf76edd0 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs @@ -20,6 +20,9 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints private readonly EditNotePiece headPiece; private readonly EditNotePiece tailPiece; + [Resolved] + private IManiaHitObjectComposer composer { get; set; } + public HoldNotePlacementBlueprint() : base(new HoldNote()) { @@ -39,8 +42,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints if (Column != null) { - headPiece.Y = positionAt(HitObject.StartTime); - tailPiece.Y = positionAt(HitObject.EndTime); + headPiece.Y = Parent.ToLocalSpace(composer.ScreenSpacePositionAtTime(HitObject.StartTime, Column)).Y; + tailPiece.Y = Parent.ToLocalSpace(composer.ScreenSpacePositionAtTime(HitObject.EndTime, Column)).Y; } var topPosition = new Vector2(headPiece.DrawPosition.X, Math.Min(headPiece.DrawPosition.Y, tailPiece.DrawPosition.Y)); @@ -86,21 +89,5 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints originalStartTime = HitObject.StartTime = startTime; } } - - [Resolved] - private IScrollingInfo scrollingInfo { get; set; } - - private float positionAt(double time) - { - var pos = scrollingInfo.Algorithm.PositionAt(time, - EditorClock.CurrentTime, - scrollingInfo.TimeRange.Value, - Column.HitObjectContainer.DrawHeight); - - if (scrollingInfo.Direction.Value == ScrollingDirection.Down) - pos = Column.HitObjectContainer.DrawHeight - pos; - - return Column.HitObjectContainer.ToSpaceOfOtherDrawable(new Vector2(0, pos), Parent).Y; - } } } diff --git a/osu.Game.Rulesets.Mania/Edit/IManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/IManiaHitObjectComposer.cs index f64bab1fae..f1915cd85a 100644 --- a/osu.Game.Rulesets.Mania/Edit/IManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/IManiaHitObjectComposer.cs @@ -10,6 +10,8 @@ namespace osu.Game.Rulesets.Mania.Edit { Column ColumnAt(Vector2 screenSpacePosition); + Vector2 ScreenSpacePositionAtTime(double time, Column column = null); + int TotalColumns { get; } } } diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index 3287c10531..0cae26b51c 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -53,6 +53,7 @@ namespace osu.Game.Rulesets.Mania.Edit var hoc = column.HitObjectContainer; + // convert to local space of column so we can snap and fetch correct location. Vector2 localPosition = hoc.ToLocalSpace(screenSpacePosition); var scrollInfo = drawableRuleset.ScrollingInfo; @@ -65,24 +66,31 @@ namespace osu.Game.Rulesets.Mania.Edit localPosition.Y = hoc.DrawHeight - localPosition.Y; } - double targetTime = scrollInfo.Algorithm.TimeAt(localPosition.Y, - EditorClock.CurrentTime, - scrollInfo.TimeRange.Value, - hoc.DrawHeight); + double targetTime = scrollInfo.Algorithm.TimeAt(localPosition.Y, EditorClock.CurrentTime, scrollInfo.TimeRange.Value, hoc.DrawHeight); + // apply beat snapping targetTime = BeatSnapProvider.SnapTime(targetTime); - localPosition = new Vector2( - hoc.DrawWidth / 2, - scrollInfo.Algorithm.PositionAt(targetTime, EditorClock.CurrentTime, scrollInfo.TimeRange.Value, hoc.DrawHeight)); + // convert back to screen space + screenSpacePosition = ScreenSpacePositionAtTime(targetTime, column); + + return new ManiaSnapResult(screenSpacePosition, targetTime, column); + } + + public Vector2 ScreenSpacePositionAtTime(double time, Column column = null) + { + var hoc = (column ?? Playfield.GetColumn(0)).HitObjectContainer; + var scrollInfo = drawableRuleset.ScrollingInfo; + + var pos = scrollInfo.Algorithm.PositionAt(time, EditorClock.CurrentTime, scrollInfo.TimeRange.Value, hoc.DrawHeight); if (scrollInfo.Direction.Value == ScrollingDirection.Down) { - // reapply the above. - localPosition.Y = hoc.DrawHeight - localPosition.Y; + // as explained above + pos = hoc.DrawHeight - pos; } - return new ManiaSnapResult(hoc.ToScreenSpace(localPosition), BeatSnapProvider.SnapTime(targetTime), column); + return hoc.ToScreenSpace(new Vector2(hoc.DrawWidth / 2, pos)); } protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) From b5a7023312d72670a155b6435354cccd817aa748 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 20 May 2020 21:46:52 +0900 Subject: [PATCH 1691/2376] Seek to start time after placement, not end --- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 883288d6d7..10dffc6aa8 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -257,7 +257,7 @@ namespace osu.Game.Rulesets.Edit { EditorBeatmap.Add(hitObject); - adjustableClock.Seek(hitObject.GetEndTime()); + adjustableClock.Seek(hitObject.StartTime); } showGridFor(Enumerable.Empty()); From e09a1bf546d7bdd12cdd5670c2d0b2398c8242a8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 20 May 2020 21:50:52 +0900 Subject: [PATCH 1692/2376] Only seek forwards if not already beyond the placed object --- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 10dffc6aa8..67216b019d 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -257,7 +257,8 @@ namespace osu.Game.Rulesets.Edit { EditorBeatmap.Add(hitObject); - adjustableClock.Seek(hitObject.StartTime); + if (adjustableClock.CurrentTime < hitObject.StartTime) + adjustableClock.Seek(hitObject.StartTime); } showGridFor(Enumerable.Empty()); From e018d0744152e92df3e4559b7ce98551c30a57f2 Mon Sep 17 00:00:00 2001 From: Shivam Date: Wed, 20 May 2020 16:30:38 +0200 Subject: [PATCH 1693/2376] Use one constant for STABLE_CONFIG location string --- osu.Game.Tournament/IPC/FileBasedIPC.cs | 4 +--- osu.Game.Tournament/Models/StableInfo.cs | 4 ++++ osu.Game.Tournament/Screens/SetupScreen.cs | 1 - osu.Game.Tournament/Screens/StablePathSelectScreen.cs | 4 +--- osu.Game.Tournament/TournamentGameBase.cs | 8 ++++---- 5 files changed, 10 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tournament/IPC/FileBasedIPC.cs b/osu.Game.Tournament/IPC/FileBasedIPC.cs index 6d1cd7cc3c..d2d74e94b2 100644 --- a/osu.Game.Tournament/IPC/FileBasedIPC.cs +++ b/osu.Game.Tournament/IPC/FileBasedIPC.cs @@ -40,8 +40,6 @@ namespace osu.Game.Tournament.IPC [Resolved] private StableInfo stableInfo { get; set; } - private const string stable_config = "tournament/stable.json"; - public Storage IPCStorage { get; private set; } [Resolved] @@ -196,7 +194,7 @@ namespace osu.Game.Tournament.IPC private void saveStablePath() { - using (var stream = tournamentStorage.GetStream(stable_config, FileAccess.Write, FileMode.Create)) + using (var stream = tournamentStorage.GetStream(StableInfo.STABLE_CONFIG, FileAccess.Write, FileMode.Create)) using (var sw = new StreamWriter(stream)) { sw.Write(JsonConvert.SerializeObject(stableInfo, diff --git a/osu.Game.Tournament/Models/StableInfo.cs b/osu.Game.Tournament/Models/StableInfo.cs index 4818842151..63423ca6fa 100644 --- a/osu.Game.Tournament/Models/StableInfo.cs +++ b/osu.Game.Tournament/Models/StableInfo.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using Newtonsoft.Json; using osu.Framework.Bindables; namespace osu.Game.Tournament.Models @@ -13,5 +14,8 @@ namespace osu.Game.Tournament.Models public class StableInfo { public Bindable StablePath = new Bindable(string.Empty); + + [JsonIgnore] + public const string STABLE_CONFIG = "tournament/stable.json"; } } diff --git a/osu.Game.Tournament/Screens/SetupScreen.cs b/osu.Game.Tournament/Screens/SetupScreen.cs index 1c479bdec4..9f8f81aa80 100644 --- a/osu.Game.Tournament/Screens/SetupScreen.cs +++ b/osu.Game.Tournament/Screens/SetupScreen.cs @@ -28,7 +28,6 @@ namespace osu.Game.Tournament.Screens private LoginOverlay loginOverlay; private ActionableInfo resolution; - private const string stable_config = "tournament/stable.json"; [Resolved] private MatchIPCInfo ipc { get; set; } diff --git a/osu.Game.Tournament/Screens/StablePathSelectScreen.cs b/osu.Game.Tournament/Screens/StablePathSelectScreen.cs index 5c488ae352..a42a5dc0fc 100644 --- a/osu.Game.Tournament/Screens/StablePathSelectScreen.cs +++ b/osu.Game.Tournament/Screens/StablePathSelectScreen.cs @@ -26,8 +26,6 @@ namespace osu.Game.Tournament.Screens { private DirectorySelector directorySelector; - private const string stable_config = "tournament/stable.json"; - [Resolved] private StableInfo stableInfo { get; set; } @@ -150,7 +148,7 @@ namespace osu.Game.Tournament.Screens try { - using (var stream = storage.GetStream(stable_config, FileAccess.Write, FileMode.Create)) + using (var stream = storage.GetStream(StableInfo.STABLE_CONFIG, FileAccess.Write, FileMode.Create)) using (var sw = new StreamWriter(stream)) { sw.Write(JsonConvert.SerializeObject(stableInfo, diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 31c56c7fc4..00946399fb 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -147,7 +147,10 @@ namespace osu.Game.Tournament private void readStableConfig() { - if (storage.Exists(stable_config)) + if (stableInfo == null) + stableInfo = new StableInfo(); + + if (storage.Exists(StableInfo.STABLE_CONFIG)) { using (Stream stream = storage.GetStream(stable_config, FileAccess.Read, FileMode.Open)) using (var sr = new StreamReader(stream)) @@ -156,9 +159,6 @@ namespace osu.Game.Tournament } } - if (stableInfo == null) - stableInfo = new StableInfo(); - dependencies.Cache(stableInfo); } From 1b8d657eade8b02ace6f42d5d97d45aa55e320c1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 20 May 2020 23:46:47 +0900 Subject: [PATCH 1694/2376] Implement score panel list --- .../Visual/Ranking/TestSceneScorePanelList.cs | 69 ++++++++++++++++ osu.Game/Screens/Ranking/ScorePanel.cs | 20 +++-- osu.Game/Screens/Ranking/ScorePanelList.cs | 80 +++++++++++++++++++ 3 files changed, 163 insertions(+), 6 deletions(-) create mode 100644 osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs create mode 100644 osu.Game/Screens/Ranking/ScorePanelList.cs diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs new file mode 100644 index 0000000000..4964af8784 --- /dev/null +++ b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs @@ -0,0 +1,69 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Graphics; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Screens.Ranking; +using osu.Game.Tests.Beatmaps; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual.Ranking +{ + public class TestSceneScorePanelList : OsuTestScene + { + public TestSceneScorePanelList() + { + var list = new ScorePanelList + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + + Add(list); + + list.AddScore(createScore()); + list.AddScore(createScore()); + list.AddScore(createScore()); + list.AddScore(createScore()); + list.AddScore(createScore()); + list.AddScore(createScore()); + list.AddScore(createScore()); + list.AddScore(createScore()); + list.AddScore(createScore()); + list.AddScore(createScore()); + list.AddScore(createScore()); + list.AddScore(createScore()); + list.AddScore(createScore()); + list.AddScore(createScore()); + } + + private ScoreInfo createScore() => new ScoreInfo + { + User = new User + { + Id = 2, + Username = "peppy", + CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + }, + Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, + Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }, + TotalScore = 2845370, + Accuracy = 0.95, + MaxCombo = 999, + Rank = ScoreRank.S, + Date = DateTimeOffset.Now, + Statistics = + { + { HitResult.Miss, 1 }, + { HitResult.Meh, 50 }, + { HitResult.Good, 100 }, + { HitResult.Great, 300 }, + } + }; + } +} diff --git a/osu.Game/Screens/Ranking/ScorePanel.cs b/osu.Game/Screens/Ranking/ScorePanel.cs index bf57cb4dd9..baca2fd9e1 100644 --- a/osu.Game/Screens/Ranking/ScorePanel.cs +++ b/osu.Game/Screens/Ranking/ScorePanel.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; using osu.Game.Scoring; using osu.Game.Screens.Ranking.Contracted; using osu.Game.Screens.Ranking.Expanded; @@ -75,8 +76,7 @@ namespace osu.Game.Screens.Ranking private static readonly Color4 contracted_middle_layer_colour = Color4Extensions.FromHex("#353535"); public event Action StateChanged; - - private readonly ScoreInfo score; + public readonly ScoreInfo Score; private Container topLayerContainer; private Drawable topLayerBackground; @@ -90,7 +90,7 @@ namespace osu.Game.Screens.Ranking public ScorePanel(ScoreInfo score) { - this.score = score; + Score = score; } [BackgroundDependencyLoader] @@ -189,8 +189,8 @@ namespace osu.Game.Screens.Ranking topLayerBackground.FadeColour(expanded_top_layer_colour, resize_duration, Easing.OutQuint); middleLayerBackground.FadeColour(expanded_middle_layer_colour, resize_duration, Easing.OutQuint); - topLayerContentContainer.Add(middleLayerContent = new ExpandedPanelTopContent(score.User).With(d => d.Alpha = 0)); - middleLayerContentContainer.Add(topLayerContent = new ExpandedPanelMiddleContent(score).With(d => d.Alpha = 0)); + topLayerContentContainer.Add(middleLayerContent = new ExpandedPanelTopContent(Score.User).With(d => d.Alpha = 0)); + middleLayerContentContainer.Add(topLayerContent = new ExpandedPanelMiddleContent(Score).With(d => d.Alpha = 0)); break; case PanelState.Contracted: @@ -199,7 +199,7 @@ namespace osu.Game.Screens.Ranking topLayerBackground.FadeColour(contracted_top_layer_colour, resize_duration, Easing.OutQuint); middleLayerBackground.FadeColour(contracted_middle_layer_colour, resize_duration, Easing.OutQuint); - middleLayerContentContainer.Add(topLayerContent = new ContractedPanelMiddleContent(score).With(d => d.Alpha = 0)); + middleLayerContentContainer.Add(topLayerContent = new ContractedPanelMiddleContent(Score).With(d => d.Alpha = 0)); break; } @@ -222,5 +222,13 @@ namespace osu.Game.Screens.Ranking middleLayerContent?.FadeIn(content_fade_duration); } } + + protected override bool OnClick(ClickEvent e) + { + if (State == PanelState.Contracted) + State = PanelState.Expanded; + + return true; + } } } diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs new file mode 100644 index 0000000000..cc6842b2dd --- /dev/null +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -0,0 +1,80 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Utils; +using osu.Game.Scoring; +using osuTK; + +namespace osu.Game.Screens.Ranking +{ + public class ScorePanelList : CompositeDrawable + { + private readonly Flow panels; + private ScorePanel expandedPanel; + + public ScorePanelList() + { + RelativeSizeAxes = Axes.Both; + + InternalChild = panels = new Flow + { + Anchor = Anchor.Centre, + Origin = Anchor.Custom, + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + }; + } + + public void AddScore(ScoreInfo score) + { + var panel = new ScorePanel(score) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }; + + panel.StateChanged += s => onPanelStateChanged(panel, s); + + // Todo: Temporary + panel.State = expandedPanel == null ? PanelState.Expanded : PanelState.Contracted; + + panels.Add(panel); + } + + public void RemoveScore(ScoreInfo score) => panels.RemoveAll(p => p.Score == score); + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + if (expandedPanel != null) + { + var firstPanel = panels.FlowingChildren.First(); + var target = expandedPanel.DrawPosition.X - firstPanel.DrawPosition.X + expandedPanel.DrawSize.X / 2; + + panels.OriginPosition = new Vector2((float)Interpolation.Lerp(panels.OriginPosition.X, target, Math.Clamp(Math.Abs(Time.Elapsed) / 80, 0, 1)), panels.DrawHeight / 2); + } + } + + private void onPanelStateChanged(ScorePanel panel, PanelState state) + { + if (state == PanelState.Contracted) + return; + + if (expandedPanel != null) + expandedPanel.State = PanelState.Contracted; + expandedPanel = panel; + } + + private class Flow : FillFlowContainer + { + // Todo: Order is wrong. + public override IEnumerable FlowingChildren => AliveInternalChildren.OfType().OrderBy(s => s.Score.TotalScore); + } + } +} From acba1f3ad667fdeae03a1708329e4c49f2ce4006 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 20 May 2020 23:46:54 +0900 Subject: [PATCH 1695/2376] Integrate score panel list into results screen --- osu.Game/Screens/Ranking/ResultsScreen.cs | 38 +++++++---------------- 1 file changed, 11 insertions(+), 27 deletions(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index f2458d9f1f..652d158fbb 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -46,7 +46,7 @@ namespace osu.Game.Screens.Ranking private readonly bool allowRetry; private Drawable bottomPanel; - private Container contractedPanels; + private ScorePanelList panels; public ResultsScreen(ScoreInfo score, bool allowRetry = true) { @@ -63,28 +63,9 @@ namespace osu.Game.Screens.Ranking { new ResultsScrollContainer { - Children = new Drawable[] + Child = panels = new ScorePanelList { - new ScorePanel(Score) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - State = PanelState.Expanded - }, - new OsuScrollContainer(Direction.Horizontal) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - ScrollbarVisible = false, - Child = contractedPanels = new FillFlowContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Direction = FillDirection.Horizontal, - AutoSizeAxes = Axes.Both - } - } + RelativeSizeAxes = Axes.Both, } }, bottomPanel = new Container @@ -117,6 +98,8 @@ namespace osu.Game.Screens.Ranking } }; + panels.AddScore(Score); + if (player != null && allowRetry) { buttons.Add(new RetryButton { Width = 300 }); @@ -141,12 +124,13 @@ namespace osu.Game.Screens.Ranking req.Success += r => { - contractedPanels.ChildrenEnumerable = r.Scores.Select(s => s.CreateScoreInfo(rulesets)).Select(s => new ScorePanel(s) + foreach (var s in r.Scores.Select(s => s.CreateScoreInfo(rulesets))) { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - State = PanelState.Contracted - }); + if (s.OnlineScoreID == Score.OnlineScoreID) + continue; + + panels.AddScore(s); + } }; api.Queue(req); From 15ebe38303307f7bd5b6a24bc10bf72c3b926690 Mon Sep 17 00:00:00 2001 From: Shivam Date: Wed, 20 May 2020 17:13:35 +0200 Subject: [PATCH 1696/2376] Return null if path is not found, for clarity --- osu.Game.Tournament/IPC/FileBasedIPC.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tournament/IPC/FileBasedIPC.cs b/osu.Game.Tournament/IPC/FileBasedIPC.cs index d2d74e94b2..875bc4b4cd 100644 --- a/osu.Game.Tournament/IPC/FileBasedIPC.cs +++ b/osu.Game.Tournament/IPC/FileBasedIPC.cs @@ -184,7 +184,7 @@ namespace osu.Game.Tournament.IPC } } - return stableInstallPath; + return null; } finally { From b1c957c5e1aec15b346cbbd61db973bdce1a1f76 Mon Sep 17 00:00:00 2001 From: Shivam Date: Wed, 20 May 2020 17:25:53 +0200 Subject: [PATCH 1697/2376] invert if-statement and early return + reuse of checkExists --- osu.Game.Tournament/IPC/FileBasedIPC.cs | 2 +- .../Screens/StablePathSelectScreen.cs | 56 +++++++++---------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/osu.Game.Tournament/IPC/FileBasedIPC.cs b/osu.Game.Tournament/IPC/FileBasedIPC.cs index 875bc4b4cd..cc19c9eaba 100644 --- a/osu.Game.Tournament/IPC/FileBasedIPC.cs +++ b/osu.Game.Tournament/IPC/FileBasedIPC.cs @@ -157,7 +157,7 @@ namespace osu.Game.Tournament.IPC return IPCStorage; } - private static bool checkExists(string p) => File.Exists(Path.Combine(p, "ipc.txt")); + public bool checkExists(string p) => File.Exists(Path.Combine(p, "ipc.txt")); private string findStablePath() { diff --git a/osu.Game.Tournament/Screens/StablePathSelectScreen.cs b/osu.Game.Tournament/Screens/StablePathSelectScreen.cs index a42a5dc0fc..dbb7a3b900 100644 --- a/osu.Game.Tournament/Screens/StablePathSelectScreen.cs +++ b/osu.Game.Tournament/Screens/StablePathSelectScreen.cs @@ -140,43 +140,43 @@ namespace osu.Game.Tournament.Screens private void changePath(Storage storage) { var target = directorySelector.CurrentDirectory.Value.FullName; + var fileBasedIpc = ipc as FileBasedIPC; Logger.Log($"Changing Stable CE location to {target}"); - if (File.Exists(Path.Combine(target, "ipc.txt"))) - { - stableInfo.StablePath.Value = target; - - try - { - using (var stream = storage.GetStream(StableInfo.STABLE_CONFIG, FileAccess.Write, FileMode.Create)) - using (var sw = new StreamWriter(stream)) - { - sw.Write(JsonConvert.SerializeObject(stableInfo, - new JsonSerializerSettings - { - Formatting = Formatting.Indented, - NullValueHandling = NullValueHandling.Ignore, - DefaultValueHandling = DefaultValueHandling.Ignore, - })); - } - - var fileBasedIpc = ipc as FileBasedIPC; - fileBasedIpc?.LocateStableStorage(); - sceneManager?.SetScreen(typeof(SetupScreen)); - } - catch (Exception e) - { - Logger.Log($"Error during migration: {e.Message}", level: LogLevel.Error); - } - } - else + if (!fileBasedIpc.checkExists(target)) { overlay = new DialogOverlay(); overlay.Push(new IPCErrorDialog("This is an invalid IPC Directory", "Select a directory that contains an osu! stable cutting edge installation and make sure it has an empty ipc.txt file in it.")); AddInternal(overlay); Logger.Log("Folder is not an osu! stable CE directory"); + return; // Return an error in the picker that the directory does not contain ipc.txt } + + stableInfo.StablePath.Value = target; + + try + { + using (var stream = storage.GetStream(StableInfo.STABLE_CONFIG, FileAccess.Write, FileMode.Create)) + using (var sw = new StreamWriter(stream)) + { + sw.Write(JsonConvert.SerializeObject(stableInfo, + new JsonSerializerSettings + { + Formatting = Formatting.Indented, + NullValueHandling = NullValueHandling.Ignore, + DefaultValueHandling = DefaultValueHandling.Ignore, + })); + } + + + fileBasedIpc?.LocateStableStorage(); + sceneManager?.SetScreen(typeof(SetupScreen)); + } + catch (Exception e) + { + Logger.Log($"Error during migration: {e.Message}", level: LogLevel.Error); + } } private void autoDetect() From a5c2f97a76d0700d3498a78041160354f7851da4 Mon Sep 17 00:00:00 2001 From: Shivam Date: Wed, 20 May 2020 22:15:51 +0200 Subject: [PATCH 1698/2376] use common const in TournamentGameBase --- osu.Game.Tournament/TournamentGameBase.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 00946399fb..7d7d4f84aa 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -33,8 +33,6 @@ namespace osu.Game.Tournament { private const string bracket_filename = "bracket.json"; - private const string stable_config = "tournament/stable.json"; - private LadderInfo ladder; private Storage storage; @@ -152,7 +150,7 @@ namespace osu.Game.Tournament if (storage.Exists(StableInfo.STABLE_CONFIG)) { - using (Stream stream = storage.GetStream(stable_config, FileAccess.Read, FileMode.Open)) + using (Stream stream = storage.GetStream(StableInfo.STABLE_CONFIG, FileAccess.Read, FileMode.Open)) using (var sr = new StreamReader(stream)) { stableInfo = JsonConvert.DeserializeObject(sr.ReadToEnd()); From d2416ce30d6e5a8925310a1fae04fc97ceedf6d3 Mon Sep 17 00:00:00 2001 From: Shivam Date: Wed, 20 May 2020 22:16:37 +0200 Subject: [PATCH 1699/2376] removed redundant code and use existing checkExists --- osu.Game.Tournament/IPC/FileBasedIPC.cs | 25 +++-------- .../Screens/StablePathSelectScreen.cs | 44 +++++++------------ 2 files changed, 21 insertions(+), 48 deletions(-) diff --git a/osu.Game.Tournament/IPC/FileBasedIPC.cs b/osu.Game.Tournament/IPC/FileBasedIPC.cs index cc19c9eaba..74de5904e8 100644 --- a/osu.Game.Tournament/IPC/FileBasedIPC.cs +++ b/osu.Game.Tournament/IPC/FileBasedIPC.cs @@ -167,7 +167,7 @@ namespace osu.Game.Tournament.IPC { List> stableFindMethods = new List> { - findFromJsonConfig, + readFromStableInfo, findFromEnvVar, findFromRegistry, findFromLocalAppData, @@ -180,6 +180,7 @@ namespace osu.Game.Tournament.IPC if (stableInstallPath != null) { + saveStablePath(stableInstallPath); return stableInstallPath; } } @@ -192,8 +193,10 @@ namespace osu.Game.Tournament.IPC } } - private void saveStablePath() + private void saveStablePath(string path) { + stableInfo.StablePath.Value = path; + using (var stream = tournamentStorage.GetStream(StableInfo.STABLE_CONFIG, FileAccess.Write, FileMode.Create)) using (var sw = new StreamWriter(stream)) { @@ -215,11 +218,7 @@ namespace osu.Game.Tournament.IPC string stableInstallPath = Environment.GetEnvironmentVariable("OSU_STABLE_PATH"); if (checkExists(stableInstallPath)) - { - stableInfo.StablePath.Value = stableInstallPath; - saveStablePath(); return stableInstallPath; - } } catch { @@ -228,7 +227,7 @@ namespace osu.Game.Tournament.IPC return null; } - private string findFromJsonConfig() + private string readFromStableInfo() { try { @@ -250,11 +249,7 @@ namespace osu.Game.Tournament.IPC string stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"osu!"); if (checkExists(stableInstallPath)) - { - stableInfo.StablePath.Value = stableInstallPath; - saveStablePath(); return stableInstallPath; - } return null; } @@ -265,11 +260,7 @@ namespace osu.Game.Tournament.IPC string stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".osu"); if (checkExists(stableInstallPath)) - { - stableInfo.StablePath.Value = stableInstallPath; - saveStablePath(); return stableInstallPath; - } return null; } @@ -284,11 +275,7 @@ namespace osu.Game.Tournament.IPC stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty).ToString().Split('"')[1].Replace("osu!.exe", ""); if (checkExists(stableInstallPath)) - { - stableInfo.StablePath.Value = stableInstallPath; - saveStablePath(); return stableInstallPath; - } return null; } diff --git a/osu.Game.Tournament/Screens/StablePathSelectScreen.cs b/osu.Game.Tournament/Screens/StablePathSelectScreen.cs index dbb7a3b900..68fdaa34f8 100644 --- a/osu.Game.Tournament/Screens/StablePathSelectScreen.cs +++ b/osu.Game.Tournament/Screens/StablePathSelectScreen.cs @@ -139,39 +139,26 @@ namespace osu.Game.Tournament.Screens private void changePath(Storage storage) { - var target = directorySelector.CurrentDirectory.Value.FullName; - var fileBasedIpc = ipc as FileBasedIPC; - Logger.Log($"Changing Stable CE location to {target}"); - - if (!fileBasedIpc.checkExists(target)) - { - overlay = new DialogOverlay(); - overlay.Push(new IPCErrorDialog("This is an invalid IPC Directory", "Select a directory that contains an osu! stable cutting edge installation and make sure it has an empty ipc.txt file in it.")); - AddInternal(overlay); - Logger.Log("Folder is not an osu! stable CE directory"); - return; - // Return an error in the picker that the directory does not contain ipc.txt - } - - stableInfo.StablePath.Value = target; - try { - using (var stream = storage.GetStream(StableInfo.STABLE_CONFIG, FileAccess.Write, FileMode.Create)) - using (var sw = new StreamWriter(stream)) + var target = directorySelector.CurrentDirectory.Value.FullName; + stableInfo.StablePath.Value = target; + var fileBasedIpc = ipc as FileBasedIPC; + Logger.Log($"Changing Stable CE location to {target}"); + + if (!fileBasedIpc.checkExists(target)) { - sw.Write(JsonConvert.SerializeObject(stableInfo, - new JsonSerializerSettings - { - Formatting = Formatting.Indented, - NullValueHandling = NullValueHandling.Ignore, - DefaultValueHandling = DefaultValueHandling.Ignore, - })); + overlay = new DialogOverlay(); + overlay.Push(new IPCErrorDialog("This is an invalid IPC Directory", "Select a directory that contains an osu! stable cutting edge installation and make sure it has an empty ipc.txt file in it.")); + AddInternal(overlay); + Logger.Log("Folder is not an osu! stable CE directory"); + return; + // Return an error in the picker that the directory does not contain ipc.txt } - - fileBasedIpc?.LocateStableStorage(); - sceneManager?.SetScreen(typeof(SetupScreen)); + + fileBasedIpc.LocateStableStorage(); + sceneManager.SetScreen(typeof(SetupScreen)); } catch (Exception e) { @@ -181,7 +168,6 @@ namespace osu.Game.Tournament.Screens private void autoDetect() { - stableInfo.StablePath.Value = string.Empty; // This forces findStablePath() to look elsewhere. var fileBasedIpc = ipc as FileBasedIPC; fileBasedIpc?.LocateStableStorage(); From 585100207c05cb0bd0ddd50db488bbba15b53c60 Mon Sep 17 00:00:00 2001 From: Shivam Date: Wed, 20 May 2020 22:30:31 +0200 Subject: [PATCH 1700/2376] make CheckExists static public and removed unnecessary code --- osu.Game.Tournament/IPC/FileBasedIPC.cs | 10 ++--- .../Screens/StablePathSelectScreen.cs | 40 +++++++------------ 2 files changed, 20 insertions(+), 30 deletions(-) diff --git a/osu.Game.Tournament/IPC/FileBasedIPC.cs b/osu.Game.Tournament/IPC/FileBasedIPC.cs index 74de5904e8..d93bce8dfa 100644 --- a/osu.Game.Tournament/IPC/FileBasedIPC.cs +++ b/osu.Game.Tournament/IPC/FileBasedIPC.cs @@ -157,7 +157,7 @@ namespace osu.Game.Tournament.IPC return IPCStorage; } - public bool checkExists(string p) => File.Exists(Path.Combine(p, "ipc.txt")); + public static bool CheckExists(string p) => File.Exists(Path.Combine(p, "ipc.txt")); private string findStablePath() { @@ -217,7 +217,7 @@ namespace osu.Game.Tournament.IPC Logger.Log("Trying to find stable with environment variables"); string stableInstallPath = Environment.GetEnvironmentVariable("OSU_STABLE_PATH"); - if (checkExists(stableInstallPath)) + if (CheckExists(stableInstallPath)) return stableInstallPath; } catch @@ -248,7 +248,7 @@ namespace osu.Game.Tournament.IPC Logger.Log("Trying to find stable in %LOCALAPPDATA%"); string stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"osu!"); - if (checkExists(stableInstallPath)) + if (CheckExists(stableInstallPath)) return stableInstallPath; return null; @@ -259,7 +259,7 @@ namespace osu.Game.Tournament.IPC Logger.Log("Trying to find stable in dotfolders"); string stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".osu"); - if (checkExists(stableInstallPath)) + if (CheckExists(stableInstallPath)) return stableInstallPath; return null; @@ -274,7 +274,7 @@ namespace osu.Game.Tournament.IPC using (RegistryKey key = Registry.ClassesRoot.OpenSubKey("osu")) stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty).ToString().Split('"')[1].Replace("osu!.exe", ""); - if (checkExists(stableInstallPath)) + if (CheckExists(stableInstallPath)) return stableInstallPath; return null; diff --git a/osu.Game.Tournament/Screens/StablePathSelectScreen.cs b/osu.Game.Tournament/Screens/StablePathSelectScreen.cs index 68fdaa34f8..dcc26b8b1e 100644 --- a/osu.Game.Tournament/Screens/StablePathSelectScreen.cs +++ b/osu.Game.Tournament/Screens/StablePathSelectScreen.cs @@ -1,9 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.IO; -using Newtonsoft.Json; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -139,31 +137,23 @@ namespace osu.Game.Tournament.Screens private void changePath(Storage storage) { - try + var target = directorySelector.CurrentDirectory.Value.FullName; + stableInfo.StablePath.Value = target; + Logger.Log($"Changing Stable CE location to {target}"); + + if (!FileBasedIPC.CheckExists(target)) { - var target = directorySelector.CurrentDirectory.Value.FullName; - stableInfo.StablePath.Value = target; - var fileBasedIpc = ipc as FileBasedIPC; - Logger.Log($"Changing Stable CE location to {target}"); - - if (!fileBasedIpc.checkExists(target)) - { - overlay = new DialogOverlay(); - overlay.Push(new IPCErrorDialog("This is an invalid IPC Directory", "Select a directory that contains an osu! stable cutting edge installation and make sure it has an empty ipc.txt file in it.")); - AddInternal(overlay); - Logger.Log("Folder is not an osu! stable CE directory"); - return; - // Return an error in the picker that the directory does not contain ipc.txt - } - - - fileBasedIpc.LocateStableStorage(); - sceneManager.SetScreen(typeof(SetupScreen)); - } - catch (Exception e) - { - Logger.Log($"Error during migration: {e.Message}", level: LogLevel.Error); + overlay = new DialogOverlay(); + overlay.Push(new IPCErrorDialog("This is an invalid IPC Directory", "Select a directory that contains an osu! stable cutting edge installation and make sure it has an empty ipc.txt file in it.")); + AddInternal(overlay); + Logger.Log("Folder is not an osu! stable CE directory"); + return; + // Return an error in the picker that the directory does not contain ipc.txt } + + var fileBasedIpc = ipc as FileBasedIPC; + fileBasedIpc?.LocateStableStorage(); + sceneManager?.SetScreen(typeof(SetupScreen)); } private void autoDetect() From ce223a2bd84c6362fd1f78aa6bef4feadc8bacb8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 May 2020 10:58:30 +0900 Subject: [PATCH 1701/2376] Silence hit sounds while seeking --- .../Objects/Drawables/DrawableHitObject.cs | 6 ++++- .../Rulesets/UI/FrameStabilityContainer.cs | 22 +++++++++++++++---- osu.Game/Screens/Play/GameplayClock.cs | 5 +++++ 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index c32d4e441e..d594909cda 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -17,6 +17,7 @@ using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; using osu.Game.Configuration; +using osu.Game.Screens.Play; using osuTK.Graphics; namespace osu.Game.Rulesets.Objects.Drawables @@ -348,6 +349,9 @@ namespace osu.Game.Rulesets.Objects.Drawables { } + [Resolved(canBeNull: true)] + private GameplayClock gameplayClock { get; set; } + /// /// Plays all the hit sounds for this . /// This is invoked automatically when this is hit. @@ -356,7 +360,7 @@ namespace osu.Game.Rulesets.Objects.Drawables { const float balance_adjust_amount = 0.4f; - if (Samples != null) + if (Samples != null && gameplayClock?.IsSeeking != true) { Samples.Balance.Value = balance_adjust_amount * (userPositionalHitSounds.Value ? SamplePlaybackPosition - 0.5f : 0); Samples.Play(); diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index 3ba28aad45..bc9401a095 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -29,14 +29,16 @@ namespace osu.Game.Rulesets.UI /// internal bool FrameStablePlayback = true; - [Cached] - public GameplayClock GameplayClock { get; } + public GameplayClock GameplayClock => stabilityGameplayClock; + + [Cached(typeof(GameplayClock))] + private readonly StabilityGameplayClock stabilityGameplayClock; public FrameStabilityContainer(double gameplayStartTime = double.MinValue) { RelativeSizeAxes = Axes.Both; - GameplayClock = new GameplayClock(framedClock = new FramedClock(manualClock = new ManualClock())); + stabilityGameplayClock = new StabilityGameplayClock(framedClock = new FramedClock(manualClock = new ManualClock())); this.gameplayStartTime = gameplayStartTime; } @@ -57,7 +59,7 @@ namespace osu.Game.Rulesets.UI { if (clock != null) { - parentGameplayClock = clock; + stabilityGameplayClock.ParentGameplayClock = parentGameplayClock = clock; GameplayClock.IsPaused.BindTo(clock.IsPaused); } } @@ -187,5 +189,17 @@ namespace osu.Game.Rulesets.UI } public ReplayInputHandler ReplayInputHandler { get; set; } + + private class StabilityGameplayClock : GameplayClock + { + public IFrameBasedClock ParentGameplayClock; + + public StabilityGameplayClock(FramedClock underlyingClock) + : base(underlyingClock) + { + } + + public override bool IsSeeking => ParentGameplayClock != null && Math.Abs(CurrentTime - ParentGameplayClock.CurrentTime) > 200; + } } } diff --git a/osu.Game/Screens/Play/GameplayClock.cs b/osu.Game/Screens/Play/GameplayClock.cs index d5f75f6ad1..4f2cf5005c 100644 --- a/osu.Game/Screens/Play/GameplayClock.cs +++ b/osu.Game/Screens/Play/GameplayClock.cs @@ -31,6 +31,11 @@ namespace osu.Game.Screens.Play public bool IsRunning => underlyingClock.IsRunning; + /// + /// Whether an ongoing seek operation is active. + /// + public virtual bool IsSeeking => false; + public void ProcessFrame() { // we do not want to process the underlying clock. From c0e68f98540303aa02932718d1d96f2bf8a94c20 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 May 2020 10:59:30 +0900 Subject: [PATCH 1702/2376] Also support taiko drum --- osu.Game.Rulesets.Taiko/UI/InputDrum.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs index 38026517d9..06ccd45cb8 100644 --- a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs +++ b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs @@ -12,6 +12,7 @@ using osu.Framework.Input.Bindings; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Rulesets.Taiko.Audio; +using osu.Game.Screens.Play; using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.UI @@ -145,6 +146,9 @@ namespace osu.Game.Rulesets.Taiko.UI centreHit.Colour = colours.Pink; } + [Resolved(canBeNull: true)] + private GameplayClock gameplayClock { get; set; } + public bool OnPressed(TaikoAction action) { Drawable target = null; @@ -157,14 +161,16 @@ namespace osu.Game.Rulesets.Taiko.UI target = centreHit; back = centre; - drumSample.Centre?.Play(); + if (gameplayClock?.IsSeeking != true) + drumSample.Centre?.Play(); } else if (action == RimAction) { target = rimHit; back = rim; - drumSample.Rim?.Play(); + if (gameplayClock?.IsSeeking != true) + drumSample.Rim?.Play(); } if (target != null) From 83a5913b8d7a4ddb3adc9610b39e3e5e28041933 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 May 2020 12:11:39 +0900 Subject: [PATCH 1703/2376] Undo beat snapping related changes --- .../Edit/Blueprints/ManiaPlacementBlueprint.cs | 16 ++-------------- .../Edit/ManiaHitObjectComposer.cs | 17 +++++------------ 2 files changed, 7 insertions(+), 26 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs index 184356b89c..3fb03d642f 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs @@ -24,15 +24,10 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints protected Column Column; /// - /// The current beat-snapped mouse position, snapped to the closest column. + /// The current mouse position, snapped to the closest column. /// protected Vector2 SnappedMousePosition { get; private set; } - /// - /// The gameplay time at the current beat-snapped mouse position (). - /// - protected double SnappedTime { get; private set; } - /// /// The width of the closest column to the current mouse position. /// @@ -44,9 +39,6 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints [Resolved] private IScrollingInfo scrollingInfo { get; set; } - [Resolved(CanBeNull = true)] - private IDistanceSnapProvider snapProvider { get; set; } - protected ManiaPlacementBlueprint(T hitObject) : base(hitObject) { @@ -62,7 +54,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints return base.OnMouseDown(e); HitObject.Column = Column.Index; - BeginPlacement(SnappedTime, true); + BeginPlacement(TimeAt(e.ScreenSpaceMousePosition), true); return true; } @@ -78,10 +70,6 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints // Snap to the column var parentPos = Parent.ToLocalSpace(Column.ToScreenSpace(new Vector2(Column.DrawWidth / 2, 0))); SnappedMousePosition = new Vector2(parentPos.X, Parent.ToLocalSpace(screenSpacePosition).Y); - - SnappedTime = TimeAt(screenSpacePosition); - if (snapProvider != null) - (SnappedMousePosition, SnappedTime) = snapProvider.GetSnappedPosition(SnappedMousePosition, SnappedTime); } protected double TimeAt(Vector2 screenSpacePosition) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index 475320ece3..7677ac6f07 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -82,18 +82,9 @@ namespace osu.Game.Rulesets.Mania.Edit public override (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time) { - var beatSnapped = beatSnapGrid.GetSnappedPosition(position); + var hoc = Playfield.GetColumn(0).HitObjectContainer; - if (beatSnapped != null) - return beatSnapped.Value; - - return base.GetSnappedPosition(position, getTimeFromPosition(ToScreenSpace(position))); - } - - private double getTimeFromPosition(Vector2 screenSpacePosition) - { - var hoc = Playfield.Stages[0].HitObjectContainer; - float targetPosition = hoc.ToLocalSpace(screenSpacePosition).Y; + float targetPosition = hoc.ToLocalSpace(ToScreenSpace(position)).Y; if (drawableRuleset.ScrollingInfo.Direction.Value == ScrollingDirection.Down) { @@ -103,10 +94,12 @@ namespace osu.Game.Rulesets.Mania.Edit targetPosition = hoc.DrawHeight - targetPosition; } - return drawableRuleset.ScrollingInfo.Algorithm.TimeAt(targetPosition, + double targetTime = drawableRuleset.ScrollingInfo.Algorithm.TimeAt(targetPosition, EditorClock.CurrentTime, drawableRuleset.ScrollingInfo.TimeRange.Value, hoc.DrawHeight); + + return base.GetSnappedPosition(position, targetTime); } protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) From 6d29ff092869b2e58ae983c8919bec93b2b6cc9b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 21 May 2020 12:13:02 +0900 Subject: [PATCH 1704/2376] Fix banana showers not using cancellation token --- osu.Game.Rulesets.Catch/Objects/BananaShower.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/BananaShower.cs b/osu.Game.Rulesets.Catch/Objects/BananaShower.cs index 96ab66048a..3a0b5ace53 100644 --- a/osu.Game.Rulesets.Catch/Objects/BananaShower.cs +++ b/osu.Game.Rulesets.Catch/Objects/BananaShower.cs @@ -18,10 +18,10 @@ namespace osu.Game.Rulesets.Catch.Objects protected override void CreateNestedHitObjects(CancellationToken cancellationToken) { base.CreateNestedHitObjects(cancellationToken); - createBananas(); + createBananas(cancellationToken); } - private void createBananas() + private void createBananas(CancellationToken cancellationToken) { double spacing = Duration; while (spacing > 100) @@ -32,6 +32,8 @@ namespace osu.Game.Rulesets.Catch.Objects for (double i = StartTime; i <= EndTime; i += spacing) { + cancellationToken.ThrowIfCancellationRequested(); + AddNested(new Banana { Samples = Samples, From 922b793a5aed03ee2ed1db3ae668eafc90a6eda8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 May 2020 13:04:35 +0900 Subject: [PATCH 1705/2376] Update hit object composer tests --- .../TestSceneManiaHitObjectComposer.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs index 6274bb1005..bad3d7854e 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs @@ -42,6 +42,7 @@ namespace osu.Game.Rulesets.Mania.Tests public void TestDragOffscreenSelectionVerticallyUpScroll() { DrawableHitObject lastObject = null; + double originalTime = 0; Vector2 originalPosition = Vector2.Zero; setScrollStep(ScrollingDirection.Up); @@ -49,6 +50,7 @@ namespace osu.Game.Rulesets.Mania.Tests AddStep("seek to last object", () => { lastObject = this.ChildrenOfType().Single(d => d.HitObject == composer.EditorBeatmap.HitObjects.Last()); + originalTime = lastObject.HitObject.StartTime; Clock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime); }); @@ -64,19 +66,20 @@ namespace osu.Game.Rulesets.Mania.Tests AddStep("move mouse downwards", () => { - InputManager.MoveMouseTo(lastObject, new Vector2(0, 20)); + InputManager.MoveMouseTo(lastObject, new Vector2(0, lastObject.ScreenSpaceDrawQuad.Height * 2)); InputManager.ReleaseButton(MouseButton.Left); }); AddAssert("hitobjects not moved columns", () => composer.EditorBeatmap.HitObjects.All(h => ((ManiaHitObject)h).Column == 0)); AddAssert("hitobjects moved downwards", () => lastObject.DrawPosition.Y - originalPosition.Y > 0); - AddAssert("hitobjects not moved too far", () => lastObject.DrawPosition.Y - originalPosition.Y < 50); + AddAssert("hitobject has moved time", () => lastObject.HitObject.StartTime == originalTime + 125); } [Test] public void TestDragOffscreenSelectionVerticallyDownScroll() { DrawableHitObject lastObject = null; + double originalTime = 0; Vector2 originalPosition = Vector2.Zero; setScrollStep(ScrollingDirection.Down); @@ -84,6 +87,7 @@ namespace osu.Game.Rulesets.Mania.Tests AddStep("seek to last object", () => { lastObject = this.ChildrenOfType().Single(d => d.HitObject == composer.EditorBeatmap.HitObjects.Last()); + originalTime = lastObject.HitObject.StartTime; Clock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime); }); @@ -99,13 +103,13 @@ namespace osu.Game.Rulesets.Mania.Tests AddStep("move mouse upwards", () => { - InputManager.MoveMouseTo(lastObject, new Vector2(0, -20)); + InputManager.MoveMouseTo(lastObject, new Vector2(0, -lastObject.ScreenSpaceDrawQuad.Height * 2)); InputManager.ReleaseButton(MouseButton.Left); }); AddAssert("hitobjects not moved columns", () => composer.EditorBeatmap.HitObjects.All(h => ((ManiaHitObject)h).Column == 0)); AddAssert("hitobjects moved upwards", () => originalPosition.Y - lastObject.DrawPosition.Y > 0); - AddAssert("hitobjects not moved too far", () => originalPosition.Y - lastObject.DrawPosition.Y < 50); + AddAssert("hitobject has moved time", () => lastObject.HitObject.StartTime == originalTime + 125); } [Test] @@ -207,7 +211,7 @@ namespace osu.Game.Rulesets.Mania.Tests }; for (int i = 0; i < 10; i++) - EditorBeatmap.Add(new Note { StartTime = 100 * i }); + EditorBeatmap.Add(new Note { StartTime = 125 * i }); } } } From 5ad7842b917e862d3d5ce722ff0b0169a7660d53 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 May 2020 13:33:02 +0900 Subject: [PATCH 1706/2376] Move ScreenSpacePositionAtTime to inside Column implementation --- .../ManiaPlacementBlueprintTestScene.cs | 2 -- .../ManiaSelectionBlueprintTestScene.cs | 2 -- .../Blueprints/HoldNotePlacementBlueprint.cs | 5 ++--- .../Edit/IManiaHitObjectComposer.cs | 2 -- .../Edit/ManiaHitObjectComposer.cs | 18 +----------------- osu.Game.Rulesets.Mania/UI/Column.cs | 13 +++++++++++++ .../UI/Scrolling/ScrollingPlayfield.cs | 4 ++-- 7 files changed, 18 insertions(+), 28 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs index be3e205f36..aac77c9c1c 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs @@ -49,8 +49,6 @@ namespace osu.Game.Rulesets.Mania.Tests public Column ColumnAt(Vector2 screenSpacePosition) => column; - public Vector2 ScreenSpacePositionAtTime(double time, Column column = null) => Vector2.Zero; - public int TotalColumns => 1; } } diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaSelectionBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/ManiaSelectionBlueprintTestScene.cs index 3d654466ed..b598893e8c 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaSelectionBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaSelectionBlueprintTestScene.cs @@ -33,8 +33,6 @@ namespace osu.Game.Rulesets.Mania.Tests public Column ColumnAt(Vector2 screenSpacePosition) => column; - public Vector2 ScreenSpacePositionAtTime(double time, Column column = null) => Vector2.Zero; - public int TotalColumns => 1; } } diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs index 31bf76edd0..8689d479b4 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs @@ -8,7 +8,6 @@ using osu.Framework.Input.Events; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Edit.Blueprints.Components; using osu.Game.Rulesets.Mania.Objects; -using osu.Game.Rulesets.UI.Scrolling; using osuTK; using osuTK.Input; @@ -42,8 +41,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints if (Column != null) { - headPiece.Y = Parent.ToLocalSpace(composer.ScreenSpacePositionAtTime(HitObject.StartTime, Column)).Y; - tailPiece.Y = Parent.ToLocalSpace(composer.ScreenSpacePositionAtTime(HitObject.EndTime, Column)).Y; + headPiece.Y = Parent.ToLocalSpace(Column.ScreenSpacePositionAtTime(HitObject.StartTime, Column)).Y; + tailPiece.Y = Parent.ToLocalSpace(Column.ScreenSpacePositionAtTime(HitObject.EndTime, Column)).Y; } var topPosition = new Vector2(headPiece.DrawPosition.X, Math.Min(headPiece.DrawPosition.Y, tailPiece.DrawPosition.Y)); diff --git a/osu.Game.Rulesets.Mania/Edit/IManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/IManiaHitObjectComposer.cs index f1915cd85a..f64bab1fae 100644 --- a/osu.Game.Rulesets.Mania/Edit/IManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/IManiaHitObjectComposer.cs @@ -10,8 +10,6 @@ namespace osu.Game.Rulesets.Mania.Edit { Column ColumnAt(Vector2 screenSpacePosition); - Vector2 ScreenSpacePositionAtTime(double time, Column column = null); - int TotalColumns { get; } } } diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index 0cae26b51c..5eafaefe37 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -72,27 +72,11 @@ namespace osu.Game.Rulesets.Mania.Edit targetTime = BeatSnapProvider.SnapTime(targetTime); // convert back to screen space - screenSpacePosition = ScreenSpacePositionAtTime(targetTime, column); + screenSpacePosition = column.ScreenSpacePositionAtTime(targetTime, column); return new ManiaSnapResult(screenSpacePosition, targetTime, column); } - public Vector2 ScreenSpacePositionAtTime(double time, Column column = null) - { - var hoc = (column ?? Playfield.GetColumn(0)).HitObjectContainer; - var scrollInfo = drawableRuleset.ScrollingInfo; - - var pos = scrollInfo.Algorithm.PositionAt(time, EditorClock.CurrentTime, scrollInfo.TimeRange.Value, hoc.DrawHeight); - - if (scrollInfo.Direction.Value == ScrollingDirection.Down) - { - // as explained above - pos = hoc.DrawHeight - pos; - } - - return hoc.ToScreenSpace(new Vector2(hoc.DrawWidth / 2, pos)); - } - protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) { drawableRuleset = new DrawableManiaEditRuleset(ruleset, beatmap, mods); diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 506a07f26b..3f85f449ce 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -140,5 +140,18 @@ namespace osu.Game.Rulesets.Mania.UI public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) // This probably shouldn't exist as is, but the columns in the stage are separated by a 1px border => DrawRectangle.Inflate(new Vector2(Stage.COLUMN_SPACING / 2, 0)).Contains(ToLocalSpace(screenSpacePos)); + + public Vector2 ScreenSpacePositionAtTime(double time, Column column = null) + { + var pos = ScrollingInfo.Algorithm.PositionAt(time, Time.Current, ScrollingInfo.TimeRange.Value, HitObjectContainer.DrawHeight); + + if (ScrollingInfo.Direction.Value == ScrollingDirection.Down) + { + // as explained above + pos = HitObjectContainer.DrawHeight - pos; + } + + return HitObjectContainer.ToScreenSpace(new Vector2(HitObjectContainer.DrawWidth / 2, pos)); + } } } diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs index bf2203e176..fd143a3687 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs @@ -15,12 +15,12 @@ namespace osu.Game.Rulesets.UI.Scrolling protected readonly IBindable Direction = new Bindable(); [Resolved] - private IScrollingInfo scrollingInfo { get; set; } + protected IScrollingInfo ScrollingInfo { get; private set; } [BackgroundDependencyLoader] private void load() { - Direction.BindTo(scrollingInfo.Direction); + Direction.BindTo(ScrollingInfo.Direction); } protected sealed override HitObjectContainer CreateHitObjectContainer() => new ScrollingHitObjectContainer(); From bac78707de161819947b5b26ec4d7ea830ee1699 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 May 2020 14:25:37 +0900 Subject: [PATCH 1707/2376] Move more logic to column to both clean things up and fix tests --- .../ManiaPlacementBlueprintTestScene.cs | 9 +++++++++ .../Edit/ManiaHitObjectComposer.cs | 17 +---------------- osu.Game.Rulesets.Mania/UI/Column.cs | 16 ++++++++++++++++ .../Tests/Visual/PlacementBlueprintTestScene.cs | 7 +++++-- 4 files changed, 31 insertions(+), 18 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs index aac77c9c1c..547786847b 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs @@ -7,6 +7,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Timing; +using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Edit; using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.UI; @@ -43,6 +44,14 @@ namespace osu.Game.Rulesets.Mania.Tests }); } + protected override SnapResult SnapForBlueprint(PlacementBlueprint blueprint) + { + var time = column.TimeAtScreenSpacePosition(InputManager.CurrentState.Mouse.Position); + var pos = column.ScreenSpacePositionAtTime(time); + + return new ManiaSnapResult(pos, time, column); + } + protected override Container CreateHitObjectContainer() => new ScrollingTestContainer(ScrollingDirection.Down) { RelativeSizeAxes = Axes.Both }; protected override void AddHitObject(DrawableHitObject hitObject) => column.Add((DrawableManiaHitObject)hitObject); diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index 5eafaefe37..9085033140 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -51,22 +51,7 @@ namespace osu.Game.Rulesets.Mania.Edit if (column == null) return new SnapResult(screenSpacePosition, null); - var hoc = column.HitObjectContainer; - - // convert to local space of column so we can snap and fetch correct location. - Vector2 localPosition = hoc.ToLocalSpace(screenSpacePosition); - - var scrollInfo = drawableRuleset.ScrollingInfo; - - if (scrollInfo.Direction.Value == ScrollingDirection.Down) - { - // We're dealing with screen coordinates in which the position decreases towards the centre of the screen resulting in an increase in start time. - // The scrolling algorithm instead assumes a top anchor meaning an increase in time corresponds to an increase in position, - // so when scrolling downwards the coordinates need to be flipped. - localPosition.Y = hoc.DrawHeight - localPosition.Y; - } - - double targetTime = scrollInfo.Algorithm.TimeAt(localPosition.Y, EditorClock.CurrentTime, scrollInfo.TimeRange.Value, hoc.DrawHeight); + double targetTime = column.TimeAtScreenSpacePosition(screenSpacePosition); // apply beat snapping targetTime = BeatSnapProvider.SnapTime(targetTime); diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 3f85f449ce..c582eb1c75 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -153,5 +153,21 @@ namespace osu.Game.Rulesets.Mania.UI return HitObjectContainer.ToScreenSpace(new Vector2(HitObjectContainer.DrawWidth / 2, pos)); } + + public double TimeAtScreenSpacePosition(Vector2 screenSpacePosition) + { + // convert to local space of column so we can snap and fetch correct location. + Vector2 localPosition = HitObjectContainer.ToLocalSpace(screenSpacePosition); + + if (ScrollingInfo.Direction.Value == ScrollingDirection.Down) + { + // We're dealing with screen coordinates in which the position decreases towards the centre of the screen resulting in an increase in start time. + // The scrolling algorithm instead assumes a top anchor meaning an increase in time corresponds to an increase in position, + // so when scrolling downwards the coordinates need to be flipped. + localPosition.Y = HitObjectContainer.DrawHeight - localPosition.Y; + } + + return ScrollingInfo.Algorithm.TimeAt(localPosition.Y, Time.Current, ScrollingInfo.TimeRange.Value, HitObjectContainer.DrawHeight); + } } } diff --git a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs index a4e629b6f5..feecea473c 100644 --- a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs +++ b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs @@ -71,9 +71,12 @@ namespace osu.Game.Tests.Visual { base.Update(); - currentBlueprint.UpdatePosition(new SnapResult(InputManager.CurrentState.Mouse.Position, null)); + currentBlueprint.UpdatePosition(SnapForBlueprint(currentBlueprint)); } + protected virtual SnapResult SnapForBlueprint(PlacementBlueprint blueprint) => + new SnapResult(InputManager.CurrentState.Mouse.Position, null); + public override void Add(Drawable drawable) { base.Add(drawable); @@ -81,7 +84,7 @@ namespace osu.Game.Tests.Visual if (drawable is PlacementBlueprint blueprint) { blueprint.Show(); - blueprint.UpdatePosition(new SnapResult(InputManager.CurrentState.Mouse.Position, null)); + blueprint.UpdatePosition(SnapForBlueprint(blueprint)); } } From a9a1c00cf1d2b54a757bc6a69a70cee31a39ce04 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 May 2020 14:38:40 +0900 Subject: [PATCH 1708/2376] Move responsibility placement blueprint's StartTime set to within --- .../Edit/Blueprints/ManiaPlacementBlueprint.cs | 2 ++ .../HitCircles/HitCirclePlacementBlueprint.cs | 6 +++++- .../Blueprints/Sliders/SliderPlacementBlueprint.cs | 2 ++ .../Blueprints/Spinners/SpinnerPlacementBlueprint.cs | 4 ---- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 2 -- osu.Game/Rulesets/Edit/PlacementBlueprint.cs | 10 +++++++++- 6 files changed, 18 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs index 8d3b3ea583..d173da9d9a 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs @@ -50,6 +50,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints public override void UpdatePosition(SnapResult result) { + base.UpdatePosition(result); + if (!PlacementActive) Column = (result as ManiaSnapResult)?.Column; } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs index e12dec2668..3dbbdcc5d0 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs @@ -39,6 +39,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles return base.OnMouseDown(e); } - public override void UpdatePosition(SnapResult result) => HitObject.Position = ToLocalSpace(result.ScreenSpacePosition); + public override void UpdatePosition(SnapResult result) + { + base.UpdatePosition(result); + HitObject.Position = ToLocalSpace(result.ScreenSpacePosition); + } } } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 59ec92c79e..4b99cc23ed 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -69,6 +69,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders public override void UpdatePosition(SnapResult result) { + base.UpdatePosition(result); + switch (state) { case PlacementState.Initial: diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs index 546f0e5981..cc4ed0eccf 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs @@ -59,9 +59,5 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners return true; } - - public override void UpdatePosition(SnapResult result) - { - } } } diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 1e328e6b6b..6edd01cd15 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -244,8 +244,6 @@ namespace osu.Game.Rulesets.Edit public void BeginPlacement(HitObject hitObject) { EditorBeatmap.PlacementObject.Value = hitObject; - - hitObject.StartTime = SnapScreenSpacePositionToValidTime(inputManager.CurrentState.Mouse.Position).Time ?? EditorClock.CurrentTime; } public void EndPlacement(HitObject hitObject, bool commit) diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index bab9bf71ef..2fd8c4b9d9 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -10,6 +10,7 @@ using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Objects; +using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Compose; using osuTK; @@ -83,11 +84,18 @@ namespace osu.Game.Rulesets.Edit PlacementActive = false; } + [Resolved(canBeNull: true)] + private IFrameBasedClock editorClock { get; set; } + /// /// Updates the position of this to a new screen-space position. /// /// The snap result information. - public abstract void UpdatePosition(SnapResult snapResult); + public virtual void UpdatePosition(SnapResult snapResult) + { + if (!PlacementActive) + HitObject.StartTime = snapResult.Time ?? editorClock?.CurrentTime ?? Time.Current; + } /// /// Invokes , From 776b842fdbbabff264fd6120833be30bacd57053 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 May 2020 14:53:36 +0900 Subject: [PATCH 1709/2376] Remove unused using --- osu.Game/Rulesets/Edit/PlacementBlueprint.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index 2fd8c4b9d9..f0b63f8ea5 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -10,7 +10,6 @@ using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Objects; -using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Compose; using osuTK; From ce8b6b7383d2c32d7518b9a1091c1af1825bd334 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 May 2020 15:15:24 +0900 Subject: [PATCH 1710/2376] Correctly account for blueprint origins --- osu.Game.Rulesets.Mania/UI/Column.cs | 55 ++++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index c582eb1c75..0fdefe6dc9 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -17,6 +17,7 @@ using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; using osuTK; using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; namespace osu.Game.Rulesets.Mania.UI { @@ -145,10 +146,21 @@ namespace osu.Game.Rulesets.Mania.UI { var pos = ScrollingInfo.Algorithm.PositionAt(time, Time.Current, ScrollingInfo.TimeRange.Value, HitObjectContainer.DrawHeight); - if (ScrollingInfo.Direction.Value == ScrollingDirection.Down) + switch (ScrollingInfo.Direction.Value) { - // as explained above - pos = HitObjectContainer.DrawHeight - pos; + case ScrollingDirection.Down: + // We're dealing with screen coordinates in which the position decreases towards the centre of the screen resulting in an increase in start time. + // The scrolling algorithm instead assumes a top anchor meaning an increase in time corresponds to an increase in position, + // so when scrolling downwards the coordinates need to be flipped. + pos = HitObjectContainer.DrawHeight - pos; + + // Blueprints are centred on the mouse position, such that the hitobject position is anchored at the top or bottom of the blueprint depending on the scroll direction. + pos -= DefaultNotePiece.NOTE_HEIGHT / 2; + break; + + case ScrollingDirection.Up: + pos += DefaultNotePiece.NOTE_HEIGHT / 2; + break; } return HitObjectContainer.ToScreenSpace(new Vector2(HitObjectContainer.DrawWidth / 2, pos)); @@ -159,15 +171,42 @@ namespace osu.Game.Rulesets.Mania.UI // convert to local space of column so we can snap and fetch correct location. Vector2 localPosition = HitObjectContainer.ToLocalSpace(screenSpacePosition); - if (ScrollingInfo.Direction.Value == ScrollingDirection.Down) + switch (ScrollingInfo.Direction.Value) { - // We're dealing with screen coordinates in which the position decreases towards the centre of the screen resulting in an increase in start time. - // The scrolling algorithm instead assumes a top anchor meaning an increase in time corresponds to an increase in position, - // so when scrolling downwards the coordinates need to be flipped. - localPosition.Y = HitObjectContainer.DrawHeight - localPosition.Y; + case ScrollingDirection.Down: + // as above + localPosition.Y = HitObjectContainer.DrawHeight - localPosition.Y; + break; } + // offset for the fact that blueprints are centered, as above. + localPosition.Y -= DefaultNotePiece.NOTE_HEIGHT / 2; + return ScrollingInfo.Algorithm.TimeAt(localPosition.Y, Time.Current, ScrollingInfo.TimeRange.Value, HitObjectContainer.DrawHeight); } + + /// + /// Converts a mouse position to a hitobject position. + /// + /// + /// Blueprints are centred on the mouse position, such that the hitobject position is anchored at the top or bottom of the blueprint depending on the scroll direction. + /// + /// The mouse position. + /// The resulting hitobject position, acnhored at the top or bottom of the blueprint depending on the scroll direction. + private Vector2 mouseToHitObjectPosition(Vector2 mousePosition) + { + switch (ScrollingInfo.Direction.Value) + { + case ScrollingDirection.Up: + mousePosition.Y -= DefaultNotePiece.NOTE_HEIGHT / 2; + break; + + case ScrollingDirection.Down: + mousePosition.Y += DefaultNotePiece.NOTE_HEIGHT / 2; + break; + } + + return mousePosition; + } } } From a756e6d21241080204da38772a8e34ff9d9946a9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 May 2020 15:16:30 +0900 Subject: [PATCH 1711/2376] Add xmldoc and remove unnecessary parameter --- .../Edit/Blueprints/HoldNotePlacementBlueprint.cs | 4 ++-- osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs | 2 +- osu.Game.Rulesets.Mania/UI/Column.cs | 8 +++++++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs index 8689d479b4..b757c17a48 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs @@ -41,8 +41,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints if (Column != null) { - headPiece.Y = Parent.ToLocalSpace(Column.ScreenSpacePositionAtTime(HitObject.StartTime, Column)).Y; - tailPiece.Y = Parent.ToLocalSpace(Column.ScreenSpacePositionAtTime(HitObject.EndTime, Column)).Y; + headPiece.Y = Parent.ToLocalSpace(Column.ScreenSpacePositionAtTime(HitObject.StartTime)).Y; + tailPiece.Y = Parent.ToLocalSpace(Column.ScreenSpacePositionAtTime(HitObject.EndTime)).Y; } var topPosition = new Vector2(headPiece.DrawPosition.X, Math.Min(headPiece.DrawPosition.Y, tailPiece.DrawPosition.Y)); diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index 9085033140..cfb04c8e50 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Mania.Edit targetTime = BeatSnapProvider.SnapTime(targetTime); // convert back to screen space - screenSpacePosition = column.ScreenSpacePositionAtTime(targetTime, column); + screenSpacePosition = column.ScreenSpacePositionAtTime(targetTime); return new ManiaSnapResult(screenSpacePosition, targetTime, column); } diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 0fdefe6dc9..f7339fdacd 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -142,7 +142,10 @@ namespace osu.Game.Rulesets.Mania.UI // This probably shouldn't exist as is, but the columns in the stage are separated by a 1px border => DrawRectangle.Inflate(new Vector2(Stage.COLUMN_SPACING / 2, 0)).Contains(ToLocalSpace(screenSpacePos)); - public Vector2 ScreenSpacePositionAtTime(double time, Column column = null) + /// + /// Given a time, return the screen space position within this column. + /// + public Vector2 ScreenSpacePositionAtTime(double time) { var pos = ScrollingInfo.Algorithm.PositionAt(time, Time.Current, ScrollingInfo.TimeRange.Value, HitObjectContainer.DrawHeight); @@ -166,6 +169,9 @@ namespace osu.Game.Rulesets.Mania.UI return HitObjectContainer.ToScreenSpace(new Vector2(HitObjectContainer.DrawWidth / 2, pos)); } + /// + /// Given a position in screen space, return the time within this column. + /// public double TimeAtScreenSpacePosition(Vector2 screenSpacePosition) { // convert to local space of column so we can snap and fetch correct location. From 7dd3b3eeb56473e200d07af163204022126b5e11 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 May 2020 15:16:59 +0900 Subject: [PATCH 1712/2376] Remove unused method --- osu.Game.Rulesets.Mania/UI/Column.cs | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index f7339fdacd..2d88670d77 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -190,29 +190,5 @@ namespace osu.Game.Rulesets.Mania.UI return ScrollingInfo.Algorithm.TimeAt(localPosition.Y, Time.Current, ScrollingInfo.TimeRange.Value, HitObjectContainer.DrawHeight); } - - /// - /// Converts a mouse position to a hitobject position. - /// - /// - /// Blueprints are centred on the mouse position, such that the hitobject position is anchored at the top or bottom of the blueprint depending on the scroll direction. - /// - /// The mouse position. - /// The resulting hitobject position, acnhored at the top or bottom of the blueprint depending on the scroll direction. - private Vector2 mouseToHitObjectPosition(Vector2 mousePosition) - { - switch (ScrollingInfo.Direction.Value) - { - case ScrollingDirection.Up: - mousePosition.Y -= DefaultNotePiece.NOTE_HEIGHT / 2; - break; - - case ScrollingDirection.Down: - mousePosition.Y += DefaultNotePiece.NOTE_HEIGHT / 2; - break; - } - - return mousePosition; - } } } From 0db1ea6a9d460c208e5cf513625b34968f686aa2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 May 2020 15:47:12 +0900 Subject: [PATCH 1713/2376] Fix failing tests --- .../TestSceneManiaHitObjectComposer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs index bad3d7854e..1a3fa29d4a 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs @@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Mania.Tests AddStep("move mouse downwards", () => { - InputManager.MoveMouseTo(lastObject, new Vector2(0, lastObject.ScreenSpaceDrawQuad.Height * 2)); + InputManager.MoveMouseTo(lastObject, new Vector2(0, lastObject.ScreenSpaceDrawQuad.Height * 4)); InputManager.ReleaseButton(MouseButton.Left); }); @@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Mania.Tests AddStep("move mouse upwards", () => { - InputManager.MoveMouseTo(lastObject, new Vector2(0, -lastObject.ScreenSpaceDrawQuad.Height * 2)); + InputManager.MoveMouseTo(lastObject, new Vector2(0, -lastObject.ScreenSpaceDrawQuad.Height * 4)); InputManager.ReleaseButton(MouseButton.Left); }); From 8a47e2431bbe7c3207e75004d61fd5fcbce103f0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 May 2020 17:13:22 +0900 Subject: [PATCH 1714/2376] Move distance snap grid implementation to OsuHitObjectComposer --- .../Edit/OsuHitObjectComposer.cs | 64 +++++++++++++++- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 73 +++---------------- .../Compose/Components/BlueprintContainer.cs | 5 -- 3 files changed, 74 insertions(+), 68 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index cdf78a5902..9ba3e30445 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -4,6 +4,10 @@ using System; using System.Collections.Generic; using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Caching; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; @@ -12,6 +16,7 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.UI; using osu.Game.Screens.Edit.Compose.Components; +using osuTK; namespace osu.Game.Rulesets.Osu.Edit { @@ -32,9 +37,66 @@ namespace osu.Game.Rulesets.Osu.Edit new SpinnerCompositionTool() }; + [BackgroundDependencyLoader] + private void load() + { + EditorBeatmap.SelectedHitObjects.CollectionChanged += (_, __) => updateDistanceSnapGrid(); + EditorBeatmap.PlacementObject.ValueChanged += _ => updateDistanceSnapGrid(); + + LayerBelowRuleset.Add(distanceSnapGridContainer = new Container { RelativeSizeAxes = Axes.Both }); + } + protected override ComposeBlueprintContainer CreateBlueprintContainer() => new OsuBlueprintContainer(HitObjects); - protected override DistanceSnapGrid CreateDistanceSnapGrid(IEnumerable selectedHitObjects) + private DistanceSnapGrid distanceSnapGrid; + private Container distanceSnapGridContainer; + + private readonly Cached distanceSnapGridCache = new Cached(); + private double? lastDistanceSnapGridTime; + + protected override void Update() + { + base.Update(); + + if (!(BlueprintContainer.CurrentTool is SelectTool)) + { + if (EditorClock.CurrentTime != lastDistanceSnapGridTime) + { + distanceSnapGridCache.Invalidate(); + lastDistanceSnapGridTime = EditorClock.CurrentTime; + } + + if (!distanceSnapGridCache.IsValid) + updateDistanceSnapGrid(); + } + } + + public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) + { + if (distanceSnapGrid == null) + return base.SnapScreenSpacePositionToValidTime(screenSpacePosition); + + (Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition)); + + return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time); + } + + private void updateDistanceSnapGrid() + { + distanceSnapGridContainer.Clear(); + distanceSnapGridCache.Invalidate(); + + if (BlueprintContainer.CurrentTool is SelectTool && !EditorBeatmap.SelectedHitObjects.Any()) + return; + + if ((distanceSnapGrid = createDistanceSnapGrid(EditorBeatmap.SelectedHitObjects)) != null) + { + distanceSnapGridContainer.Add(distanceSnapGrid); + distanceSnapGridCache.Validate(); + } + } + + private DistanceSnapGrid createDistanceSnapGrid(IEnumerable selectedHitObjects) { if (BlueprintContainer.CurrentTool is SpinnerCompositionTool) return null; diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index b437d81054..fd8af0afd5 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -3,8 +3,8 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; using System.Linq; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -52,8 +52,9 @@ namespace osu.Game.Rulesets.Edit protected ComposeBlueprintContainer BlueprintContainer { get; private set; } private DrawableEditRulesetWrapper drawableRulesetWrapper; - private Container distanceSnapGridContainer; - private DistanceSnapGrid distanceSnapGrid; + + protected readonly Container LayerBelowRuleset = new Container { RelativeSizeAxes = Axes.Both }; + private readonly List layerContainers = new List(); private InputManager inputManager; @@ -87,7 +88,7 @@ namespace osu.Game.Rulesets.Edit var layerBelowRuleset = drawableRulesetWrapper.CreatePlayfieldAdjustmentContainer().WithChildren(new Drawable[] { - distanceSnapGridContainer = new Container { RelativeSizeAxes = Axes.Both }, + LayerBelowRuleset, new EditorPlayfieldBorder { RelativeSizeAxes = Axes.Both } }); @@ -139,7 +140,7 @@ namespace osu.Game.Rulesets.Edit setSelectTool(); - BlueprintContainer.SelectionChanged += selectionChanged; + EditorBeatmap.SelectedHitObjects.CollectionChanged += selectionChanged; } protected override bool OnKeyDown(KeyDownEvent e) @@ -165,16 +166,6 @@ namespace osu.Game.Rulesets.Edit inputManager = GetContainingInputManager(); } - private double lastGridUpdateTime; - - protected override void Update() - { - base.Update(); - - if (EditorClock.CurrentTime != lastGridUpdateTime && !(BlueprintContainer.CurrentTool is SelectTool)) - showGridFor(Enumerable.Empty()); - } - protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); @@ -188,19 +179,13 @@ namespace osu.Game.Rulesets.Edit }); } - private void selectionChanged(IEnumerable selectedHitObjects) + private void selectionChanged(object sender, NotifyCollectionChangedEventArgs changedArgs) { - var hitObjects = selectedHitObjects.ToArray(); - - if (hitObjects.Any()) + if (EditorBeatmap.SelectedHitObjects.Any()) { // ensure in selection mode if a selection is made. setSelectTool(); - - showGridFor(hitObjects); } - else - distanceSnapGridContainer.Hide(); } private void setSelectTool() => toolboxCollection.Items.First().Select(); @@ -209,30 +194,12 @@ namespace osu.Game.Rulesets.Edit { BlueprintContainer.CurrentTool = tool; - if (tool is SelectTool) - distanceSnapGridContainer.Hide(); - else - { + if (!(tool is SelectTool)) EditorBeatmap.SelectedHitObjects.Clear(); - showGridFor(Enumerable.Empty()); - } - } - - private void showGridFor(IEnumerable selectedHitObjects) - { - distanceSnapGridContainer.Clear(); - distanceSnapGrid = CreateDistanceSnapGrid(selectedHitObjects); - - if (distanceSnapGrid != null) - { - distanceSnapGridContainer.Child = distanceSnapGrid; - distanceSnapGridContainer.Show(); - } - - lastGridUpdateTime = EditorClock.CurrentTime; } public override IEnumerable HitObjects => drawableRulesetWrapper.Playfield.AllHitObjects; + public override bool CursorInPlacementArea => drawableRulesetWrapper.Playfield.ReceivePositionalInputAt(inputManager.CurrentState.Mouse.Position); protected abstract IReadOnlyList CompositionTools { get; } @@ -257,21 +224,11 @@ namespace osu.Game.Rulesets.Edit if (adjustableClock.CurrentTime < hitObject.StartTime) adjustableClock.Seek(hitObject.StartTime); } - - showGridFor(Enumerable.Empty()); } public void Delete(HitObject hitObject) => EditorBeatmap.Remove(hitObject); - public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) - { - if (distanceSnapGrid == null) return new SnapResult(screenSpacePosition, null); - - // TODO: move distance snap grid to OsuHitObjectComposer. - (Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition)); - - return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time); - } + public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, null); public override float GetBeatSnapDistanceAt(double referenceTime) { @@ -321,14 +278,6 @@ namespace osu.Game.Rulesets.Edit /// public abstract bool CursorInPlacementArea { get; } - /// - /// Creates the applicable for a selection. - /// - /// The selection. - /// The for . If empty, a grid is returned for the current point in time. - [CanBeNull] - protected virtual DistanceSnapGrid CreateDistanceSnapGrid([NotNull] IEnumerable selectedHitObjects) => null; - public abstract SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition); public abstract float GetBeatSnapDistanceAt(double referenceTime); diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index e38df3d812..1e8a35c047 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.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.Collections.Specialized; using System.Diagnostics; using System.Linq; @@ -29,8 +28,6 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public abstract class BlueprintContainer : CompositeDrawable, IKeyBindingHandler { - public event Action> SelectionChanged; - protected DragBox DragBox { get; private set; } protected Container SelectionBlueprints { get; private set; } @@ -88,8 +85,6 @@ namespace osu.Game.Screens.Edit.Compose.Components SelectionBlueprints.FirstOrDefault(b => b.HitObject == o)?.Deselect(); break; } - - SelectionChanged?.Invoke(selectedHitObjects); }; } From 700b5e0c73c75566a9b73267977111c819e6737c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 21 May 2020 17:47:14 +0900 Subject: [PATCH 1715/2376] Adjust design --- .../ContractedPanelMiddleContent.cs | 30 ++++++------------- osu.Game/Screens/Ranking/ScorePanel.cs | 6 ++-- 2 files changed, 12 insertions(+), 24 deletions(-) diff --git a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs index 1d7d5c4130..a263a03a77 100644 --- a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs @@ -65,28 +65,16 @@ namespace osu.Game.Screens.Ranking.Contracted }, Children = new Drawable[] { - // Buffered container is used to prevent 1px bleed outside the masking region - new BufferedContainer + new Box { RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex("444") - }, - new UserCoverBackground - { - RelativeSizeAxes = Axes.Both, - User = score.User, - }, - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientVertical(Color4.Black.Opacity(0.5f), Color4Extensions.FromHex("#444")) - }, - } + Colour = Color4Extensions.FromHex("444") + }, + new UserCoverBackground + { + RelativeSizeAxes = Axes.Both, + User = score.User, + Colour = ColourInfo.GradientVertical(Color4.White.Opacity(0.5f), Color4Extensions.FromHex("#444").Opacity(0)) }, new FillFlowContainer { @@ -100,7 +88,7 @@ namespace osu.Game.Screens.Ranking.Contracted { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Size = new Vector2(140), + Size = new Vector2(110), Masking = true, CornerExponent = 2.5f, CornerRadius = 20, diff --git a/osu.Game/Screens/Ranking/ScorePanel.cs b/osu.Game/Screens/Ranking/ScorePanel.cs index baca2fd9e1..305d4ee921 100644 --- a/osu.Game/Screens/Ranking/ScorePanel.cs +++ b/osu.Game/Screens/Ranking/ScorePanel.cs @@ -23,12 +23,12 @@ namespace osu.Game.Screens.Ranking /// /// Width of the panel when contracted. /// - public const float CONTRACTED_WIDTH = 160; + public const float CONTRACTED_WIDTH = 130; /// /// Height of the panel when contracted. /// - private const float contracted_height = 385; + private const float contracted_height = 355; /// /// Width of the panel when expanded. @@ -48,7 +48,7 @@ namespace osu.Game.Screens.Ranking /// /// Height of the top layer when the panel is contracted. /// - private const float contracted_top_layer_height = 40; + private const float contracted_top_layer_height = 30; /// /// Duration for the panel to resize into its expanded/contracted size. From b6a1d1a2fc5d9a1927b4994861b23c6f46f09fd4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 21 May 2020 18:07:31 +0900 Subject: [PATCH 1716/2376] Improve transforms between state changes --- osu.Game/Screens/Ranking/ScorePanel.cs | 8 ++++---- osu.Game/Screens/Ranking/ScorePanelList.cs | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Ranking/ScorePanel.cs b/osu.Game/Screens/Ranking/ScorePanel.cs index 305d4ee921..2f6146a5e7 100644 --- a/osu.Game/Screens/Ranking/ScorePanel.cs +++ b/osu.Game/Screens/Ranking/ScorePanel.cs @@ -175,9 +175,6 @@ namespace osu.Game.Screens.Ranking private void updateState() { - topLayerContainer.MoveToY(0, resize_duration, Easing.OutQuint); - middleLayerContainer.MoveToY(0, resize_duration, Easing.OutQuint); - topLayerContent?.FadeOut(content_fade_duration).Expire(); middleLayerContent?.FadeOut(content_fade_duration).Expire(); @@ -203,7 +200,10 @@ namespace osu.Game.Screens.Ranking break; } - using (BeginDelayedSequence(resize_duration + top_layer_expand_delay, true)) + bool topLayerExpanded = topLayerContainer.Y < 0; + + // If the top layer was already expanded, then we don't need to wait for the resize and can instead transform immediately. This looks better when changing the panel state. + using (BeginDelayedSequence(topLayerExpanded ? 0 : resize_duration + top_layer_expand_delay, true)) { switch (state) { diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index cc6842b2dd..894be7e775 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -68,6 +68,7 @@ namespace osu.Game.Screens.Ranking if (expandedPanel != null) expandedPanel.State = PanelState.Contracted; + expandedPanel = panel; } From 9f868be872f0d55d9209536489008606f5dc171d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 21 May 2020 18:39:22 +0900 Subject: [PATCH 1717/2376] Create common TestScoreInfo type --- .../TestSceneContractedPanelMiddleContent.cs | 32 +-------- .../TestSceneExpandedPanelMiddleContent.cs | 31 +-------- .../TestSceneExpandedPanelTopContent.cs | 4 +- .../Visual/Ranking/TestSceneResultsScreen.cs | 26 +------- .../Visual/Ranking/TestSceneScorePanel.cs | 65 +++---------------- .../Visual/Ranking/TestSceneScorePanelList.cs | 59 ++++------------- osu.Game/Tests/TestScoreInfo.cs | 50 ++++++++++++++ 7 files changed, 80 insertions(+), 187 deletions(-) create mode 100644 osu.Game/Tests/TestScoreInfo.cs diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs index f7694c10ec..e1e00e3c2b 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -14,10 +13,7 @@ using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets; -using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; -using osu.Game.Rulesets.Osu.Mods; -using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking.Contracted; @@ -37,7 +33,7 @@ namespace osu.Game.Tests.Visual.Ranking { var author = new User { Username = "mapper_name" }; - AddStep("show example score", () => showPanel(createTestBeatmap(author), createTestScore())); + AddStep("show example score", () => showPanel(createTestBeatmap(author), new TestScoreInfo(new OsuRuleset().RulesetInfo))); AddAssert("mapper name present", () => this.ChildrenOfType().Any(spriteText => spriteText.Text == "mapper_name")); } @@ -45,7 +41,7 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestMapWithUnknownMapper() { - AddStep("show example score", () => showPanel(createTestBeatmap(null), createTestScore())); + AddStep("show example score", () => showPanel(createTestBeatmap(null), new TestScoreInfo(new OsuRuleset().RulesetInfo))); AddAssert("mapped by text not present", () => this.ChildrenOfType().All(spriteText => !containsAny(spriteText.Text, "mapped", "by"))); @@ -66,30 +62,6 @@ namespace osu.Game.Tests.Visual.Ranking return new TestWorkingBeatmap(beatmap); } - private ScoreInfo createTestScore() => new ScoreInfo - { - User = new User - { - Id = 2, - Username = "peppy", - CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", - }, - Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, - Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }, - TotalScore = 999999, - Accuracy = 0.95, - MaxCombo = 999, - Rank = ScoreRank.S, - Date = DateTimeOffset.Now, - Statistics = - { - { HitResult.Miss, 1 }, - { HitResult.Meh, 50 }, - { HitResult.Good, 100 }, - { HitResult.Great, 300 }, - } - }; - private bool containsAny(string text, params string[] stringsToMatch) => stringsToMatch.Any(text.Contains); private class ContractedPanelMiddleContentContainer : Container diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs index 106b4187ee..69511b85c0 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -14,10 +13,7 @@ using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets; -using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; -using osu.Game.Rulesets.Osu.Mods; -using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking.Expanded; @@ -37,7 +33,7 @@ namespace osu.Game.Tests.Visual.Ranking { var author = new User { Username = "mapper_name" }; - AddStep("show example score", () => showPanel(createTestBeatmap(author), createTestScore())); + AddStep("show example score", () => showPanel(createTestBeatmap(author), new TestScoreInfo(new OsuRuleset().RulesetInfo))); AddAssert("mapper name present", () => this.ChildrenOfType().Any(spriteText => spriteText.Text == "mapper_name")); } @@ -45,7 +41,7 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestMapWithUnknownMapper() { - AddStep("show example score", () => showPanel(createTestBeatmap(null), createTestScore())); + AddStep("show example score", () => showPanel(createTestBeatmap(null), new TestScoreInfo(new OsuRuleset().RulesetInfo))); AddAssert("mapped by text not present", () => this.ChildrenOfType().All(spriteText => !containsAny(spriteText.Text, "mapped", "by"))); @@ -66,29 +62,6 @@ namespace osu.Game.Tests.Visual.Ranking return new TestWorkingBeatmap(beatmap); } - private ScoreInfo createTestScore() => new ScoreInfo - { - User = new User - { - Id = 2, - Username = "peppy", - }, - Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, - Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }, - TotalScore = 999999, - Accuracy = 0.95, - MaxCombo = 999, - Rank = ScoreRank.S, - Date = DateTimeOffset.Now, - Statistics = - { - { HitResult.Miss, 1 }, - { HitResult.Meh, 50 }, - { HitResult.Good, 100 }, - { HitResult.Great, 300 }, - } - }; - private bool containsAny(string text, params string[] stringsToMatch) => stringsToMatch.Any(text.Contains); private class ExpandedPanelMiddleContentContainer : Container diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelTopContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelTopContent.cs index afaa607099..a32bcbe7f0 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelTopContent.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelTopContent.cs @@ -5,8 +5,8 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Game.Rulesets.Osu; using osu.Game.Screens.Ranking.Expanded; -using osu.Game.Users; using osuTK; namespace osu.Game.Tests.Visual.Ranking @@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.Ranking RelativeSizeAxes = Axes.Both, Colour = Color4Extensions.FromHex("#444"), }, - new ExpandedPanelTopContent(new User { Id = 2, Username = "peppy" }), + new ExpandedPanelTopContent(new TestScoreInfo(new OsuRuleset().RulesetInfo).User), } }; } diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index aa0ce89d93..242766ad4b 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -11,13 +9,10 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Rulesets.Osu; -using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; -using osu.Game.Tests.Beatmaps; -using osu.Game.Users; namespace osu.Game.Tests.Visual.Ranking { @@ -41,26 +36,7 @@ namespace osu.Game.Tests.Visual.Ranking Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo); } - private TestSoloResults createResultsScreen() => new TestSoloResults(new ScoreInfo - { - TotalScore = 2845370, - Accuracy = 0.98, - MaxCombo = 123, - Rank = ScoreRank.A, - Date = DateTimeOffset.Now, - Statistics = new Dictionary - { - { HitResult.Great, 50 }, - { HitResult.Good, 20 }, - { HitResult.Meh, 50 }, - { HitResult.Miss, 1 } - }, - Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, - User = new User - { - Username = "peppy", - } - }); + private TestSoloResults createResultsScreen() => new TestSoloResults(new TestScoreInfo(new OsuRuleset().RulesetInfo)); [Test] public void ResultsWithoutPlayer() diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs index 0dbafb18bc..fdb77c14a3 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs @@ -1,17 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using NUnit.Framework; using osu.Framework.Graphics; -using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; -using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Ranking; -using osu.Game.Tests.Beatmaps; -using osu.Game.Users; namespace osu.Game.Tests.Visual.Ranking { @@ -20,9 +15,7 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestDRank() { - var score = createScore(); - score.Accuracy = 0.5; - score.Rank = ScoreRank.D; + var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.5, Rank = ScoreRank.D }; addPanelStep(score); } @@ -30,9 +23,7 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestCRank() { - var score = createScore(); - score.Accuracy = 0.75; - score.Rank = ScoreRank.C; + var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.75, Rank = ScoreRank.C }; addPanelStep(score); } @@ -40,9 +31,7 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestBRank() { - var score = createScore(); - score.Accuracy = 0.85; - score.Rank = ScoreRank.B; + var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.85, Rank = ScoreRank.B }; addPanelStep(score); } @@ -50,9 +39,7 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestARank() { - var score = createScore(); - score.Accuracy = 0.925; - score.Rank = ScoreRank.A; + var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.925, Rank = ScoreRank.A }; addPanelStep(score); } @@ -60,9 +47,7 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestSRank() { - var score = createScore(); - score.Accuracy = 0.975; - score.Rank = ScoreRank.S; + var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.975, Rank = ScoreRank.S }; addPanelStep(score); } @@ -70,9 +55,7 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestAlmostSSRank() { - var score = createScore(); - score.Accuracy = 0.9999; - score.Rank = ScoreRank.S; + var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.9999, Rank = ScoreRank.S }; addPanelStep(score); } @@ -80,9 +63,7 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestSSRank() { - var score = createScore(); - score.Accuracy = 1; - score.Rank = ScoreRank.X; + var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 1, Rank = ScoreRank.X }; addPanelStep(score); } @@ -90,9 +71,7 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestAllHitResults() { - var score = createScore(); - score.Statistics[HitResult.Perfect] = 350; - score.Statistics[HitResult.Ok] = 200; + var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Statistics = { [HitResult.Perfect] = 350, [HitResult.Ok] = 200 } }; addPanelStep(score); } @@ -100,9 +79,7 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestContractedPanel() { - var score = createScore(); - score.Accuracy = 0.925; - score.Rank = ScoreRank.A; + var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.925, Rank = ScoreRank.A }; addPanelStep(score, PanelState.Contracted); } @@ -116,29 +93,5 @@ namespace osu.Game.Tests.Visual.Ranking State = state }; }); - - private ScoreInfo createScore() => new ScoreInfo - { - User = new User - { - Id = 2, - Username = "peppy", - CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", - }, - Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, - Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }, - TotalScore = 2845370, - Accuracy = 0.95, - MaxCombo = 999, - Rank = ScoreRank.S, - Date = DateTimeOffset.Now, - Statistics = - { - { HitResult.Miss, 1 }, - { HitResult.Meh, 50 }, - { HitResult.Good, 100 }, - { HitResult.Great, 300 }, - } - }; } } diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs index 4964af8784..81a9b22992 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs @@ -1,16 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using osu.Framework.Graphics; -using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; -using osu.Game.Rulesets.Osu.Mods; -using osu.Game.Rulesets.Scoring; -using osu.Game.Scoring; using osu.Game.Screens.Ranking; -using osu.Game.Tests.Beatmaps; -using osu.Game.Users; namespace osu.Game.Tests.Visual.Ranking { @@ -26,44 +19,20 @@ namespace osu.Game.Tests.Visual.Ranking Add(list); - list.AddScore(createScore()); - list.AddScore(createScore()); - list.AddScore(createScore()); - list.AddScore(createScore()); - list.AddScore(createScore()); - list.AddScore(createScore()); - list.AddScore(createScore()); - list.AddScore(createScore()); - list.AddScore(createScore()); - list.AddScore(createScore()); - list.AddScore(createScore()); - list.AddScore(createScore()); - list.AddScore(createScore()); - list.AddScore(createScore()); + list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo)); + list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo)); + list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo)); + list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo)); + list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo)); + list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo)); + list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo)); + list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo)); + list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo)); + list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo)); + list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo)); + list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo)); + list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo)); + list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo)); } - - private ScoreInfo createScore() => new ScoreInfo - { - User = new User - { - Id = 2, - Username = "peppy", - CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", - }, - Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, - Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }, - TotalScore = 2845370, - Accuracy = 0.95, - MaxCombo = 999, - Rank = ScoreRank.S, - Date = DateTimeOffset.Now, - Statistics = - { - { HitResult.Miss, 1 }, - { HitResult.Meh, 50 }, - { HitResult.Good, 100 }, - { HitResult.Great, 300 }, - } - }; } } diff --git a/osu.Game/Tests/TestScoreInfo.cs b/osu.Game/Tests/TestScoreInfo.cs new file mode 100644 index 0000000000..155129e181 --- /dev/null +++ b/osu.Game/Tests/TestScoreInfo.cs @@ -0,0 +1,50 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Tests.Beatmaps; +using osu.Game.Users; + +namespace osu.Game.Tests +{ + public class TestScoreInfo : ScoreInfo + { + public TestScoreInfo(RulesetInfo ruleset) + { + User = new User + { + Id = 2, + Username = "peppy", + CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + }; + + Beatmap = new TestBeatmap(ruleset).BeatmapInfo; + Mods = new Mod[] { new TestModHardRock(), new TestModDoubleTime() }; + + TotalScore = 2845370; + Accuracy = 0.95; + MaxCombo = 999; + Rank = ScoreRank.S; + Date = DateTimeOffset.Now; + + Statistics[HitResult.Miss] = 1; + Statistics[HitResult.Meh] = 50; + Statistics[HitResult.Good] = 100; + Statistics[HitResult.Great] = 300; + } + + private class TestModHardRock : ModHardRock + { + public override double ScoreMultiplier => 1; + } + + private class TestModDoubleTime : ModDoubleTime + { + public override double ScoreMultiplier => 1; + } + } +} From 45b59f574dcd09ba389adc85ec2619a177f92e5e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 21 May 2020 18:43:12 +0900 Subject: [PATCH 1718/2376] Fix TestSceneResultsScreen crashing --- osu.Game/Tests/TestScoreInfo.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Tests/TestScoreInfo.cs b/osu.Game/Tests/TestScoreInfo.cs index 155129e181..1193a29d70 100644 --- a/osu.Game/Tests/TestScoreInfo.cs +++ b/osu.Game/Tests/TestScoreInfo.cs @@ -23,6 +23,8 @@ namespace osu.Game.Tests }; Beatmap = new TestBeatmap(ruleset).BeatmapInfo; + Ruleset = ruleset; + RulesetID = ruleset.ID ?? 0; Mods = new Mod[] { new TestModHardRock(), new TestModDoubleTime() }; TotalScore = 2845370; From 717869225e55f0ff2a6cd27d9f797ffb9f62b868 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 21 May 2020 19:51:36 +0900 Subject: [PATCH 1719/2376] Rework list to use a scroll container + add spacing --- osu.Game/Screens/Ranking/ScorePanelList.cs | 60 ++++++++++++++-------- 1 file changed, 38 insertions(+), 22 deletions(-) diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index 894be7e775..52a9f27db8 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -1,12 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Utils; +using osu.Game.Graphics.Containers; using osu.Game.Scoring; using osuTK; @@ -14,19 +13,36 @@ namespace osu.Game.Screens.Ranking { public class ScorePanelList : CompositeDrawable { - private readonly Flow panels; + /// + /// Normal spacing between all panels. + /// + private const float panel_spacing = 5; + + /// + /// Spacing around both sides of the expanded panel. This is added on top of . + /// + private const float expanded_panel_spacing = 15; + + private readonly Flow flow; + private readonly ScrollContainer scroll; + private ScorePanel expandedPanel; public ScorePanelList() { RelativeSizeAxes = Axes.Both; - InternalChild = panels = new Flow + InternalChild = scroll = new OsuScrollContainer(Direction.Horizontal) { - Anchor = Anchor.Centre, - Origin = Anchor.Custom, - Direction = FillDirection.Horizontal, - AutoSizeAxes = Axes.Both, + RelativeSizeAxes = Axes.Both, + Child = flow = new Flow + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(panel_spacing, 0), + AutoSizeAxes = Axes.Both, + } }; } @@ -34,8 +50,8 @@ namespace osu.Game.Screens.Ranking { var panel = new ScorePanel(score) { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, }; panel.StateChanged += s => onPanelStateChanged(panel, s); @@ -43,22 +59,14 @@ namespace osu.Game.Screens.Ranking // Todo: Temporary panel.State = expandedPanel == null ? PanelState.Expanded : PanelState.Contracted; - panels.Add(panel); + flow.Add(panel); } - public void RemoveScore(ScoreInfo score) => panels.RemoveAll(p => p.Score == score); - - protected override void UpdateAfterChildren() + protected override void Update() { - base.UpdateAfterChildren(); + base.Update(); - if (expandedPanel != null) - { - var firstPanel = panels.FlowingChildren.First(); - var target = expandedPanel.DrawPosition.X - firstPanel.DrawPosition.X + expandedPanel.DrawSize.X / 2; - - panels.OriginPosition = new Vector2((float)Interpolation.Lerp(panels.OriginPosition.X, target, Math.Clamp(Math.Abs(Time.Elapsed) / 80, 0, 1)), panels.DrawHeight / 2); - } + flow.Padding = new MarginPadding { Horizontal = DrawWidth / 2f - ScorePanel.EXPANDED_WIDTH / 2f - expanded_panel_spacing }; } private void onPanelStateChanged(ScorePanel panel, PanelState state) @@ -67,9 +75,17 @@ namespace osu.Game.Screens.Ranking return; if (expandedPanel != null) + { + expandedPanel.Margin = new MarginPadding(0); expandedPanel.State = PanelState.Contracted; + } expandedPanel = panel; + expandedPanel.Margin = new MarginPadding { Horizontal = expanded_panel_spacing }; + + float panelOffset = flow.IndexOf(expandedPanel) * (ScorePanel.CONTRACTED_WIDTH + panel_spacing); + + scroll.ScrollTo(panelOffset); } private class Flow : FillFlowContainer From 7b82a5d792d4fae9103bff89de4db4e81a8e5063 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 21 May 2020 20:48:08 +0900 Subject: [PATCH 1720/2376] Fix score order --- osu.Game/Screens/Ranking/ScorePanelList.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index 52a9f27db8..0e0ed4f60d 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -90,8 +90,7 @@ namespace osu.Game.Screens.Ranking private class Flow : FillFlowContainer { - // Todo: Order is wrong. - public override IEnumerable FlowingChildren => AliveInternalChildren.OfType().OrderBy(s => s.Score.TotalScore); + public override IEnumerable FlowingChildren => AliveInternalChildren.OfType().OrderByDescending(s => s.Score.TotalScore).ThenByDescending(s => s.Score.OnlineScoreID); } } } From d0f74c2b683e949ba780c7d6e8011a184b5468d6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 21 May 2020 20:48:25 +0900 Subject: [PATCH 1721/2376] Refactor initial state --- .../Visual/Ranking/TestSceneScorePanelList.cs | 47 ++++++++++------ osu.Game/Screens/Ranking/ResultsScreen.cs | 4 +- osu.Game/Screens/Ranking/ScorePanelList.cs | 54 +++++++++---------- 3 files changed, 58 insertions(+), 47 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs index 81a9b22992..f00bf7e151 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs @@ -1,38 +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 NUnit.Framework; using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; using osu.Game.Rulesets.Osu; using osu.Game.Screens.Ranking; +using osuTK.Graphics; namespace osu.Game.Tests.Visual.Ranking { public class TestSceneScorePanelList : OsuTestScene { - public TestSceneScorePanelList() + private ScorePanelList list; + + [SetUp] + public void Setup() => Schedule(() => { - var list = new ScorePanelList + Child = list = new ScorePanelList(new TestScoreInfo(new OsuRuleset().RulesetInfo)) { Anchor = Anchor.Centre, Origin = Anchor.Centre, }; - Add(list); + Add(new Box + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.Y, + Width = 1, + Colour = Color4.Red + }); + }); - list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo)); - list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo)); - list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo)); - list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo)); - list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo)); - list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo)); - list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo)); - list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo)); - list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo)); - list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo)); - list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo)); - list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo)); - list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo)); - list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo)); + [Test] + public void TestSingleScore() + { + } + + [Test] + public void TestManyScores() + { + AddStep("add many scores", () => + { + for (int i = 0; i < 20; i++) + list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo)); + }); } } } diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 652d158fbb..cdceaa939e 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -63,7 +63,7 @@ namespace osu.Game.Screens.Ranking { new ResultsScrollContainer { - Child = panels = new ScorePanelList + Child = panels = new ScorePanelList(Score) { RelativeSizeAxes = Axes.Both, } @@ -98,8 +98,6 @@ namespace osu.Game.Screens.Ranking } }; - panels.AddScore(Score); - if (player != null && allowRetry) { buttons.Add(new RetryButton { Width = 300 }); diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index 0e0ed4f60d..c2fd487767 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -28,7 +28,7 @@ namespace osu.Game.Screens.Ranking private ScorePanel expandedPanel; - public ScorePanelList() + public ScorePanelList(ScoreInfo initialScore) { RelativeSizeAxes = Axes.Both; @@ -44,48 +44,48 @@ namespace osu.Game.Screens.Ranking AutoSizeAxes = Axes.Both, } }; + + AddScore(initialScore); + ShowScore(initialScore); } public void AddScore(ScoreInfo score) { - var panel = new ScorePanel(score) + flow.Add(new ScorePanel(score) { Anchor = Anchor.Centre, Origin = Anchor.Centre, - }; + }.With(p => + { + p.StateChanged += s => + { + if (s == PanelState.Expanded) + ShowScore(score); + }; + })); + } - panel.StateChanged += s => onPanelStateChanged(panel, s); + public void ShowScore(ScoreInfo score) + { + foreach (var p in flow.Where(p => p.Score != score)) + p.State = PanelState.Contracted; - // Todo: Temporary - panel.State = expandedPanel == null ? PanelState.Expanded : PanelState.Contracted; + if (expandedPanel != null) + expandedPanel.Margin = new MarginPadding(0); - flow.Add(panel); + expandedPanel = flow.Single(p => p.Score == score); + expandedPanel.State = PanelState.Expanded; + expandedPanel.Margin = new MarginPadding { Horizontal = expanded_panel_spacing }; + + float scrollOffset = flow.IndexOf(expandedPanel) * (ScorePanel.CONTRACTED_WIDTH + panel_spacing); + scroll.ScrollTo(scrollOffset); } protected override void Update() { base.Update(); - flow.Padding = new MarginPadding { Horizontal = DrawWidth / 2f - ScorePanel.EXPANDED_WIDTH / 2f - expanded_panel_spacing }; - } - - private void onPanelStateChanged(ScorePanel panel, PanelState state) - { - if (state == PanelState.Contracted) - return; - - if (expandedPanel != null) - { - expandedPanel.Margin = new MarginPadding(0); - expandedPanel.State = PanelState.Contracted; - } - - expandedPanel = panel; - expandedPanel.Margin = new MarginPadding { Horizontal = expanded_panel_spacing }; - - float panelOffset = flow.IndexOf(expandedPanel) * (ScorePanel.CONTRACTED_WIDTH + panel_spacing); - - scroll.ScrollTo(panelOffset); + flow.Padding = new MarginPadding { Horizontal = DrawWidth / 2f - expandedPanel.DrawWidth / 2f - expanded_panel_spacing }; } private class Flow : FillFlowContainer From 45244683de150597b66234e5ee78b78c0f718189 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 21 May 2020 22:07:06 +0900 Subject: [PATCH 1722/2376] Fix scrolling (1-frame + maintain scroll position) --- .../Visual/Ranking/TestSceneScorePanelList.cs | 49 +++++++++++- osu.Game/Screens/Ranking/ScorePanel.cs | 71 +++++++++------- osu.Game/Screens/Ranking/ScorePanelList.cs | 80 ++++++++++++++++--- 3 files changed, 154 insertions(+), 46 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs index f00bf7e151..89aef377c8 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs @@ -1,10 +1,14 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; +using osu.Framework.Testing; +using osu.Framework.Utils; using osu.Game.Rulesets.Osu; +using osu.Game.Scoring; using osu.Game.Screens.Ranking; using osuTK.Graphics; @@ -12,12 +16,13 @@ namespace osu.Game.Tests.Visual.Ranking { public class TestSceneScorePanelList : OsuTestScene { + private ScoreInfo initialScore; private ScorePanelList list; [SetUp] public void Setup() => Schedule(() => { - Child = list = new ScorePanelList(new TestScoreInfo(new OsuRuleset().RulesetInfo)) + Child = list = new ScorePanelList(initialScore = new TestScoreInfo(new OsuRuleset().RulesetInfo)) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -36,16 +41,52 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestSingleScore() { + assertPanelCentred(); } [Test] - public void TestManyScores() + public void TestAddManyScoresAfter() { - AddStep("add many scores", () => + AddStep("add scores", () => { for (int i = 0; i < 20; i++) - list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo)); + list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo) { TotalScore = initialScore.TotalScore - i - 1 }); }); + + assertPanelCentred(); } + + [Test] + public void TestAddManyScoresBefore() + { + AddStep("add scores", () => + { + for (int i = 0; i < 20; i++) + list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo) { TotalScore = initialScore.TotalScore + i + 1 }); + }); + + assertPanelCentred(); + } + + [Test] + public void TestAddManyPanelsOnBothSides() + { + AddStep("add scores after", () => + { + for (int i = 0; i < 20; i++) + list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo) { TotalScore = initialScore.TotalScore - i - 1 }); + + for (int i = 0; i < 20; i++) + list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo) { TotalScore = initialScore.TotalScore + i + 1 }); + }); + + assertPanelCentred(); + } + + private void assertPanelCentred() => AddUntilStep("expanded panel centred", () => + { + var expandedPanel = list.ChildrenOfType().Single(p => p.State == PanelState.Expanded); + return Precision.AlmostEquals(expandedPanel.ScreenSpaceDrawQuad.Centre.X, list.ScreenSpaceDrawQuad.Centre.X, 1); + }); } } diff --git a/osu.Game/Screens/Ranking/ScorePanel.cs b/osu.Game/Screens/Ranking/ScorePanel.cs index 2f6146a5e7..2933bbddd1 100644 --- a/osu.Game/Screens/Ranking/ScorePanel.cs +++ b/osu.Game/Screens/Ranking/ScorePanel.cs @@ -78,6 +78,8 @@ namespace osu.Game.Screens.Ranking public event Action StateChanged; public readonly ScoreInfo Score; + private Container content; + private Container topLayerContainer; private Drawable topLayerBackground; private Container topLayerContentContainer; @@ -96,41 +98,46 @@ namespace osu.Game.Screens.Ranking [BackgroundDependencyLoader] private void load() { - InternalChildren = new Drawable[] + InternalChild = content = new Container { - topLayerContainer = new Container + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] { - Name = "Top layer", - RelativeSizeAxes = Axes.X, - Height = 120, - Children = new Drawable[] + topLayerContainer = new Container { - new Container + Name = "Top layer", + RelativeSizeAxes = Axes.X, + Height = 120, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - CornerRadius = 20, - CornerExponent = 2.5f, - Masking = true, - Child = topLayerBackground = new Box { RelativeSizeAxes = Axes.Both } - }, - topLayerContentContainer = new Container { RelativeSizeAxes = Axes.Both } - } - }, - middleLayerContainer = new Container - { - Name = "Middle layer", - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + new Container + { + RelativeSizeAxes = Axes.Both, + CornerRadius = 20, + CornerExponent = 2.5f, + Masking = true, + Child = topLayerBackground = new Box { RelativeSizeAxes = Axes.Both } + }, + topLayerContentContainer = new Container { RelativeSizeAxes = Axes.Both } + } + }, + middleLayerContainer = new Container { - new Container + Name = "Middle layer", + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - CornerRadius = 20, - CornerExponent = 2.5f, - Masking = true, - Child = middleLayerBackground = new Box { RelativeSizeAxes = Axes.Both } - }, - middleLayerContentContainer = new Container { RelativeSizeAxes = Axes.Both } + new Container + { + RelativeSizeAxes = Axes.Both, + CornerRadius = 20, + CornerExponent = 2.5f, + Masking = true, + Child = middleLayerBackground = new Box { RelativeSizeAxes = Axes.Both } + }, + middleLayerContentContainer = new Container { RelativeSizeAxes = Axes.Both } + } } } }; @@ -181,7 +188,7 @@ namespace osu.Game.Screens.Ranking switch (state) { case PanelState.Expanded: - this.ResizeTo(new Vector2(EXPANDED_WIDTH, expanded_height), resize_duration, Easing.OutQuint); + Size = new Vector2(EXPANDED_WIDTH, expanded_height); topLayerBackground.FadeColour(expanded_top_layer_colour, resize_duration, Easing.OutQuint); middleLayerBackground.FadeColour(expanded_middle_layer_colour, resize_duration, Easing.OutQuint); @@ -191,7 +198,7 @@ namespace osu.Game.Screens.Ranking break; case PanelState.Contracted: - this.ResizeTo(new Vector2(CONTRACTED_WIDTH, contracted_height), resize_duration, Easing.OutQuint); + Size = new Vector2(CONTRACTED_WIDTH, contracted_height); topLayerBackground.FadeColour(contracted_top_layer_colour, resize_duration, Easing.OutQuint); middleLayerBackground.FadeColour(contracted_middle_layer_colour, resize_duration, Easing.OutQuint); @@ -200,6 +207,8 @@ namespace osu.Game.Screens.Ranking break; } + content.ResizeTo(Size, resize_duration, Easing.OutQuint); + bool topLayerExpanded = topLayerContainer.Y < 0; // If the top layer was already expanded, then we don't need to wait for the resize and can instead transform immediately. This looks better when changing the panel state. diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index c2fd487767..6dd21ec49d 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -24,7 +24,7 @@ namespace osu.Game.Screens.Ranking private const float expanded_panel_spacing = 15; private readonly Flow flow; - private readonly ScrollContainer scroll; + private readonly Scroll scroll; private ScorePanel expandedPanel; @@ -32,7 +32,7 @@ namespace osu.Game.Screens.Ranking { RelativeSizeAxes = Axes.Both; - InternalChild = scroll = new OsuScrollContainer(Direction.Horizontal) + InternalChild = scroll = new Scroll { RelativeSizeAxes = Axes.Both, Child = flow = new Flow @@ -46,9 +46,13 @@ namespace osu.Game.Screens.Ranking }; AddScore(initialScore); - ShowScore(initialScore); + presentScore(initialScore); } + /// + /// Adds a to this list. + /// + /// The to add. public void AddScore(ScoreInfo score) { flow.Add(new ScorePanel(score) @@ -60,24 +64,45 @@ namespace osu.Game.Screens.Ranking p.StateChanged += s => { if (s == PanelState.Expanded) - ShowScore(score); + presentScore(score); }; })); + + // We want the scroll position to remain relative to the expanded panel. When a new panel is added after the expanded panel, nothing needs to be done. + // But when a panel is added before the expanded panel, we need to offset the scroll position by the width of the new panel. + if (expandedPanel != null && flow.GetPanelIndex(score) < flow.GetPanelIndex(expandedPanel.Score)) + { + // A somewhat hacky property is used here because we need to: + // 1) Scroll after the scroll container's visible range is updated. + // 2) Scroll before the scroll container's scroll position is updated. + // Without this, we would have a 1-frame positioning error which looks very jarring. + scroll.InstantScrollTarget = (scroll.InstantScrollTarget ?? scroll.Target) + ScorePanel.CONTRACTED_WIDTH + panel_spacing; + } } - public void ShowScore(ScoreInfo score) + /// + /// Brings a to the centre of the screen and expands it. + /// + /// The to present. + private void presentScore(ScoreInfo score) { + // Contract the old panel. foreach (var p in flow.Where(p => p.Score != score)) + { p.State = PanelState.Contracted; + p.Margin = new MarginPadding(); + } - if (expandedPanel != null) - expandedPanel.Margin = new MarginPadding(0); - + // Expand the new panel. expandedPanel = flow.Single(p => p.Score == score); expandedPanel.State = PanelState.Expanded; expandedPanel.Margin = new MarginPadding { Horizontal = expanded_panel_spacing }; - float scrollOffset = flow.IndexOf(expandedPanel) * (ScorePanel.CONTRACTED_WIDTH + panel_spacing); + // Scroll to the new panel. This is done manually since we need: + // 1) To scroll after the scroll container's visible range is updated. + // 2) To account for the centre anchor/origins of panels. + // In the end, it's easier to compute the scroll position manually. + float scrollOffset = flow.GetPanelIndex(expandedPanel.Score) * (ScorePanel.CONTRACTED_WIDTH + panel_spacing); scroll.ScrollTo(scrollOffset); } @@ -85,12 +110,45 @@ namespace osu.Game.Screens.Ranking { base.Update(); - flow.Padding = new MarginPadding { Horizontal = DrawWidth / 2f - expandedPanel.DrawWidth / 2f - expanded_panel_spacing }; + // Add padding to both sides such that the centre of an expanded panel on either side is in the middle of the screen. + flow.Padding = new MarginPadding { Horizontal = DrawWidth / 2f - ScorePanel.EXPANDED_WIDTH / 2f - expanded_panel_spacing }; } private class Flow : FillFlowContainer { - public override IEnumerable FlowingChildren => AliveInternalChildren.OfType().OrderByDescending(s => s.Score.TotalScore).ThenByDescending(s => s.Score.OnlineScoreID); + public override IEnumerable FlowingChildren => applySorting(AliveInternalChildren); + + public int GetPanelIndex(ScoreInfo score) => applySorting(Children).OfType().TakeWhile(s => s.Score != score).Count(); + + private IEnumerable applySorting(IEnumerable drawables) => drawables.OfType() + .OrderByDescending(s => s.Score.TotalScore) + .ThenByDescending(s => s.Score.OnlineScoreID); + } + + private class Scroll : OsuScrollContainer + { + public new float Target => base.Target; + + public Scroll() + : base(Direction.Horizontal) + { + } + + /// + /// The target that will be scrolled to instantaneously next frame. + /// + public float? InstantScrollTarget; + + protected override void UpdateAfterChildren() + { + if (InstantScrollTarget != null) + { + ScrollTo(InstantScrollTarget.Value, false); + InstantScrollTarget = null; + } + + base.UpdateAfterChildren(); + } } } } From f5c80ac2d5c654794219db95fb5e2c8d48b5a7ce Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 21 May 2020 22:07:24 +0900 Subject: [PATCH 1723/2376] Remove vertical line --- .../Visual/Ranking/TestSceneScorePanelList.cs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs index 89aef377c8..b32b3afbda 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs @@ -4,13 +4,11 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; -using osu.Framework.Graphics.Shapes; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Rulesets.Osu; using osu.Game.Scoring; using osu.Game.Screens.Ranking; -using osuTK.Graphics; namespace osu.Game.Tests.Visual.Ranking { @@ -27,15 +25,6 @@ namespace osu.Game.Tests.Visual.Ranking Anchor = Anchor.Centre, Origin = Anchor.Centre, }; - - Add(new Box - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.Y, - Width = 1, - Colour = Color4.Red - }); }); [Test] From 899b9f8060928a5ed60984bc5eb073314ab63692 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 21 May 2020 22:26:04 +0900 Subject: [PATCH 1724/2376] Fix incorrect sorting order --- osu.Game/Screens/Ranking/ScorePanelList.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index 6dd21ec49d..ed6d07d078 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -122,7 +122,7 @@ namespace osu.Game.Screens.Ranking private IEnumerable applySorting(IEnumerable drawables) => drawables.OfType() .OrderByDescending(s => s.Score.TotalScore) - .ThenByDescending(s => s.Score.OnlineScoreID); + .ThenBy(s => s.Score.OnlineScoreID); } private class Scroll : OsuScrollContainer From 8702a1b5a57ba9a70657b244eaec33797727caf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 21 May 2020 20:10:51 +0200 Subject: [PATCH 1725/2376] Fix test scene regression --- .../Visual/Gameplay/TestSceneScrollingHitObjects.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs index 0d15e495e3..20b040dbc3 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs @@ -16,6 +16,7 @@ using osu.Game.Configuration; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Timing; using osu.Game.Rulesets.UI.Scrolling; using osuTK; @@ -221,7 +222,7 @@ namespace osu.Game.Tests.Visual.Gameplay private class TestDrawableControlPoint : DrawableHitObject { public TestDrawableControlPoint(ScrollingDirection direction, double time) - : base(new HitObject { StartTime = time }) + : base(new HitObject { StartTime = time, HitWindows = HitWindows.Empty }) { Origin = Anchor.Centre; @@ -252,7 +253,7 @@ namespace osu.Game.Tests.Visual.Gameplay private class TestDrawableHitObject : DrawableHitObject { public TestDrawableHitObject(double time) - : base(new HitObject { StartTime = time }) + : base(new HitObject { StartTime = time, HitWindows = HitWindows.Empty }) { Origin = Anchor.Custom; OriginPosition = new Vector2(75 / 4.0f); From 24d898c87031a62b21c98812e0ff2939392f7d5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 21 May 2020 21:35:12 +0200 Subject: [PATCH 1726/2376] Demonstrate failure case in visual test scene --- .../Gameplay/TestSceneScrollingHitObjects.cs | 37 +++++++++++++++++-- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs index 20b040dbc3..2f15e549f7 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs @@ -78,19 +78,18 @@ namespace osu.Game.Tests.Visual.Gameplay } }; - setUpHitObjects(); + hitObjectSpawnDelegate?.Cancel(); }); - private void setUpHitObjects() + private void setUpHitObjects() => AddStep("set up hit objects", () => { scrollContainers.ForEach(c => c.ControlPoints.Add(new MultiplierControlPoint(0))); for (int i = spawn_rate / 2; i <= time_range; i += spawn_rate) addHitObject(Time.Current + i); - hitObjectSpawnDelegate?.Cancel(); hitObjectSpawnDelegate = Scheduler.AddDelayed(() => addHitObject(Time.Current + time_range), spawn_rate, true); - } + }); private IList testControlPoints => new List { @@ -102,6 +101,8 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestScrollAlgorithms() { + setUpHitObjects(); + AddStep("constant scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Constant)); AddStep("overlapping scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Overlapping)); AddStep("sequential scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Sequential)); @@ -114,6 +115,8 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestConstantScrollLifetime() { + setUpHitObjects(); + AddStep("set constant scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Constant)); // scroll container time range must be less than the rate of spawning hitobjects // otherwise the hitobjects will spawn already partly visible on screen and look wrong @@ -123,14 +126,40 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestSequentialScrollLifetime() { + setUpHitObjects(); + AddStep("set sequential scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Sequential)); AddStep("set time range", () => scrollContainers.ForEach(c => c.TimeRange = time_range / 2.0)); AddStep("add control points", () => addControlPoints(testControlPoints, Time.Current)); } + [Test] + public void TestSlowSequentialScroll() + { + AddStep("set sequential scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Sequential)); + AddStep("set time range", () => scrollContainers.ForEach(c => c.TimeRange = time_range)); + AddStep("add control points", () => addControlPoints( + new List + { + new MultiplierControlPoint { Velocity = 0.1 } + }, + Time.Current + time_range)); + + // All of the hit objects added below should be immediately visible on screen + AddStep("add hit objects", () => + { + for (int i = 0; i < 20; ++i) + { + addHitObject(Time.Current + time_range * (2 + 0.1 * i)); + } + }); + } + [Test] public void TestOverlappingScrollLifetime() { + setUpHitObjects(); + AddStep("set overlapping scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Overlapping)); AddStep("set time range", () => scrollContainers.ForEach(c => c.TimeRange = time_range / 2.0)); AddStep("add control points", () => addControlPoints(testControlPoints, Time.Current)); From 4299bd05b4ca268e192a4f4d469f69bed4c6415b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 21 May 2020 21:39:15 +0200 Subject: [PATCH 1727/2376] Add test cases for sequential scroll algorithm --- .../ScrollAlgorithms/SequentialScrollTest.cs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/ScrollAlgorithms/SequentialScrollTest.cs b/osu.Game.Tests/ScrollAlgorithms/SequentialScrollTest.cs index 1f0c069f8d..bd578dcbc4 100644 --- a/osu.Game.Tests/ScrollAlgorithms/SequentialScrollTest.cs +++ b/osu.Game.Tests/ScrollAlgorithms/SequentialScrollTest.cs @@ -29,8 +29,22 @@ namespace osu.Game.Tests.ScrollAlgorithms [Test] public void TestDisplayStartTime() { - // Sequential scroll algorithm approximates the start time - // This should be fixed in the future + // easy cases - time range adjusted for velocity fits within control point duration + Assert.AreEqual(2500, algorithm.GetDisplayStartTime(5000, 0, 2500, 1)); // 5000 - (2500 / 1) + Assert.AreEqual(13750, algorithm.GetDisplayStartTime(15000, 0, 2500, 1)); // 15000 - (2500 / 2) + Assert.AreEqual(20000, algorithm.GetDisplayStartTime(25000, 0, 2500, 1)); // 25000 - (2500 / 0.5) + + // hard case - time range adjusted for velocity exceeds control point duration + + // 1st multiplier point takes 10000 / 2500 = 4 scroll lengths + // 2nd multiplier point takes 10000 / (2500 / 2) = 8 scroll lengths + // 3rd multiplier point takes 2500 / (2500 * 2) = 0.5 scroll lengths up to hitobject start + + // absolute position of the hitobject = 1000 * (4 + 8 + 0.5) = 12500 + // minus one scroll length allowance = 12500 - 1000 = 11500 = 11.5 [scroll lengths] + // therefore the start time lies within the second multiplier point (because 11.5 < 4 + 8) + // its exact time position is = 10000 + 7.5 * (2500 / 2) = 19375 + Assert.AreEqual(19375, algorithm.GetDisplayStartTime(22500, 0, 2500, 1000)); } [Test] From 6f388b731ee97aedebc375370b429539cf8946d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 21 May 2020 21:41:56 +0200 Subject: [PATCH 1728/2376] Fix display start time in sequential scroll algorithm --- .../UI/Scrolling/Algorithms/SequentialScrollAlgorithm.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/UI/Scrolling/Algorithms/SequentialScrollAlgorithm.cs b/osu.Game/Rulesets/UI/Scrolling/Algorithms/SequentialScrollAlgorithm.cs index 41f9ebdb82..0052c877f6 100644 --- a/osu.Game/Rulesets/UI/Scrolling/Algorithms/SequentialScrollAlgorithm.cs +++ b/osu.Game/Rulesets/UI/Scrolling/Algorithms/SequentialScrollAlgorithm.cs @@ -22,8 +22,7 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms public double GetDisplayStartTime(double originTime, float offset, double timeRange, float scrollLength) { - double adjustedTime = TimeAt(-offset, originTime, timeRange, scrollLength); - return adjustedTime - timeRange - 1000; + return TimeAt(-(scrollLength + offset), originTime, timeRange, scrollLength); } public float GetLength(double startTime, double endTime, double timeRange, float scrollLength) From 8a105bdbcfc554f4bd62217782d01632722a047a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 22 May 2020 11:19:57 +0900 Subject: [PATCH 1729/2376] Remove unused ColumnAt method --- osu.Game.Rulesets.Mania/Edit/IManiaHitObjectComposer.cs | 2 -- osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs | 9 +-------- osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs | 2 +- osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs | 8 -------- 4 files changed, 2 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/IManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/IManiaHitObjectComposer.cs index 48e6b63064..5d9ad21cb7 100644 --- a/osu.Game.Rulesets.Mania/Edit/IManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/IManiaHitObjectComposer.cs @@ -8,8 +8,6 @@ namespace osu.Game.Rulesets.Mania.Edit { public interface IManiaHitObjectComposer { - Column ColumnAt(Vector2 screenSpacePosition); - ManiaPlayfield Playfield { get; } Vector2 ScreenSpacePositionAtTime(double time, Column column = null); diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index 4795bdd8e2..d4cfab840d 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -44,13 +44,6 @@ namespace osu.Game.Rulesets.Mania.Edit inputManager = GetContainingInputManager(); } - /// - /// Retrieves the column that intersects a screen-space position. - /// - /// The screen-space position. - /// The column which intersects with . - public Column ColumnAt(Vector2 screenSpacePosition) => drawableRuleset.GetColumnByPosition(screenSpacePosition); - private DependencyContainer dependencies; protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) @@ -85,7 +78,7 @@ namespace osu.Game.Rulesets.Mania.Edit public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) { - var column = ColumnAt(screenSpacePosition); + var column = Playfield.GetColumnByPosition(screenSpacePosition); if (column == null) return new SnapResult(screenSpacePosition, null); diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs index 83049ff959..4ea71652bc 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Mania.Edit private void performColumnMovement(int lastColumn, MoveSelectionEvent moveEvent) { - var currentColumn = composer.ColumnAt(moveEvent.ScreenSpacePosition); + var currentColumn = composer.Playfield.GetColumnByPosition(moveEvent.ScreenSpacePosition); if (currentColumn == null) return; diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index f3f843f366..94b5ee9486 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -23,7 +23,6 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; -using osuTK; namespace osu.Game.Rulesets.Mania.UI { @@ -108,13 +107,6 @@ namespace osu.Game.Rulesets.Mania.UI private void updateTimeRange() => TimeRange.Value = configTimeRange.Value * speedAdjustmentTrack.AggregateTempo.Value * speedAdjustmentTrack.AggregateFrequency.Value; - /// - /// Retrieves the column that intersects a screen-space position. - /// - /// The screen-space position. - /// The column which intersects with . - public Column GetColumnByPosition(Vector2 screenSpacePosition) => Playfield.GetColumnByPosition(screenSpacePosition); - public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new ManiaPlayfieldAdjustmentContainer(); protected override Playfield CreatePlayfield() => new ManiaPlayfield(Beatmap.Stages); From 9a2889abc54d2c5217bb5adbbd591be7c8dc7d97 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 22 May 2020 11:32:35 +0900 Subject: [PATCH 1730/2376] Remove remaining left-over test implementations --- .../ManiaPlacementBlueprintTestScene.cs | 3 --- .../ManiaSelectionBlueprintTestScene.cs | 9 +-------- .../TestSceneManiaBeatSnapGrid.cs | 5 ----- 3 files changed, 1 insertion(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs index 9a50802454..fd18907d96 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs @@ -15,7 +15,6 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Tests.Visual; -using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Mania.Tests @@ -57,7 +56,5 @@ namespace osu.Game.Rulesets.Mania.Tests protected override void AddHitObject(DrawableHitObject hitObject) => column.Add((DrawableManiaHitObject)hitObject); public ManiaPlayfield Playfield => null; - - public Vector2 ScreenSpacePositionAtTime(double time, Column column = null) => Vector2.Zero; } } diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaSelectionBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/ManiaSelectionBlueprintTestScene.cs index f7dffbbc1a..35fe596e98 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaSelectionBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaSelectionBlueprintTestScene.cs @@ -7,7 +7,6 @@ using osu.Framework.Timing; using osu.Game.Rulesets.Mania.Edit; using osu.Game.Rulesets.Mania.UI; using osu.Game.Tests.Visual; -using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Mania.Tests @@ -18,11 +17,9 @@ namespace osu.Game.Rulesets.Mania.Tests [Cached(Type = typeof(IAdjustableClock))] private readonly IAdjustableClock clock = new StopwatchClock(); - private readonly Column column; - protected ManiaSelectionBlueprintTestScene() { - Add(column = new Column(0) + Add(new Column(0) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -31,10 +28,6 @@ namespace osu.Game.Rulesets.Mania.Tests }); } - public Column ColumnAt(Vector2 screenSpacePosition) => column; - public ManiaPlayfield Playfield => null; - - public Vector2 ScreenSpacePositionAtTime(double time, Column column = null) => Vector2.Zero; } } diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaBeatSnapGrid.cs index feda3cfb81..ce9546415f 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaBeatSnapGrid.cs @@ -14,7 +14,6 @@ using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Screens.Edit; using osu.Game.Tests.Visual; -using osuTK; namespace osu.Game.Rulesets.Mania.Tests { @@ -66,10 +65,6 @@ namespace osu.Game.Rulesets.Mania.Tests return true; } - public Column ColumnAt(Vector2 screenSpacePosition) => null; - public ManiaPlayfield Playfield { get; } - - public Vector2 ScreenSpacePositionAtTime(double time, Column column = null) => Vector2.Zero; } } From f364d0e8328070f77232472b1db3dcee7d9f4bdf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 22 May 2020 11:35:26 +0900 Subject: [PATCH 1731/2376] Reduce IManiaHitObjectComposer scope --- .../ManiaPlacementBlueprintTestScene.cs | 5 +---- .../ManiaSelectionBlueprintTestScene.cs | 9 ++------- .../Edit/IManiaHitObjectComposer.cs | 5 +---- .../Edit/ManiaHitObjectComposer.cs | 11 +---------- osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs | 4 ++-- osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs | 8 -------- 6 files changed, 7 insertions(+), 35 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs index 547786847b..fd18907d96 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs @@ -15,7 +15,6 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Tests.Visual; -using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Mania.Tests @@ -56,8 +55,6 @@ namespace osu.Game.Rulesets.Mania.Tests protected override void AddHitObject(DrawableHitObject hitObject) => column.Add((DrawableManiaHitObject)hitObject); - public Column ColumnAt(Vector2 screenSpacePosition) => column; - - public int TotalColumns => 1; + public ManiaPlayfield Playfield => null; } } diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaSelectionBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/ManiaSelectionBlueprintTestScene.cs index b598893e8c..35fe596e98 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaSelectionBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaSelectionBlueprintTestScene.cs @@ -7,7 +7,6 @@ using osu.Framework.Timing; using osu.Game.Rulesets.Mania.Edit; using osu.Game.Rulesets.Mania.UI; using osu.Game.Tests.Visual; -using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Mania.Tests @@ -18,11 +17,9 @@ namespace osu.Game.Rulesets.Mania.Tests [Cached(Type = typeof(IAdjustableClock))] private readonly IAdjustableClock clock = new StopwatchClock(); - private readonly Column column; - protected ManiaSelectionBlueprintTestScene() { - Add(column = new Column(0) + Add(new Column(0) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -31,8 +28,6 @@ namespace osu.Game.Rulesets.Mania.Tests }); } - public Column ColumnAt(Vector2 screenSpacePosition) => column; - - public int TotalColumns => 1; + public ManiaPlayfield Playfield => null; } } diff --git a/osu.Game.Rulesets.Mania/Edit/IManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/IManiaHitObjectComposer.cs index f64bab1fae..3818d0e15d 100644 --- a/osu.Game.Rulesets.Mania/Edit/IManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/IManiaHitObjectComposer.cs @@ -2,14 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using osu.Game.Rulesets.Mania.UI; -using osuTK; namespace osu.Game.Rulesets.Mania.Edit { public interface IManiaHitObjectComposer { - Column ColumnAt(Vector2 screenSpacePosition); - - int TotalColumns { get; } + ManiaPlayfield Playfield { get; } } } diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index cfb04c8e50..8367b4f5e9 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -26,13 +26,6 @@ namespace osu.Game.Rulesets.Mania.Edit { } - /// - /// Retrieves the column that intersects a screen-space position. - /// - /// The screen-space position. - /// The column which intersects with . - public Column ColumnAt(Vector2 screenSpacePosition) => drawableRuleset.GetColumnByPosition(screenSpacePosition); - private DependencyContainer dependencies; protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) @@ -42,11 +35,9 @@ namespace osu.Game.Rulesets.Mania.Edit public IScrollingInfo ScrollingInfo => drawableRuleset.ScrollingInfo; - public int TotalColumns => Playfield.TotalColumns; - public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) { - var column = ColumnAt(screenSpacePosition); + var column = Playfield.GetColumnByPosition(screenSpacePosition); if (column == null) return new SnapResult(screenSpacePosition, null); diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs index 55245198c8..4ea71652bc 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Mania.Edit private void performColumnMovement(int lastColumn, MoveSelectionEvent moveEvent) { - var currentColumn = composer.ColumnAt(moveEvent.ScreenSpacePosition); + var currentColumn = composer.Playfield.GetColumnByPosition(moveEvent.ScreenSpacePosition); if (currentColumn == null) return; @@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Mania.Edit maxColumn = obj.Column; } - columnDelta = Math.Clamp(columnDelta, -minColumn, composer.TotalColumns - 1 - maxColumn); + columnDelta = Math.Clamp(columnDelta, -minColumn, composer.Playfield.TotalColumns - 1 - maxColumn); foreach (var obj in SelectedHitObjects.OfType()) obj.Column += columnDelta; diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index f3f843f366..94b5ee9486 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -23,7 +23,6 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; -using osuTK; namespace osu.Game.Rulesets.Mania.UI { @@ -108,13 +107,6 @@ namespace osu.Game.Rulesets.Mania.UI private void updateTimeRange() => TimeRange.Value = configTimeRange.Value * speedAdjustmentTrack.AggregateTempo.Value * speedAdjustmentTrack.AggregateFrequency.Value; - /// - /// Retrieves the column that intersects a screen-space position. - /// - /// The screen-space position. - /// The column which intersects with . - public Column GetColumnByPosition(Vector2 screenSpacePosition) => Playfield.GetColumnByPosition(screenSpacePosition); - public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new ManiaPlayfieldAdjustmentContainer(); protected override Playfield CreatePlayfield() => new ManiaPlayfield(Beatmap.Stages); From b2667bbb0210d00b9f6502362fc632f93bb9fa92 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 22 May 2020 11:45:58 +0900 Subject: [PATCH 1732/2376] Move protected implementation down --- .../Edit/ManiaHitObjectComposer.cs | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index 82a55b4965..683e921cbf 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -53,29 +53,6 @@ namespace osu.Game.Rulesets.Mania.Edit public IScrollingInfo ScrollingInfo => drawableRuleset.ScrollingInfo; - protected override void UpdateAfterChildren() - { - base.UpdateAfterChildren(); - - if (BlueprintContainer.CurrentTool is SelectTool) - { - if (EditorBeatmap.SelectedHitObjects.Any()) - { - beatSnapGrid.SelectionTimeRange = (EditorBeatmap.SelectedHitObjects.Min(h => h.StartTime), EditorBeatmap.SelectedHitObjects.Max(h => h.GetEndTime())); - } - else - beatSnapGrid.SelectionTimeRange = null; - } - else - { - var result = SnapScreenSpacePositionToValidTime(inputManager.CurrentState.Mouse.Position); - if (result.Time is double time) - beatSnapGrid.SelectionTimeRange = (time, time); - else - beatSnapGrid.SelectionTimeRange = null; - } - } - public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) { var column = Playfield.GetColumnByPosition(screenSpacePosition); @@ -111,5 +88,28 @@ namespace osu.Game.Rulesets.Mania.Edit new NoteCompositionTool(), new HoldNoteCompositionTool() }; + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + if (BlueprintContainer.CurrentTool is SelectTool) + { + if (EditorBeatmap.SelectedHitObjects.Any()) + { + beatSnapGrid.SelectionTimeRange = (EditorBeatmap.SelectedHitObjects.Min(h => h.StartTime), EditorBeatmap.SelectedHitObjects.Max(h => h.GetEndTime())); + } + else + beatSnapGrid.SelectionTimeRange = null; + } + else + { + var result = SnapScreenSpacePositionToValidTime(inputManager.CurrentState.Mouse.Position); + if (result.Time is double time) + beatSnapGrid.SelectionTimeRange = (time, time); + else + beatSnapGrid.SelectionTimeRange = null; + } + } } } From d529a2aefac633bde0bcdf32e215e0d89af7bff6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 22 May 2020 12:28:01 +0900 Subject: [PATCH 1733/2376] Remove left-over function --- .../Edit/ManiaBeatSnapGrid.cs | 32 ------------------- 1 file changed, 32 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs index fa8f8a755a..e52cd5774c 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs @@ -11,12 +11,10 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps; using osu.Game.Graphics; -using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Screens.Edit; -using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Mania.Edit @@ -171,36 +169,6 @@ namespace osu.Game.Rulesets.Mania.Edit } } - public (Vector2 position, double time)? GetSnappedPosition(Vector2 position) - { - float minDist = float.PositiveInfinity; - DrawableGridLine minDistLine = null; - - Vector2 minDistLinePosition = Vector2.Zero; - - foreach (var grid in grids) - { - foreach (var line in grid.Objects.OfType()) - { - Vector2 linePos = line.ToSpaceOfOtherDrawable(line.OriginPosition, this); - float d = Vector2.Distance(position, linePos); - - if (d < minDist) - { - minDist = d; - minDistLine = line; - minDistLinePosition = linePos; - } - } - } - - if (minDistLine == null) - return null; - - float noteOffset = (scrollingInfo.Direction.Value == ScrollingDirection.Up ? 1 : -1) * DefaultNotePiece.NOTE_HEIGHT / 2; - return (new Vector2(position.X, minDistLinePosition.Y + noteOffset), minDistLine.HitObject.StartTime); - } - private class DrawableGridLine : DrawableHitObject { [Resolved] From ce35d09e7dac2987f3cb439366eba75e0db0d7a1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 22 May 2020 12:45:37 +0900 Subject: [PATCH 1734/2376] Fix incorrect alpha application to lines on rewinding --- osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs | 8 ++++++-- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs index e52cd5774c..b5b6c08fca 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs @@ -199,10 +199,14 @@ namespace osu.Game.Rulesets.Mania.Edit : Anchor.BottomLeft; } + protected override void UpdateInitialTransforms() + { + // don't perform any fading – we are handling that ourselves. + } + protected override void UpdateStateTransforms(ArmedState state) { - using (BeginAbsoluteSequence(HitObject.StartTime + 1000)) - this.FadeOut(); + LifetimeEnd = HitObject.StartTime + visible_range; } } } diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index d594909cda..44afb7a227 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -257,7 +257,7 @@ namespace osu.Game.Rulesets.Objects.Drawables } } - if (state.Value != ArmedState.Idle && LifetimeEnd == double.MaxValue || HitObject.HitWindows == null) + if (LifetimeEnd == double.MaxValue && (state.Value != ArmedState.Idle || HitObject.HitWindows == null)) Expire(); // apply any custom state overrides From dd09d7830d108ff1de968035507f66199fc2ab57 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 22 May 2020 16:37:28 +0900 Subject: [PATCH 1735/2376] Cache and resolve editor clock as EditorClock in all cases --- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 14 +++++-------- osu.Game/Rulesets/Edit/PlacementBlueprint.cs | 11 +++++----- .../Edit/Components/PlaybackControl.cs | 11 +++++----- .../Edit/Components/TimeInfoContainer.cs | 5 ++--- .../Timelines/Summary/Parts/MarkerPart.cs | 15 +++++++------ .../Timelines/Summary/SummaryTimeline.cs | 5 ++--- .../Compose/Components/BlueprintContainer.cs | 5 ++--- .../Compose/Components/Timeline/Timeline.cs | 21 +++++++++---------- osu.Game/Screens/Edit/Editor.cs | 3 +-- osu.Game/Screens/Edit/Timing/TimingScreen.cs | 2 +- osu.Game/Tests/Visual/EditorClockTestScene.cs | 3 +-- 11 files changed, 41 insertions(+), 54 deletions(-) diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index fd8af0afd5..1987148aed 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -11,7 +11,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Framework.Logging; -using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Configuration; @@ -38,14 +37,11 @@ namespace osu.Game.Rulesets.Edit protected readonly Ruleset Ruleset; [Resolved] - protected IFrameBasedClock EditorClock { get; private set; } + protected EditorClock EditorClock { get; private set; } [Resolved] protected EditorBeatmap EditorBeatmap { get; private set; } - [Resolved] - private IAdjustableClock adjustableClock { get; set; } - [Resolved] protected IBeatSnapProvider BeatSnapProvider { get; private set; } @@ -68,7 +64,7 @@ namespace osu.Game.Rulesets.Edit } [BackgroundDependencyLoader] - private void load(IFrameBasedClock framedClock) + private void load() { Config = Dependencies.Get().GetConfigFor(Ruleset); @@ -76,7 +72,7 @@ namespace osu.Game.Rulesets.Edit { drawableRulesetWrapper = new DrawableEditRulesetWrapper(CreateDrawableRuleset(Ruleset, EditorBeatmap.PlayableBeatmap)) { - Clock = framedClock, + Clock = EditorClock, ProcessCustomClock = false }; } @@ -221,8 +217,8 @@ namespace osu.Game.Rulesets.Edit { EditorBeatmap.Add(hitObject); - if (adjustableClock.CurrentTime < hitObject.StartTime) - adjustableClock.Seek(hitObject.StartTime); + if (EditorClock.CurrentTime < hitObject.StartTime) + EditorClock.Seek(hitObject.StartTime); } } diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index f0b63f8ea5..20584c66e5 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -6,10 +6,10 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; -using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Objects; +using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Compose; using osuTK; @@ -30,7 +30,8 @@ namespace osu.Game.Rulesets.Edit /// protected readonly HitObject HitObject; - protected IClock EditorClock { get; private set; } + [Resolved] + protected EditorClock EditorClock { get; private set; } private readonly IBindable beatmap = new Bindable(); @@ -49,12 +50,10 @@ namespace osu.Game.Rulesets.Edit } [BackgroundDependencyLoader] - private void load(IBindable beatmap, IAdjustableClock clock) + private void load(IBindable beatmap) { this.beatmap.BindTo(beatmap); - EditorClock = clock; - ApplyDefaultsToHitObject(); } @@ -84,7 +83,7 @@ namespace osu.Game.Rulesets.Edit } [Resolved(canBeNull: true)] - private IFrameBasedClock editorClock { get; set; } + private EditorClock editorClock { get; set; } /// /// Updates the position of this to a new screen-space position. diff --git a/osu.Game/Screens/Edit/Components/PlaybackControl.cs b/osu.Game/Screens/Edit/Components/PlaybackControl.cs index 897c6ec531..59b3d1c565 100644 --- a/osu.Game/Screens/Edit/Components/PlaybackControl.cs +++ b/osu.Game/Screens/Edit/Components/PlaybackControl.cs @@ -13,7 +13,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; -using osu.Framework.Timing; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -26,7 +25,7 @@ namespace osu.Game.Screens.Edit.Components private IconButton playButton; [Resolved] - private IAdjustableClock adjustableClock { get; set; } + private EditorClock editorClock { get; set; } private readonly BindableNumber tempo = new BindableDouble(1); @@ -87,17 +86,17 @@ namespace osu.Game.Screens.Edit.Components private void togglePause() { - if (adjustableClock.IsRunning) - adjustableClock.Stop(); + if (editorClock.IsRunning) + editorClock.Stop(); else - adjustableClock.Start(); + editorClock.Start(); } protected override void Update() { base.Update(); - playButton.Icon = adjustableClock.IsRunning ? FontAwesome.Regular.PauseCircle : FontAwesome.Regular.PlayCircle; + playButton.Icon = editorClock.IsRunning ? FontAwesome.Regular.PauseCircle : FontAwesome.Regular.PlayCircle; } private class PlaybackTabControl : OsuTabControl diff --git a/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs b/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs index 4bf21d240a..c1f54d7938 100644 --- a/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs +++ b/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs @@ -5,7 +5,6 @@ using osu.Framework.Graphics; using osu.Game.Graphics.Sprites; using System; using osu.Framework.Allocation; -using osu.Framework.Timing; using osu.Game.Graphics; namespace osu.Game.Screens.Edit.Components @@ -15,7 +14,7 @@ namespace osu.Game.Screens.Edit.Components private readonly OsuSpriteText trackTimer; [Resolved] - private IAdjustableClock adjustableClock { get; set; } + private EditorClock editorClock { get; set; } public TimeInfoContainer() { @@ -35,7 +34,7 @@ namespace osu.Game.Screens.Edit.Components { base.Update(); - trackTimer.Text = TimeSpan.FromMilliseconds(adjustableClock.CurrentTime).ToString(@"mm\:ss\:fff"); + trackTimer.Text = TimeSpan.FromMilliseconds(editorClock.CurrentTime).ToString(@"mm\:ss\:fff"); } } } diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs index 5d638d7919..82581dfc56 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Framework.Threading; -using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Graphics; @@ -20,14 +19,14 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts /// public class MarkerPart : TimelinePart { - private readonly Drawable marker; + private Drawable marker; - private readonly IAdjustableClock adjustableClock; + [Resolved] + private EditorClock editorClock { get; set; } - public MarkerPart(IAdjustableClock adjustableClock) + [BackgroundDependencyLoader] + private void load() { - this.adjustableClock = adjustableClock; - Add(marker = new MarkerVisualisation()); } @@ -59,14 +58,14 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts return; float markerPos = Math.Clamp(ToLocalSpace(screenPosition).X, 0, DrawWidth); - adjustableClock.Seek(markerPos / DrawWidth * Beatmap.Value.Track.Length); + editorClock.Seek(markerPos / DrawWidth * editorClock.TrackLength); }); } protected override void Update() { base.Update(); - marker.X = (float)adjustableClock.CurrentTime; + marker.X = (float)editorClock.CurrentTime; } protected override void LoadBeatmap(WorkingBeatmap beatmap) diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs index 20db2cac21..02cd4bccb4 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs @@ -6,7 +6,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Framework.Timing; using osu.Game.Graphics; using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts; @@ -18,11 +17,11 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary public class SummaryTimeline : BottomBarContainer { [BackgroundDependencyLoader] - private void load(OsuColour colours, IAdjustableClock adjustableClock) + private void load(OsuColour colours) { Children = new Drawable[] { - new MarkerPart(adjustableClock) { RelativeSizeAxes = Axes.Both }, + new MarkerPart { RelativeSizeAxes = Axes.Both }, new ControlPointPart { Anchor = Anchor.Centre, diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 1e8a35c047..fba7671fca 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -13,7 +13,6 @@ using osu.Framework.Graphics.Primitives; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; -using osu.Framework.Timing; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; @@ -38,7 +37,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private IEditorChangeHandler changeHandler { get; set; } [Resolved] - private IAdjustableClock adjustableClock { get; set; } + private EditorClock editorClock { get; set; } [Resolved] private EditorBeatmap beatmap { get; set; } @@ -144,7 +143,7 @@ namespace osu.Game.Screens.Edit.Compose.Components if (clickedBlueprint == null) return false; - adjustableClock?.Seek(clickedBlueprint.HitObject.StartTime); + editorClock?.Seek(clickedBlueprint.HitObject.StartTime); return true; } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index ec2b11c0cf..121f3dc213 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -9,7 +9,6 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Audio; using osu.Framework.Input.Events; -using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; @@ -25,7 +24,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline public readonly IBindable Beatmap = new Bindable(); [Resolved] - private IAdjustableClock adjustableClock { get; set; } + private EditorClock editorClock { get; set; } public Timeline() { @@ -101,7 +100,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Content.Margin = new MarginPadding { Horizontal = DrawWidth / 2 }; // This needs to happen after transforms are updated, but before the scroll position is updated in base.UpdateAfterChildren - if (adjustableClock.IsRunning) + if (editorClock.IsRunning) scrollToTrackTime(); } @@ -111,21 +110,21 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (handlingDragInput) seekTrackToCurrent(); - else if (!adjustableClock.IsRunning) + else if (!editorClock.IsRunning) { // The track isn't running. There are two cases we have to be wary of: // 1) The user flick-drags on this timeline: We want the track to follow us // 2) The user changes the track time through some other means (scrolling in the editor or overview timeline): We want to follow the track time // The simplest way to cover both cases is by checking whether the scroll position has changed and the audio hasn't been changed externally - if (Current != lastScrollPosition && adjustableClock.CurrentTime == lastTrackTime) + if (Current != lastScrollPosition && editorClock.CurrentTime == lastTrackTime) seekTrackToCurrent(); else scrollToTrackTime(); } lastScrollPosition = Current; - lastTrackTime = adjustableClock.CurrentTime; + lastTrackTime = editorClock.CurrentTime; } private void seekTrackToCurrent() @@ -133,7 +132,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (!track.IsLoaded) return; - adjustableClock.Seek(Current / Content.DrawWidth * track.Length); + editorClock.Seek(Current / Content.DrawWidth * track.Length); } private void scrollToTrackTime() @@ -141,7 +140,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (!track.IsLoaded || track.Length == 0) return; - ScrollTo((float)(adjustableClock.CurrentTime / track.Length) * Content.DrawWidth, false); + ScrollTo((float)(editorClock.CurrentTime / track.Length) * Content.DrawWidth, false); } protected override bool OnMouseDown(MouseDownEvent e) @@ -164,15 +163,15 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private void beginUserDrag() { handlingDragInput = true; - trackWasPlaying = adjustableClock.IsRunning; - adjustableClock.Stop(); + trackWasPlaying = editorClock.IsRunning; + editorClock.Stop(); } private void endUserDrag() { handlingDragInput = false; if (trackWasPlaying) - adjustableClock.Start(); + editorClock.Start(); } [Resolved] diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 54e4af94a4..54c5a23c3e 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -83,8 +83,7 @@ namespace osu.Game.Screens.Edit clock = new EditorClock(Beatmap.Value, beatDivisor) { IsCoupled = false }; clock.ChangeSource(sourceClock); - dependencies.CacheAs(clock); - dependencies.CacheAs(clock); + dependencies.CacheAs(clock); // todo: remove caching of this and consume via editorBeatmap? dependencies.Cache(beatDivisor); diff --git a/osu.Game/Screens/Edit/Timing/TimingScreen.cs b/osu.Game/Screens/Edit/Timing/TimingScreen.cs index d9da3ff92d..f3d1ec2cbb 100644 --- a/osu.Game/Screens/Edit/Timing/TimingScreen.cs +++ b/osu.Game/Screens/Edit/Timing/TimingScreen.cs @@ -23,7 +23,7 @@ namespace osu.Game.Screens.Edit.Timing private Bindable selectedGroup = new Bindable(); [Resolved] - private IAdjustableClock clock { get; set; } + private EditorClock clock { get; set; } protected override Drawable CreateMainContent() => new GridContainer { diff --git a/osu.Game/Tests/Visual/EditorClockTestScene.cs b/osu.Game/Tests/Visual/EditorClockTestScene.cs index 830e6ed363..f0ec638fc9 100644 --- a/osu.Game/Tests/Visual/EditorClockTestScene.cs +++ b/osu.Game/Tests/Visual/EditorClockTestScene.cs @@ -30,8 +30,7 @@ namespace osu.Game.Tests.Visual var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); dependencies.Cache(BeatDivisor); - dependencies.CacheAs(Clock); - dependencies.CacheAs(Clock); + dependencies.CacheAs(Clock); return dependencies; } From d18eb663b157d63a2c46816d86f5c3194604bc40 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 22 May 2020 16:40:52 +0900 Subject: [PATCH 1736/2376] Add tweening seek support to EditorClock --- .../Edit/OsuHitObjectComposer.cs | 3 +- .../Visual/Editing/TimelineTestScene.cs | 13 +- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 2 +- .../Timelines/Summary/Parts/MarkerPart.cs | 2 +- .../Compose/Components/BlueprintContainer.cs | 2 +- osu.Game/Screens/Edit/Editor.cs | 1 + osu.Game/Screens/Edit/EditorClock.cs | 129 ++++++++++++++++-- osu.Game/Screens/Edit/Timing/TimingScreen.cs | 2 +- 8 files changed, 129 insertions(+), 25 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 9ba3e30445..df14b11e8a 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -104,7 +104,8 @@ namespace osu.Game.Rulesets.Osu.Edit var objects = selectedHitObjects.ToList(); if (objects.Count == 0) - return createGrid(h => h.StartTime <= EditorClock.CurrentTime); + // use accurate time value to give more instantaneous feedback to the user. + return createGrid(h => h.StartTime <= EditorClock.CurrentTimeAccurate); double minTime = objects.Min(h => h.StartTime); return createGrid(h => h.StartTime < minTime, objects.Count + 1); diff --git a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs index 01ef7e6170..3652d41e67 100644 --- a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs +++ b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs @@ -7,7 +7,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; @@ -69,7 +68,7 @@ namespace osu.Game.Tests.Visual.Editing private IBindable beatmap { get; set; } [Resolved] - private IAdjustableClock adjustableClock { get; set; } + private EditorClock adjustableClock { get; set; } public AudioVisualiser() { @@ -102,7 +101,7 @@ namespace osu.Game.Tests.Visual.Editing private class StartStopButton : OsuButton { - private IAdjustableClock adjustableClock; + private EditorClock editorClock; private bool started; public StartStopButton() @@ -115,21 +114,21 @@ namespace osu.Game.Tests.Visual.Editing } [BackgroundDependencyLoader] - private void load(IAdjustableClock adjustableClock) + private void load(EditorClock editorClock) { - this.adjustableClock = adjustableClock; + this.editorClock = editorClock; } private void onClick() { if (started) { - adjustableClock.Stop(); + editorClock.Stop(); Text = "Start"; } else { - adjustableClock.Start(); + editorClock.Start(); Text = "Stop"; } diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 1987148aed..edbdd41d81 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -218,7 +218,7 @@ namespace osu.Game.Rulesets.Edit EditorBeatmap.Add(hitObject); if (EditorClock.CurrentTime < hitObject.StartTime) - EditorClock.Seek(hitObject.StartTime); + EditorClock.SeekTo(hitObject.StartTime); } } diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs index 82581dfc56..9e9ac93d23 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs @@ -58,7 +58,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts return; float markerPos = Math.Clamp(ToLocalSpace(screenPosition).X, 0, DrawWidth); - editorClock.Seek(markerPos / DrawWidth * editorClock.TrackLength); + editorClock.SeekTo(markerPos / DrawWidth * editorClock.TrackLength); }); } diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index fba7671fca..cc417bbb10 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -143,7 +143,7 @@ namespace osu.Game.Screens.Edit.Compose.Components if (clickedBlueprint == null) return false; - editorClock?.Seek(clickedBlueprint.HitObject.StartTime); + editorClock?.SeekTo(clickedBlueprint.HitObject.StartTime); return true; } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 54c5a23c3e..9f61589c36 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -84,6 +84,7 @@ namespace osu.Game.Screens.Edit clock.ChangeSource(sourceClock); dependencies.CacheAs(clock); + AddInternal(clock); // todo: remove caching of this and consume via editorBeatmap? dependencies.Cache(beatDivisor); diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs index e5e47507f3..321a25170a 100644 --- a/osu.Game/Screens/Edit/EditorClock.cs +++ b/osu.Game/Screens/Edit/EditorClock.cs @@ -3,6 +3,8 @@ using System; using System.Linq; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Transforms; using osu.Framework.Utils; using osu.Framework.Timing; using osu.Game.Beatmaps; @@ -13,7 +15,7 @@ namespace osu.Game.Screens.Edit /// /// A decoupled clock which adds editor-specific functionality, such as snapping to a user-defined beat divisor. /// - public class EditorClock : DecoupleableInterpolatingFramedClock + public class EditorClock : Component, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock { public readonly double TrackLength; @@ -21,12 +23,11 @@ namespace osu.Game.Screens.Edit private readonly BindableBeatDivisor beatDivisor; - public EditorClock(WorkingBeatmap beatmap, BindableBeatDivisor beatDivisor) - { - this.beatDivisor = beatDivisor; + private readonly DecoupleableInterpolatingFramedClock underlyingClock; - ControlPointInfo = beatmap.Beatmap.ControlPointInfo; - TrackLength = beatmap.Track.Length; + public EditorClock(WorkingBeatmap beatmap, BindableBeatDivisor beatDivisor) + : this(beatmap.Beatmap.ControlPointInfo, beatmap.Track.Length, beatDivisor) + { } public EditorClock(ControlPointInfo controlPointInfo, double trackLength, BindableBeatDivisor beatDivisor) @@ -35,6 +36,8 @@ namespace osu.Game.Screens.Edit ControlPointInfo = controlPointInfo; TrackLength = trackLength; + + underlyingClock = new DecoupleableInterpolatingFramedClock(); } /// @@ -79,20 +82,30 @@ namespace osu.Game.Screens.Edit private void seek(int direction, bool snapped, double amount = 1) { + double current = CurrentTime; + + // if a seek transform is active, use its end time instead of the reported current time. + var existingTransform = Transforms.OfType().FirstOrDefault(); + + // but only if the requested direction is in the same direction as the transform. + // this allows quick pivoting rather than resetting the transform for the first opposite direction movement. + if (existingTransform != null && Math.Sign(existingTransform.EndValue - current) == Math.Sign(direction)) + current = existingTransform.EndValue; + if (amount <= 0) throw new ArgumentException("Value should be greater than zero", nameof(amount)); - var timingPoint = ControlPointInfo.TimingPointAt(CurrentTime); + var timingPoint = ControlPointInfo.TimingPointAt(current); - if (direction < 0 && timingPoint.Time == CurrentTime) + if (direction < 0 && timingPoint.Time == current) // When going backwards and we're at the boundary of two timing points, we compute the seek distance with the timing point which we are seeking into - timingPoint = ControlPointInfo.TimingPointAt(CurrentTime - 1); + timingPoint = ControlPointInfo.TimingPointAt(current - 1); double seekAmount = timingPoint.BeatLength / beatDivisor.Value * amount; - double seekTime = CurrentTime + seekAmount * direction; + double seekTime = current + seekAmount * direction; if (!snapped || ControlPointInfo.TimingPoints.Count == 0) { - Seek(seekTime); + SeekTo(seekTime); return; } @@ -110,7 +123,7 @@ namespace osu.Game.Screens.Edit // Due to the rounding above, we may end up on the current beat. This will effectively cause 0 seeking to happen, but we don't want this. // Instead, we'll go to the next beat in the direction when this is the case - if (Precision.AlmostEquals(CurrentTime, seekTime)) + if (Precision.AlmostEquals(current, seekTime)) { closestBeat += direction > 0 ? 1 : -1; seekTime = timingPoint.Time + closestBeat * seekAmount; @@ -125,7 +138,97 @@ namespace osu.Game.Screens.Edit // Ensure the sought point is within the boundaries seekTime = Math.Clamp(seekTime, 0, TrackLength); - Seek(seekTime); + SeekTo(seekTime); + } + + /// + /// The current time of this clock, include any active transform seeks performed via . + /// + public double CurrentTimeAccurate => + Transforms.OfType().FirstOrDefault()?.EndValue ?? CurrentTime; + + public double CurrentTime => underlyingClock.CurrentTime; + + public void Reset() + { + ClearTransforms(); + underlyingClock.Reset(); + } + + public void Start() + { + ClearTransforms(); + underlyingClock.Start(); + } + + public void Stop() + { + underlyingClock.Stop(); + } + + public bool Seek(double position) + { + ClearTransforms(); + return underlyingClock.Seek(position); + } + + public void ResetSpeedAdjustments() => underlyingClock.ResetSpeedAdjustments(); + + double IAdjustableClock.Rate + { + get => underlyingClock.Rate; + set => underlyingClock.Rate = value; + } + + double IClock.Rate => underlyingClock.Rate; + + public bool IsRunning => underlyingClock.IsRunning; + + public void ProcessFrame() => underlyingClock.ProcessFrame(); + + public double ElapsedFrameTime => underlyingClock.ElapsedFrameTime; + + public double FramesPerSecond => underlyingClock.FramesPerSecond; + + public FrameTimeInfo TimeInfo => underlyingClock.TimeInfo; + + public void ChangeSource(IClock source) => underlyingClock.ChangeSource(source); + + public IClock Source => underlyingClock.Source; + + public bool IsCoupled + { + get => underlyingClock.IsCoupled; + set => underlyingClock.IsCoupled = value; + } + + private const double transform_time = 300; + + public void SeekTo(double seekDestination) + { + if (IsRunning) + Seek(seekDestination); + else + transformSeekTo(seekDestination, transform_time, Easing.OutQuint); + } + + private void transformSeekTo(double seek, double duration = 0, Easing easing = Easing.None) + => this.TransformTo(this.PopulateTransform(new TransformSeek(), seek, duration, easing)); + + private double currentTime + { + get => underlyingClock.CurrentTime; + set => underlyingClock.Seek(value); + } + + private class TransformSeek : Transform + { + public override string TargetMember => nameof(currentTime); + + protected override void Apply(EditorClock clock, double time) => + clock.currentTime = Interpolation.ValueAt(time, StartValue, EndValue, StartTime, EndTime, Easing); + + protected override void ReadIntoStartValue(EditorClock clock) => StartValue = clock.currentTime; } } } diff --git a/osu.Game/Screens/Edit/Timing/TimingScreen.cs b/osu.Game/Screens/Edit/Timing/TimingScreen.cs index f3d1ec2cbb..59af88a049 100644 --- a/osu.Game/Screens/Edit/Timing/TimingScreen.cs +++ b/osu.Game/Screens/Edit/Timing/TimingScreen.cs @@ -50,7 +50,7 @@ namespace osu.Game.Screens.Edit.Timing selectedGroup.BindValueChanged(selected => { if (selected.NewValue != null) - clock.Seek(selected.NewValue.Time); + clock.SeekTo(selected.NewValue.Time); }); } From 866db629d6ce60b534e9082652c37c888c3cb42b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 22 May 2020 18:23:24 +0900 Subject: [PATCH 1737/2376] Fix remaining test failures --- .../Visual/Editing/TestScenePlaybackControl.cs | 7 +++---- osu.Game.Tests/Visual/Editing/TimelineTestScene.cs | 14 +++++--------- osu.Game/Screens/Edit/EditorClock.cs | 5 +++++ .../Tests/Visual/PlacementBlueprintTestScene.cs | 3 ++- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestScenePlaybackControl.cs b/osu.Game.Tests/Visual/Editing/TestScenePlaybackControl.cs index 3af976cae0..6aa884a197 100644 --- a/osu.Game.Tests/Visual/Editing/TestScenePlaybackControl.cs +++ b/osu.Game.Tests/Visual/Editing/TestScenePlaybackControl.cs @@ -4,8 +4,8 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Timing; using osu.Game.Beatmaps; +using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Components; using osuTK; @@ -17,9 +17,8 @@ namespace osu.Game.Tests.Visual.Editing [BackgroundDependencyLoader] private void load() { - var clock = new DecoupleableInterpolatingFramedClock { IsCoupled = false }; - Dependencies.CacheAs(clock); - Dependencies.CacheAs(clock); + var clock = new EditorClock { IsCoupled = false }; + Dependencies.CacheAs(clock); var playback = new PlaybackControl { diff --git a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs index 01ef7e6170..2e7ccc8ad3 100644 --- a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs +++ b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs @@ -102,7 +102,9 @@ namespace osu.Game.Tests.Visual.Editing private class StartStopButton : OsuButton { - private IAdjustableClock adjustableClock; + [Resolved] + private EditorClock editorClock { get; set; } + private bool started; public StartStopButton() @@ -114,22 +116,16 @@ namespace osu.Game.Tests.Visual.Editing Action = onClick; } - [BackgroundDependencyLoader] - private void load(IAdjustableClock adjustableClock) - { - this.adjustableClock = adjustableClock; - } - private void onClick() { if (started) { - adjustableClock.Stop(); + editorClock.Stop(); Text = "Start"; } else { - adjustableClock.Start(); + editorClock.Start(); Text = "Stop"; } diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs index e5e47507f3..d2bb1c8984 100644 --- a/osu.Game/Screens/Edit/EditorClock.cs +++ b/osu.Game/Screens/Edit/EditorClock.cs @@ -37,6 +37,11 @@ namespace osu.Game.Screens.Edit TrackLength = trackLength; } + public EditorClock() + : this(new ControlPointInfo(), 1000, new BindableBeatDivisor()) + { + } + /// /// Seek to the closest snappable beat from a time. /// diff --git a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs index feecea473c..c3d74f21aa 100644 --- a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs +++ b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs @@ -8,6 +8,7 @@ using osu.Framework.Timing; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Compose; namespace osu.Game.Tests.Visual @@ -32,7 +33,7 @@ namespace osu.Game.Tests.Visual protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - dependencies.CacheAs(new StopwatchClock()); + dependencies.CacheAs(new EditorClock()); return dependencies; } From 3e0ee310d0e0d7dd529c1e7c8a3e59f4a4e8ec7e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 22 May 2020 18:30:39 +0900 Subject: [PATCH 1738/2376] Remove now incorrect comment --- osu.Game/Rulesets/Edit/PlacementBlueprint.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index f0b63f8ea5..e71ccc33a4 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -64,9 +64,7 @@ namespace osu.Game.Rulesets.Edit /// Whether this call is committing a value for HitObject.StartTime and continuing with further adjustments. protected void BeginPlacement(bool commitStart = false) { - // applies snapping to above time placementHandler.BeginPlacement(HitObject); - PlacementActive |= commitStart; } From af30d1201f0934fc49ee4857e0157611bdc4114c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 22 May 2020 18:57:28 +0900 Subject: [PATCH 1739/2376] Fix slider path control point blueprint not working correctly --- .../Blueprints/Sliders/Components/PathControlPointPiece.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs index 834bf1892f..c06904c0c2 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs @@ -162,8 +162,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components if (ControlPoint == slider.Path.ControlPoints[0]) { // Special handling for the head control point - the position of the slider changes which means the snapped position and time have to be taken into account - var result = snapProvider?.SnapScreenSpacePositionToValidTime(e.MousePosition); - Vector2 movementDelta = (result?.ScreenSpacePosition ?? e.MousePosition) - slider.Position; + var result = snapProvider?.SnapScreenSpacePositionToValidTime(e.ScreenSpaceMousePosition); + + Vector2 movementDelta = Parent.ToLocalSpace(result?.ScreenSpacePosition ?? e.ScreenSpaceMousePosition) - slider.Position; slider.Position += movementDelta; slider.StartTime = result?.Time ?? slider.StartTime; From 5ea33f4c046ffdde090f21c67a9a26772083a24b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 22 May 2020 19:23:07 +0900 Subject: [PATCH 1740/2376] Fix incorrect rounding in DragBar --- osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs | 4 ++-- .../Compose/Components/Timeline/TimelineHitObjectBlueprint.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index ec2b11c0cf..61ed1743a9 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -181,8 +181,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline [Resolved] private IBeatSnapProvider beatSnapProvider { get; set; } - public SnapResult SnapScreenSpacePositionToValidTime(Vector2 position) => - new SnapResult(position, beatSnapProvider.SnapTime(getTimeFromPosition(Content.ToLocalSpace(position)))); + public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => + new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(getTimeFromPosition(Content.ToLocalSpace(screenSpacePosition)))); private double getTimeFromPosition(Vector2 localPosition) => (localPosition.X / Content.DrawWidth) * track.Length; diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index 03e05b75c5..dd2f7a833e 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -282,7 +282,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline case IHasRepeats repeatHitObject: // find the number of repeats which can fit in the requested time. var lengthOfOneRepeat = repeatHitObject.Duration / (repeatHitObject.RepeatCount + 1); - var proposedCount = Math.Max(0, (int)((time - hitObject.StartTime) / lengthOfOneRepeat) - 1); + var proposedCount = Math.Max(0, (int)Math.Round((time - hitObject.StartTime) / lengthOfOneRepeat) - 1); if (proposedCount == repeatHitObject.RepeatCount) return; From 8b79e142257652c1914d5381f8942f4cf20c1ae6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 22 May 2020 19:49:49 +0900 Subject: [PATCH 1741/2376] Fix remaining test regressions --- osu.Game.Tests/Visual/Editing/TimelineTestScene.cs | 5 ++--- osu.Game/Screens/Edit/Timing/TimingScreen.cs | 3 +-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs index 2e7ccc8ad3..fdb8781563 100644 --- a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs +++ b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs @@ -7,7 +7,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; @@ -69,7 +68,7 @@ namespace osu.Game.Tests.Visual.Editing private IBindable beatmap { get; set; } [Resolved] - private IAdjustableClock adjustableClock { get; set; } + private EditorClock editorClock { get; set; } public AudioVisualiser() { @@ -96,7 +95,7 @@ namespace osu.Game.Tests.Visual.Editing base.Update(); if (beatmap.Value.Track.IsLoaded) - marker.X = (float)(adjustableClock.CurrentTime / beatmap.Value.Track.Length); + marker.X = (float)(editorClock.CurrentTime / beatmap.Value.Track.Length); } } diff --git a/osu.Game/Screens/Edit/Timing/TimingScreen.cs b/osu.Game/Screens/Edit/Timing/TimingScreen.cs index f3d1ec2cbb..f22d7291d9 100644 --- a/osu.Game/Screens/Edit/Timing/TimingScreen.cs +++ b/osu.Game/Screens/Edit/Timing/TimingScreen.cs @@ -7,7 +7,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; @@ -62,7 +61,7 @@ namespace osu.Game.Screens.Edit.Timing private IBindableList controlGroups; [Resolved] - private IFrameBasedClock clock { get; set; } + private EditorClock clock { get; set; } [Resolved] protected IBindable Beatmap { get; private set; } From 50059860498d63d2dbe8cd77b5840f2bf628a9da Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 22 May 2020 20:18:47 +0900 Subject: [PATCH 1742/2376] Cleanup test --- .../TestSceneContractedPanelMiddleContent.cs | 25 +++---------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs index e1e00e3c2b..972ac26b84 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs @@ -9,16 +9,13 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Framework.Testing; using osu.Game.Beatmaps; -using osu.Game.Graphics.Sprites; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Scoring; using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking.Contracted; using osu.Game.Tests.Beatmaps; -using osu.Game.Users; using osuTK; namespace osu.Game.Tests.Visual.Ranking @@ -29,22 +26,9 @@ namespace osu.Game.Tests.Visual.Ranking private RulesetStore rulesetStore { get; set; } [Test] - public void TestMapWithKnownMapper() + public void TestShowPanel() { - var author = new User { Username = "mapper_name" }; - - AddStep("show example score", () => showPanel(createTestBeatmap(author), new TestScoreInfo(new OsuRuleset().RulesetInfo))); - - AddAssert("mapper name present", () => this.ChildrenOfType().Any(spriteText => spriteText.Text == "mapper_name")); - } - - [Test] - public void TestMapWithUnknownMapper() - { - AddStep("show example score", () => showPanel(createTestBeatmap(null), new TestScoreInfo(new OsuRuleset().RulesetInfo))); - - AddAssert("mapped by text not present", () => - this.ChildrenOfType().All(spriteText => !containsAny(spriteText.Text, "mapped", "by"))); + AddStep("show example score", () => showPanel(createTestBeatmap(), new TestScoreInfo(new OsuRuleset().RulesetInfo))); } private void showPanel(WorkingBeatmap workingBeatmap, ScoreInfo score) @@ -52,10 +36,9 @@ namespace osu.Game.Tests.Visual.Ranking Child = new ContractedPanelMiddleContentContainer(workingBeatmap, score); } - private WorkingBeatmap createTestBeatmap(User author) + private WorkingBeatmap createTestBeatmap() { var beatmap = new TestBeatmap(rulesetStore.GetRuleset(0)); - beatmap.Metadata.Author = author; beatmap.Metadata.Title = "Verrrrrrrrrrrrrrrrrrry looooooooooooooooooooooooong beatmap title"; beatmap.Metadata.Artist = "Verrrrrrrrrrrrrrrrrrry looooooooooooooooooooooooong beatmap artist"; @@ -75,7 +58,7 @@ namespace osu.Game.Tests.Visual.Ranking Anchor = Anchor.Centre; Origin = Anchor.Centre; - Size = new Vector2(ScorePanel.CONTRACTED_WIDTH, 700); + Size = new Vector2(ScorePanel.CONTRACTED_WIDTH, 460); Children = new Drawable[] { new Box From 6bcc4c95cc0794260e82a7d70cfe06aa31e7c33f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 22 May 2020 20:19:23 +0900 Subject: [PATCH 1743/2376] Schedule api callback --- osu.Game/Screens/Ranking/ResultsScreen.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index cdceaa939e..af748d8336 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -120,7 +120,7 @@ namespace osu.Game.Screens.Ranking var req = new GetScoresRequest(Score.Beatmap, Score.Ruleset); - req.Success += r => + req.Success += r => Schedule(() => { foreach (var s in r.Scores.Select(s => s.CreateScoreInfo(rulesets))) { @@ -129,7 +129,7 @@ namespace osu.Game.Screens.Ranking panels.AddScore(s); } - }; + }); api.Queue(req); } From 80388feac4f6bbc7f87b617476c142f55511fa78 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 22 May 2020 20:39:02 +0900 Subject: [PATCH 1744/2376] Disable scroll user scroll controls in list --- osu.Game/Screens/Ranking/ResultsScreen.cs | 69 +++++++++++++--------- osu.Game/Screens/Ranking/ScorePanelList.cs | 4 ++ 2 files changed, 46 insertions(+), 27 deletions(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index af748d8336..133bdcca4a 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -59,42 +59,57 @@ namespace osu.Game.Screens.Ranking { FillFlowContainer buttons; - InternalChildren = new[] + InternalChild = new GridContainer { - new ResultsScrollContainer + RelativeSizeAxes = Axes.Both, + Content = new[] { - Child = panels = new ScorePanelList(Score) + new Drawable[] { - RelativeSizeAxes = Axes.Both, - } - }, - bottomPanel = new Container - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.X, - Height = TwoLayerButton.SIZE_EXTENDED.Y, - Alpha = 0, - Children = new Drawable[] + new ResultsScrollContainer + { + Child = panels = new ScorePanelList(Score) + { + RelativeSizeAxes = Axes.Both, + } + } + }, + new[] { - new Box + bottomPanel = new Container { - RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex("#333") - }, - buttons = new FillFlowContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Both, - Spacing = new Vector2(5), - Direction = FillDirection.Horizontal, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + Height = TwoLayerButton.SIZE_EXTENDED.Y, + Alpha = 0, Children = new Drawable[] { - new ReplayDownloadButton(Score) { Width = 300 }, + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#333") + }, + buttons = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(5), + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + new ReplayDownloadButton(Score) { Width = 300 }, + } + } } } } + }, + RowDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize) } }; @@ -171,7 +186,7 @@ namespace osu.Game.Screens.Ranking protected override void Update() { base.Update(); - content.Height = Math.Max(768, DrawHeight); + content.Height = Math.Max(768 - TwoLayerButton.SIZE_EXTENDED.Y, DrawHeight); } } } diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index ed6d07d078..97a132b9ff 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -149,6 +149,10 @@ namespace osu.Game.Screens.Ranking base.UpdateAfterChildren(); } + + public override bool HandlePositionalInput => false; + + public override bool HandleNonPositionalInput => false; } } } From 12d65f305f24cdfd8dc0792d94eb4c6c5fa2f020 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 22 May 2020 22:11:55 +0900 Subject: [PATCH 1745/2376] Simplify and fix incorrect seeking --- osu.Game/Screens/Edit/EditorClock.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs index 214b45f654..dd934c10cd 100644 --- a/osu.Game/Screens/Edit/EditorClock.cs +++ b/osu.Game/Screens/Edit/EditorClock.cs @@ -87,15 +87,7 @@ namespace osu.Game.Screens.Edit private void seek(int direction, bool snapped, double amount = 1) { - double current = CurrentTime; - - // if a seek transform is active, use its end time instead of the reported current time. - var existingTransform = Transforms.OfType().FirstOrDefault(); - - // but only if the requested direction is in the same direction as the transform. - // this allows quick pivoting rather than resetting the transform for the first opposite direction movement. - if (existingTransform != null && Math.Sign(existingTransform.EndValue - current) == Math.Sign(direction)) - current = existingTransform.EndValue; + double current = CurrentTimeAccurate; if (amount <= 0) throw new ArgumentException("Value should be greater than zero", nameof(amount)); From 83f4ba107f6b97185c0487bb75e45516e73aa52e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 22 May 2020 22:41:06 +0900 Subject: [PATCH 1746/2376] Fix defaults not being applied correctly to blueprints after StartTime is changed --- osu.Game/Rulesets/Edit/PlacementBlueprint.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index e71ccc33a4..90a114bcdc 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -34,6 +34,8 @@ namespace osu.Game.Rulesets.Edit private readonly IBindable beatmap = new Bindable(); + private Bindable startTimeBindable; + [Resolved] private IPlacementHandler placementHandler { get; set; } @@ -55,7 +57,8 @@ namespace osu.Game.Rulesets.Edit EditorClock = clock; - ApplyDefaultsToHitObject(); + startTimeBindable = HitObject.StartTimeBindable.GetBoundCopy(); + startTimeBindable.BindValueChanged(_ => ApplyDefaultsToHitObject(), true); } /// From 3d3cc2c15efbf4ac33e1a70cea6ec9dd4ecb8f5b Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 22 May 2020 17:26:37 +0300 Subject: [PATCH 1747/2376] Dispose BeatmapOnlineLookupQueue cache download request --- osu.Game/Beatmaps/BeatmapManager.cs | 7 ++++++- .../Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs | 7 ++++++- osu.Game/OsuGameBase.cs | 1 + 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 7aaf0ca08d..b286c054e9 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -34,7 +34,7 @@ namespace osu.Game.Beatmaps /// /// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps. /// - public partial class BeatmapManager : DownloadableArchiveModelManager + public partial class BeatmapManager : DownloadableArchiveModelManager, IDisposable { /// /// Fired when a single difficulty has been hidden. @@ -433,6 +433,11 @@ namespace osu.Game.Beatmaps return endTime - startTime; } + public void Dispose() + { + onlineLookupQueue?.Dispose(); + } + /// /// A dummy WorkingBeatmap for the purpose of retrieving a beatmap for star difficulty calculation. /// diff --git a/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs b/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs index 2c79a664c5..d47d37806e 100644 --- a/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs +++ b/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs @@ -23,7 +23,7 @@ namespace osu.Game.Beatmaps { public partial class BeatmapManager { - private class BeatmapOnlineLookupQueue + private class BeatmapOnlineLookupQueue : IDisposable { private readonly IAPIProvider api; private readonly Storage storage; @@ -180,6 +180,11 @@ namespace osu.Game.Beatmaps return false; } + public void Dispose() + { + cacheDownloadRequest?.Dispose(); + } + [Serializable] [SuppressMessage("ReSharper", "InconsistentNaming")] private class CachedOnlineBeatmapLookup diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index c367c3b636..453587df18 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -337,6 +337,7 @@ namespace osu.Game { base.Dispose(isDisposing); RulesetStore?.Dispose(); + BeatmapManager?.Dispose(); contextFactory.FlushConnections(); } From 554be1c4222eec67b7926c2a4a802b6130aeae35 Mon Sep 17 00:00:00 2001 From: Olle Kelderman Date: Fri, 22 May 2020 19:25:05 +0200 Subject: [PATCH 1748/2376] add the ability to set the size of the Tournament Client to an arbitrary value instead of a fixed 1080p option --- osu.Game.Tournament/Screens/SetupScreen.cs | 79 ++++++++++++++++++---- 1 file changed, 67 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tournament/Screens/SetupScreen.cs b/osu.Game.Tournament/Screens/SetupScreen.cs index c91379b2d6..f9ec29d0c6 100644 --- a/osu.Game.Tournament/Screens/SetupScreen.cs +++ b/osu.Game.Tournament/Screens/SetupScreen.cs @@ -25,7 +25,7 @@ namespace osu.Game.Tournament.Screens private FillFlowContainer fillFlow; private LoginOverlay loginOverlay; - private ActionableInfo resolution; + private ActionableInfoWithNumberBox resolution; [Resolved] private MatchIPCInfo ipc { get; set; } @@ -108,18 +108,22 @@ namespace osu.Game.Tournament.Screens Items = rulesets.AvailableRulesets, Current = LadderInfo.Ruleset, }, - resolution = new ActionableInfo + resolution = new ActionableInfoWithNumberBox { Label = "Stream area resolution", - ButtonText = "Set to 1080p", - Action = () => + ButtonText = "Set size", + Action = i => { - windowSize.Value = new Size((int)(1920 / TournamentSceneManager.STREAM_AREA_WIDTH * TournamentSceneManager.REQUIRED_WIDTH), 1080); + i = Math.Clamp(i, 480, 2160); + windowSize.Value = new Size((int)(i * aspect_ratio / TournamentSceneManager.STREAM_AREA_WIDTH * TournamentSceneManager.REQUIRED_WIDTH), i); + resolution.NumberValue = i; } }, }; } + private const float aspect_ratio = 16f / 9f; + protected override void Update() { base.Update(); @@ -149,7 +153,7 @@ namespace osu.Game.Tournament.Screens private class ActionableInfo : LabelledDrawable { - private OsuButton button; + protected OsuButton Button; public ActionableInfo() : base(true) @@ -158,22 +162,22 @@ namespace osu.Game.Tournament.Screens public string ButtonText { - set => button.Text = value; + set => Button.Text = value; } public string Value { - set => valueText.Text = value; + set => ValueText.Text = value; } public bool Failing { - set => valueText.Colour = value ? Color4.Red : Color4.White; + set => ValueText.Colour = value ? Color4.Red : Color4.White; } public Action Action; - private TournamentSpriteText valueText; + protected TournamentSpriteText ValueText; protected override Drawable CreateComponent() => new Container { @@ -181,12 +185,12 @@ namespace osu.Game.Tournament.Screens RelativeSizeAxes = Axes.X, Children = new Drawable[] { - valueText = new TournamentSpriteText + ValueText = new TournamentSpriteText { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, }, - button = new TriangleButton + Button = new TriangleButton { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, @@ -196,5 +200,56 @@ namespace osu.Game.Tournament.Screens } }; } + + private class ActionableInfoWithNumberBox : ActionableInfo + { + public new Action Action; + + private OsuNumberBox numberBox; + + public int NumberValue + { + get + { + int.TryParse(numberBox.Text, out var val); + return val; + } + set => numberBox.Text = value.ToString(); + } + + protected override Drawable CreateComponent() => new Container + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Children = new Drawable[] + { + ValueText = new TournamentSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + numberBox = new OsuNumberBox + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Width = 100, + Margin = new MarginPadding + { + Right = 110 + } + }, + Button = new TriangleButton + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Size = new Vector2(100, 30), + Action = () => + { + if (numberBox.Text.Length > 0) Action?.Invoke(NumberValue); + } + }, + } + }; + } } } From 0717dab8e4b730fcb11ff5d422ddf2f418aad01d Mon Sep 17 00:00:00 2001 From: Shivam Date: Fri, 22 May 2020 19:51:08 +0200 Subject: [PATCH 1749/2376] Add StablePathSelectScreen visual test --- .../TestSceneStablePathSelectScreens.cs | 30 +++++++++++++++++++ .../Screens/StablePathSelectScreen.cs | 5 ++-- 2 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 osu.Game.Tournament.Tests/Screens/TestSceneStablePathSelectScreens.cs diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneStablePathSelectScreens.cs b/osu.Game.Tournament.Tests/Screens/TestSceneStablePathSelectScreens.cs new file mode 100644 index 0000000000..f0c89ba4ca --- /dev/null +++ b/osu.Game.Tournament.Tests/Screens/TestSceneStablePathSelectScreens.cs @@ -0,0 +1,30 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Tournament.Screens; +using osu.Framework.Platform; + +namespace osu.Game.Tournament.Tests.Screens +{ + public class TestSceneStablePathSelectScreens : TournamentTestScene + { + + public TestSceneStablePathSelectScreens() + { + AddStep("Add screen", () => Add(new TestSceneStablePathSelectScreen())); + } + + private class TestSceneStablePathSelectScreen : StablePathSelectScreen + { + protected override void changePath(Storage storage) + { + Expire(); + } + + protected override void autoDetect() + { + Expire(); + } + } + } +} diff --git a/osu.Game.Tournament/Screens/StablePathSelectScreen.cs b/osu.Game.Tournament/Screens/StablePathSelectScreen.cs index dcc26b8b1e..f706c42e1d 100644 --- a/osu.Game.Tournament/Screens/StablePathSelectScreen.cs +++ b/osu.Game.Tournament/Screens/StablePathSelectScreen.cs @@ -14,6 +14,7 @@ using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Overlays; +using osu.Game.Overlays.Dialog; using osu.Game.Tournament.IPC; using osu.Game.Tournament.Components; using osuTK; @@ -135,7 +136,7 @@ namespace osu.Game.Tournament.Screens }); } - private void changePath(Storage storage) + protected virtual void changePath(Storage storage) { var target = directorySelector.CurrentDirectory.Value.FullName; stableInfo.StablePath.Value = target; @@ -156,7 +157,7 @@ namespace osu.Game.Tournament.Screens sceneManager?.SetScreen(typeof(SetupScreen)); } - private void autoDetect() + protected virtual void autoDetect() { var fileBasedIpc = ipc as FileBasedIPC; fileBasedIpc?.LocateStableStorage(); From c6345ba6c94c41e53108b317c3199a3c98ed2cc1 Mon Sep 17 00:00:00 2001 From: Shivam Date: Fri, 22 May 2020 20:01:26 +0200 Subject: [PATCH 1750/2376] corrected styling issues --- .../Screens/TestSceneStablePathSelectScreens.cs | 5 ++--- osu.Game.Tournament/Screens/StablePathSelectScreen.cs | 9 ++++----- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneStablePathSelectScreens.cs b/osu.Game.Tournament.Tests/Screens/TestSceneStablePathSelectScreens.cs index f0c89ba4ca..4dfd4d35c8 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneStablePathSelectScreens.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneStablePathSelectScreens.cs @@ -8,7 +8,6 @@ namespace osu.Game.Tournament.Tests.Screens { public class TestSceneStablePathSelectScreens : TournamentTestScene { - public TestSceneStablePathSelectScreens() { AddStep("Add screen", () => Add(new TestSceneStablePathSelectScreen())); @@ -16,12 +15,12 @@ namespace osu.Game.Tournament.Tests.Screens private class TestSceneStablePathSelectScreen : StablePathSelectScreen { - protected override void changePath(Storage storage) + protected override void ChangePath(Storage storage) { Expire(); } - protected override void autoDetect() + protected override void AutoDetect() { Expire(); } diff --git a/osu.Game.Tournament/Screens/StablePathSelectScreen.cs b/osu.Game.Tournament/Screens/StablePathSelectScreen.cs index f706c42e1d..609c601106 100644 --- a/osu.Game.Tournament/Screens/StablePathSelectScreen.cs +++ b/osu.Game.Tournament/Screens/StablePathSelectScreen.cs @@ -14,7 +14,6 @@ using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Overlays; -using osu.Game.Overlays.Dialog; using osu.Game.Tournament.IPC; using osu.Game.Tournament.Components; using osuTK; @@ -109,7 +108,7 @@ namespace osu.Game.Tournament.Screens Origin = Anchor.Centre, Width = 300, Text = "Select stable path", - Action = () => changePath(storage) + Action = () => ChangePath(storage) }, new TriangleButton { @@ -117,7 +116,7 @@ namespace osu.Game.Tournament.Screens Origin = Anchor.Centre, Width = 300, Text = "Auto detect", - Action = autoDetect + Action = AutoDetect }, } } @@ -136,7 +135,7 @@ namespace osu.Game.Tournament.Screens }); } - protected virtual void changePath(Storage storage) + protected virtual void ChangePath(Storage storage) { var target = directorySelector.CurrentDirectory.Value.FullName; stableInfo.StablePath.Value = target; @@ -157,7 +156,7 @@ namespace osu.Game.Tournament.Screens sceneManager?.SetScreen(typeof(SetupScreen)); } - protected virtual void autoDetect() + protected virtual void AutoDetect() { var fileBasedIpc = ipc as FileBasedIPC; fileBasedIpc?.LocateStableStorage(); From bc82c2d3b7b6995f77473ea9686f8404e16c3686 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 23 May 2020 10:44:53 +0900 Subject: [PATCH 1751/2376] Move drawable addition above event bindings --- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 9ba3e30445..10f0855e33 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -40,10 +40,10 @@ namespace osu.Game.Rulesets.Osu.Edit [BackgroundDependencyLoader] private void load() { + LayerBelowRuleset.Add(distanceSnapGridContainer = new Container { RelativeSizeAxes = Axes.Both }); + EditorBeatmap.SelectedHitObjects.CollectionChanged += (_, __) => updateDistanceSnapGrid(); EditorBeatmap.PlacementObject.ValueChanged += _ => updateDistanceSnapGrid(); - - LayerBelowRuleset.Add(distanceSnapGridContainer = new Container { RelativeSizeAxes = Axes.Both }); } protected override ComposeBlueprintContainer CreateBlueprintContainer() => new OsuBlueprintContainer(HitObjects); From a8dbfe279159b3157b573f99a125c1fd4eda9060 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 23 May 2020 10:57:17 +0900 Subject: [PATCH 1752/2376] Fix distance snap grid not disappearing when exiting playfield --- .../Edit/OsuHitObjectComposer.cs | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 10f0855e33..62287574ea 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -86,10 +86,24 @@ namespace osu.Game.Rulesets.Osu.Edit distanceSnapGridContainer.Clear(); distanceSnapGridCache.Invalidate(); - if (BlueprintContainer.CurrentTool is SelectTool && !EditorBeatmap.SelectedHitObjects.Any()) - return; + switch (BlueprintContainer.CurrentTool) + { + case SelectTool _: + if (!EditorBeatmap.SelectedHitObjects.Any()) + return; - if ((distanceSnapGrid = createDistanceSnapGrid(EditorBeatmap.SelectedHitObjects)) != null) + distanceSnapGrid = createDistanceSnapGrid(EditorBeatmap.SelectedHitObjects); + break; + + default: + if (!CursorInPlacementArea) + return; + + distanceSnapGrid = createDistanceSnapGrid(Enumerable.Empty()); + break; + } + + if (distanceSnapGrid != null) { distanceSnapGridContainer.Add(distanceSnapGrid); distanceSnapGridCache.Validate(); From 224a3ff462cbbaf45896578511f2769bc0134dda Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 23 May 2020 11:44:45 +0900 Subject: [PATCH 1753/2376] Add note about gameplay mechanics in osu!lazer --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 59d72247f5..336bf33f7e 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,8 @@ Rhythm is just a *click* away. The future of [osu!](https://osu.ppy.sh) and the This project is under heavy development, but is in a stable state. Users are encouraged to try it out and keep it installed alongside the stable *osu!* client. It will continue to evolve to the point of eventually replacing the existing stable client as an update. +**IMPORTANT:** Gameplay mechanics (and other features which you may have come to know and love) are in a constant state of flux. Game balance and final quality-of-life passses come at the end of development, preceeded by experimentation and changes which may potentially **reduce playability or usability**. This is done in order to allow us to move forward as developers and designers more efficiently. If this offends you, please consider sticking to the stable releases of osu! (found on the website). We are not yet open to heated discussion over game mechanics and will not be using github as a forum for such discussions just yet. + We are accepting bug reports (please report with as much detail as possible and follow the existing issue templates). Feature requests are also welcome, but understand that our focus is on completing the game to feature parity before adding new features. A few resources are available as starting points to getting involved and understanding the project: - Detailed release changelogs are available on the [official osu! site](https://osu.ppy.sh/home/changelog/lazer). From edc46d1dce0e8f82e924de8becdb30a787b71031 Mon Sep 17 00:00:00 2001 From: Berkan Diler Date: Sat, 23 May 2020 16:18:55 +0200 Subject: [PATCH 1754/2376] Fix osu.Game.Benchmarks --- osu.Game.Benchmarks/BenchmarkBeatmapParsing.cs | 6 +++--- osu.Game.Benchmarks/osu.Game.Benchmarks.csproj | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game.Benchmarks/BenchmarkBeatmapParsing.cs b/osu.Game.Benchmarks/BenchmarkBeatmapParsing.cs index 394fd75488..a6b7c8fcdb 100644 --- a/osu.Game.Benchmarks/BenchmarkBeatmapParsing.cs +++ b/osu.Game.Benchmarks/BenchmarkBeatmapParsing.cs @@ -8,7 +8,7 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; using osu.Game.IO; using osu.Game.IO.Archives; -using osu.Game.Resources; +using osu.Game.Tests.Resources; namespace osu.Game.Benchmarks { @@ -18,8 +18,8 @@ namespace osu.Game.Benchmarks public override void SetUp() { - using (var resources = new DllResourceStore(OsuResources.ResourceAssembly)) - using (var archive = resources.GetStream("Beatmaps/241526 Soleily - Renatus.osz")) + using (var resources = new DllResourceStore(typeof(TestResources).Assembly)) + using (var archive = resources.GetStream($"Resources/Archives/241526 Soleily - Renatus.osz")) using (var reader = new ZipArchiveReader(archive)) reader.GetStream("Soleily - Renatus (Gamu) [Insane].osu").CopyTo(beatmapStream); } diff --git a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj index 88fe8f1150..41e726e05c 100644 --- a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj +++ b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj @@ -13,6 +13,7 @@ + From de60d509e8ac00b882d091b099f9171dfb4081f3 Mon Sep 17 00:00:00 2001 From: Berkan Diler Date: Sat, 23 May 2020 17:01:34 +0200 Subject: [PATCH 1755/2376] Remove redundant string interpolation to fix CI --- osu.Game.Benchmarks/BenchmarkBeatmapParsing.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Benchmarks/BenchmarkBeatmapParsing.cs b/osu.Game.Benchmarks/BenchmarkBeatmapParsing.cs index a6b7c8fcdb..1d207d04c7 100644 --- a/osu.Game.Benchmarks/BenchmarkBeatmapParsing.cs +++ b/osu.Game.Benchmarks/BenchmarkBeatmapParsing.cs @@ -19,7 +19,7 @@ namespace osu.Game.Benchmarks public override void SetUp() { using (var resources = new DllResourceStore(typeof(TestResources).Assembly)) - using (var archive = resources.GetStream($"Resources/Archives/241526 Soleily - Renatus.osz")) + using (var archive = resources.GetStream("Resources/Archives/241526 Soleily - Renatus.osz")) using (var reader = new ZipArchiveReader(archive)) reader.GetStream("Soleily - Renatus (Gamu) [Insane].osu").CopyTo(beatmapStream); } From c071fe61407440f6035d35f3fef2949036ad402e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 24 May 2020 13:44:11 +0900 Subject: [PATCH 1756/2376] Add the ability to export skins --- osu.Game/Beatmaps/BeatmapManager.cs | 23 --------------- osu.Game/Database/ArchiveModelManager.cs | 28 +++++++++++++++++++ .../Overlays/Settings/Sections/SkinSection.cs | 24 ++++++++++++++++ 3 files changed, 52 insertions(+), 23 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index b286c054e9..f626b45e42 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -27,7 +27,6 @@ using osu.Game.Online.API.Requests; using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; using Decoder = osu.Game.Beatmaps.Formats.Decoder; -using ZipArchive = SharpCompress.Archives.Zip.ZipArchive; namespace osu.Game.Beatmaps { @@ -66,7 +65,6 @@ namespace osu.Game.Beatmaps private readonly AudioManager audioManager; private readonly GameHost host; private readonly BeatmapOnlineLookupQueue onlineLookupQueue; - private readonly Storage exportStorage; public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, AudioManager audioManager, GameHost host = null, WorkingBeatmap defaultBeatmap = null) @@ -83,7 +81,6 @@ namespace osu.Game.Beatmaps beatmaps.BeatmapRestored += b => beatmapRestored.Value = new WeakReference(b); onlineLookupQueue = new BeatmapOnlineLookupQueue(api, storage); - exportStorage = storage.GetStorageForDirectory("exports"); } protected override ArchiveDownloadRequest CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) => @@ -214,26 +211,6 @@ namespace osu.Game.Beatmaps workingCache.Remove(working); } - /// - /// Exports a to an .osz package. - /// - /// The to export. - public void Export(BeatmapSetInfo set) - { - var localSet = QueryBeatmapSet(s => s.ID == set.ID); - - using (var archive = ZipArchive.Create()) - { - foreach (var file in localSet.Files) - archive.AddEntry(file.Filename, Files.Storage.GetStream(file.FileInfo.StoragePath)); - - using (var outputStream = exportStorage.GetStream($"{set}.osz", FileAccess.Write, FileMode.Create)) - archive.SaveTo(outputStream); - - exportStorage.OpenInNativeExplorer(); - } - } - private readonly WeakList workingCache = new WeakList(); /// diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 33b16cbaaf..3db367555f 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -22,6 +22,7 @@ using osu.Game.IO.Archives; using osu.Game.IPC; using osu.Game.Overlays.Notifications; using osu.Game.Utils; +using SharpCompress.Archives.Zip; using SharpCompress.Common; using FileInfo = osu.Game.IO.FileInfo; @@ -82,6 +83,8 @@ namespace osu.Game.Database // ReSharper disable once NotAccessedField.Local (we should keep a reference to this so it is not finalised) private ArchiveImportIPCChannel ipc; + private readonly Storage exportStorage; + protected ArchiveModelManager(Storage storage, IDatabaseContextFactory contextFactory, MutableDatabaseBackedStoreWithFileIncludes modelStore, IIpcHost importHost = null) { ContextFactory = contextFactory; @@ -90,6 +93,8 @@ namespace osu.Game.Database ModelStore.ItemAdded += item => handleEvent(() => itemAdded.Value = new WeakReference(item)); ModelStore.ItemRemoved += item => handleEvent(() => itemRemoved.Value = new WeakReference(item)); + exportStorage = storage.GetStorageForDirectory("exports"); + Files = new FileStore(contextFactory, storage); if (importHost != null) @@ -369,6 +374,29 @@ namespace osu.Game.Database return item; }, cancellationToken, TaskCreationOptions.HideScheduler, import_scheduler).Unwrap(); + /// + /// Exports an item to an legacy (.zip based) package. + /// + /// The item to export. + public void Export(TModel item) + { + var retrievedItem = ModelStore.ConsumableItems.FirstOrDefault(s => s.ID == item.ID); + + if (retrievedItem == null) + throw new ArgumentException("Specified model count not be found", nameof(item)); + + using (var archive = ZipArchive.Create()) + { + foreach (var file in retrievedItem.Files) + archive.AddEntry(file.Filename, Files.Storage.GetStream(file.FileInfo.StoragePath)); + + using (var outputStream = exportStorage.GetStream($"{item}{HandledExtensions.First()}", FileAccess.Write, FileMode.Create)) + archive.SaveTo(outputStream); + + exportStorage.OpenInNativeExplorer(); + } + } + public void UpdateFile(TModel model, TFileModel file, Stream contents) { using (var usage = ContextFactory.GetForWrite()) diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 94080f5592..a89f2c26c8 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -7,6 +7,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Framework.Logging; using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; using osu.Game.Skinning; @@ -34,13 +35,33 @@ namespace osu.Game.Overlays.Settings.Sections private IBindable> managerAdded; private IBindable> managerRemoved; + private SettingsButton exportButton; + + private Bindable currentSkin; + [BackgroundDependencyLoader] private void load(OsuConfigManager config) { FlowContent.Spacing = new Vector2(0, 5); + Children = new Drawable[] { skinDropdown = new SkinSettingsDropdown(), + exportButton = new SettingsButton + { + Text = "Export selected skin", + Action = () => + { + try + { + skins.Export(skins.CurrentSkin.Value.SkinInfo); + } + catch (Exception) + { + Logger.Log("Could not export current skin", level: LogLevel.Error); + } + } + }, new SettingsSlider { LabelText = "Menu cursor size", @@ -81,6 +102,9 @@ namespace osu.Game.Overlays.Settings.Sections skinDropdown.Bindable = dropdownBindable; skinDropdown.Items = skins.GetAllUsableSkins().ToArray(); + currentSkin = skins.CurrentSkin.GetBoundCopy(); + currentSkin.BindValueChanged(skin => exportButton.Enabled.Value = skin.NewValue.SkinInfo.ID > 0); + // Todo: This should not be necessary when OsuConfigManager is databased if (skinDropdown.Items.All(s => s.ID != configBindable.Value)) configBindable.Value = 0; From f277b0c99f2a53f7b7bd92e0f748e1e206fe452c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 24 May 2020 22:30:56 +0900 Subject: [PATCH 1757/2376] Use better formatting for skin display (matching BeatmapMetadata) --- osu.Game/Skinning/SkinInfo.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Skinning/SkinInfo.cs b/osu.Game/Skinning/SkinInfo.cs index 6b9627188e..b9fe44ef3b 100644 --- a/osu.Game/Skinning/SkinInfo.cs +++ b/osu.Game/Skinning/SkinInfo.cs @@ -24,8 +24,6 @@ namespace osu.Game.Skinning public bool DeletePending { get; set; } - public string FullName => $"\"{Name}\" by {Creator}"; - public static SkinInfo Default { get; } = new SkinInfo { Name = "osu!lazer", @@ -34,6 +32,10 @@ namespace osu.Game.Skinning public bool Equals(SkinInfo other) => other != null && ID == other.ID; - public override string ToString() => FullName; + public override string ToString() + { + string author = Creator == null ? string.Empty : $"({Creator})"; + return $"{Name} {author}".Trim(); + } } } From 234fa2844574e81ac520b90974af0350b750f070 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 24 May 2020 22:34:31 +0900 Subject: [PATCH 1758/2376] Ensure export filename is valid --- osu.Game/Database/ArchiveModelManager.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 3db367555f..9fc1bfceb5 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -390,7 +390,7 @@ namespace osu.Game.Database foreach (var file in retrievedItem.Files) archive.AddEntry(file.Filename, Files.Storage.GetStream(file.FileInfo.StoragePath)); - using (var outputStream = exportStorage.GetStream($"{item}{HandledExtensions.First()}", FileAccess.Write, FileMode.Create)) + using (var outputStream = exportStorage.GetStream($"{getValidFilename(item.ToString())}{HandledExtensions.First()}", FileAccess.Write, FileMode.Create)) archive.SaveTo(outputStream); exportStorage.OpenInNativeExplorer(); @@ -738,5 +738,12 @@ namespace osu.Game.Database } #endregion + + private string getValidFilename(string filename) + { + foreach (char c in Path.GetInvalidFileNameChars()) + filename = filename.Replace(c, '_'); + return filename; + } } } From 904d17224f75260783a416170ce6b53ee1edd8c5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 24 May 2020 23:09:38 +0900 Subject: [PATCH 1759/2376] Fix english --- osu.Game/Database/ArchiveModelManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 9fc1bfceb5..f21f708f95 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -375,7 +375,7 @@ namespace osu.Game.Database }, cancellationToken, TaskCreationOptions.HideScheduler, import_scheduler).Unwrap(); /// - /// Exports an item to an legacy (.zip based) package. + /// Exports an item to a legacy (.zip based) package. /// /// The item to export. public void Export(TModel item) @@ -383,7 +383,7 @@ namespace osu.Game.Database var retrievedItem = ModelStore.ConsumableItems.FirstOrDefault(s => s.ID == item.ID); if (retrievedItem == null) - throw new ArgumentException("Specified model count not be found", nameof(item)); + throw new ArgumentException("Specified model could not be found", nameof(item)); using (var archive = ZipArchive.Create()) { From 8ab65e4c5d3083682ea2fde406dbfcd229fca359 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 24 May 2020 23:15:24 +0900 Subject: [PATCH 1760/2376] Move implementation into own class --- .../Overlays/Settings/Sections/SkinSection.cs | 53 +++++++++++-------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index a89f2c26c8..b84b9fec37 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -35,10 +35,6 @@ namespace osu.Game.Overlays.Settings.Sections private IBindable> managerAdded; private IBindable> managerRemoved; - private SettingsButton exportButton; - - private Bindable currentSkin; - [BackgroundDependencyLoader] private void load(OsuConfigManager config) { @@ -47,21 +43,7 @@ namespace osu.Game.Overlays.Settings.Sections Children = new Drawable[] { skinDropdown = new SkinSettingsDropdown(), - exportButton = new SettingsButton - { - Text = "Export selected skin", - Action = () => - { - try - { - skins.Export(skins.CurrentSkin.Value.SkinInfo); - } - catch (Exception) - { - Logger.Log("Could not export current skin", level: LogLevel.Error); - } - } - }, + new ExportSkinButton(), new SettingsSlider { LabelText = "Menu cursor size", @@ -102,9 +84,6 @@ namespace osu.Game.Overlays.Settings.Sections skinDropdown.Bindable = dropdownBindable; skinDropdown.Items = skins.GetAllUsableSkins().ToArray(); - currentSkin = skins.CurrentSkin.GetBoundCopy(); - currentSkin.BindValueChanged(skin => exportButton.Enabled.Value = skin.NewValue.SkinInfo.ID > 0); - // Todo: This should not be necessary when OsuConfigManager is databased if (skinDropdown.Items.All(s => s.ID != configBindable.Value)) configBindable.Value = 0; @@ -141,5 +120,35 @@ namespace osu.Game.Overlays.Settings.Sections protected override DropdownMenu CreateMenu() => base.CreateMenu().With(m => m.MaxHeight = 200); } } + + private class ExportSkinButton : SettingsButton + { + [Resolved] + private SkinManager skins { get; set; } + + private Bindable currentSkin; + + [BackgroundDependencyLoader] + private void load() + { + Text = "Export selected skin"; + Action = export; + + currentSkin = skins.CurrentSkin.GetBoundCopy(); + currentSkin.BindValueChanged(skin => Enabled.Value = skin.NewValue.SkinInfo.ID > 0, true); + } + + private void export() + { + try + { + skins.Export(currentSkin.Value.SkinInfo); + } + catch (Exception e) + { + Logger.Log($"Could not export current skin: {e.Message}", level: LogLevel.Error); + } + } + } } } From 1062e07ec12f017da02a903f0ad1189468e8fe53 Mon Sep 17 00:00:00 2001 From: Olle Kelderman Date: Sun, 24 May 2020 22:24:46 +0200 Subject: [PATCH 1761/2376] refactor and implemented feedback: - button text change - renamed ActionableInfoWithNumberBox to ResolutionSelector and moved the clamping logic inside it - also removed the ugly right margin and added the FillFlowContainer --- osu.Game.Tournament/Screens/SetupScreen.cs | 99 +++++++++++----------- 1 file changed, 51 insertions(+), 48 deletions(-) diff --git a/osu.Game.Tournament/Screens/SetupScreen.cs b/osu.Game.Tournament/Screens/SetupScreen.cs index f9ec29d0c6..33eefbe553 100644 --- a/osu.Game.Tournament/Screens/SetupScreen.cs +++ b/osu.Game.Tournament/Screens/SetupScreen.cs @@ -25,7 +25,7 @@ namespace osu.Game.Tournament.Screens private FillFlowContainer fillFlow; private LoginOverlay loginOverlay; - private ActionableInfoWithNumberBox resolution; + private ResolutionSelector resolution; [Resolved] private MatchIPCInfo ipc { get; set; } @@ -108,15 +108,13 @@ namespace osu.Game.Tournament.Screens Items = rulesets.AvailableRulesets, Current = LadderInfo.Ruleset, }, - resolution = new ActionableInfoWithNumberBox + resolution = new ResolutionSelector { Label = "Stream area resolution", - ButtonText = "Set size", + ButtonText = "Set height", Action = i => { - i = Math.Clamp(i, 480, 2160); windowSize.Value = new Size((int)(i * aspect_ratio / TournamentSceneManager.STREAM_AREA_WIDTH * TournamentSceneManager.REQUIRED_WIDTH), i); - resolution.NumberValue = i; } }, }; @@ -167,17 +165,18 @@ namespace osu.Game.Tournament.Screens public string Value { - set => ValueText.Text = value; + set => valueText.Text = value; } public bool Failing { - set => ValueText.Colour = value ? Color4.Red : Color4.White; + set => valueText.Colour = value ? Color4.Red : Color4.White; } public Action Action; - protected TournamentSpriteText ValueText; + private TournamentSpriteText valueText; + protected FillFlowContainer FlowContainer; protected override Drawable CreateComponent() => new Container { @@ -185,71 +184,75 @@ namespace osu.Game.Tournament.Screens RelativeSizeAxes = Axes.X, Children = new Drawable[] { - ValueText = new TournamentSpriteText + valueText = new TournamentSpriteText { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, }, - Button = new TriangleButton + FlowContainer = new FillFlowContainer { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, - Size = new Vector2(100, 30), - Action = () => Action?.Invoke() - }, + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(10, 0), + Children = new Drawable[] + { + Button = new TriangleButton + { + Size = new Vector2(100, 30), + Action = () => Action?.Invoke() + } + } + } } }; } - private class ActionableInfoWithNumberBox : ActionableInfo + private class ResolutionSelector : ActionableInfo { + private const int height_min_allowed_value = 480; + private const int height_max_allowed_value = 2160; // 4k public new Action Action; private OsuNumberBox numberBox; - public int NumberValue + protected override Drawable CreateComponent() { - get + var drawable = base.CreateComponent(); + FlowContainer.Insert(0, numberBox = new OsuNumberBox { - int.TryParse(numberBox.Text, out var val); - return val; - } - set => numberBox.Text = value.ToString(); - } + Width = 100 + }); + FlowContainer.SetLayoutPosition(Button, 1); - protected override Drawable CreateComponent() => new Container - { - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Children = new Drawable[] + base.Action = () => { - ValueText = new TournamentSpriteText + if (numberBox.Text.Length > 0) { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - }, - numberBox = new OsuNumberBox - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Width = 100, - Margin = new MarginPadding + // box contains text + if (!int.TryParse(numberBox.Text, out var number)) { - Right = 110 + // at this point, the only reason we can arrive here is if the input number was too big to parse into an int + // so clamp to max allowed value + number = height_max_allowed_value; } - }, - Button = new TriangleButton - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Size = new Vector2(100, 30), - Action = () => + else { - if (numberBox.Text.Length > 0) Action?.Invoke(NumberValue); + number = Math.Clamp(number, height_min_allowed_value, height_max_allowed_value); } - }, - } - }; + + // in case number got clamped, reset number in numberBox + numberBox.Text = number.ToString(); + + Action?.Invoke(number); + } + else + { + // TODO: input box was empty, give user feedback? do nothing? + } + }; + return drawable; + } } } } From a174117880874df70d5cd07d210db4117a1d7cee Mon Sep 17 00:00:00 2001 From: Olle Kelderman Date: Mon, 25 May 2020 00:55:10 +0200 Subject: [PATCH 1762/2376] fix flowcontainer order properly and removed todo as its decided to do nothing there for now --- osu.Game.Tournament/Screens/SetupScreen.cs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tournament/Screens/SetupScreen.cs b/osu.Game.Tournament/Screens/SetupScreen.cs index 33eefbe553..bf328987fe 100644 --- a/osu.Game.Tournament/Screens/SetupScreen.cs +++ b/osu.Game.Tournament/Screens/SetupScreen.cs @@ -151,7 +151,7 @@ namespace osu.Game.Tournament.Screens private class ActionableInfo : LabelledDrawable { - protected OsuButton Button; + private OsuButton button; public ActionableInfo() : base(true) @@ -160,7 +160,7 @@ namespace osu.Game.Tournament.Screens public string ButtonText { - set => Button.Text = value; + set => button.Text = value; } public string Value @@ -197,7 +197,7 @@ namespace osu.Game.Tournament.Screens Spacing = new Vector2(10, 0), Children = new Drawable[] { - Button = new TriangleButton + button = new TriangleButton { Size = new Vector2(100, 30), Action = () => Action?.Invoke() @@ -219,11 +219,10 @@ namespace osu.Game.Tournament.Screens protected override Drawable CreateComponent() { var drawable = base.CreateComponent(); - FlowContainer.Insert(0, numberBox = new OsuNumberBox + FlowContainer.Insert(-1, numberBox = new OsuNumberBox { Width = 100 }); - FlowContainer.SetLayoutPosition(Button, 1); base.Action = () => { @@ -246,10 +245,6 @@ namespace osu.Game.Tournament.Screens Action?.Invoke(number); } - else - { - // TODO: input box was empty, give user feedback? do nothing? - } }; return drawable; } From 1977affe7e6fac5e263011ba20768d1401dcafe3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 May 2020 09:27:11 +0900 Subject: [PATCH 1763/2376] Fix OpenInNativeExplorer not working correctly for wrapped storages --- osu.Game/IO/WrappedStorage.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/IO/WrappedStorage.cs b/osu.Game/IO/WrappedStorage.cs index 646faba9eb..cd775df0fd 100644 --- a/osu.Game/IO/WrappedStorage.cs +++ b/osu.Game/IO/WrappedStorage.cs @@ -69,7 +69,9 @@ namespace osu.Game.IO public override void DeleteDatabase(string name) => UnderlyingStorage.DeleteDatabase(MutatePath(name)); - public override void OpenInNativeExplorer() => UnderlyingStorage.OpenInNativeExplorer(); + public override void OpenInNativeExplorer() => UnderlyingStorage.OpenPathInNativeExplorer(subPath); + + public override void OpenPathInNativeExplorer(string path) => UnderlyingStorage.OpenPathInNativeExplorer(MutatePath(path)); public override Storage GetStorageForDirectory(string path) { From 6904d5d2470c9164f593527a1bdc9d59a6d3f3bd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 May 2020 13:12:53 +0900 Subject: [PATCH 1764/2376] Remove unnecessary override --- osu.Game/IO/WrappedStorage.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/IO/WrappedStorage.cs b/osu.Game/IO/WrappedStorage.cs index cd775df0fd..1dd3afbfae 100644 --- a/osu.Game/IO/WrappedStorage.cs +++ b/osu.Game/IO/WrappedStorage.cs @@ -69,8 +69,6 @@ namespace osu.Game.IO public override void DeleteDatabase(string name) => UnderlyingStorage.DeleteDatabase(MutatePath(name)); - public override void OpenInNativeExplorer() => UnderlyingStorage.OpenPathInNativeExplorer(subPath); - public override void OpenPathInNativeExplorer(string path) => UnderlyingStorage.OpenPathInNativeExplorer(MutatePath(path)); public override Storage GetStorageForDirectory(string path) From b44beb413729bb40dc5294c194454ea0707ddf3e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 May 2020 15:40:25 +0900 Subject: [PATCH 1765/2376] Remove double resolution of EditorClock --- osu.Game/Rulesets/Edit/PlacementBlueprint.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index 1e0507fc0a..e2bb8b5995 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Edit /// protected readonly HitObject HitObject; - [Resolved] + [Resolved(canBeNull: true)] protected EditorClock EditorClock { get; private set; } private readonly IBindable beatmap = new Bindable(); @@ -85,9 +85,6 @@ namespace osu.Game.Rulesets.Edit PlacementActive = false; } - [Resolved(canBeNull: true)] - private EditorClock editorClock { get; set; } - /// /// Updates the position of this to a new screen-space position. /// @@ -95,7 +92,7 @@ namespace osu.Game.Rulesets.Edit public virtual void UpdatePosition(SnapResult snapResult) { if (!PlacementActive) - HitObject.StartTime = snapResult.Time ?? editorClock?.CurrentTime ?? Time.Current; + HitObject.StartTime = snapResult.Time ?? EditorClock?.CurrentTime ?? Time.Current; } /// From cd65fc860b56f7fb2132e547e93ab068e49b308b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 25 May 2020 16:15:55 +0900 Subject: [PATCH 1766/2376] Remove extra default application --- osu.Game/Rulesets/Edit/PlacementBlueprint.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index e2bb8b5995..3541a78faa 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -56,8 +56,6 @@ namespace osu.Game.Rulesets.Edit { this.beatmap.BindTo(beatmap); - ApplyDefaultsToHitObject(); - startTimeBindable = HitObject.StartTimeBindable.GetBoundCopy(); startTimeBindable.BindValueChanged(_ => ApplyDefaultsToHitObject(), true); } From 6f4cd6111cbdeb04d4e22f88f1693bd9f05e9dad Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 25 May 2020 16:50:49 +0900 Subject: [PATCH 1767/2376] Drop obsoletion status for now --- osu.Game/Rulesets/Objects/HitObject.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs index 8ff2bdefb3..6f9053d7cb 100644 --- a/osu.Game/Rulesets/Objects/HitObject.cs +++ b/osu.Game/Rulesets/Objects/HitObject.cs @@ -146,7 +146,6 @@ namespace osu.Game.Rulesets.Objects #pragma warning restore 618 } - [Obsolete("Use the overload with cancellation support instead.")] // can be removed 20201115 protected virtual void CreateNestedHitObjects() { } From d09b579aeee7ca9ca75f78e3bc67754882f1800e Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 25 May 2020 09:32:24 +0000 Subject: [PATCH 1768/2376] Bump DiffPlex from 1.6.1 to 1.6.2 Bumps [DiffPlex](https://github.com/mmanela/diffplex) from 1.6.1 to 1.6.2. - [Release notes](https://github.com/mmanela/diffplex/releases) - [Commits](https://github.com/mmanela/diffplex/commits) Signed-off-by: dependabot-preview[bot] --- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 010ef8578a..dbaf0697a0 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -19,7 +19,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 88b0c7dd8a..664a629369 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -75,7 +75,7 @@ - + From c4665048dbd036ee6f492eb45f00bde4a38eedd7 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 25 May 2020 09:34:26 +0000 Subject: [PATCH 1769/2376] Bump SharpCompress from 0.25.0 to 0.25.1 Bumps [SharpCompress](https://github.com/adamhathcock/sharpcompress) from 0.25.0 to 0.25.1. - [Release notes](https://github.com/adamhathcock/sharpcompress/releases) - [Commits](https://github.com/adamhathcock/sharpcompress/compare/0.25...0.25.1) Signed-off-by: dependabot-preview[bot] --- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index dbaf0697a0..d8feb4df24 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -27,7 +27,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 664a629369..6d74be5f85 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -81,7 +81,7 @@ - + From af5fac471e56a2b819983ac49067e094e6ea8850 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 May 2020 13:54:30 +0900 Subject: [PATCH 1770/2376] Remove unnecessary size propagation in HitObjectComposer --- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 37 +++++---------------- 1 file changed, 9 insertions(+), 28 deletions(-) diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 1987148aed..e5479251b5 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -51,8 +51,6 @@ namespace osu.Game.Rulesets.Edit protected readonly Container LayerBelowRuleset = new Container { RelativeSizeAxes = Axes.Both }; - private readonly List layerContainers = new List(); - private InputManager inputManager; private RadioButtonCollection toolboxCollection; @@ -82,17 +80,6 @@ namespace osu.Game.Rulesets.Edit return; } - var layerBelowRuleset = drawableRulesetWrapper.CreatePlayfieldAdjustmentContainer().WithChildren(new Drawable[] - { - LayerBelowRuleset, - new EditorPlayfieldBorder { RelativeSizeAxes = Axes.Both } - }); - - var layerAboveRuleset = drawableRulesetWrapper.CreatePlayfieldAdjustmentContainer().WithChild(BlueprintContainer = CreateBlueprintContainer()); - - layerContainers.Add(layerBelowRuleset); - layerContainers.Add(layerAboveRuleset); - InternalChild = new GridContainer { RelativeSizeAxes = Axes.Both, @@ -116,9 +103,16 @@ namespace osu.Game.Rulesets.Edit RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - layerBelowRuleset, + // layers below playfield + drawableRulesetWrapper.CreatePlayfieldAdjustmentContainer().WithChildren(new Drawable[] + { + LayerBelowRuleset, + new EditorPlayfieldBorder { RelativeSizeAxes = Axes.Both } + }), drawableRulesetWrapper, - layerAboveRuleset + // layers above playfield + drawableRulesetWrapper.CreatePlayfieldAdjustmentContainer() + .WithChild(BlueprintContainer = CreateBlueprintContainer()) } } }, @@ -162,19 +156,6 @@ namespace osu.Game.Rulesets.Edit inputManager = GetContainingInputManager(); } - protected override void UpdateAfterChildren() - { - base.UpdateAfterChildren(); - - layerContainers.ForEach(l => - { - l.Anchor = drawableRulesetWrapper.Playfield.Anchor; - l.Origin = drawableRulesetWrapper.Playfield.Origin; - l.Position = drawableRulesetWrapper.Playfield.Position; - l.Size = drawableRulesetWrapper.Playfield.Size; - }); - } - private void selectionChanged(object sender, NotifyCollectionChangedEventArgs changedArgs) { if (EditorBeatmap.SelectedHitObjects.Any()) From b8130bd3669f447a8179c13ba889207f261c262c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 May 2020 18:26:21 +0900 Subject: [PATCH 1771/2376] Make mania selection blueprint abstract --- .../Edit/Blueprints/ManiaSelectionBlueprint.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs index b8574b804e..0089a9fbee 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs @@ -11,7 +11,7 @@ using osuTK; namespace osu.Game.Rulesets.Mania.Edit.Blueprints { - public class ManiaSelectionBlueprint : OverlaySelectionBlueprint + public abstract class ManiaSelectionBlueprint : OverlaySelectionBlueprint { public new DrawableManiaHitObject DrawableObject => (DrawableManiaHitObject)base.DrawableObject; @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints [Resolved] private IManiaHitObjectComposer composer { get; set; } - public ManiaSelectionBlueprint(DrawableHitObject drawableObject) + protected ManiaSelectionBlueprint(DrawableHitObject drawableObject) : base(drawableObject) { RelativeSizeAxes = Axes.None; From 2c16619ecd9efa009601eefd0fdc3068fc4e4838 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 May 2020 18:26:28 +0900 Subject: [PATCH 1772/2376] Move time to position conversion to ScrollingHitObjectContainer --- osu.Game.Rulesets.Mania/UI/Column.cs | 50 ----------- .../Scrolling/ScrollingHitObjectContainer.cs | 87 +++++++++++++++++++ .../UI/Scrolling/ScrollingPlayfield.cs | 13 +++ 3 files changed, 100 insertions(+), 50 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index be31954099..511d6c8623 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -17,7 +17,6 @@ using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; using osuTK; using osu.Game.Rulesets.Mania.Beatmaps; -using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; namespace osu.Game.Rulesets.Mania.UI { @@ -143,54 +142,5 @@ namespace osu.Game.Rulesets.Mania.UI public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) // This probably shouldn't exist as is, but the columns in the stage are separated by a 1px border => DrawRectangle.Inflate(new Vector2(Stage.COLUMN_SPACING / 2, 0)).Contains(ToLocalSpace(screenSpacePos)); - - /// - /// Given a time, return the screen space position within this column. - /// - public Vector2 ScreenSpacePositionAtTime(double time) - { - var pos = ScrollingInfo.Algorithm.PositionAt(time, Time.Current, ScrollingInfo.TimeRange.Value, HitObjectContainer.DrawHeight); - - switch (ScrollingInfo.Direction.Value) - { - case ScrollingDirection.Down: - // We're dealing with screen coordinates in which the position decreases towards the centre of the screen resulting in an increase in start time. - // The scrolling algorithm instead assumes a top anchor meaning an increase in time corresponds to an increase in position, - // so when scrolling downwards the coordinates need to be flipped. - pos = HitObjectContainer.DrawHeight - pos; - - // Blueprints are centred on the mouse position, such that the hitobject position is anchored at the top or bottom of the blueprint depending on the scroll direction. - pos -= DefaultNotePiece.NOTE_HEIGHT / 2; - break; - - case ScrollingDirection.Up: - pos += DefaultNotePiece.NOTE_HEIGHT / 2; - break; - } - - return HitObjectContainer.ToScreenSpace(new Vector2(HitObjectContainer.DrawWidth / 2, pos)); - } - - /// - /// Given a position in screen space, return the time within this column. - /// - public double TimeAtScreenSpacePosition(Vector2 screenSpacePosition) - { - // convert to local space of column so we can snap and fetch correct location. - Vector2 localPosition = HitObjectContainer.ToLocalSpace(screenSpacePosition); - - switch (ScrollingInfo.Direction.Value) - { - case ScrollingDirection.Down: - // as above - localPosition.Y = HitObjectContainer.DrawHeight - localPosition.Y; - break; - } - - // offset for the fact that blueprints are centered, as above. - localPosition.Y -= DefaultNotePiece.NOTE_HEIGHT / 2; - - return ScrollingInfo.Algorithm.TimeAt(localPosition.Y, Time.Current, ScrollingInfo.TimeRange.Value, HitObjectContainer.DrawHeight); - } } } diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index 15e625872d..4ef2c04f23 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics; using osu.Framework.Layout; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; +using osuTK; namespace osu.Game.Rulesets.UI.Scrolling { @@ -78,6 +79,92 @@ namespace osu.Game.Rulesets.UI.Scrolling hitObjectInitialStateCache.Clear(); } + public double TimeAtScreenSpace(Vector2 screenSpacePosition) + { + // convert to local space of column so we can snap and fetch correct location. + Vector2 localPosition = ToLocalSpace(screenSpacePosition); + + float position = 0; + + switch (scrollingInfo.Direction.Value) + { + case ScrollingDirection.Up: + case ScrollingDirection.Down: + position = localPosition.Y; + break; + + case ScrollingDirection.Right: + case ScrollingDirection.Left: + position = localPosition.X; + break; + } + + flipPositionIfRequired(ref position); + + return scrollingInfo.Algorithm.TimeAt(position, Time.Current, scrollingInfo.TimeRange.Value, getLength()); + } + + public Vector2 ScreenSpacePositionAtTime(double time) + { + var pos = scrollingInfo.Algorithm.PositionAt(time, Time.Current, scrollingInfo.TimeRange.Value, getLength()); + + flipPositionIfRequired(ref pos); + + switch (scrollingInfo.Direction.Value) + { + case ScrollingDirection.Up: + case ScrollingDirection.Down: + return ToScreenSpace(new Vector2(getBredth() / 2, pos)); + + default: + return ToScreenSpace(new Vector2(pos, getBredth() / 2)); + } + } + + private float getLength() + { + switch (scrollingInfo.Direction.Value) + { + case ScrollingDirection.Left: + case ScrollingDirection.Right: + return DrawWidth; + + default: + return DrawHeight; + } + } + + private float getBredth() + { + switch (scrollingInfo.Direction.Value) + { + case ScrollingDirection.Up: + case ScrollingDirection.Down: + return DrawWidth; + + default: + return DrawHeight; + } + } + + private void flipPositionIfRequired(ref float position) + { + // We're dealing with screen coordinates in which the position decreases towards the centre of the screen resulting in an increase in start time. + // The scrolling algorithm instead assumes a top anchor meaning an increase in time corresponds to an increase in position, + // so when scrolling downwards the coordinates need to be flipped. + + switch (scrollingInfo.Direction.Value) + { + case ScrollingDirection.Down: + position = DrawHeight - position; + break; + + case ScrollingDirection.Right: + position = DrawWidth - position; + break; + } + } + private void onDefaultsApplied(DrawableHitObject drawableObject) { // The cache may not exist if the hitobject state hasn't been computed yet (e.g. if the hitobject was added + defaults applied in the same frame). diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs index fd143a3687..1ccde9b1e3 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Rulesets.Objects.Drawables; +using osuTK; namespace osu.Game.Rulesets.UI.Scrolling { @@ -23,6 +24,18 @@ namespace osu.Game.Rulesets.UI.Scrolling Direction.BindTo(ScrollingInfo.Direction); } + /// + /// Given a position in screen space, return the time within this column. + /// + public virtual double TimeAtScreenSpacePosition(Vector2 screenSpacePosition) => + ((ScrollingHitObjectContainer)HitObjectContainer).TimeAtScreenSpace(screenSpacePosition); + + /// + /// Given a time, return the screen space position within this column. + /// + public virtual Vector2 ScreenSpacePositionAtTime(double time) + => ((ScrollingHitObjectContainer)HitObjectContainer).ScreenSpacePositionAtTime(time); + protected sealed override HitObjectContainer CreateHitObjectContainer() => new ScrollingHitObjectContainer(); } } From e7442ec3a287d7887747bb392e5502bb8fcca387 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 May 2020 19:21:53 +0900 Subject: [PATCH 1773/2376] Remove need for ManiaSnapResult --- .../ManiaPlacementBlueprintTestScene.cs | 2 +- .../Blueprints/HoldNotePlacementBlueprint.cs | 4 ++-- .../Blueprints/ManiaPlacementBlueprint.cs | 2 +- .../Edit/Blueprints/NotePlacementBlueprint.cs | 4 ++-- .../Edit/ManiaHitObjectComposer.cs | 19 ++-------------- .../Edit/ManiaSnapResult.cs | 20 ----------------- .../Edit/OsuHitObjectComposer.cs | 2 +- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 22 ++++++++++++++++++- osu.Game/Rulesets/Edit/SnapResult.cs | 6 ++++- 9 files changed, 35 insertions(+), 46 deletions(-) delete mode 100644 osu.Game.Rulesets.Mania/Edit/ManiaSnapResult.cs diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs index fd18907d96..1119a66f63 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Mania.Tests var time = column.TimeAtScreenSpacePosition(InputManager.CurrentState.Mouse.Position); var pos = column.ScreenSpacePositionAtTime(time); - return new ManiaSnapResult(pos, time, column); + return new SnapResult(pos, time, column); } protected override Container CreateHitObjectContainer() => new ScrollingTestContainer(ScrollingDirection.Down) { RelativeSizeAxes = Axes.Both }; diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs index b757c17a48..cd549ab6aa 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs @@ -78,9 +78,9 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints } else { - if (result is ManiaSnapResult maniaResult) + if (result.Playfield != null) { - headPiece.Width = tailPiece.Width = maniaResult.Column.DrawWidth; + headPiece.Width = tailPiece.Width = result.Playfield.DrawWidth; headPiece.X = tailPiece.X = ToLocalSpace(result.ScreenSpacePosition).X; } diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs index d173da9d9a..27a279e044 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs @@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints base.UpdatePosition(result); if (!PlacementActive) - Column = (result as ManiaSnapResult)?.Column; + Column = result.Playfield as Column; } } } diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs index 5f6db2e6dd..684004b558 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs @@ -26,9 +26,9 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints { base.UpdatePosition(result); - if (result is ManiaSnapResult maniaResult) + if (result.Playfield != null) { - piece.Width = maniaResult.Column.DrawWidth; + piece.Width = result.Playfield.DrawWidth; piece.Position = ToLocalSpace(result.ScreenSpacePosition); } } diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index 683e921cbf..83bf202674 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -53,23 +53,8 @@ namespace osu.Game.Rulesets.Mania.Edit public IScrollingInfo ScrollingInfo => drawableRuleset.ScrollingInfo; - public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) - { - var column = Playfield.GetColumnByPosition(screenSpacePosition); - - if (column == null) - return new SnapResult(screenSpacePosition, null); - - double targetTime = column.TimeAtScreenSpacePosition(screenSpacePosition); - - // apply beat snapping - targetTime = BeatSnapProvider.SnapTime(targetTime); - - // convert back to screen space - screenSpacePosition = column.ScreenSpacePositionAtTime(targetTime); - - return new ManiaSnapResult(screenSpacePosition, targetTime, column); - } + protected override Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) => + Playfield.GetColumnByPosition(screenSpacePosition); protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) { diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaSnapResult.cs b/osu.Game.Rulesets.Mania/Edit/ManiaSnapResult.cs deleted file mode 100644 index b94f5e51c4..0000000000 --- a/osu.Game.Rulesets.Mania/Edit/ManiaSnapResult.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.Edit; -using osu.Game.Rulesets.Mania.UI; -using osuTK; - -namespace osu.Game.Rulesets.Mania.Edit -{ - public class ManiaSnapResult : SnapResult - { - public readonly Column Column; - - public ManiaSnapResult(Vector2 screenSpacePosition, double time, Column column) - : base(screenSpacePosition, time) - { - Column = column; - } - } -} diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 62287574ea..9e7dc11fdd 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -78,7 +78,7 @@ namespace osu.Game.Rulesets.Osu.Edit (Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition)); - return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time); + return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, PlayfieldAtScreenSpacePosition(screenSpacePosition)); } private void updateDistanceSnapGrid() diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 1987148aed..b26284c060 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -19,6 +19,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI; +using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Components.RadioButtons; using osu.Game.Screens.Edit.Compose; @@ -224,7 +225,26 @@ namespace osu.Game.Rulesets.Edit public void Delete(HitObject hitObject) => EditorBeatmap.Remove(hitObject); - public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, null); + protected virtual Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) => drawableRulesetWrapper.Playfield; + + public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) + { + var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition); + double? targetTime = null; + + if (playfield is ScrollingPlayfield scrollingPlayfield) + { + targetTime = scrollingPlayfield.TimeAtScreenSpacePosition(screenSpacePosition); + + // apply beat snapping + targetTime = BeatSnapProvider.SnapTime(targetTime.Value); + + // convert back to screen space + screenSpacePosition = scrollingPlayfield.ScreenSpacePositionAtTime(targetTime.Value); + } + + return new SnapResult(screenSpacePosition, targetTime, playfield); + } public override float GetBeatSnapDistanceAt(double referenceTime) { diff --git a/osu.Game/Rulesets/Edit/SnapResult.cs b/osu.Game/Rulesets/Edit/SnapResult.cs index 5d07d7b233..31dd2b9496 100644 --- a/osu.Game/Rulesets/Edit/SnapResult.cs +++ b/osu.Game/Rulesets/Edit/SnapResult.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Game.Rulesets.UI; using osuTK; namespace osu.Game.Rulesets.Edit @@ -20,10 +21,13 @@ namespace osu.Game.Rulesets.Edit /// public double? Time; - public SnapResult(Vector2 screenSpacePosition, double? time) + public readonly Playfield Playfield; + + public SnapResult(Vector2 screenSpacePosition, double? time, Playfield playfield = null) { ScreenSpacePosition = screenSpacePosition; Time = time; + Playfield = playfield; } } } From 827345ed88f6dd6792ebd1cfc4a16989cd163723 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 May 2020 20:21:06 +0900 Subject: [PATCH 1774/2376] Fix mania offsets --- .../Blueprints/HoldNotePlacementBlueprint.cs | 17 ++++++++++++++ .../Edit/ManiaHitObjectComposer.cs | 23 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs index cd549ab6aa..500b26917d 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs @@ -8,6 +8,7 @@ using osu.Framework.Input.Events; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Edit.Blueprints.Components; using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.UI.Scrolling; using osuTK; using osuTK.Input; @@ -22,6 +23,9 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints [Resolved] private IManiaHitObjectComposer composer { get; set; } + [Resolved] + private IScrollingInfo scrollingInfo { get; set; } + public HoldNotePlacementBlueprint() : base(new HoldNote()) { @@ -43,6 +47,19 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints { headPiece.Y = Parent.ToLocalSpace(Column.ScreenSpacePositionAtTime(HitObject.StartTime)).Y; tailPiece.Y = Parent.ToLocalSpace(Column.ScreenSpacePositionAtTime(HitObject.EndTime)).Y; + + switch (scrollingInfo.Direction.Value) + { + case ScrollingDirection.Down: + headPiece.Y -= headPiece.DrawHeight / 2; + tailPiece.Y -= tailPiece.DrawHeight / 2; + break; + + case ScrollingDirection.Up: + headPiece.Y += headPiece.DrawHeight / 2; + tailPiece.Y += tailPiece.DrawHeight / 2; + break; + } } var topPosition = new Vector2(headPiece.DrawPosition.X, Math.Min(headPiece.DrawPosition.Y, tailPiece.DrawPosition.Y)); diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index 83bf202674..73cbadc97c 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Input; +using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; @@ -56,6 +57,28 @@ namespace osu.Game.Rulesets.Mania.Edit protected override Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) => Playfield.GetColumnByPosition(screenSpacePosition); + public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) + { + var result = base.SnapScreenSpacePositionToValidTime(screenSpacePosition); + + switch (ScrollingInfo.Direction.Value) + { + case ScrollingDirection.Down: + result.ScreenSpacePosition -= new Vector2(0, getNoteHeight() / 2); + break; + + case ScrollingDirection.Up: + result.ScreenSpacePosition += new Vector2(0, getNoteHeight() / 2); + break; + } + + return result; + } + + private float getNoteHeight() => + Playfield.GetColumn(0).ToScreenSpace(new Vector2(DefaultNotePiece.NOTE_HEIGHT)).Y - + Playfield.GetColumn(0).ToScreenSpace(Vector2.Zero).Y; + protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) { drawableRuleset = new DrawableManiaEditRuleset(ruleset, beatmap, mods); From 8fc60d12c910dabdb765a93a403ab928d488a506 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 May 2020 22:09:09 +0900 Subject: [PATCH 1775/2376] Add back comments --- .../Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index 4ef2c04f23..544468fd47 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -79,6 +79,9 @@ namespace osu.Game.Rulesets.UI.Scrolling hitObjectInitialStateCache.Clear(); } + /// + /// Given a position in screen space, return the time within this column. + /// public double TimeAtScreenSpace(Vector2 screenSpacePosition) { // convert to local space of column so we can snap and fetch correct location. @@ -104,6 +107,9 @@ namespace osu.Game.Rulesets.UI.Scrolling return scrollingInfo.Algorithm.TimeAt(position, Time.Current, scrollingInfo.TimeRange.Value, getLength()); } + /// + /// Given a time, return the screen space position within this column. + /// public Vector2 ScreenSpacePositionAtTime(double time) { var pos = scrollingInfo.Algorithm.PositionAt(time, Time.Current, scrollingInfo.TimeRange.Value, getLength()); From cf341998c360fb3389dea953344c8bbc49cd11b3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 May 2020 22:55:21 +0900 Subject: [PATCH 1776/2376] 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 f0f16d3763..b7d08fb120 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index d8feb4df24..d5017a436f 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -24,7 +24,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 6d74be5f85..19a36f1e1f 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From 4c3900cfc8a390f61b0252abec07cb61f6309c87 Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 25 May 2020 17:16:40 +0200 Subject: [PATCH 1777/2376] Remove unnecessary comments, simplify initialPath and clarified TestScene name --- .../Screens/TestSceneStablePathSelectScreens.cs | 8 ++++---- osu.Game.Tournament/Screens/StablePathSelectScreen.cs | 11 +---------- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneStablePathSelectScreens.cs b/osu.Game.Tournament.Tests/Screens/TestSceneStablePathSelectScreens.cs index 4dfd4d35c8..ce0626dd0f 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneStablePathSelectScreens.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneStablePathSelectScreens.cs @@ -6,14 +6,14 @@ using osu.Framework.Platform; namespace osu.Game.Tournament.Tests.Screens { - public class TestSceneStablePathSelectScreens : TournamentTestScene + public class TestSceneStablePathSelectScreen : TournamentTestScene { - public TestSceneStablePathSelectScreens() + public TestSceneStablePathSelectScreen() { - AddStep("Add screen", () => Add(new TestSceneStablePathSelectScreen())); + AddStep("Add screen", () => Add(new StablePathSelectTestScreen())); } - private class TestSceneStablePathSelectScreen : StablePathSelectScreen + private class StablePathSelectTestScreen : StablePathSelectScreen { protected override void ChangePath(Storage storage) { diff --git a/osu.Game.Tournament/Screens/StablePathSelectScreen.cs b/osu.Game.Tournament/Screens/StablePathSelectScreen.cs index 609c601106..d2c7225909 100644 --- a/osu.Game.Tournament/Screens/StablePathSelectScreen.cs +++ b/osu.Game.Tournament/Screens/StablePathSelectScreen.cs @@ -38,14 +38,7 @@ namespace osu.Game.Tournament.Screens [BackgroundDependencyLoader(true)] private void load(Storage storage, OsuColour colours) { - // begin selection in the parent directory of the current storage location - var initialPath = new DirectoryInfo(storage.GetFullPath(string.Empty)).Parent?.FullName; - - if (!string.IsNullOrEmpty(stableInfo.StablePath.Value)) - { - // If the original path info for osu! stable is not empty, set it to the parent directory of that location - initialPath = new DirectoryInfo(stableInfo.StablePath.Value).Parent?.FullName; - } + var initialPath = new DirectoryInfo(storage.GetFullPath(stableInfo.StablePath.Value ?? string.Empty)).Parent?.FullName; AddRangeInternal(new Drawable[] { @@ -148,7 +141,6 @@ namespace osu.Game.Tournament.Screens AddInternal(overlay); Logger.Log("Folder is not an osu! stable CE directory"); return; - // Return an error in the picker that the directory does not contain ipc.txt } var fileBasedIpc = ipc as FileBasedIPC; @@ -163,7 +155,6 @@ namespace osu.Game.Tournament.Screens if (fileBasedIpc?.IPCStorage == null) { - // Could not auto detect overlay = new DialogOverlay(); overlay.Push(new IPCErrorDialog("Failed to auto detect", "An osu! stable cutting-edge installation could not be auto detected.\nPlease try and manually point to the directory.")); AddInternal(overlay); From 719da489221850d009c7a87389e3c25edf9a9bf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 25 May 2020 20:11:00 +0200 Subject: [PATCH 1778/2376] Rename delegate argument --- osu.Game.Tournament/Screens/SetupScreen.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tournament/Screens/SetupScreen.cs b/osu.Game.Tournament/Screens/SetupScreen.cs index bf328987fe..0ecab449ec 100644 --- a/osu.Game.Tournament/Screens/SetupScreen.cs +++ b/osu.Game.Tournament/Screens/SetupScreen.cs @@ -112,9 +112,9 @@ namespace osu.Game.Tournament.Screens { Label = "Stream area resolution", ButtonText = "Set height", - Action = i => + Action = height => { - windowSize.Value = new Size((int)(i * aspect_ratio / TournamentSceneManager.STREAM_AREA_WIDTH * TournamentSceneManager.REQUIRED_WIDTH), i); + windowSize.Value = new Size((int)(height * aspect_ratio / TournamentSceneManager.STREAM_AREA_WIDTH * TournamentSceneManager.REQUIRED_WIDTH), height); } }, }; From ca68d94cf7aa7acec4d638f61934836bd3c2b3c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 25 May 2020 20:18:17 +0200 Subject: [PATCH 1779/2376] Invert if to reduce nesting --- osu.Game.Tournament/Screens/SetupScreen.cs | 34 +++++++++++----------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tournament/Screens/SetupScreen.cs b/osu.Game.Tournament/Screens/SetupScreen.cs index 0ecab449ec..f8895f4703 100644 --- a/osu.Game.Tournament/Screens/SetupScreen.cs +++ b/osu.Game.Tournament/Screens/SetupScreen.cs @@ -226,25 +226,25 @@ namespace osu.Game.Tournament.Screens base.Action = () => { - if (numberBox.Text.Length > 0) + if (string.IsNullOrEmpty(numberBox.Text)) + return; + + // box contains text + if (!int.TryParse(numberBox.Text, out var number)) { - // box contains text - if (!int.TryParse(numberBox.Text, out var number)) - { - // at this point, the only reason we can arrive here is if the input number was too big to parse into an int - // so clamp to max allowed value - number = height_max_allowed_value; - } - else - { - number = Math.Clamp(number, height_min_allowed_value, height_max_allowed_value); - } - - // in case number got clamped, reset number in numberBox - numberBox.Text = number.ToString(); - - Action?.Invoke(number); + // at this point, the only reason we can arrive here is if the input number was too big to parse into an int + // so clamp to max allowed value + number = height_max_allowed_value; } + else + { + number = Math.Clamp(number, height_min_allowed_value, height_max_allowed_value); + } + + // in case number got clamped, reset number in numberBox + numberBox.Text = number.ToString(); + + Action?.Invoke(number); }; return drawable; } From 748f7fcd8b914ff7e84d0f29463142b18a6b244d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 25 May 2020 20:20:26 +0200 Subject: [PATCH 1780/2376] Rename constants --- osu.Game.Tournament/Screens/SetupScreen.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tournament/Screens/SetupScreen.cs b/osu.Game.Tournament/Screens/SetupScreen.cs index f8895f4703..e1594de69e 100644 --- a/osu.Game.Tournament/Screens/SetupScreen.cs +++ b/osu.Game.Tournament/Screens/SetupScreen.cs @@ -210,8 +210,8 @@ namespace osu.Game.Tournament.Screens private class ResolutionSelector : ActionableInfo { - private const int height_min_allowed_value = 480; - private const int height_max_allowed_value = 2160; // 4k + private const int minimum_window_height = 480; + private const int maximum_window_height = 2160; // 4k public new Action Action; private OsuNumberBox numberBox; @@ -234,11 +234,11 @@ namespace osu.Game.Tournament.Screens { // at this point, the only reason we can arrive here is if the input number was too big to parse into an int // so clamp to max allowed value - number = height_max_allowed_value; + number = maximum_window_height; } else { - number = Math.Clamp(number, height_min_allowed_value, height_max_allowed_value); + number = Math.Clamp(number, minimum_window_height, maximum_window_height); } // in case number got clamped, reset number in numberBox From d69111c665079d495906232e24a49c640c7fe6e0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 May 2020 10:17:34 +0900 Subject: [PATCH 1781/2376] Fix spelling of breadth --- .../Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index 544468fd47..9b84b67241 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -120,10 +120,10 @@ namespace osu.Game.Rulesets.UI.Scrolling { case ScrollingDirection.Up: case ScrollingDirection.Down: - return ToScreenSpace(new Vector2(getBredth() / 2, pos)); + return ToScreenSpace(new Vector2(getBreadth() / 2, pos)); default: - return ToScreenSpace(new Vector2(pos, getBredth() / 2)); + return ToScreenSpace(new Vector2(pos, getBreadth() / 2)); } } @@ -140,7 +140,7 @@ namespace osu.Game.Rulesets.UI.Scrolling } } - private float getBredth() + private float getBreadth() { switch (scrollingInfo.Direction.Value) { From 417e31d77f87b66087e00c8918c83bef0fbfcbf3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 May 2020 10:17:56 +0900 Subject: [PATCH 1782/2376] Rename function for consistency --- osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs | 2 +- osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index 9b84b67241..c817d84d5c 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -82,7 +82,7 @@ namespace osu.Game.Rulesets.UI.Scrolling /// /// Given a position in screen space, return the time within this column. /// - public double TimeAtScreenSpace(Vector2 screenSpacePosition) + public double TimeAtScreenSpacePosition(Vector2 screenSpacePosition) { // convert to local space of column so we can snap and fetch correct location. Vector2 localPosition = ToLocalSpace(screenSpacePosition); diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs index 1ccde9b1e3..9dac3f4de1 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.UI.Scrolling /// Given a position in screen space, return the time within this column. /// public virtual double TimeAtScreenSpacePosition(Vector2 screenSpacePosition) => - ((ScrollingHitObjectContainer)HitObjectContainer).TimeAtScreenSpace(screenSpacePosition); + ((ScrollingHitObjectContainer)HitObjectContainer).TimeAtScreenSpacePosition(screenSpacePosition); /// /// Given a time, return the screen space position within this column. From 13bd6be8a3e2bf97ab9fb0bf19c7cf732ddcd5cb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 May 2020 11:29:56 +0900 Subject: [PATCH 1783/2376] Convert wait steps into until steps --- .../Background/TestSceneUserDimBackgrounds.cs | 53 ++++++------------- 1 file changed, 17 insertions(+), 36 deletions(-) diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs index 76d0c7a50f..edba462bc2 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs @@ -79,11 +79,9 @@ namespace osu.Game.Tests.Visual.Background InputManager.MoveMouseTo(playerLoader.ScreenPos); InputManager.MoveMouseTo(playerLoader.VisualSettingsPos); }); - waitForDim(); - AddAssert("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied()); + AddUntilStep("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied()); AddStep("Stop background preview", () => InputManager.MoveMouseTo(playerLoader.ScreenPos)); - waitForDim(); - AddAssert("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && playerLoader.IsBlurCorrect()); + AddUntilStep("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && playerLoader.IsBlurCorrect()); } /// @@ -97,8 +95,7 @@ namespace osu.Game.Tests.Visual.Background performFullSetup(); AddStep("Trigger hover event", () => playerLoader.TriggerOnHover()); AddAssert("Background retained from song select", () => songSelect.IsBackgroundCurrent()); - waitForDim(); - AddAssert("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied()); + AddUntilStep("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied()); } /// @@ -114,15 +111,13 @@ namespace osu.Game.Tests.Visual.Background player.ReplacesBackground.Value = true; player.StoryboardEnabled.Value = true; }); - waitForDim(); - AddAssert("Background is invisible, storyboard is visible", () => songSelect.IsBackgroundInvisible() && player.IsStoryboardVisible); + AddUntilStep("Background is invisible, storyboard is visible", () => songSelect.IsBackgroundInvisible() && player.IsStoryboardVisible); AddStep("Disable Storyboard", () => { player.ReplacesBackground.Value = false; player.StoryboardEnabled.Value = false; }); - waitForDim(); - AddAssert("Background is visible, storyboard is invisible", () => songSelect.IsBackgroundVisible() && !player.IsStoryboardVisible); + AddUntilStep("Background is visible, storyboard is invisible", () => songSelect.IsBackgroundVisible() && !player.IsStoryboardVisible); } /// @@ -134,8 +129,7 @@ namespace osu.Game.Tests.Visual.Background performFullSetup(); createFakeStoryboard(); AddStep("Exit to song select", () => player.Exit()); - waitForDim(); - AddAssert("Background is visible", () => songSelect.IsBackgroundVisible()); + AddUntilStep("Background is visible", () => songSelect.IsBackgroundVisible()); } /// @@ -145,14 +139,11 @@ namespace osu.Game.Tests.Visual.Background public void DisableUserDimBackgroundTest() { performFullSetup(); - waitForDim(); - AddAssert("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied()); + AddUntilStep("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied()); AddStep("Enable user dim", () => songSelect.DimEnabled.Value = false); - waitForDim(); - AddAssert("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && songSelect.IsUserBlurDisabled()); + AddUntilStep("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && songSelect.IsUserBlurDisabled()); AddStep("Disable user dim", () => songSelect.DimEnabled.Value = true); - waitForDim(); - AddAssert("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied()); + AddUntilStep("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied()); } /// @@ -170,11 +161,9 @@ namespace osu.Game.Tests.Visual.Background }); AddStep("Enable user dim", () => player.DimmableStoryboard.EnableUserDim.Value = true); AddStep("Set dim level to 1", () => songSelect.DimLevel.Value = 1f); - waitForDim(); - AddAssert("Storyboard is invisible", () => !player.IsStoryboardVisible); + AddUntilStep("Storyboard is invisible", () => !player.IsStoryboardVisible); AddStep("Disable user dim", () => player.DimmableStoryboard.EnableUserDim.Value = false); - waitForDim(); - AddAssert("Storyboard is visible", () => player.IsStoryboardVisible); + AddUntilStep("Storyboard is visible", () => player.IsStoryboardVisible); } /// @@ -185,11 +174,9 @@ namespace osu.Game.Tests.Visual.Background { performFullSetup(true); AddStep("Pause", () => player.Pause()); - waitForDim(); - AddAssert("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied()); + AddUntilStep("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied()); AddStep("Unpause", () => player.Resume()); - waitForDim(); - AddAssert("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied()); + AddUntilStep("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied()); } /// @@ -203,8 +190,7 @@ namespace osu.Game.Tests.Visual.Background AddStep("Transition to Results", () => player.Push(results = new FadeAccessibleResults(new ScoreInfo { User = new User { Username = "osu!" } }))); AddUntilStep("Wait for results is current", () => results.IsCurrentScreen()); - waitForDim(); - AddAssert("Screen is undimmed, original background retained", () => + AddUntilStep("Screen is undimmed, original background retained", () => songSelect.IsBackgroundUndimmed() && songSelect.IsBackgroundCurrent() && results.IsBlurCorrect()); } @@ -216,8 +202,7 @@ namespace osu.Game.Tests.Visual.Background { performFullSetup(); AddStep("Exit to song select", () => player.Exit()); - waitForDim(); - AddAssert("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && songSelect.IsBlurCorrect()); + AddUntilStep("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && songSelect.IsBlurCorrect()); } /// @@ -229,15 +214,11 @@ namespace osu.Game.Tests.Visual.Background performFullSetup(); AddStep("Move mouse to Visual Settings", () => InputManager.MoveMouseTo(playerLoader.VisualSettingsPos)); AddStep("Resume PlayerLoader", () => player.Restart()); - waitForDim(); - AddAssert("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied()); + AddUntilStep("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied()); AddStep("Move mouse to center of screen", () => InputManager.MoveMouseTo(playerLoader.ScreenPos)); - waitForDim(); - AddAssert("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && playerLoader.IsBlurCorrect()); + AddUntilStep("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && playerLoader.IsBlurCorrect()); } - private void waitForDim() => AddWaitStep("Wait for dim", 5); - private void createFakeStoryboard() => AddStep("Create storyboard", () => { player.StoryboardEnabled.Value = false; From 2bf066d72c6099c47bd967a11424b35c13004500 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 May 2020 11:30:36 +0900 Subject: [PATCH 1784/2376] Rename tests to match convention --- .../Background/TestSceneUserDimBackgrounds.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs index edba462bc2..d601f40afe 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs @@ -68,7 +68,7 @@ namespace osu.Game.Tests.Visual.Background /// Check if properly triggers the visual settings preview when a user hovers over the visual settings panel. /// [Test] - public void PlayerLoaderSettingsHoverTest() + public void TestPlayerLoaderSettingsHover() { setupUserSettings(); AddStep("Start player loader", () => songSelect.Push(playerLoader = new TestPlayerLoader(player = new LoadBlockingTestPlayer { BlockLoad = true }))); @@ -90,7 +90,7 @@ namespace osu.Game.Tests.Visual.Background /// We need to check that in this scenario, the dim and blur is still properly applied after entering player. /// [Test] - public void PlayerLoaderTransitionTest() + public void TestPlayerLoaderTransition() { performFullSetup(); AddStep("Trigger hover event", () => playerLoader.TriggerOnHover()); @@ -102,7 +102,7 @@ namespace osu.Game.Tests.Visual.Background /// Make sure the background is fully invisible (Alpha == 0) when the background should be disabled by the storyboard. /// [Test] - public void StoryboardBackgroundVisibilityTest() + public void TestStoryboardBackgroundVisibility() { performFullSetup(); createFakeStoryboard(); @@ -124,7 +124,7 @@ namespace osu.Game.Tests.Visual.Background /// When exiting player, the screen that it suspends/exits to needs to have a fully visible (Alpha == 1) background. /// [Test] - public void StoryboardTransitionTest() + public void TestStoryboardTransition() { performFullSetup(); createFakeStoryboard(); @@ -136,7 +136,7 @@ namespace osu.Game.Tests.Visual.Background /// Ensure is properly accepting user-defined visual changes for a background. /// [Test] - public void DisableUserDimBackgroundTest() + public void TestDisableUserDimBackground() { performFullSetup(); AddUntilStep("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied()); @@ -150,7 +150,7 @@ namespace osu.Game.Tests.Visual.Background /// Ensure is properly accepting user-defined visual changes for a storyboard. /// [Test] - public void DisableUserDimStoryboardTest() + public void TestDisableUserDimStoryboard() { performFullSetup(); createFakeStoryboard(); @@ -170,7 +170,7 @@ namespace osu.Game.Tests.Visual.Background /// Check if the visual settings container retains dim and blur when pausing /// [Test] - public void PauseTest() + public void TestPause() { performFullSetup(true); AddStep("Pause", () => player.Pause()); @@ -183,7 +183,7 @@ namespace osu.Game.Tests.Visual.Background /// Check if the visual settings container removes user dim when suspending for /// [Test] - public void TransitionTest() + public void TestTransition() { performFullSetup(); FadeAccessibleResults results = null; @@ -198,7 +198,7 @@ namespace osu.Game.Tests.Visual.Background /// Check if background gets undimmed and unblurred when leaving for /// [Test] - public void TransitionOutTest() + public void TestTransitionOut() { performFullSetup(); AddStep("Exit to song select", () => player.Exit()); @@ -209,7 +209,7 @@ namespace osu.Game.Tests.Visual.Background /// Check if hovering on the visual settings dialogue after resuming from player still previews the background dim. /// [Test] - public void ResumeFromPlayerTest() + public void TestResumeFromPlayer() { performFullSetup(); AddStep("Move mouse to Visual Settings", () => InputManager.MoveMouseTo(playerLoader.VisualSettingsPos)); From d041de63ce3f312e22a1cb7a23cdc5a292c33da8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 May 2020 13:00:32 +0900 Subject: [PATCH 1785/2376] Allow SelectionHandler to provide custom context menu items without local hover check --- .../Compose/Components/SelectionHandler.cs | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 764eae1056..9ecda9fdb8 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -244,14 +244,21 @@ namespace osu.Game.Screens.Edit.Compose.Components #region Context Menu - public virtual MenuItem[] ContextMenuItems + public MenuItem[] ContextMenuItems { get { if (!selectedBlueprints.Any(b => b.IsHovered)) return Array.Empty(); - var items = new List + var items = new List(); + + items.AddRange(GetContextMenuItemsForSelection(selectedBlueprints)); + + if (selectedBlueprints.Count == 1) + items.AddRange(selectedBlueprints[0].ContextMenuItems); + + items.AddRange(new[] { new OsuMenuItem("Sound") { @@ -263,15 +270,20 @@ namespace osu.Game.Screens.Edit.Compose.Components } }, new OsuMenuItem("Delete", MenuItemType.Destructive, deleteSelected), - }; - - if (selectedBlueprints.Count == 1) - items.AddRange(selectedBlueprints[0].ContextMenuItems); + }); return items.ToArray(); } } + /// + /// Provide context menu items relevant to current selection. Calling base is not required. + /// + /// The current selection. + /// The relevant menu items. + protected virtual IEnumerable GetContextMenuItemsForSelection(IEnumerable selection) + => Enumerable.Empty(); + private MenuItem createHitSampleMenuItem(string name, string sampleName) { return new TernaryStateMenuItem(name, MenuItemType.Standard, setHitSampleState) From aaf5596f9c0f619fc9eb63ab046f21a03885bb45 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 26 May 2020 15:54:07 +0900 Subject: [PATCH 1786/2376] Cleanup test --- .../TestSceneContractedPanelMiddleContent.cs | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs index 972ac26b84..76cfe75b59 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -15,7 +14,6 @@ using osu.Game.Rulesets.Osu; using osu.Game.Scoring; using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking.Contracted; -using osu.Game.Tests.Beatmaps; using osuTK; namespace osu.Game.Tests.Visual.Ranking @@ -28,7 +26,7 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestShowPanel() { - AddStep("show example score", () => showPanel(createTestBeatmap(), new TestScoreInfo(new OsuRuleset().RulesetInfo))); + AddStep("show example score", () => showPanel(CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)), new TestScoreInfo(new OsuRuleset().RulesetInfo))); } private void showPanel(WorkingBeatmap workingBeatmap, ScoreInfo score) @@ -36,17 +34,6 @@ namespace osu.Game.Tests.Visual.Ranking Child = new ContractedPanelMiddleContentContainer(workingBeatmap, score); } - private WorkingBeatmap createTestBeatmap() - { - var beatmap = new TestBeatmap(rulesetStore.GetRuleset(0)); - beatmap.Metadata.Title = "Verrrrrrrrrrrrrrrrrrry looooooooooooooooooooooooong beatmap title"; - beatmap.Metadata.Artist = "Verrrrrrrrrrrrrrrrrrry looooooooooooooooooooooooong beatmap artist"; - - return new TestWorkingBeatmap(beatmap); - } - - private bool containsAny(string text, params string[] stringsToMatch) => stringsToMatch.Any(text.Contains); - private class ContractedPanelMiddleContentContainer : Container { [Cached] From 1768cbd1319ff8a6ab1da72846c2ec4c0a2ab65d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 26 May 2020 15:56:56 +0900 Subject: [PATCH 1787/2376] Format score same as expanded panel --- .../Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs index a263a03a77..8cd0e7025e 100644 --- a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs @@ -160,7 +160,7 @@ namespace osu.Game.Screens.Ranking.Contracted { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Text = score.TotalScore.ToString(), + Text = score.TotalScore.ToString("N0"), Font = OsuFont.GetFont(size: 20, weight: FontWeight.Medium, fixedWidth: true), Spacing = new Vector2(-1, 0) }, From 906a317a3d35151baae2339a77671fbd82f34231 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 26 May 2020 16:26:53 +0900 Subject: [PATCH 1788/2376] Reduce casting --- osu.Game/Screens/Ranking/ScorePanelList.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index 97a132b9ff..df2c66203b 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -118,11 +118,11 @@ namespace osu.Game.Screens.Ranking { public override IEnumerable FlowingChildren => applySorting(AliveInternalChildren); - public int GetPanelIndex(ScoreInfo score) => applySorting(Children).OfType().TakeWhile(s => s.Score != score).Count(); + public int GetPanelIndex(ScoreInfo score) => applySorting(Children).TakeWhile(s => s.Score != score).Count(); - private IEnumerable applySorting(IEnumerable drawables) => drawables.OfType() - .OrderByDescending(s => s.Score.TotalScore) - .ThenBy(s => s.Score.OnlineScoreID); + private IEnumerable applySorting(IEnumerable drawables) => drawables.OfType() + .OrderByDescending(s => s.Score.TotalScore) + .ThenBy(s => s.Score.OnlineScoreID); } private class Scroll : OsuScrollContainer From a1ece4f308d51317b5d2c6d25df1555a434f9f00 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 26 May 2020 16:26:58 +0900 Subject: [PATCH 1789/2376] Add expansion/contraction test --- .../Visual/Ranking/TestSceneScorePanel.cs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs index fdb77c14a3..250fdc5ebd 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs @@ -12,6 +12,8 @@ namespace osu.Game.Tests.Visual.Ranking { public class TestSceneScorePanel : OsuTestScene { + private ScorePanel panel; + [Test] public void TestDRank() { @@ -84,9 +86,24 @@ namespace osu.Game.Tests.Visual.Ranking addPanelStep(score, PanelState.Contracted); } + [Test] + public void TestExpandAndContract() + { + var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.925, Rank = ScoreRank.A }; + + addPanelStep(score, PanelState.Contracted); + AddWaitStep("wait for transition", 10); + + AddStep("expand panel", () => panel.State = PanelState.Expanded); + AddWaitStep("wait for transition", 10); + + AddStep("contract panel", () => panel.State = PanelState.Contracted); + AddWaitStep("wait for transition", 10); + } + private void addPanelStep(ScoreInfo score, PanelState state = PanelState.Expanded) => AddStep("add panel", () => { - Child = new ScorePanel(score) + Child = panel = new ScorePanel(score) { Anchor = Anchor.Centre, Origin = Anchor.Centre, From c86a003ef94eccc990c6e13f7d0ea7159de03ec6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 26 May 2020 16:27:41 +0900 Subject: [PATCH 1790/2376] Adjust transition for smaller sizes --- osu.Game/Screens/Ranking/ScorePanel.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/Ranking/ScorePanel.cs b/osu.Game/Screens/Ranking/ScorePanel.cs index 2933bbddd1..a99b48e8f0 100644 --- a/osu.Game/Screens/Ranking/ScorePanel.cs +++ b/osu.Game/Screens/Ranking/ScorePanel.cs @@ -102,12 +102,14 @@ namespace osu.Game.Screens.Ranking { Anchor = Anchor.Centre, Origin = Anchor.Centre, + Size = new Vector2(40), Children = new Drawable[] { topLayerContainer = new Container { Name = "Top layer", RelativeSizeAxes = Axes.X, + Alpha = 0, Height = 120, Children = new Drawable[] { @@ -214,6 +216,8 @@ namespace osu.Game.Screens.Ranking // If the top layer was already expanded, then we don't need to wait for the resize and can instead transform immediately. This looks better when changing the panel state. using (BeginDelayedSequence(topLayerExpanded ? 0 : resize_duration + top_layer_expand_delay, true)) { + topLayerContainer.FadeIn(); + switch (state) { case PanelState.Expanded: From de0b6ec9f1941c64e19ab486748484351871074b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 26 May 2020 17:00:41 +0900 Subject: [PATCH 1791/2376] Create abstract implementation --- osu.Game/Screens/Play/Player.cs | 2 +- osu.Game/Screens/Play/ReplayPlayer.cs | 2 +- osu.Game/Screens/Ranking/ResultsScreen.cs | 27 ++++++++-------- osu.Game/Screens/Ranking/SoloResultsScreen.cs | 32 +++++++++++++++++++ osu.Game/Screens/Select/PlaySongSelect.cs | 2 +- 5 files changed, 49 insertions(+), 16 deletions(-) create mode 100644 osu.Game/Screens/Ranking/SoloResultsScreen.cs diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 3d4b20bec4..36198bcc65 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -479,7 +479,7 @@ namespace osu.Game.Screens.Play protected override bool OnScroll(ScrollEvent e) => mouseWheelDisabled.Value && !GameplayClockContainer.IsPaused.Value; - protected virtual ResultsScreen CreateResults(ScoreInfo score) => new ResultsScreen(score); + protected virtual ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score); #region Fail Logic diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index f0c76163f1..b443603128 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -30,7 +30,7 @@ namespace osu.Game.Screens.Play this.Push(CreateResults(DrawableRuleset.ReplayScore.ScoreInfo)); } - protected override ResultsScreen CreateResults(ScoreInfo score) => new ResultsScreen(score, false); + protected override ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, false); protected override ScoreInfo CreateScore() => score.ScoreInfo; } diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 133bdcca4a..8db9cdc547 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -2,7 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Linq; +using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -12,8 +12,6 @@ using osu.Framework.Screens; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; -using osu.Game.Online.API.Requests; -using osu.Game.Rulesets; using osu.Game.Scoring; using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Play; @@ -21,7 +19,7 @@ using osuTK; namespace osu.Game.Screens.Ranking { - public class ResultsScreen : OsuScreen + public abstract class ResultsScreen : OsuScreen { protected const float BACKGROUND_BLUR = 20; @@ -38,9 +36,6 @@ namespace osu.Game.Screens.Ranking [Resolved] private IAPIProvider api { get; set; } - [Resolved] - private RulesetStore rulesets { get; set; } - public readonly ScoreInfo Score; private readonly bool allowRetry; @@ -133,22 +128,28 @@ namespace osu.Game.Screens.Ranking { base.LoadComplete(); - var req = new GetScoresRequest(Score.Beatmap, Score.Ruleset); - - req.Success += r => Schedule(() => + var req = FetchScores(scores => Schedule(() => { - foreach (var s in r.Scores.Select(s => s.CreateScoreInfo(rulesets))) + foreach (var s in scores) { if (s.OnlineScoreID == Score.OnlineScoreID) continue; panels.AddScore(s); } - }); + })); - api.Queue(req); + if (req != null) + api.Queue(req); } + /// + /// Performs a fetch/refresh of scores to be displayed. + /// + /// A callback which should be called when fetching is completed. Scheduling is not required. + /// An responsible for the fetch operation. This will be queued and performed automatically. + protected virtual APIRequest FetchScores(Action> scoresCallback) => null; + public override void OnEntering(IScreen last) { base.OnEntering(last); diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs new file mode 100644 index 0000000000..2b00748ed8 --- /dev/null +++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs @@ -0,0 +1,32 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Rulesets; +using osu.Game.Scoring; + +namespace osu.Game.Screens.Ranking +{ + public class SoloResultsScreen : ResultsScreen + { + [Resolved] + private RulesetStore rulesets { get; set; } + + public SoloResultsScreen(ScoreInfo score, bool allowRetry = true) + : base(score, allowRetry) + { + } + + protected override APIRequest FetchScores(Action> scoresCallback) + { + var req = new GetScoresRequest(Score.Beatmap, Score.Ruleset); + req.Success += r => scoresCallback?.Invoke(r.Scores.Select(s => s.CreateScoreInfo(rulesets))); + return req; + } + } +} diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index 21ddc5685d..0a4c0e2085 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -37,7 +37,7 @@ namespace osu.Game.Screens.Select } protected void PresentScore(ScoreInfo score) => - FinaliseSelection(score.Beatmap, score.Ruleset, () => this.Push(new ResultsScreen(score))); + FinaliseSelection(score.Beatmap, score.Ruleset, () => this.Push(new SoloResultsScreen(score))); protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); From 7e1e26de2a3b3bc8e249ba75b417e4d5955d7c42 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 May 2020 17:00:55 +0900 Subject: [PATCH 1792/2376] Allow HandleMovement by default --- .../Edit/Compose/Components/SelectionHandler.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 9ecda9fdb8..7ab6340e07 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -74,9 +74,16 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// Handles the selected s being moved. /// + /// + /// Just returning true is enough to allow updates to take place. + /// Custom implementation is only required if other attributes are to be considered, like changing columns. + /// /// The move event. - /// Whether any s were moved. - public virtual bool HandleMovement(MoveSelectionEvent moveEvent) => false; + /// + /// Whether any s could be moved. + /// Returning true will also propagate StartTime changes provided by the closest . + /// + public virtual bool HandleMovement(MoveSelectionEvent moveEvent) => true; public bool OnPressed(PlatformAction action) { From c07a33b24fc02d33a6dd15da10605d16bb958005 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 26 May 2020 17:31:50 +0900 Subject: [PATCH 1793/2376] Fix ctor accessibility --- osu.Game/Screens/Ranking/ResultsScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 8db9cdc547..97b56d22eb 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -43,7 +43,7 @@ namespace osu.Game.Screens.Ranking private Drawable bottomPanel; private ScorePanelList panels; - public ResultsScreen(ScoreInfo score, bool allowRetry = true) + protected ResultsScreen(ScoreInfo score, bool allowRetry = true) { Score = score; this.allowRetry = allowRetry; From 6b5b2152991f3ba617b42f274fee646f37753ab7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 May 2020 17:44:47 +0900 Subject: [PATCH 1794/2376] Split out IHasPath from IHasCurve to better define hitobjects --- .../Beatmaps/CatchBeatmapConverter.cs | 2 +- .../Objects/JuiceStream.cs | 2 +- .../Legacy/DistanceObjectPatternGenerator.cs | 2 +- .../Beatmaps/OsuBeatmapConverter.cs | 2 +- osu.Game.Rulesets.Osu/Objects/Slider.cs | 2 +- .../Beatmaps/TaikoBeatmapConverter.cs | 2 +- osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs | 10 +---- .../Formats/LegacyBeatmapDecoderTest.cs | 2 +- .../Beatmaps/Formats/OsuJsonDecoderTest.cs | 2 +- .../Beatmaps/Formats/LegacyBeatmapEncoder.cs | 39 +++++++++++-------- .../Rulesets/Objects/Legacy/ConvertSlider.cs | 2 +- osu.Game/Rulesets/Objects/Types/IHasPath.cs | 13 +++++++ .../{IHasCurve.cs => IHasPathWithRepeats.cs} | 20 +++++----- 13 files changed, 57 insertions(+), 43 deletions(-) create mode 100644 osu.Game/Rulesets/Objects/Types/IHasPath.cs rename osu.Game/Rulesets/Objects/Types/{IHasCurve.cs => IHasPathWithRepeats.cs} (77%) diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs index 90a6e609f0..27a9b63e9a 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps switch (obj) { - case IHasCurve curveData: + case IHasPathWithRepeats curveData: return new JuiceStream { StartTime = obj.StartTime, diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index d32595c2e1..24090e233a 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -14,7 +14,7 @@ using osu.Game.Rulesets.Objects.Types; namespace osu.Game.Rulesets.Catch.Objects { - public class JuiceStream : CatchHitObject, IHasCurve + public class JuiceStream : CatchHitObject, IHasPathWithRepeats { /// /// Positional distance that results in a duration of one second, before any speed adjustments. diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs index d8d5b67c0e..1bd796511b 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs @@ -474,7 +474,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy /// private IList sampleInfoListAt(double time) { - if (!(HitObject is IHasCurve curveData)) + if (!(HitObject is IHasPathWithRepeats curveData)) return HitObject.Samples; double segmentTime = (EndTime - HitObject.StartTime) / spanCount; diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs index 147d74c929..060a3919bd 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs @@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps switch (original) { - case IHasCurve curveData: + case IHasPathWithRepeats curveData: return new Slider { StartTime = original.StartTime, diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 6ba0e1c6aa..713d1a61f8 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -17,7 +17,7 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects { - public class Slider : OsuHitObject, IHasCurve + public class Slider : OsuHitObject, IHasPathWithRepeats { public double EndTime { diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index d324441285..1a47be2282 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs @@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps if (!isForCurrentRuleset && tickSpacing > 0 && osuDuration < 2 * speedAdjustedBeatLength) { - List> allSamples = obj is IHasCurve curveData ? curveData.NodeSamples : new List>(new[] { samples }); + List> allSamples = obj is IHasPathWithRepeats curveData ? curveData.NodeSamples : new List>(new[] { samples }); int i = 0; diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs index 7b11bce520..5f52160be1 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs @@ -3,9 +3,7 @@ using osu.Game.Rulesets.Objects.Types; using System; -using System.Collections.Generic; using System.Threading; -using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Judgements; @@ -17,7 +15,7 @@ using osuTK; namespace osu.Game.Rulesets.Taiko.Objects { - public class DrumRoll : TaikoHitObject, IHasCurve + public class DrumRoll : TaikoHitObject, IHasPath { /// /// Drum roll distance that results in a duration of 1 speed-adjusted beat length. @@ -115,11 +113,7 @@ namespace osu.Game.Rulesets.Taiko.Objects double IHasDistance.Distance => Duration * Velocity; - int IHasRepeats.RepeatCount { get => 0; set { } } - - List> IHasRepeats.NodeSamples => new List>(); - - SliderPath IHasCurve.Path + SliderPath IHasPath.Path => new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(1) }, ((IHasDistance)this).Distance / TaikoBeatmapConverter.LEGACY_VELOCITY_MULTIPLIER); #endregion diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index acb30a6277..dab923d75b 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -365,7 +365,7 @@ namespace osu.Game.Tests.Beatmaps.Formats { var hitObjects = decoder.Decode(stream).HitObjects; - var curveData = hitObjects[0] as IHasCurve; + var curveData = hitObjects[0] as IHasPathWithRepeats; var positionData = hitObjects[0] as IHasPosition; Assert.IsNotNull(positionData); diff --git a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs index b034e66616..b4c78ce273 100644 --- a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs @@ -95,7 +95,7 @@ namespace osu.Game.Tests.Beatmaps.Formats { var beatmap = decodeAsJson(normal); - var curveData = beatmap.HitObjects[0] as IHasCurve; + var curveData = beatmap.HitObjects[0] as IHasPathWithRepeats; var positionData = beatmap.HitObjects[0] as IHasPosition; Assert.IsNotNull(positionData); diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 7727f25967..d7e83fa471 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -233,9 +233,9 @@ namespace osu.Game.Beatmaps.Formats writer.Write(FormattableString.Invariant($"{(int)getObjectType(hitObject)},")); writer.Write(FormattableString.Invariant($"{(int)toLegacyHitSoundType(hitObject.Samples)},")); - if (hitObject is IHasCurve curveData) + if (hitObject is IHasPathWithRepeats curveData) { - addCurveData(writer, curveData, position); + addPathData(writer, curveData, position); writer.Write(getSampleBank(hitObject.Samples, zeroBanks: true)); } else @@ -263,7 +263,7 @@ namespace osu.Game.Beatmaps.Formats switch (hitObject) { - case IHasCurve _: + case IHasPath _: type |= LegacyHitObjectType.Slider; break; @@ -282,13 +282,13 @@ namespace osu.Game.Beatmaps.Formats return type; } - private void addCurveData(TextWriter writer, IHasCurve curveData, Vector2 position) + private void addPathData(TextWriter writer, IHasPath pathData, Vector2 position) { PathType? lastType = null; - for (int i = 0; i < curveData.Path.ControlPoints.Count; i++) + for (int i = 0; i < pathData.Path.ControlPoints.Count; i++) { - PathControlPoint point = curveData.Path.ControlPoints[i]; + PathControlPoint point = pathData.Path.ControlPoints[i]; if (point.Type.Value != null) { @@ -325,23 +325,28 @@ namespace osu.Game.Beatmaps.Formats if (i != 0) { writer.Write(FormattableString.Invariant($"{position.X + point.Position.Value.X}:{position.Y + point.Position.Value.Y}")); - writer.Write(i != curveData.Path.ControlPoints.Count - 1 ? "|" : ","); + writer.Write(i != pathData.Path.ControlPoints.Count - 1 ? "|" : ","); } } - writer.Write(FormattableString.Invariant($"{curveData.RepeatCount + 1},")); - writer.Write(FormattableString.Invariant($"{curveData.Path.Distance},")); + var curveData = pathData as IHasPathWithRepeats; - for (int i = 0; i < curveData.NodeSamples.Count; i++) - { - writer.Write(FormattableString.Invariant($"{(int)toLegacyHitSoundType(curveData.NodeSamples[i])}")); - writer.Write(i != curveData.NodeSamples.Count - 1 ? "|" : ","); - } + writer.Write(FormattableString.Invariant($"{(curveData?.RepeatCount ?? 0) + 1},")); + writer.Write(FormattableString.Invariant($"{pathData.Path.Distance},")); - for (int i = 0; i < curveData.NodeSamples.Count; i++) + if (curveData != null) { - writer.Write(getSampleBank(curveData.NodeSamples[i], true)); - writer.Write(i != curveData.NodeSamples.Count - 1 ? "|" : ","); + for (int i = 0; i < curveData.NodeSamples.Count; i++) + { + writer.Write(FormattableString.Invariant($"{(int)toLegacyHitSoundType(curveData.NodeSamples[i])}")); + writer.Write(i != curveData.NodeSamples.Count - 1 ? "|" : ","); + } + + for (int i = 0; i < curveData.NodeSamples.Count; i++) + { + writer.Write(getSampleBank(curveData.NodeSamples[i], true)); + writer.Write(i != curveData.NodeSamples.Count - 1 ? "|" : ","); + } } } diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs index 924182b265..73192dc42e 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs @@ -9,7 +9,7 @@ using osu.Game.Beatmaps.ControlPoints; namespace osu.Game.Rulesets.Objects.Legacy { - internal abstract class ConvertSlider : ConvertHitObject, IHasCurve, IHasLegacyLastTickOffset + internal abstract class ConvertSlider : ConvertHitObject, IHasPathWithRepeats, IHasLegacyLastTickOffset { /// /// Scoring distance with a speed-adjusted beat length of 1 second. diff --git a/osu.Game/Rulesets/Objects/Types/IHasPath.cs b/osu.Game/Rulesets/Objects/Types/IHasPath.cs new file mode 100644 index 0000000000..567c24a4a2 --- /dev/null +++ b/osu.Game/Rulesets/Objects/Types/IHasPath.cs @@ -0,0 +1,13 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Rulesets.Objects.Types +{ + public interface IHasPath : IHasDistance + { + /// + /// The curve. + /// + SliderPath Path { get; } + } +} diff --git a/osu.Game/Rulesets/Objects/Types/IHasCurve.cs b/osu.Game/Rulesets/Objects/Types/IHasPathWithRepeats.cs similarity index 77% rename from osu.Game/Rulesets/Objects/Types/IHasCurve.cs rename to osu.Game/Rulesets/Objects/Types/IHasPathWithRepeats.cs index e98a888bd7..fba0fd7aff 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasCurve.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasPathWithRepeats.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osuTK; namespace osu.Game.Rulesets.Objects.Types @@ -8,15 +9,16 @@ namespace osu.Game.Rulesets.Objects.Types /// /// A HitObject that has a curve. /// - public interface IHasCurve : IHasDistance, IHasRepeats + public interface IHasPathWithRepeats : IHasPath, IHasRepeats { - /// - /// The curve. - /// - SliderPath Path { get; } } - public static class HasCurveExtensions + [Obsolete("Use IHasPathWithRepeats instead.")] // can be removed 20201126 + public interface IHasCurve : IHasPathWithRepeats + { + } + + public static class HasPathWithRepeatsExtensions { /// /// Computes the position on the curve relative to how much of the has been completed. @@ -24,7 +26,7 @@ namespace osu.Game.Rulesets.Objects.Types /// The curve. /// [0, 1] where 0 is the start time of the and 1 is the end time of the . /// The position on the curve. - public static Vector2 CurvePositionAt(this IHasCurve obj, double progress) + public static Vector2 CurvePositionAt(this IHasPathWithRepeats obj, double progress) => obj.Path.PositionAt(obj.ProgressAt(progress)); /// @@ -33,7 +35,7 @@ namespace osu.Game.Rulesets.Objects.Types /// The curve. /// [0, 1] where 0 is the start time of the and 1 is the end time of the . /// [0, 1] where 0 is the beginning of the curve and 1 is the end of the curve. - public static double ProgressAt(this IHasCurve obj, double progress) + public static double ProgressAt(this IHasPathWithRepeats obj, double progress) { double p = progress * obj.SpanCount() % 1; if (obj.SpanAt(progress) % 2 == 1) @@ -47,7 +49,7 @@ namespace osu.Game.Rulesets.Objects.Types /// The curve. /// [0, 1] where 0 is the beginning of the curve and 1 is the end of the curve. /// [0, SpanCount) where 0 is the first run. - public static int SpanAt(this IHasCurve obj, double progress) + public static int SpanAt(this IHasPathWithRepeats obj, double progress) => (int)(progress * obj.SpanCount()); } } From b8e0a6f12725e464f179f03e53dd9f20625cd635 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 May 2020 12:37:44 +0900 Subject: [PATCH 1795/2376] Move sett from EndTime to Duration --- osu.Game.Rulesets.Catch/Objects/JuiceStream.cs | 10 +++++----- osu.Game.Rulesets.Osu/Objects/Slider.cs | 8 ++++---- .../Objects/Drawables/DrawableSwell.cs | 2 +- .../Gameplay/TestSceneDrawableScrollingRuleset.cs | 6 +++--- .../Objects/Legacy/Catch/ConvertHitObjectParser.cs | 6 +++--- .../Objects/Legacy/Catch/ConvertSpinner.cs | 4 ++-- .../Objects/Legacy/ConvertHitObjectParser.cs | 14 +++++++------- osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs | 6 +++--- .../Objects/Legacy/Mania/ConvertHitObjectParser.cs | 8 ++++---- .../Rulesets/Objects/Legacy/Mania/ConvertHold.cs | 4 ++-- .../Objects/Legacy/Mania/ConvertSpinner.cs | 4 ++-- .../Objects/Legacy/Osu/ConvertHitObjectParser.cs | 6 +++--- .../Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs | 4 ++-- .../Objects/Legacy/Taiko/ConvertHitObjectParser.cs | 6 +++--- .../Objects/Legacy/Taiko/ConvertSpinner.cs | 4 ++-- osu.Game/Rulesets/Objects/Types/IHasEndTime.cs | 6 +++--- .../Timeline/TimelineHitObjectBlueprint.cs | 2 +- 17 files changed, 50 insertions(+), 50 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index d32595c2e1..7c8d57e689 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -115,15 +115,15 @@ namespace osu.Game.Rulesets.Catch.Objects } } - public double EndTime + public float EndX => X + this.CurvePositionAt(1).X / CatchPlayfield.BASE_WIDTH; + + public double Duration { - get => StartTime + this.SpanCount() * Path.Distance / Velocity; + get => this.SpanCount() * Path.Distance / Velocity; set => throw new System.NotSupportedException($"Adjust via {nameof(RepeatCount)} instead"); // can be implemented if/when needed. } - public float EndX => X + this.CurvePositionAt(1).X / CatchPlayfield.BASE_WIDTH; - - public double Duration => EndTime - StartTime; + public double EndTime => StartTime + Duration; private readonly SliderPath path = new SliderPath(); diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 6ba0e1c6aa..984a9bf956 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -19,14 +19,14 @@ namespace osu.Game.Rulesets.Osu.Objects { public class Slider : OsuHitObject, IHasCurve { - public double EndTime + public double EndTime => StartTime + this.SpanCount() * Path.Distance / Velocity; + + public double Duration { - get => StartTime + this.SpanCount() * Path.Distance / Velocity; + get => EndTime - StartTime; set => throw new System.NotSupportedException($"Adjust via {nameof(RepeatCount)} instead"); // can be implemented if/when needed. } - public double Duration => EndTime - StartTime; - private readonly Cached endPositionCache = new Cached(); public override Vector2 EndPosition => endPositionCache.IsValid ? endPositionCache.Value : endPositionCache.Value = Position + this.CurvePositionAt(1); diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 32f7acadc8..7294587b10 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -237,7 +237,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables case ArmedState.Miss: case ArmedState.Hit: - using (BeginAbsoluteSequence(Time.Current, true)) + using (BeginDelayedSequence(HitObject.Duration, true)) { this.FadeOut(transition_duration, Easing.Out); bodyContainer.ScaleTo(1.4f, transition_duration); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs index b25b81c9af..08fd849fa6 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs @@ -281,7 +281,7 @@ namespace osu.Game.Tests.Visual.Gameplay yield return new TestHitObject { StartTime = original.StartTime, - EndTime = (original as IHasEndTime)?.EndTime ?? (original.StartTime + 100) + Duration = (original as IHasEndTime)?.Duration ?? 100 }; } } @@ -292,9 +292,9 @@ namespace osu.Game.Tests.Visual.Gameplay private class TestHitObject : ConvertHitObject, IHasEndTime { - public double EndTime { get; set; } + public double EndTime => StartTime + Duration; - public double Duration => EndTime - StartTime; + public double Duration { get; set; } } private class DrawableTestHitObject : DrawableHitObject diff --git a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs index 43e8d01297..c10c8dc30f 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs @@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch }; } - protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double endTime) + protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double duration) { // Convert spinners don't create the new combo themselves, but force the next non-spinner hitobject to create a new combo // Their combo offset is still added to that next hitobject's combo index @@ -65,11 +65,11 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch return new ConvertSpinner { - EndTime = endTime + Duration = duration }; } - protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double endTime) + protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double duration) { return null; } diff --git a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs index 9de311c9d7..4b0270064a 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs @@ -10,9 +10,9 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch /// internal sealed class ConvertSpinner : ConvertHitObject, IHasEndTime, IHasXPosition, IHasCombo { - public double EndTime { get; set; } + public double EndTime => StartTime + Duration; - public double Duration => EndTime - StartTime; + public double Duration { get; set; } public float X => 256; // Required for CatchBeatmapConverter diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 9a60a0a75c..d8d90fddfa 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -189,9 +189,9 @@ namespace osu.Game.Rulesets.Objects.Legacy } else if (type.HasFlag(LegacyHitObjectType.Spinner)) { - double endTime = Math.Max(startTime, Parsing.ParseDouble(split[5]) + Offset); + double duration = Math.Max(0, Parsing.ParseDouble(split[5]) + Offset - startTime); - result = CreateSpinner(new Vector2(512, 384) / 2, combo, comboOffset, endTime); + result = CreateSpinner(new Vector2(512, 384) / 2, combo, comboOffset, duration); if (split.Length > 6) readCustomSampleBanks(split[6], bankInfo); @@ -209,7 +209,7 @@ namespace osu.Game.Rulesets.Objects.Legacy readCustomSampleBanks(string.Join(":", ss.Skip(1)), bankInfo); } - result = CreateHold(pos, combo, comboOffset, endTime + Offset); + result = CreateHold(pos, combo, comboOffset, endTime + Offset - startTime); } if (result == null) @@ -321,9 +321,9 @@ namespace osu.Game.Rulesets.Objects.Legacy /// The position of the hit object. /// Whether the hit object creates a new combo. /// When starting a new combo, the offset of the new combo relative to the current one. - /// The spinner end time. + /// The spinner duration. /// The hit object. - protected abstract HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double endTime); + protected abstract HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double duration); /// /// Creates a legacy Hold-type hit object. @@ -331,8 +331,8 @@ namespace osu.Game.Rulesets.Objects.Legacy /// The position of the hit object. /// Whether the hit object creates a new combo. /// When starting a new combo, the offset of the new combo relative to the current one. - /// The hold end time. - protected abstract HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double endTime); + /// The hold end time. + protected abstract HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double duration); private List convertSoundType(LegacyHitSoundType type, SampleBankInfo bankInfo) { diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs index 924182b265..0a3c1b40fd 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs @@ -26,13 +26,13 @@ namespace osu.Game.Rulesets.Objects.Legacy public List> NodeSamples { get; set; } public int RepeatCount { get; set; } - public double EndTime + public double Duration { - get => StartTime + this.SpanCount() * Distance / Velocity; + get => this.SpanCount() * Distance / Velocity; set => throw new System.NotSupportedException($"Adjust via {nameof(RepeatCount)} instead"); // can be implemented if/when needed. } - public double Duration => EndTime - StartTime; + public double EndTime => StartTime + Duration; public double Velocity = 1; diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs index f94c4aaa75..bc64518f40 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs @@ -37,21 +37,21 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania }; } - protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double endTime) + protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double duration) { return new ConvertSpinner { X = position.X, - EndTime = endTime + Duration = duration }; } - protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double endTime) + protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double duration) { return new ConvertHold { X = position.X, - EndTime = endTime + Duration = duration }; } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs index 1d92d638dd..dcb66163e4 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs @@ -9,8 +9,8 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania { public float X { get; set; } - public double EndTime { get; set; } + public double Duration { get; set; } - public double Duration => EndTime - StartTime; + public double EndTime => StartTime + Duration; } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSpinner.cs index 7dc13e27cd..b731f7c8d8 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSpinner.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSpinner.cs @@ -10,9 +10,9 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania /// internal sealed class ConvertSpinner : ConvertHitObject, IHasEndTime, IHasXPosition { - public double EndTime { get; set; } + public double Duration { get; set; } - public double Duration => EndTime - StartTime; + public double EndTime => StartTime + Duration; public float X { get; set; } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs index b95ec703b6..75ecab0b8f 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs @@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu }; } - protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double endTime) + protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double duration) { // Convert spinners don't create the new combo themselves, but force the next non-spinner hitobject to create a new combo // Their combo offset is still added to that next hitobject's combo index @@ -66,11 +66,11 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu return new ConvertSpinner { Position = position, - EndTime = endTime + Duration = duration }; } - protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double endTime) + protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double duration) { return null; } diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs index 8b21aab411..a231237077 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs @@ -11,9 +11,9 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu /// internal sealed class ConvertSpinner : ConvertHitObject, IHasEndTime, IHasPosition, IHasCombo { - public double EndTime { get; set; } + public double Duration { get; set; } - public double Duration => EndTime - StartTime; + public double EndTime => StartTime + Duration; public Vector2 Position { get; set; } diff --git a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitObjectParser.cs index db65a61c90..13e3e84c6a 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitObjectParser.cs @@ -33,15 +33,15 @@ namespace osu.Game.Rulesets.Objects.Legacy.Taiko }; } - protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double endTime) + protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double duration) { return new ConvertSpinner { - EndTime = endTime + Duration = duration }; } - protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double endTime) + protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double duration) { return null; } diff --git a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSpinner.cs index 8e28487f2f..0976106ec4 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSpinner.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSpinner.cs @@ -10,8 +10,8 @@ namespace osu.Game.Rulesets.Objects.Legacy.Taiko /// internal sealed class ConvertSpinner : ConvertHitObject, IHasEndTime { - public double EndTime { get; set; } + public double Duration { get; set; } - public double Duration => EndTime - StartTime; + public double EndTime => StartTime + Duration; } } diff --git a/osu.Game/Rulesets/Objects/Types/IHasEndTime.cs b/osu.Game/Rulesets/Objects/Types/IHasEndTime.cs index bc7103c60d..5eb551e15c 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasEndTime.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasEndTime.cs @@ -13,12 +13,12 @@ namespace osu.Game.Rulesets.Objects.Types /// /// The time at which the HitObject ends. /// - [JsonIgnore] - double EndTime { get; set; } + double EndTime { get; } /// /// The duration of the HitObject. /// - double Duration { get; } + [JsonIgnore] + double Duration { get; set; } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index dd2f7a833e..d6fc17f358 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -296,7 +296,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (endTimeHitObject.EndTime == snappedTime) return; - endTimeHitObject.EndTime = snappedTime; + endTimeHitObject.Duration = snappedTime - hitObject.StartTime; break; } From cbd563e80b3b82aecf9b84e474197d9c12993b16 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 May 2020 12:38:39 +0900 Subject: [PATCH 1796/2376] Rename to IHasDuration --- .../Beatmaps/CatchBeatmapConverter.cs | 2 +- .../Objects/BananaShower.cs | 2 +- .../TestSceneNotes.cs | 2 +- .../Beatmaps/ManiaBeatmapConverter.cs | 6 ++--- .../Legacy/EndTimeObjectPatternGenerator.cs | 2 +- osu.Game.Rulesets.Mania/Objects/HoldNote.cs | 2 +- .../Beatmaps/OsuBeatmapConverter.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs | 2 +- osu.Game.Rulesets.Osu/Objects/Spinner.cs | 2 +- .../Beatmaps/TaikoBeatmapConverter.cs | 2 +- osu.Game.Rulesets.Taiko/Objects/Swell.cs | 2 +- .../TestSceneDrawableScrollingRuleset.cs | 4 ++-- .../Beatmaps/Formats/LegacyBeatmapEncoder.cs | 6 ++--- osu.Game/Beatmaps/WorkingBeatmap.cs | 2 +- osu.Game/Rulesets/Objects/HitObject.cs | 4 ++-- .../Objects/Legacy/Catch/ConvertSpinner.cs | 2 +- .../Objects/Legacy/Mania/ConvertHold.cs | 2 +- .../Objects/Legacy/Mania/ConvertSpinner.cs | 2 +- .../Objects/Legacy/Osu/ConvertSpinner.cs | 2 +- .../Objects/Legacy/Taiko/ConvertSpinner.cs | 2 +- .../Rulesets/Objects/Types/IHasDistance.cs | 2 +- .../Rulesets/Objects/Types/IHasDuration.cs | 24 +++++++++++++++++++ .../Rulesets/Objects/Types/IHasEndTime.cs | 20 ++++------------ .../Rulesets/Objects/Types/IHasRepeats.cs | 2 +- .../Scrolling/ScrollingHitObjectContainer.cs | 2 +- .../Timeline/TimelineHitObjectBlueprint.cs | 4 ++-- 27 files changed, 60 insertions(+), 48 deletions(-) create mode 100644 osu.Game/Rulesets/Objects/Types/IHasDuration.cs diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs index 90a6e609f0..0b370cf911 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps LegacyLastTickOffset = (obj as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0 }.Yield(); - case IHasEndTime endTime: + case IHasDuration endTime: return new BananaShower { StartTime = obj.StartTime, diff --git a/osu.Game.Rulesets.Catch/Objects/BananaShower.cs b/osu.Game.Rulesets.Catch/Objects/BananaShower.cs index 3a0b5ace53..04a995c77e 100644 --- a/osu.Game.Rulesets.Catch/Objects/BananaShower.cs +++ b/osu.Game.Rulesets.Catch/Objects/BananaShower.cs @@ -7,7 +7,7 @@ using osu.Game.Rulesets.Objects.Types; namespace osu.Game.Rulesets.Catch.Objects { - public class BananaShower : CatchHitObject, IHasEndTime + public class BananaShower : CatchHitObject, IHasDuration { public override FruitVisualRepresentation VisualRepresentation => FruitVisualRepresentation.Banana; diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs index ea6a1e2e6a..dd5fd93710 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs @@ -156,7 +156,7 @@ namespace osu.Game.Rulesets.Mania.Tests foreach (var obj in content.OfType()) { - if (!(obj.HitObject is IHasEndTime endTime)) + if (!(obj.HitObject is IHasDuration endTime)) continue; foreach (var nested in obj.NestedHitObjects) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index 1c8116754f..32abf5e7f9 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -54,7 +54,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps } else { - float percentSliderOrSpinner = (float)beatmap.HitObjects.Count(h => h is IHasEndTime) / beatmap.HitObjects.Count; + float percentSliderOrSpinner = (float)beatmap.HitObjects.Count(h => h is IHasDuration) / beatmap.HitObjects.Count; if (percentSliderOrSpinner < 0.2) TargetColumns = 7; else if (percentSliderOrSpinner < 0.3 || roundedCircleSize >= 5) @@ -175,7 +175,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps break; } - case IHasEndTime endTimeData: + case IHasDuration endTimeData: { conversion = new EndTimeObjectPatternGenerator(Random, original, beatmap, originalBeatmap); @@ -231,7 +231,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps var pattern = new Pattern(); - if (HitObject is IHasEndTime endTimeData) + if (HitObject is IHasDuration endTimeData) { pattern.Add(new HoldNote { diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs index 907bed0d65..d5286a3779 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy public EndTimeObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, IBeatmap originalBeatmap) : base(random, hitObject, beatmap, new Pattern(), originalBeatmap) { - endTime = (HitObject as IHasEndTime)?.EndTime ?? 0; + endTime = (HitObject as IHasDuration)?.EndTime ?? 0; } public override IEnumerable Generate() diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs index e6f722a8a9..a100c9a58e 100644 --- a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Mania.Objects /// /// Represents a hit object which requires pressing, holding, and releasing a key. /// - public class HoldNote : ManiaHitObject, IHasEndTime + public class HoldNote : ManiaHitObject, IHasDuration { public double EndTime { diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs index 147d74c929..3490d96b61 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs @@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / beatmap.ControlPointInfo.DifficultyPointAt(original.StartTime).SpeedMultiplier : 1 }.Yield(); - case IHasEndTime endTimeData: + case IHasDuration endTimeData: return new Spinner { StartTime = original.StartTime, diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs index 7b1941b7f9..5d191119b9 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs @@ -70,7 +70,7 @@ namespace osu.Game.Rulesets.Osu.Mods break; // already hit or beyond the hittable end time. - if (h.IsHit || (h.HitObject is IHasEndTime hasEnd && time > hasEnd.EndTime)) + if (h.IsHit || (h.HitObject is IHasDuration hasEnd && time > hasEnd.EndTime)) continue; switch (h) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs b/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs index 297a0fea79..3cad52faeb 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs @@ -61,7 +61,7 @@ namespace osu.Game.Rulesets.Osu.Mods } // Keep wiggling sliders and spinners for their duration - if (!(osuObject is IHasEndTime endTime)) + if (!(osuObject is IHasDuration endTime)) return; amountWiggles = (int)(endTime.Duration / wiggle_duration); diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs index 0b8d03d118..418375c090 100644 --- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Spinner.cs @@ -11,7 +11,7 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects { - public class Spinner : OsuHitObject, IHasEndTime + public class Spinner : OsuHitObject, IHasDuration { public double EndTime { diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index d324441285..a4ec7211ca 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs @@ -150,7 +150,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps break; } - case IHasEndTime endTimeData: + case IHasDuration endTimeData: { double hitMultiplier = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty, 3, 5, 7.5) * swell_hit_multiplier; diff --git a/osu.Game.Rulesets.Taiko/Objects/Swell.cs b/osu.Game.Rulesets.Taiko/Objects/Swell.cs index 390f8d1f3b..8a63a89951 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Swell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Swell.cs @@ -10,7 +10,7 @@ using osu.Game.Rulesets.Taiko.Judgements; namespace osu.Game.Rulesets.Taiko.Objects { - public class Swell : TaikoHitObject, IHasEndTime + public class Swell : TaikoHitObject, IHasDuration { public double EndTime { diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs index 08fd849fa6..bd7e894cf8 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs @@ -281,7 +281,7 @@ namespace osu.Game.Tests.Visual.Gameplay yield return new TestHitObject { StartTime = original.StartTime, - Duration = (original as IHasEndTime)?.Duration ?? 100 + Duration = (original as IHasDuration)?.Duration ?? 100 }; } } @@ -290,7 +290,7 @@ namespace osu.Game.Tests.Visual.Gameplay #region HitObject - private class TestHitObject : ConvertHitObject, IHasEndTime + private class TestHitObject : ConvertHitObject, IHasDuration { public double EndTime => StartTime + Duration; diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 7727f25967..a8982238cf 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -240,7 +240,7 @@ namespace osu.Game.Beatmaps.Formats } else { - if (hitObject is IHasEndTime) + if (hitObject is IHasDuration) addEndTimeData(writer, hitObject); writer.Write(getSampleBank(hitObject.Samples)); @@ -267,7 +267,7 @@ namespace osu.Game.Beatmaps.Formats type |= LegacyHitObjectType.Slider; break; - case IHasEndTime _: + case IHasDuration _: if (beatmap.BeatmapInfo.RulesetID == 3) type |= LegacyHitObjectType.Hold; else @@ -347,7 +347,7 @@ namespace osu.Game.Beatmaps.Formats private void addEndTimeData(TextWriter writer, HitObject hitObject) { - var endTimeData = (IHasEndTime)hitObject; + var endTimeData = (IHasDuration)hitObject; var type = getObjectType(hitObject); char suffix = ','; diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 8126311cbd..ac399e37c4 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -63,7 +63,7 @@ namespace osu.Game.Beatmaps length = emptyLength; break; - case IHasEndTime endTime: + case IHasDuration endTime: length = endTime.EndTime + excess_length; break; diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs index 6f9053d7cb..e2cc98813a 100644 --- a/osu.Game/Rulesets/Objects/HitObject.cs +++ b/osu.Game/Rulesets/Objects/HitObject.cs @@ -175,10 +175,10 @@ namespace osu.Game.Rulesets.Objects /// Returns the end time of this object. /// /// - /// This returns the where available, falling back to otherwise. + /// This returns the where available, falling back to otherwise. /// /// The object. /// The end time of this object. - public static double GetEndTime(this HitObject hitObject) => (hitObject as IHasEndTime)?.EndTime ?? hitObject.StartTime; + public static double GetEndTime(this HitObject hitObject) => (hitObject as IHasDuration)?.EndTime ?? hitObject.StartTime; } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs index 4b0270064a..014494ec54 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs @@ -8,7 +8,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch /// /// Legacy osu!catch Spinner-type, used for parsing Beatmaps. /// - internal sealed class ConvertSpinner : ConvertHitObject, IHasEndTime, IHasXPosition, IHasCombo + internal sealed class ConvertSpinner : ConvertHitObject, IHasDuration, IHasXPosition, IHasCombo { public double EndTime => StartTime + Duration; diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs index dcb66163e4..2fa4766c1d 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs @@ -5,7 +5,7 @@ using osu.Game.Rulesets.Objects.Types; namespace osu.Game.Rulesets.Objects.Legacy.Mania { - internal sealed class ConvertHold : ConvertHitObject, IHasXPosition, IHasEndTime + internal sealed class ConvertHold : ConvertHitObject, IHasXPosition, IHasDuration { public float X { get; set; } diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSpinner.cs index b731f7c8d8..c05aaceb9c 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSpinner.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSpinner.cs @@ -8,7 +8,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania /// /// Legacy osu!mania Spinner-type, used for parsing Beatmaps. /// - internal sealed class ConvertSpinner : ConvertHitObject, IHasEndTime, IHasXPosition + internal sealed class ConvertSpinner : ConvertHitObject, IHasDuration, IHasXPosition { public double Duration { get; set; } diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs index a231237077..e9e5ca8c94 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs @@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu /// /// Legacy osu! Spinner-type, used for parsing Beatmaps. /// - internal sealed class ConvertSpinner : ConvertHitObject, IHasEndTime, IHasPosition, IHasCombo + internal sealed class ConvertSpinner : ConvertHitObject, IHasDuration, IHasPosition, IHasCombo { public double Duration { get; set; } diff --git a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSpinner.cs index 0976106ec4..1d5ecb1ef3 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSpinner.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSpinner.cs @@ -8,7 +8,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Taiko /// /// Legacy osu!taiko Spinner-type, used for parsing Beatmaps. /// - internal sealed class ConvertSpinner : ConvertHitObject, IHasEndTime + internal sealed class ConvertSpinner : ConvertHitObject, IHasDuration { public double Duration { get; set; } diff --git a/osu.Game/Rulesets/Objects/Types/IHasDistance.cs b/osu.Game/Rulesets/Objects/Types/IHasDistance.cs index e7f552115e..b497ca5da3 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasDistance.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasDistance.cs @@ -6,7 +6,7 @@ namespace osu.Game.Rulesets.Objects.Types /// /// A HitObject that has a positional length. /// - public interface IHasDistance : IHasEndTime + public interface IHasDistance : IHasDuration { /// /// The positional length of the HitObject. diff --git a/osu.Game/Rulesets/Objects/Types/IHasDuration.cs b/osu.Game/Rulesets/Objects/Types/IHasDuration.cs new file mode 100644 index 0000000000..2433f9597e --- /dev/null +++ b/osu.Game/Rulesets/Objects/Types/IHasDuration.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 Newtonsoft.Json; + +namespace osu.Game.Rulesets.Objects.Types +{ + /// + /// A HitObject that ends at a different time than its start time. + /// + public interface IHasDuration + { + /// + /// The time at which the HitObject ends. + /// + double EndTime { get; } + + /// + /// The duration of the HitObject. + /// + [JsonIgnore] + double Duration { get; set; } + } +} diff --git a/osu.Game/Rulesets/Objects/Types/IHasEndTime.cs b/osu.Game/Rulesets/Objects/Types/IHasEndTime.cs index 5eb551e15c..7395223c7e 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasEndTime.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasEndTime.cs @@ -1,24 +1,12 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using Newtonsoft.Json; +using System; namespace osu.Game.Rulesets.Objects.Types { - /// - /// A HitObject that ends at a different time than its start time. - /// - public interface IHasEndTime + [Obsolete("Use IHasDuration instead.")] // can be removed 20201126 + public interface IHasEndTime : IHasDuration { - /// - /// The time at which the HitObject ends. - /// - double EndTime { get; } - - /// - /// The duration of the HitObject. - /// - [JsonIgnore] - double Duration { get; set; } } } diff --git a/osu.Game/Rulesets/Objects/Types/IHasRepeats.cs b/osu.Game/Rulesets/Objects/Types/IHasRepeats.cs index 256b1f3963..7a3fb16196 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasRepeats.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasRepeats.cs @@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Objects.Types /// /// A HitObject that spans some length. /// - public interface IHasRepeats : IHasEndTime + public interface IHasRepeats : IHasDuration { /// /// The amount of times the HitObject repeats. diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index c817d84d5c..0dc3324559 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -270,7 +270,7 @@ namespace osu.Game.Rulesets.UI.Scrolling // Cant use AddOnce() since the delegate is re-constructed every invocation private void computeInitialStateRecursive(DrawableHitObject hitObject) => hitObject.Schedule(() => { - if (hitObject.HitObject is IHasEndTime e) + if (hitObject.HitObject is IHasDuration e) { switch (direction.Value) { diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index d6fc17f358..b95b3842b3 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -72,7 +72,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline shadowComponents.Add(circle); - if (hitObject is IHasEndTime) + if (hitObject is IHasDuration) { DragBar dragBarUnderlay; Container extensionBar; @@ -290,7 +290,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline repeatHitObject.RepeatCount = proposedCount; break; - case IHasEndTime endTimeHitObject: + case IHasDuration endTimeHitObject: var snappedTime = Math.Max(hitObject.StartTime, beatSnapProvider.SnapTime(time)); if (endTimeHitObject.EndTime == snappedTime) From f5c974dd897b0a7006b944e0103e4158d3d7667a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 May 2020 13:40:16 +0900 Subject: [PATCH 1797/2376] Hide non-alive selection blueprints by default --- .../Edit/Blueprints/OsuSelectionBlueprint.cs | 2 ++ osu.Game/Rulesets/Edit/OverlaySelectionBlueprint.cs | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs index b0e13808a5..8dd550bb96 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs @@ -12,6 +12,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints { protected new T HitObject => (T)DrawableObject.HitObject; + protected override bool AlwaysShowWhenSelected => true; + protected OsuSelectionBlueprint(DrawableHitObject drawableObject) : base(drawableObject) { diff --git a/osu.Game/Rulesets/Edit/OverlaySelectionBlueprint.cs b/osu.Game/Rulesets/Edit/OverlaySelectionBlueprint.cs index 8202d3a1d1..75200e3027 100644 --- a/osu.Game/Rulesets/Edit/OverlaySelectionBlueprint.cs +++ b/osu.Game/Rulesets/Edit/OverlaySelectionBlueprint.cs @@ -15,7 +15,12 @@ namespace osu.Game.Rulesets.Edit /// public readonly DrawableHitObject DrawableObject; - protected override bool ShouldBeAlive => (DrawableObject.IsAlive && DrawableObject.IsPresent) || State == SelectionState.Selected; + /// + /// Whether the blueprint should be shown even when the is not alive. + /// + protected virtual bool AlwaysShowWhenSelected => false; + + protected override bool ShouldBeAlive => (DrawableObject.IsAlive && DrawableObject.IsPresent) || (AlwaysShowWhenSelected && State == SelectionState.Selected); protected OverlaySelectionBlueprint(DrawableHitObject drawableObject) : base(drawableObject.HitObject) From f989f1aa0034d6283ee3d49169316a80713c0a55 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 May 2020 16:08:47 +0900 Subject: [PATCH 1798/2376] Change event flow to avoid firing store delete events on update --- osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs | 4 ++-- osu.Game/Database/ArchiveModelManager.cs | 8 ++++---- osu.Game/Database/IModelManager.cs | 2 +- osu.Game/Database/MutableDatabaseBackedStore.cs | 16 +++++++++++----- osu.Game/Online/DownloadTrackingComposite.cs | 8 ++++---- osu.Game/OsuGameBase.cs | 2 +- osu.Game/Overlays/MusicController.cs | 12 ++++++------ .../Overlays/Settings/Sections/SkinSection.cs | 10 +++++----- .../Multi/Match/Components/ReadyButton.cs | 8 ++++---- osu.Game/Screens/Multi/Match/MatchSubScreen.cs | 8 ++++---- osu.Game/Screens/Select/BeatmapCarousel.cs | 8 ++++---- osu.Game/Screens/Select/Carousel/TopLocalRank.cs | 6 +++--- 12 files changed, 49 insertions(+), 43 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index 43fab186aa..5eb11a3264 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -156,7 +156,7 @@ namespace osu.Game.Tests.Beatmaps.IO var manager = osu.Dependencies.Get(); // ReSharper disable once AccessToModifiedClosure - manager.ItemAdded.BindValueChanged(_ => Interlocked.Increment(ref itemAddRemoveFireCount)); + manager.ItemUpdated.BindValueChanged(_ => Interlocked.Increment(ref itemAddRemoveFireCount)); manager.ItemRemoved.BindValueChanged(_ => Interlocked.Increment(ref itemAddRemoveFireCount)); var imported = await LoadOszIntoOsu(osu); @@ -166,7 +166,7 @@ namespace osu.Game.Tests.Beatmaps.IO imported.Hash += "-changed"; manager.Update(imported); - Assert.AreEqual(0, itemAddRemoveFireCount -= 2); + Assert.AreEqual(0, itemAddRemoveFireCount -= 1); checkBeatmapSetCount(osu, 1); checkBeatmapCount(osu, 12); diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index f21f708f95..ae55a7b14a 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -55,12 +55,12 @@ namespace osu.Game.Database public Action PostNotification { protected get; set; } /// - /// Fired when a new becomes available in the database. + /// Fired when a new or updated becomes available in the database. /// This is not guaranteed to run on the update thread. /// - public IBindable> ItemAdded => itemAdded; + public IBindable> ItemUpdated => itemUpdated; - private readonly Bindable> itemAdded = new Bindable>(); + private readonly Bindable> itemUpdated = new Bindable>(); /// /// Fired when a is removed from the database. @@ -90,7 +90,7 @@ namespace osu.Game.Database ContextFactory = contextFactory; ModelStore = modelStore; - ModelStore.ItemAdded += item => handleEvent(() => itemAdded.Value = new WeakReference(item)); + ModelStore.ItemUpdated += item => handleEvent(() => itemUpdated.Value = new WeakReference(item)); ModelStore.ItemRemoved += item => handleEvent(() => itemRemoved.Value = new WeakReference(item)); exportStorage = storage.GetStorageForDirectory("exports"); diff --git a/osu.Game/Database/IModelManager.cs b/osu.Game/Database/IModelManager.cs index 852b385798..7f7e5565f1 100644 --- a/osu.Game/Database/IModelManager.cs +++ b/osu.Game/Database/IModelManager.cs @@ -13,7 +13,7 @@ namespace osu.Game.Database public interface IModelManager where TModel : class { - IBindable> ItemAdded { get; } + IBindable> ItemUpdated { get; } IBindable> ItemRemoved { get; } } diff --git a/osu.Game/Database/MutableDatabaseBackedStore.cs b/osu.Game/Database/MutableDatabaseBackedStore.cs index 4ca1eef989..c9d0c4bc41 100644 --- a/osu.Game/Database/MutableDatabaseBackedStore.cs +++ b/osu.Game/Database/MutableDatabaseBackedStore.cs @@ -16,7 +16,14 @@ namespace osu.Game.Database public abstract class MutableDatabaseBackedStore : DatabaseBackedStore where T : class, IHasPrimaryKey, ISoftDelete { - public event Action ItemAdded; + /// + /// Fired when an item was added or updated. + /// + public event Action ItemUpdated; + + /// + /// Fired when an item was removed. + /// public event Action ItemRemoved; protected MutableDatabaseBackedStore(IDatabaseContextFactory contextFactory, Storage storage = null) @@ -41,7 +48,7 @@ namespace osu.Game.Database context.Attach(item); } - ItemAdded?.Invoke(item); + ItemUpdated?.Invoke(item); } /// @@ -53,8 +60,7 @@ namespace osu.Game.Database using (var usage = ContextFactory.GetForWrite()) usage.Context.Update(item); - ItemRemoved?.Invoke(item); - ItemAdded?.Invoke(item); + ItemUpdated?.Invoke(item); } /// @@ -91,7 +97,7 @@ namespace osu.Game.Database item.DeletePending = false; } - ItemAdded?.Invoke(item); + ItemUpdated?.Invoke(item); return true; } diff --git a/osu.Game/Online/DownloadTrackingComposite.cs b/osu.Game/Online/DownloadTrackingComposite.cs index 47de7d75ed..5d9cf612bb 100644 --- a/osu.Game/Online/DownloadTrackingComposite.cs +++ b/osu.Game/Online/DownloadTrackingComposite.cs @@ -34,7 +34,7 @@ namespace osu.Game.Online Model.Value = model; } - private IBindable> managerAdded; + private IBindable> managedUpdated; private IBindable> managerRemoved; private IBindable>> managerDownloadBegan; private IBindable>> managerDownloadFailed; @@ -56,8 +56,8 @@ namespace osu.Game.Online managerDownloadBegan.BindValueChanged(downloadBegan); managerDownloadFailed = manager.DownloadFailed.GetBoundCopy(); managerDownloadFailed.BindValueChanged(downloadFailed); - managerAdded = manager.ItemAdded.GetBoundCopy(); - managerAdded.BindValueChanged(itemAdded); + managedUpdated = manager.ItemUpdated.GetBoundCopy(); + managedUpdated.BindValueChanged(itemUpdated); managerRemoved = manager.ItemRemoved.GetBoundCopy(); managerRemoved.BindValueChanged(itemRemoved); } @@ -128,7 +128,7 @@ namespace osu.Game.Online private void onRequestFailure(Exception e) => Schedule(() => attachDownload(null)); - private void itemAdded(ValueChangedEvent> weakItem) + private void itemUpdated(ValueChangedEvent> weakItem) { if (weakItem.NewValue.TryGetTarget(out var item)) setDownloadStateFromManager(item, DownloadState.LocallyAvailable); diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 453587df18..e7446975b9 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -192,7 +192,7 @@ namespace osu.Game ScoreManager.Delete(getBeatmapScores(item), true); }); - BeatmapManager.ItemAdded.BindValueChanged(i => + BeatmapManager.ItemUpdated.BindValueChanged(i => { if (i.NewValue.TryGetTarget(out var item)) ScoreManager.Undelete(getBeatmapScores(item), true); diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 35f3cb0e25..92cf490be2 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -60,14 +60,14 @@ namespace osu.Game.Overlays [Resolved(canBeNull: true)] private OnScreenDisplay onScreenDisplay { get; set; } - private IBindable> managerAdded; + private IBindable> managerUpdated; private IBindable> managerRemoved; [BackgroundDependencyLoader] private void load() { - managerAdded = beatmaps.ItemAdded.GetBoundCopy(); - managerAdded.BindValueChanged(beatmapAdded); + managerUpdated = beatmaps.ItemUpdated.GetBoundCopy(); + managerUpdated.BindValueChanged(beatmapUpdated); managerRemoved = beatmaps.ItemRemoved.GetBoundCopy(); managerRemoved.BindValueChanged(beatmapRemoved); @@ -98,14 +98,14 @@ namespace osu.Game.Overlays /// public bool IsPlaying => current?.Track.IsRunning ?? false; - private void beatmapAdded(ValueChangedEvent> weakSet) + private void beatmapUpdated(ValueChangedEvent> weakSet) { if (weakSet.NewValue.TryGetTarget(out var set)) { Schedule(() => { - if (!beatmapSets.Contains(set)) - beatmapSets.Add(set); + beatmapSets.Remove(set); + beatmapSets.Add(set); }); } } diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index b84b9fec37..04390a1193 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -32,7 +32,7 @@ namespace osu.Game.Overlays.Settings.Sections [Resolved] private SkinManager skins { get; set; } - private IBindable> managerAdded; + private IBindable> managerUpdated; private IBindable> managerRemoved; [BackgroundDependencyLoader] @@ -73,8 +73,8 @@ namespace osu.Game.Overlays.Settings.Sections }, }; - managerAdded = skins.ItemAdded.GetBoundCopy(); - managerAdded.BindValueChanged(itemAdded); + managerUpdated = skins.ItemUpdated.GetBoundCopy(); + managerUpdated.BindValueChanged(itemUpdated); managerRemoved = skins.ItemRemoved.GetBoundCopy(); managerRemoved.BindValueChanged(itemRemoved); @@ -92,10 +92,10 @@ namespace osu.Game.Overlays.Settings.Sections dropdownBindable.BindValueChanged(skin => configBindable.Value = skin.NewValue.ID); } - private void itemAdded(ValueChangedEvent> weakItem) + private void itemUpdated(ValueChangedEvent> weakItem) { if (weakItem.NewValue.TryGetTarget(out var item)) - Schedule(() => skinDropdown.Items = skinDropdown.Items.Append(item).ToArray()); + Schedule(() => skinDropdown.Items = skinDropdown.Items.Where(i => !i.Equals(item)).Append(item).ToArray()); } private void itemRemoved(ValueChangedEvent> weakItem) diff --git a/osu.Game/Screens/Multi/Match/Components/ReadyButton.cs b/osu.Game/Screens/Multi/Match/Components/ReadyButton.cs index 4420b2d58a..e1f86fcc97 100644 --- a/osu.Game/Screens/Multi/Match/Components/ReadyButton.cs +++ b/osu.Game/Screens/Multi/Match/Components/ReadyButton.cs @@ -32,14 +32,14 @@ namespace osu.Game.Screens.Multi.Match.Components Text = "Start"; } - private IBindable> managerAdded; + private IBindable> managerUpdated; private IBindable> managerRemoved; [BackgroundDependencyLoader] private void load(OsuColour colours) { - managerAdded = beatmaps.ItemAdded.GetBoundCopy(); - managerAdded.BindValueChanged(beatmapAdded); + managerUpdated = beatmaps.ItemUpdated.GetBoundCopy(); + managerUpdated.BindValueChanged(beatmapUpdated); managerRemoved = beatmaps.ItemRemoved.GetBoundCopy(); managerRemoved.BindValueChanged(beatmapRemoved); @@ -61,7 +61,7 @@ namespace osu.Game.Screens.Multi.Match.Components hasBeatmap = beatmaps.QueryBeatmap(b => b.OnlineBeatmapID == beatmapId) != null; } - private void beatmapAdded(ValueChangedEvent> weakSet) + private void beatmapUpdated(ValueChangedEvent> weakSet) { if (weakSet.NewValue.TryGetTarget(out var set)) { diff --git a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs index caa547ac72..e1d72d9600 100644 --- a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs +++ b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs @@ -50,7 +50,7 @@ namespace osu.Game.Screens.Multi.Match private LeaderboardChatDisplay leaderboardChatDisplay; private MatchSettingsOverlay settingsOverlay; - private IBindable> managerAdded; + private IBindable> managerUpdated; public MatchSubScreen(Room room) { @@ -183,8 +183,8 @@ namespace osu.Game.Screens.Multi.Match SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(selectedItemChanged)); SelectedItem.Value = playlist.FirstOrDefault(); - managerAdded = beatmapManager.ItemAdded.GetBoundCopy(); - managerAdded.BindValueChanged(beatmapAdded); + managerUpdated = beatmapManager.ItemUpdated.GetBoundCopy(); + managerUpdated.BindValueChanged(beatmapUpdated); } public override bool OnExiting(IScreen next) @@ -217,7 +217,7 @@ namespace osu.Game.Screens.Multi.Match Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); } - private void beatmapAdded(ValueChangedEvent> weakSet) + private void beatmapUpdated(ValueChangedEvent> weakSet) { Schedule(() => { diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index f23e1b1ef2..2d714d1794 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -131,7 +131,7 @@ namespace osu.Game.Screens.Select private CarouselRoot root; - private IBindable> itemAdded; + private IBindable> itemUpdated; private IBindable> itemRemoved; private IBindable> itemHidden; private IBindable> itemRestored; @@ -166,8 +166,8 @@ namespace osu.Game.Screens.Select RightClickScrollingEnabled.ValueChanged += enabled => scroll.RightMouseScrollbar = enabled.NewValue; RightClickScrollingEnabled.TriggerChange(); - itemAdded = beatmaps.ItemAdded.GetBoundCopy(); - itemAdded.BindValueChanged(beatmapAdded); + itemUpdated = beatmaps.ItemUpdated.GetBoundCopy(); + itemUpdated.BindValueChanged(beatmapUpdated); itemRemoved = beatmaps.ItemRemoved.GetBoundCopy(); itemRemoved.BindValueChanged(beatmapRemoved); itemHidden = beatmaps.BeatmapHidden.GetBoundCopy(); @@ -582,7 +582,7 @@ namespace osu.Game.Screens.Select RemoveBeatmapSet(item); } - private void beatmapAdded(ValueChangedEvent> weakItem) + private void beatmapUpdated(ValueChangedEvent> weakItem) { if (weakItem.NewValue.TryGetTarget(out var item)) UpdateBeatmapSet(item); diff --git a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs index aed25787b0..3ad57c1cb0 100644 --- a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs +++ b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs @@ -28,7 +28,7 @@ namespace osu.Game.Screens.Select.Carousel [Resolved] private IAPIProvider api { get; set; } - private IBindable> itemAdded; + private IBindable> itemUpdated; private IBindable> itemRemoved; public TopLocalRank(BeatmapInfo beatmap) @@ -40,8 +40,8 @@ namespace osu.Game.Screens.Select.Carousel [BackgroundDependencyLoader] private void load() { - itemAdded = scores.ItemAdded.GetBoundCopy(); - itemAdded.BindValueChanged(scoreChanged); + itemUpdated = scores.ItemUpdated.GetBoundCopy(); + itemUpdated.BindValueChanged(scoreChanged); itemRemoved = scores.ItemRemoved.GetBoundCopy(); itemRemoved.BindValueChanged(scoreChanged); From 9a060cfb3acfac28816eb68f245e365153a9af46 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 May 2020 20:44:15 +0900 Subject: [PATCH 1799/2376] Allow drag selections to occur from outside the playfield --- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 1 + osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs | 2 ++ 2 files changed, 3 insertions(+) diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 3453bfbf63..99759bde3d 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -102,6 +102,7 @@ namespace osu.Game.Rulesets.Edit { Name = "Content", RelativeSizeAxes = Axes.Both, + Masking = true, Children = new Drawable[] { // layers below playfield diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index cc417bbb10..d07cffff0c 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -44,6 +44,8 @@ namespace osu.Game.Screens.Edit.Compose.Components private readonly BindableList selectedHitObjects = new BindableList(); + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; + [Resolved(canBeNull: true)] private IPositionSnapProvider snapProvider { get; set; } From 919ff92d15775e63bbb74083a3ce05b61ac53707 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 May 2020 22:56:12 +0900 Subject: [PATCH 1800/2376] Remove unused resolved composer --- .../Edit/Blueprints/HoldNotePlacementBlueprint.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs index 500b26917d..b5ec1e1a2a 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs @@ -20,9 +20,6 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints private readonly EditNotePiece headPiece; private readonly EditNotePiece tailPiece; - [Resolved] - private IManiaHitObjectComposer composer { get; set; } - [Resolved] private IScrollingInfo scrollingInfo { get; set; } From 6be5917eb0c232d5ad34843736fc7c4781c36d02 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 May 2020 23:15:16 +0900 Subject: [PATCH 1801/2376] Remove necessity for custom mania interface caching --- .../ManiaPlacementBlueprintTestScene.cs | 4 +- .../ManiaSelectionBlueprintTestScene.cs | 4 +- .../TestSceneManiaBeatSnapGrid.cs | 60 ++++++++++++++++++- .../Blueprints/ManiaSelectionBlueprint.cs | 3 - .../Edit/IManiaHitObjectComposer.cs | 12 ---- .../Edit/ManiaBeatSnapGrid.cs | 6 +- .../Edit/ManiaHitObjectComposer.cs | 5 +- .../Edit/ManiaSelectionHandler.cs | 9 ++- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 6 +- 9 files changed, 76 insertions(+), 33 deletions(-) delete mode 100644 osu.Game.Rulesets.Mania/Edit/IManiaHitObjectComposer.cs diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs index 1119a66f63..0fe4a3c669 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Timing; using osu.Game.Rulesets.Edit; -using osu.Game.Rulesets.Mania.Edit; using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mods; @@ -19,8 +18,7 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Mania.Tests { - [Cached(Type = typeof(IManiaHitObjectComposer))] - public abstract class ManiaPlacementBlueprintTestScene : PlacementBlueprintTestScene, IManiaHitObjectComposer + public abstract class ManiaPlacementBlueprintTestScene : PlacementBlueprintTestScene { private readonly Column column; diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaSelectionBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/ManiaSelectionBlueprintTestScene.cs index 35fe596e98..149f6582ab 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaSelectionBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaSelectionBlueprintTestScene.cs @@ -4,15 +4,13 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Timing; -using osu.Game.Rulesets.Mania.Edit; using osu.Game.Rulesets.Mania.UI; using osu.Game.Tests.Visual; using osuTK.Graphics; namespace osu.Game.Rulesets.Mania.Tests { - [Cached(Type = typeof(IManiaHitObjectComposer))] - public abstract class ManiaSelectionBlueprintTestScene : SelectionBlueprintTestScene, IManiaHitObjectComposer + public abstract class ManiaSelectionBlueprintTestScene : SelectionBlueprintTestScene { [Cached(Type = typeof(IAdjustableClock))] private readonly IAdjustableClock clock = new StopwatchClock(); diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaBeatSnapGrid.cs index ce9546415f..639be0bc11 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaBeatSnapGrid.cs @@ -2,23 +2,27 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Input.Events; using osu.Framework.Timing; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Configuration; +using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Edit; using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Screens.Edit; using osu.Game.Tests.Visual; +using osuTK; namespace osu.Game.Rulesets.Mania.Tests { - [Cached(typeof(IManiaHitObjectComposer))] - public class TestSceneManiaBeatSnapGrid : EditorClockTestScene, IManiaHitObjectComposer + public class TestSceneManiaBeatSnapGrid : EditorClockTestScene { [Cached(typeof(IScrollingInfo))] private ScrollingTestContainer.TestScrollingInfo scrollingInfo = new ScrollingTestContainer.TestScrollingInfo(); @@ -50,7 +54,10 @@ namespace osu.Game.Rulesets.Mania.Tests { Clock = new FramedClock(new StopwatchClock()) }, - beatSnapGrid = new ManiaBeatSnapGrid() + new TestHitObjectComposer(Playfield) + { + Child = beatSnapGrid = new ManiaBeatSnapGrid() + } }; } @@ -67,4 +74,51 @@ namespace osu.Game.Rulesets.Mania.Tests public ManiaPlayfield Playfield { get; } } + + public class TestHitObjectComposer : HitObjectComposer + { + public override Playfield Playfield { get; } + public override IEnumerable HitObjects => Enumerable.Empty(); + public override bool CursorInPlacementArea => false; + + public TestHitObjectComposer(Playfield playfield) + { + Playfield = playfield; + } + + public Drawable Child + { + set => InternalChild = value; + } + + public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) + { + throw new System.NotImplementedException(); + } + + public override float GetBeatSnapDistanceAt(double referenceTime) + { + throw new System.NotImplementedException(); + } + + public override float DurationToDistance(double referenceTime, double duration) + { + throw new System.NotImplementedException(); + } + + public override double DistanceToDuration(double referenceTime, float distance) + { + throw new System.NotImplementedException(); + } + + public override double GetSnappedDurationFromDistance(double referenceTime, float distance) + { + throw new System.NotImplementedException(); + } + + public override float GetSnappedDistanceFromDistance(double referenceTime, float distance) + { + throw new System.NotImplementedException(); + } + } } diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs index 0089a9fbee..384f49d9b2 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs @@ -18,9 +18,6 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints [Resolved] private IScrollingInfo scrollingInfo { get; set; } - [Resolved] - private IManiaHitObjectComposer composer { get; set; } - protected ManiaSelectionBlueprint(DrawableHitObject drawableObject) : base(drawableObject) { diff --git a/osu.Game.Rulesets.Mania/Edit/IManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/IManiaHitObjectComposer.cs deleted file mode 100644 index 3818d0e15d..0000000000 --- a/osu.Game.Rulesets.Mania/Edit/IManiaHitObjectComposer.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.Mania.UI; - -namespace osu.Game.Rulesets.Mania.Edit -{ - public interface IManiaHitObjectComposer - { - ManiaPlayfield Playfield { get; } - } -} diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs index b5b6c08fca..2028cae9a5 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs @@ -11,6 +11,8 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps; using osu.Game.Graphics; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI.Scrolling; @@ -63,9 +65,9 @@ namespace osu.Game.Rulesets.Mania.Edit private (double start, double end)? selectionTimeRange; [BackgroundDependencyLoader] - private void load(IManiaHitObjectComposer composer) + private void load(HitObjectComposer composer) { - foreach (var stage in composer.Playfield.Stages) + foreach (var stage in ((ManiaPlayfield)composer.Playfield).Stages) { foreach (var column in stage.Columns) { diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index 73cbadc97c..10d344242c 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -20,8 +20,7 @@ using osuTK; namespace osu.Game.Rulesets.Mania.Edit { - [Cached(Type = typeof(IManiaHitObjectComposer))] - public class ManiaHitObjectComposer : HitObjectComposer, IManiaHitObjectComposer + public class ManiaHitObjectComposer : HitObjectComposer { private DrawableManiaEditRuleset drawableRuleset; private ManiaBeatSnapGrid beatSnapGrid; @@ -50,7 +49,7 @@ namespace osu.Game.Rulesets.Mania.Edit protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - public ManiaPlayfield Playfield => ((ManiaPlayfield)drawableRuleset.Playfield); + public new ManiaPlayfield Playfield => ((ManiaPlayfield)drawableRuleset.Playfield); public IScrollingInfo ScrollingInfo => drawableRuleset.ScrollingInfo; diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs index 4ea71652bc..65f40d7d0a 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using osu.Framework.Allocation; +using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Edit.Blueprints; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.UI.Scrolling; @@ -17,7 +18,7 @@ namespace osu.Game.Rulesets.Mania.Edit private IScrollingInfo scrollingInfo { get; set; } [Resolved] - private IManiaHitObjectComposer composer { get; set; } + private HitObjectComposer composer { get; set; } public override bool HandleMovement(MoveSelectionEvent moveEvent) { @@ -31,7 +32,9 @@ namespace osu.Game.Rulesets.Mania.Edit private void performColumnMovement(int lastColumn, MoveSelectionEvent moveEvent) { - var currentColumn = composer.Playfield.GetColumnByPosition(moveEvent.ScreenSpacePosition); + var maniaPlayfield = ((ManiaHitObjectComposer)composer).Playfield; + + var currentColumn = maniaPlayfield.GetColumnByPosition(moveEvent.ScreenSpacePosition); if (currentColumn == null) return; @@ -50,7 +53,7 @@ namespace osu.Game.Rulesets.Mania.Edit maxColumn = obj.Column; } - columnDelta = Math.Clamp(columnDelta, -minColumn, composer.Playfield.TotalColumns - 1 - maxColumn); + columnDelta = Math.Clamp(columnDelta, -minColumn, maniaPlayfield.TotalColumns - 1 - maxColumn); foreach (var obj in SelectedHitObjects.OfType()) obj.Column += columnDelta; diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 3453bfbf63..0a2df64dde 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -48,6 +48,8 @@ namespace osu.Game.Rulesets.Edit protected ComposeBlueprintContainer BlueprintContainer { get; private set; } + public override Playfield Playfield => drawableRulesetWrapper.Playfield; + private DrawableEditRulesetWrapper drawableRulesetWrapper; protected readonly Container LayerBelowRuleset = new Container { RelativeSizeAxes = Axes.Both }; @@ -260,11 +262,13 @@ namespace osu.Game.Rulesets.Edit [Cached(typeof(IPositionSnapProvider))] public abstract class HitObjectComposer : CompositeDrawable, IPositionSnapProvider { - internal HitObjectComposer() + protected HitObjectComposer() { RelativeSizeAxes = Axes.Both; } + public abstract Playfield Playfield { get; } + /// /// All the s. /// From e4de20f0a5b6721519d6fa1baa4b2b2daf9222e6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 May 2020 11:54:27 +0900 Subject: [PATCH 1802/2376] 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 b7d08fb120..7ea1f3140b 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index d5017a436f..3d2a4f3081 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -24,7 +24,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 19a36f1e1f..8a7f75b515 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From 912c999f4051a259c58c2dc8ec265193bb8f8bd6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 May 2020 19:05:35 +0900 Subject: [PATCH 1803/2376] Fix minor typo in OsuGameBase --- osu.Game/OsuGameBase.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index e7446975b9..5e44562144 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -229,8 +229,8 @@ namespace osu.Game FileStore.Cleanup(); - if (API is APIAccess apiAcces) - AddInternal(apiAcces); + if (API is APIAccess apiAccess) + AddInternal(apiAccess); AddInternal(RulesetConfigCache); GlobalActionContainer globalBinding; From a55ce261307b560ab303db9cbcf183e6c50bca43 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 28 May 2020 20:46:17 +0900 Subject: [PATCH 1804/2376] Allow null score --- .../Visual/Ranking/TestSceneScorePanelList.cs | 38 ++++++++++++++++++- osu.Game/Screens/Ranking/ScorePanelList.cs | 7 +++- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs index b32b3afbda..a204d2bcbc 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs @@ -9,10 +9,11 @@ using osu.Framework.Utils; using osu.Game.Rulesets.Osu; using osu.Game.Scoring; using osu.Game.Screens.Ranking; +using osuTK.Input; namespace osu.Game.Tests.Visual.Ranking { - public class TestSceneScorePanelList : OsuTestScene + public class TestSceneScorePanelList : OsuManualInputManagerTestScene { private ScoreInfo initialScore; private ScorePanelList list; @@ -72,6 +73,41 @@ namespace osu.Game.Tests.Visual.Ranking assertPanelCentred(); } + [Test] + public void TestNullScore() + { + AddStep("create panel with null score", () => + { + Child = list = new ScorePanelList(null) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + }); + + AddStep("add many panels", () => + { + for (int i = 0; i < 20; i++) + list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo) { TotalScore = initialScore.TotalScore - i - 1 }); + + for (int i = 0; i < 20; i++) + list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo) { TotalScore = initialScore.TotalScore + i + 1 }); + }); + + AddWaitStep("wait for panel animation", 5); + + AddAssert("no panel selected", () => list.ChildrenOfType().All(p => p.State != PanelState.Expanded)); + + AddStep("expand second panel", () => + { + var expandedPanel = list.ChildrenOfType().OrderBy(p => p.DrawPosition.X).ElementAt(1); + InputManager.MoveMouseTo(expandedPanel); + InputManager.Click(MouseButton.Left); + }); + + assertPanelCentred(); + } + private void assertPanelCentred() => AddUntilStep("expanded panel centred", () => { var expandedPanel = list.ChildrenOfType().Single(p => p.State == PanelState.Expanded); diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index df2c66203b..a30d911f04 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -45,8 +45,11 @@ namespace osu.Game.Screens.Ranking } }; - AddScore(initialScore); - presentScore(initialScore); + if (initialScore != null) + { + AddScore(initialScore); + presentScore(initialScore); + } } /// From 666cbd0f40f2cf59a78d644caa3c1cb6a53b9588 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 28 May 2020 21:08:47 +0900 Subject: [PATCH 1805/2376] Allow selected score to be programmatically changed --- .../Visual/Ranking/TestSceneScorePanelList.cs | 128 ++++++++++++------ osu.Game/Screens/Ranking/ResultsScreen.cs | 3 +- osu.Game/Screens/Ranking/ScorePanelList.cs | 51 ++++--- 3 files changed, 120 insertions(+), 62 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs index a204d2bcbc..588cddea7d 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; @@ -9,58 +10,121 @@ using osu.Framework.Utils; using osu.Game.Rulesets.Osu; using osu.Game.Scoring; using osu.Game.Screens.Ranking; -using osuTK.Input; namespace osu.Game.Tests.Visual.Ranking { public class TestSceneScorePanelList : OsuManualInputManagerTestScene { - private ScoreInfo initialScore; private ScorePanelList list; - [SetUp] - public void Setup() => Schedule(() => + [Test] + public void TestEmptyList() { - Child = list = new ScorePanelList(initialScore = new TestScoreInfo(new OsuRuleset().RulesetInfo)) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }; - }); + createListStep(() => new ScorePanelList()); + } [Test] - public void TestSingleScore() + public void TestEmptyListWithSelectedScore() { + createListStep(() => new ScorePanelList + { + SelectedScore = { Value = new TestScoreInfo(new OsuRuleset().RulesetInfo) } + }); + } + + [Test] + public void TestAddPanelAfterSelectingScore() + { + var score = new TestScoreInfo(new OsuRuleset().RulesetInfo); + + createListStep(() => new ScorePanelList + { + SelectedScore = { Value = score } + }); + + AddStep("add panel", () => list.AddScore(score)); + + assertScoreState(score, true); + assertPanelCentred(); + } + + [Test] + public void TestAddPanelBeforeSelectingScore() + { + var score = new TestScoreInfo(new OsuRuleset().RulesetInfo); + + createListStep(() => new ScorePanelList()); + + AddStep("add panel", () => list.AddScore(score)); + + assertScoreState(score, false); + assertPanelCentred(); + + AddStep("select score", () => list.SelectedScore.Value = score); + + assertScoreState(score, true); assertPanelCentred(); } [Test] public void TestAddManyScoresAfter() { - AddStep("add scores", () => + var initialScore = new TestScoreInfo(new OsuRuleset().RulesetInfo); + + createListStep(() => new ScorePanelList()); + + AddStep("add initial panel and select", () => + { + list.AddScore(initialScore); + list.SelectedScore.Value = initialScore; + }); + + AddStep("add many scores", () => { for (int i = 0; i < 20; i++) list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo) { TotalScore = initialScore.TotalScore - i - 1 }); }); + assertScoreState(initialScore, true); assertPanelCentred(); } [Test] public void TestAddManyScoresBefore() { + var initialScore = new TestScoreInfo(new OsuRuleset().RulesetInfo); + + createListStep(() => new ScorePanelList()); + + AddStep("add initial panel and select", () => + { + list.AddScore(initialScore); + list.SelectedScore.Value = initialScore; + }); + AddStep("add scores", () => { for (int i = 0; i < 20; i++) list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo) { TotalScore = initialScore.TotalScore + i + 1 }); }); + assertScoreState(initialScore, true); assertPanelCentred(); } [Test] public void TestAddManyPanelsOnBothSides() { + var initialScore = new TestScoreInfo(new OsuRuleset().RulesetInfo); + + createListStep(() => new ScorePanelList()); + + AddStep("add initial panel and select", () => + { + list.AddScore(initialScore); + list.SelectedScore.Value = initialScore; + }); + AddStep("add scores after", () => { for (int i = 0; i < 20; i++) @@ -70,42 +134,19 @@ namespace osu.Game.Tests.Visual.Ranking list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo) { TotalScore = initialScore.TotalScore + i + 1 }); }); + assertScoreState(initialScore, true); assertPanelCentred(); } - [Test] - public void TestNullScore() + private void createListStep(Func creationFunc) { - AddStep("create panel with null score", () => + AddStep("create list", () => Child = list = creationFunc().With(d => { - Child = list = new ScorePanelList(null) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }; - }); + d.Anchor = Anchor.Centre; + d.Origin = Anchor.Centre; + })); - AddStep("add many panels", () => - { - for (int i = 0; i < 20; i++) - list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo) { TotalScore = initialScore.TotalScore - i - 1 }); - - for (int i = 0; i < 20; i++) - list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo) { TotalScore = initialScore.TotalScore + i + 1 }); - }); - - AddWaitStep("wait for panel animation", 5); - - AddAssert("no panel selected", () => list.ChildrenOfType().All(p => p.State != PanelState.Expanded)); - - AddStep("expand second panel", () => - { - var expandedPanel = list.ChildrenOfType().OrderBy(p => p.DrawPosition.X).ElementAt(1); - InputManager.MoveMouseTo(expandedPanel); - InputManager.Click(MouseButton.Left); - }); - - assertPanelCentred(); + AddUntilStep("wait for load", () => list.IsLoaded); } private void assertPanelCentred() => AddUntilStep("expanded panel centred", () => @@ -113,5 +154,8 @@ namespace osu.Game.Tests.Visual.Ranking var expandedPanel = list.ChildrenOfType().Single(p => p.State == PanelState.Expanded); return Precision.AlmostEquals(expandedPanel.ScreenSpaceDrawQuad.Centre.X, list.ScreenSpaceDrawQuad.Centre.X, 1); }); + + private void assertScoreState(ScoreInfo score, bool expanded) + => AddUntilStep($"correct score expanded = {expanded}", () => (list.ChildrenOfType().Single(p => p.Score == score).State == PanelState.Expanded) == expanded); } } diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 97b56d22eb..a4d1a3c26b 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -63,9 +63,10 @@ namespace osu.Game.Screens.Ranking { new ResultsScrollContainer { - Child = panels = new ScorePanelList(Score) + Child = panels = new ScorePanelList { RelativeSizeAxes = Axes.Both, + SelectedScore = { Value = Score } } } }, diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index a30d911f04..89f5a47ee8 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.Containers; @@ -23,12 +24,13 @@ namespace osu.Game.Screens.Ranking /// private const float expanded_panel_spacing = 15; + public readonly Bindable SelectedScore = new Bindable(); + private readonly Flow flow; private readonly Scroll scroll; - private ScorePanel expandedPanel; - public ScorePanelList(ScoreInfo initialScore) + public ScorePanelList() { RelativeSizeAxes = Axes.Both; @@ -44,12 +46,13 @@ namespace osu.Game.Screens.Ranking AutoSizeAxes = Axes.Both, } }; + } - if (initialScore != null) - { - AddScore(initialScore); - presentScore(initialScore); - } + protected override void LoadComplete() + { + base.LoadComplete(); + + SelectedScore.BindValueChanged(selectedScoreChanged, true); } /// @@ -67,19 +70,24 @@ namespace osu.Game.Screens.Ranking p.StateChanged += s => { if (s == PanelState.Expanded) - presentScore(score); + SelectedScore.Value = p.Score; }; })); - // We want the scroll position to remain relative to the expanded panel. When a new panel is added after the expanded panel, nothing needs to be done. - // But when a panel is added before the expanded panel, we need to offset the scroll position by the width of the new panel. - if (expandedPanel != null && flow.GetPanelIndex(score) < flow.GetPanelIndex(expandedPanel.Score)) + if (SelectedScore.Value == score) + selectedScoreChanged(new ValueChangedEvent(SelectedScore.Value, SelectedScore.Value)); + else { - // A somewhat hacky property is used here because we need to: - // 1) Scroll after the scroll container's visible range is updated. - // 2) Scroll before the scroll container's scroll position is updated. - // Without this, we would have a 1-frame positioning error which looks very jarring. - scroll.InstantScrollTarget = (scroll.InstantScrollTarget ?? scroll.Target) + ScorePanel.CONTRACTED_WIDTH + panel_spacing; + // We want the scroll position to remain relative to the expanded panel. When a new panel is added after the expanded panel, nothing needs to be done. + // But when a panel is added before the expanded panel, we need to offset the scroll position by the width of the new panel. + if (expandedPanel != null && flow.GetPanelIndex(score) < flow.GetPanelIndex(expandedPanel.Score)) + { + // A somewhat hacky property is used here because we need to: + // 1) Scroll after the scroll container's visible range is updated. + // 2) Scroll before the scroll container's scroll position is updated. + // Without this, we would have a 1-frame positioning error which looks very jarring. + scroll.InstantScrollTarget = (scroll.InstantScrollTarget ?? scroll.Target) + ScorePanel.CONTRACTED_WIDTH + panel_spacing; + } } } @@ -87,17 +95,22 @@ namespace osu.Game.Screens.Ranking /// Brings a to the centre of the screen and expands it. /// /// The to present. - private void presentScore(ScoreInfo score) + private void selectedScoreChanged(ValueChangedEvent score) { // Contract the old panel. - foreach (var p in flow.Where(p => p.Score != score)) + foreach (var p in flow.Where(p => p.Score != score.OldValue)) { p.State = PanelState.Contracted; p.Margin = new MarginPadding(); } + // Find the panel corresponding to the new score. + expandedPanel = flow.SingleOrDefault(p => p.Score == score.NewValue); + + if (expandedPanel == null) + return; + // Expand the new panel. - expandedPanel = flow.Single(p => p.Score == score); expandedPanel.State = PanelState.Expanded; expandedPanel.Margin = new MarginPadding { Horizontal = expanded_panel_spacing }; From ad99d854682af9e1404ff4a4e6f1d38e41a10ab1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 28 May 2020 21:29:16 +0900 Subject: [PATCH 1806/2376] Resolve several positioning errors --- .../Visual/Ranking/TestSceneScorePanelList.cs | 69 ++++++++++++++++--- osu.Game/Screens/Ranking/ScorePanelList.cs | 15 +++- 2 files changed, 71 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs index 588cddea7d..e65dcb19b1 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs @@ -10,6 +10,7 @@ using osu.Framework.Utils; using osu.Game.Rulesets.Osu; using osu.Game.Scoring; using osu.Game.Screens.Ranking; +using osuTK.Input; namespace osu.Game.Tests.Visual.Ranking { @@ -45,7 +46,7 @@ namespace osu.Game.Tests.Visual.Ranking AddStep("add panel", () => list.AddScore(score)); assertScoreState(score, true); - assertPanelCentred(); + assertExpandedPanelCentred(); } [Test] @@ -58,16 +59,30 @@ namespace osu.Game.Tests.Visual.Ranking AddStep("add panel", () => list.AddScore(score)); assertScoreState(score, false); - assertPanelCentred(); + assertFirstPanelCentred(); AddStep("select score", () => list.SelectedScore.Value = score); assertScoreState(score, true); - assertPanelCentred(); + assertExpandedPanelCentred(); } [Test] - public void TestAddManyScoresAfter() + public void TestAddManyNonExpandedPanels() + { + createListStep(() => new ScorePanelList()); + + AddStep("add many scores", () => + { + for (int i = 0; i < 20; i++) + list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo)); + }); + + assertFirstPanelCentred(); + } + + [Test] + public void TestAddManyScoresAfterExpandedPanel() { var initialScore = new TestScoreInfo(new OsuRuleset().RulesetInfo); @@ -86,11 +101,11 @@ namespace osu.Game.Tests.Visual.Ranking }); assertScoreState(initialScore, true); - assertPanelCentred(); + assertExpandedPanelCentred(); } [Test] - public void TestAddManyScoresBefore() + public void TestAddManyScoresBeforeExpandedPanel() { var initialScore = new TestScoreInfo(new OsuRuleset().RulesetInfo); @@ -109,11 +124,11 @@ namespace osu.Game.Tests.Visual.Ranking }); assertScoreState(initialScore, true); - assertPanelCentred(); + assertExpandedPanelCentred(); } [Test] - public void TestAddManyPanelsOnBothSides() + public void TestAddManyPanelsOnBothSidesOfExpandedPanel() { var initialScore = new TestScoreInfo(new OsuRuleset().RulesetInfo); @@ -135,7 +150,36 @@ namespace osu.Game.Tests.Visual.Ranking }); assertScoreState(initialScore, true); - assertPanelCentred(); + assertExpandedPanelCentred(); + } + + [Test] + public void TestSelectMultipleScores() + { + var firstScore = new TestScoreInfo(new OsuRuleset().RulesetInfo); + var secondScore = new TestScoreInfo(new OsuRuleset().RulesetInfo); + + createListStep(() => new ScorePanelList()); + + AddStep("add scores and select first", () => + { + list.AddScore(firstScore); + list.AddScore(secondScore); + list.SelectedScore.Value = firstScore; + }); + + assertScoreState(firstScore, true); + assertScoreState(secondScore, false); + + AddStep("select second score", () => + { + InputManager.MoveMouseTo(list.ChildrenOfType().Single(p => p.Score == secondScore)); + InputManager.Click(MouseButton.Left); + }); + + assertScoreState(firstScore, false); + assertScoreState(secondScore, true); + assertExpandedPanelCentred(); } private void createListStep(Func creationFunc) @@ -149,13 +193,16 @@ namespace osu.Game.Tests.Visual.Ranking AddUntilStep("wait for load", () => list.IsLoaded); } - private void assertPanelCentred() => AddUntilStep("expanded panel centred", () => + private void assertExpandedPanelCentred() => AddUntilStep("expanded panel centred", () => { var expandedPanel = list.ChildrenOfType().Single(p => p.State == PanelState.Expanded); return Precision.AlmostEquals(expandedPanel.ScreenSpaceDrawQuad.Centre.X, list.ScreenSpaceDrawQuad.Centre.X, 1); }); + private void assertFirstPanelCentred() + => AddUntilStep("first panel centred", () => Precision.AlmostEquals(list.ChildrenOfType().First().ScreenSpaceDrawQuad.Centre.X, list.ScreenSpaceDrawQuad.Centre.X, 1)); + private void assertScoreState(ScoreInfo score, bool expanded) - => AddUntilStep($"correct score expanded = {expanded}", () => (list.ChildrenOfType().Single(p => p.Score == score).State == PanelState.Expanded) == expanded); + => AddUntilStep($"score expanded = {expanded}", () => (list.ChildrenOfType().Single(p => p.Score == score).State == PanelState.Expanded) == expanded); } } diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index 89f5a47ee8..18db3f2af4 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -98,7 +98,7 @@ namespace osu.Game.Screens.Ranking private void selectedScoreChanged(ValueChangedEvent score) { // Contract the old panel. - foreach (var p in flow.Where(p => p.Score != score.OldValue)) + foreach (var p in flow.Where(p => p.Score == score.OldValue)) { p.State = PanelState.Contracted; p.Margin = new MarginPadding(); @@ -126,8 +126,19 @@ namespace osu.Game.Screens.Ranking { base.Update(); + float offset = DrawWidth / 2f; + // Add padding to both sides such that the centre of an expanded panel on either side is in the middle of the screen. - flow.Padding = new MarginPadding { Horizontal = DrawWidth / 2f - ScorePanel.EXPANDED_WIDTH / 2f - expanded_panel_spacing }; + + if (SelectedScore.Value != null) + { + // The expanded panel has extra padding applied to it, so it needs to be included into the offset. + offset -= ScorePanel.EXPANDED_WIDTH / 2f + expanded_panel_spacing; + } + else + offset -= ScorePanel.CONTRACTED_WIDTH / 2f; + + flow.Padding = new MarginPadding { Horizontal = offset }; } private class Flow : FillFlowContainer From 47d5974f04c77d1a4e1b59beeb2bb85892fbba33 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 28 May 2020 21:40:01 +0900 Subject: [PATCH 1807/2376] Improve results screen behaviour when changing selected score --- .../Screens/Ranking/ReplayDownloadButton.cs | 3 ++ osu.Game/Screens/Ranking/ResultsScreen.cs | 28 +++++++++++-------- osu.Game/Screens/Ranking/SoloResultsScreen.cs | 2 +- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs index a36c86eafc..347fcb5f6e 100644 --- a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs +++ b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; @@ -13,6 +14,8 @@ namespace osu.Game.Screens.Ranking { public class ReplayDownloadButton : DownloadTrackingComposite { + public Bindable Score => Model; + private DownloadButton button; private ShakeContainer shakeContainer; diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index a4d1a3c26b..fbb9b95478 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -30,16 +31,17 @@ namespace osu.Game.Screens.Ranking protected override BackgroundScreen CreateBackground() => new BackgroundScreenBeatmap(Beatmap.Value); + public readonly Bindable SelectedScore = new Bindable(); + + public readonly ScoreInfo Score; + private readonly bool allowRetry; + [Resolved(CanBeNull = true)] private Player player { get; set; } [Resolved] private IAPIProvider api { get; set; } - public readonly ScoreInfo Score; - - private readonly bool allowRetry; - private Drawable bottomPanel; private ScorePanelList panels; @@ -47,6 +49,8 @@ namespace osu.Game.Screens.Ranking { Score = score; this.allowRetry = allowRetry; + + SelectedScore.Value = score; } [BackgroundDependencyLoader] @@ -66,7 +70,7 @@ namespace osu.Game.Screens.Ranking Child = panels = new ScorePanelList { RelativeSizeAxes = Axes.Both, - SelectedScore = { Value = Score } + SelectedScore = { BindTarget = SelectedScore } } } }, @@ -95,7 +99,11 @@ namespace osu.Game.Screens.Ranking Direction = FillDirection.Horizontal, Children = new Drawable[] { - new ReplayDownloadButton(Score) { Width = 300 }, + new ReplayDownloadButton(null) + { + Score = { BindTarget = SelectedScore }, + Width = 300 + }, } } } @@ -109,6 +117,9 @@ namespace osu.Game.Screens.Ranking } }; + if (Score != null) + panels.AddScore(Score); + if (player != null && allowRetry) { buttons.Add(new RetryButton { Width = 300 }); @@ -132,12 +143,7 @@ namespace osu.Game.Screens.Ranking var req = FetchScores(scores => Schedule(() => { foreach (var s in scores) - { - if (s.OnlineScoreID == Score.OnlineScoreID) - continue; - panels.AddScore(s); - } })); if (req != null) diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs index 2b00748ed8..3ae723683a 100644 --- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs +++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs @@ -25,7 +25,7 @@ namespace osu.Game.Screens.Ranking protected override APIRequest FetchScores(Action> scoresCallback) { var req = new GetScoresRequest(Score.Beatmap, Score.Ruleset); - req.Success += r => scoresCallback?.Invoke(r.Scores.Select(s => s.CreateScoreInfo(rulesets))); + req.Success += r => scoresCallback?.Invoke(r.Scores.Where(s => s.OnlineScoreID != Score.OnlineScoreID).Select(s => s.CreateScoreInfo(rulesets))); return req; } } From 013461377e89730ee381499d816c3e9bd3c4887d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 28 May 2020 21:46:02 +0900 Subject: [PATCH 1808/2376] Fix potential nullref --- .../Gameplay/TestSceneReplayDownloadButton.cs | 17 +++++++++++++++++ .../Screens/Ranking/ReplayDownloadButton.cs | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs index a35437a286..1809332bce 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs @@ -28,6 +28,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep(@"locally available state", () => downloadButton.SetDownloadState(DownloadState.LocallyAvailable)); AddStep(@"not downloaded state", () => downloadButton.SetDownloadState(DownloadState.NotDownloaded)); createButton(false); + createButtonNoScore(); } private void createButton(bool withReplay) @@ -40,6 +41,22 @@ namespace osu.Game.Tests.Visual.Gameplay Origin = Anchor.Centre, }; }); + + AddUntilStep("wait for load", () => downloadButton.IsLoaded); + } + + private void createButtonNoScore() + { + AddStep("create button with null score", () => + { + Child = downloadButton = new TestReplayDownloadButton(null) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + }); + + AddUntilStep("wait for load", () => downloadButton.IsLoaded); } private ScoreInfo getScoreInfo(bool replayAvailable) diff --git a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs index 347fcb5f6e..9d4e3af230 100644 --- a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs +++ b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs @@ -26,7 +26,7 @@ namespace osu.Game.Screens.Ranking if (State.Value == DownloadState.LocallyAvailable) return ReplayAvailability.Local; - if (!string.IsNullOrEmpty(Model.Value.Hash)) + if (!string.IsNullOrEmpty(Model.Value?.Hash)) return ReplayAvailability.Online; return ReplayAvailability.NotAvailable; From 7ae2383288109693324d02cb6c39805a4df0a3f4 Mon Sep 17 00:00:00 2001 From: Shivam Date: Thu, 28 May 2020 15:03:49 +0200 Subject: [PATCH 1809/2376] move stable config declaration and initial reading --- osu.Game.Tournament/IPC/FileBasedIPC.cs | 29 +++++++----------------- osu.Game.Tournament/Models/StableInfo.cs | 2 -- 2 files changed, 8 insertions(+), 23 deletions(-) diff --git a/osu.Game.Tournament/IPC/FileBasedIPC.cs b/osu.Game.Tournament/IPC/FileBasedIPC.cs index d93bce8dfa..6a403c5a6a 100644 --- a/osu.Game.Tournament/IPC/FileBasedIPC.cs +++ b/osu.Game.Tournament/IPC/FileBasedIPC.cs @@ -39,6 +39,7 @@ namespace osu.Game.Tournament.IPC [Resolved] private StableInfo stableInfo { get; set; } + private const string STABLE_CONFIG = "tournament/stable.json"; public Storage IPCStorage { get; private set; } @@ -161,13 +162,14 @@ namespace osu.Game.Tournament.IPC private string findStablePath() { - string stableInstallPath = string.Empty; + if (!string.IsNullOrEmpty(stableInfo.StablePath.Value)) + return stableInfo.StablePath.Value; + string stableInstallPath = string.Empty; try { List> stableFindMethods = new List> { - readFromStableInfo, findFromEnvVar, findFromRegistry, findFromLocalAppData, @@ -180,7 +182,7 @@ namespace osu.Game.Tournament.IPC if (stableInstallPath != null) { - saveStablePath(stableInstallPath); + saveStableConfig(stableInstallPath); return stableInstallPath; } } @@ -193,11 +195,12 @@ namespace osu.Game.Tournament.IPC } } - private void saveStablePath(string path) + + private void saveStableConfig(string path) { stableInfo.StablePath.Value = path; - using (var stream = tournamentStorage.GetStream(StableInfo.STABLE_CONFIG, FileAccess.Write, FileMode.Create)) + using (var stream = tournamentStorage.GetStream(STABLE_CONFIG, FileAccess.Write, FileMode.Create)) using (var sw = new StreamWriter(stream)) { sw.Write(JsonConvert.SerializeObject(stableInfo, @@ -227,22 +230,6 @@ namespace osu.Game.Tournament.IPC return null; } - private string readFromStableInfo() - { - try - { - Logger.Log("Trying to find stable through the json config"); - - if (!string.IsNullOrEmpty(stableInfo.StablePath.Value)) - return stableInfo.StablePath.Value; - } - catch - { - } - - return null; - } - private string findFromLocalAppData() { Logger.Log("Trying to find stable in %LOCALAPPDATA%"); diff --git a/osu.Game.Tournament/Models/StableInfo.cs b/osu.Game.Tournament/Models/StableInfo.cs index 63423ca6fa..873e1c5e25 100644 --- a/osu.Game.Tournament/Models/StableInfo.cs +++ b/osu.Game.Tournament/Models/StableInfo.cs @@ -15,7 +15,5 @@ namespace osu.Game.Tournament.Models { public Bindable StablePath = new Bindable(string.Empty); - [JsonIgnore] - public const string STABLE_CONFIG = "tournament/stable.json"; } } From ee591829899d2fd64938bd9ac406e88ad72b9082 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 26 May 2020 18:12:19 +0900 Subject: [PATCH 1810/2376] Implement initial structure for room scores --- .../Requests/GetRoomPlaylistScoresRequest.cs | 21 +++++ osu.Game/Online/API/RoomScore.cs | 79 +++++++++++++++++++ .../Screens/Multi/Match/MatchSubScreen.cs | 16 ++++ .../Screens/Multi/Play/TimeshiftPlayer.cs | 8 ++ .../Multi/Ranking/TimeshiftResultsScreen.cs | 34 ++++++++ 5 files changed, 158 insertions(+) create mode 100644 osu.Game/Online/API/Requests/GetRoomPlaylistScoresRequest.cs create mode 100644 osu.Game/Online/API/RoomScore.cs create mode 100644 osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs diff --git a/osu.Game/Online/API/Requests/GetRoomPlaylistScoresRequest.cs b/osu.Game/Online/API/Requests/GetRoomPlaylistScoresRequest.cs new file mode 100644 index 0000000000..dd7f80fd46 --- /dev/null +++ b/osu.Game/Online/API/Requests/GetRoomPlaylistScoresRequest.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 System.Collections.Generic; + +namespace osu.Game.Online.API.Requests +{ + public class GetRoomPlaylistScoresRequest : APIRequest> + { + private readonly int roomId; + private readonly int playlistItemId; + + public GetRoomPlaylistScoresRequest(int roomId, int playlistItemId) + { + this.roomId = roomId; + this.playlistItemId = playlistItemId; + } + + protected override string Target => $@"rooms/{roomId}/playlist/{playlistItemId}/scores"; + } +} diff --git a/osu.Game/Online/API/RoomScore.cs b/osu.Game/Online/API/RoomScore.cs new file mode 100644 index 0000000000..5d0a9539aa --- /dev/null +++ b/osu.Game/Online/API/RoomScore.cs @@ -0,0 +1,79 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using osu.Game.Online.Multiplayer; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; + +namespace osu.Game.Online.API +{ + public class RoomScore + { + [JsonProperty("id")] + public int ID { get; set; } + + [JsonProperty("user_id")] + public int UserID { get; set; } + + [JsonProperty("room_id")] + public int RoomID { get; set; } + + [JsonProperty("playlist_item_id")] + public int PlaylistItemID { get; set; } + + [JsonProperty("beatmap_id")] + public int BeatmapID { get; set; } + + [JsonProperty("rank")] + [JsonConverter(typeof(StringEnumConverter))] + public ScoreRank Rank { get; set; } + + [JsonProperty("total_score")] + public long TotalScore { get; set; } + + [JsonProperty("accuracy")] + public double Accuracy { get; set; } + + [JsonProperty("max_combo")] + public int MaxCombo { get; set; } + + [JsonProperty("mods")] + public APIMod[] Mods { get; set; } + + [JsonProperty("statistics")] + public Dictionary Statistics = new Dictionary(); + + [JsonProperty("passed")] + public bool Passed { get; set; } + + [JsonProperty("ended_at")] + public DateTimeOffset EndedAt { get; set; } + + public ScoreInfo CreateScoreInfo(PlaylistItem playlistItem) + { + var scoreInfo = new ScoreInfo + { + OnlineScoreID = ID, + TotalScore = TotalScore, + MaxCombo = MaxCombo, + Beatmap = playlistItem.Beatmap.Value, + BeatmapInfoID = playlistItem.BeatmapID, + Ruleset = playlistItem.Ruleset.Value, + RulesetID = playlistItem.RulesetID, + User = null, // todo: do we have a user object? + Accuracy = Accuracy, + Date = EndedAt, + Hash = string.Empty, // todo: temporary? + Rank = Rank, + Mods = Array.Empty(), // todo: how? + }; + + return scoreInfo; + } + } +} diff --git a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs index caa547ac72..c37f51bcb4 100644 --- a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs +++ b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs @@ -10,6 +10,8 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Game.Audio; using osu.Game.Beatmaps; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.GameTypes; using osu.Game.Rulesets.Mods; @@ -162,6 +164,9 @@ namespace osu.Game.Screens.Multi.Match }; } + [Resolved] + private IAPIProvider api { get; set; } + protected override void LoadComplete() { base.LoadComplete(); @@ -185,6 +190,17 @@ namespace osu.Game.Screens.Multi.Match managerAdded = beatmapManager.ItemAdded.GetBoundCopy(); managerAdded.BindValueChanged(beatmapAdded); + + if (roomId.Value != null) + { + var req = new GetRoomPlaylistScoresRequest(roomId.Value.Value, playlist[0].ID); + + req.Success += scores => + { + }; + + api.Queue(req); + } } public override bool OnExiting(IScreen next) diff --git a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs index 7f58de29fb..fbe9e3480f 100644 --- a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs +++ b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs @@ -14,7 +14,9 @@ using osu.Game.Online.API.Requests; using osu.Game.Online.Multiplayer; using osu.Game.Rulesets; using osu.Game.Scoring; +using osu.Game.Screens.Multi.Ranking; using osu.Game.Screens.Play; +using osu.Game.Screens.Ranking; namespace osu.Game.Screens.Multi.Play { @@ -88,6 +90,12 @@ namespace osu.Game.Screens.Multi.Play return false; } + protected override ResultsScreen CreateResults(ScoreInfo score) + { + Debug.Assert(roomId.Value != null); + return new TimeshiftResultsScreen(score, roomId.Value.Value, playlistItem); + } + protected override ScoreInfo CreateScore() { submitScore(); diff --git a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs new file mode 100644 index 0000000000..60cffc06df --- /dev/null +++ b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs @@ -0,0 +1,34 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.Multiplayer; +using osu.Game.Scoring; +using osu.Game.Screens.Ranking; + +namespace osu.Game.Screens.Multi.Ranking +{ + public class TimeshiftResultsScreen : ResultsScreen + { + private readonly int roomId; + private readonly PlaylistItem playlistItem; + + public TimeshiftResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem, bool allowRetry = true) + : base(score, allowRetry) + { + this.roomId = roomId; + this.playlistItem = playlistItem; + } + + protected override APIRequest FetchScores(Action> scoresCallback) + { + var req = new GetRoomPlaylistScoresRequest(roomId, playlistItem.ID); + req.Success += r => scoresCallback?.Invoke(r.Select(s => s.CreateScoreInfo(playlistItem))); + return req; + } + } +} From 38502ba88c29615642e986c7dce99bbe16b92e87 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 28 May 2020 14:54:51 +0900 Subject: [PATCH 1811/2376] Remove some unnecessary members --- osu.Game/Online/API/RoomScore.cs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/osu.Game/Online/API/RoomScore.cs b/osu.Game/Online/API/RoomScore.cs index 5d0a9539aa..3ad6169833 100644 --- a/osu.Game/Online/API/RoomScore.cs +++ b/osu.Game/Online/API/RoomScore.cs @@ -20,15 +20,6 @@ namespace osu.Game.Online.API [JsonProperty("user_id")] public int UserID { get; set; } - [JsonProperty("room_id")] - public int RoomID { get; set; } - - [JsonProperty("playlist_item_id")] - public int PlaylistItemID { get; set; } - - [JsonProperty("beatmap_id")] - public int BeatmapID { get; set; } - [JsonProperty("rank")] [JsonConverter(typeof(StringEnumConverter))] public ScoreRank Rank { get; set; } From f9c64d7be38558d5b737ade5053622cbff276906 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 28 May 2020 19:57:58 +0900 Subject: [PATCH 1812/2376] Implement creation of mods --- osu.Game/Online/API/RoomScore.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/API/RoomScore.cs b/osu.Game/Online/API/RoomScore.cs index 3ad6169833..cb4f47c812 100644 --- a/osu.Game/Online/API/RoomScore.cs +++ b/osu.Game/Online/API/RoomScore.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using osu.Game.Online.Multiplayer; @@ -61,7 +62,7 @@ namespace osu.Game.Online.API Date = EndedAt, Hash = string.Empty, // todo: temporary? Rank = Rank, - Mods = Array.Empty(), // todo: how? + Mods = Mods.Select(m => m.ToMod(playlistItem.Ruleset.Value.CreateInstance())).ToArray() }; return scoreInfo; From 7ac08620b80e335f747be668dfe5eaeae2a78703 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 28 May 2020 19:58:07 +0900 Subject: [PATCH 1813/2376] Add a user object for now --- osu.Game/Online/API/RoomScore.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/API/RoomScore.cs b/osu.Game/Online/API/RoomScore.cs index cb4f47c812..00907eaa38 100644 --- a/osu.Game/Online/API/RoomScore.cs +++ b/osu.Game/Online/API/RoomScore.cs @@ -7,9 +7,9 @@ using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using osu.Game.Online.Multiplayer; -using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; +using osu.Game.Users; namespace osu.Game.Online.API { @@ -18,8 +18,8 @@ namespace osu.Game.Online.API [JsonProperty("id")] public int ID { get; set; } - [JsonProperty("user_id")] - public int UserID { get; set; } + [JsonProperty("user")] + public User User { get; set; } [JsonProperty("rank")] [JsonConverter(typeof(StringEnumConverter))] @@ -57,7 +57,7 @@ namespace osu.Game.Online.API BeatmapInfoID = playlistItem.BeatmapID, Ruleset = playlistItem.Ruleset.Value, RulesetID = playlistItem.RulesetID, - User = null, // todo: do we have a user object? + User = User, Accuracy = Accuracy, Date = EndedAt, Hash = string.Empty, // todo: temporary? From d88bfa2080f7c1dcf852dfa278fd7f068dd3d8dc Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 28 May 2020 20:07:51 +0900 Subject: [PATCH 1814/2376] Cache ruleset + fix possible nullrefs --- osu.Game/Online/API/RoomScore.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/API/RoomScore.cs b/osu.Game/Online/API/RoomScore.cs index 00907eaa38..a1b08fe40e 100644 --- a/osu.Game/Online/API/RoomScore.cs +++ b/osu.Game/Online/API/RoomScore.cs @@ -7,6 +7,7 @@ using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using osu.Game.Online.Multiplayer; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Users; @@ -48,6 +49,8 @@ namespace osu.Game.Online.API public ScoreInfo CreateScoreInfo(PlaylistItem playlistItem) { + var rulesetInstance = playlistItem.Ruleset.Value.CreateInstance(); + var scoreInfo = new ScoreInfo { OnlineScoreID = ID, @@ -62,7 +65,7 @@ namespace osu.Game.Online.API Date = EndedAt, Hash = string.Empty, // todo: temporary? Rank = Rank, - Mods = Mods.Select(m => m.ToMod(playlistItem.Ruleset.Value.CreateInstance())).ToArray() + Mods = Mods?.Select(m => m.ToMod(rulesetInstance)).ToArray() ?? Array.Empty() }; return scoreInfo; From 0e28ded80fc4b078b10c9666f1399ce8fa994aa8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 28 May 2020 20:07:54 +0900 Subject: [PATCH 1815/2376] Forward statistics --- osu.Game/Online/API/RoomScore.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Online/API/RoomScore.cs b/osu.Game/Online/API/RoomScore.cs index a1b08fe40e..3c7f8c9833 100644 --- a/osu.Game/Online/API/RoomScore.cs +++ b/osu.Game/Online/API/RoomScore.cs @@ -60,6 +60,7 @@ namespace osu.Game.Online.API BeatmapInfoID = playlistItem.BeatmapID, Ruleset = playlistItem.Ruleset.Value, RulesetID = playlistItem.RulesetID, + Statistics = Statistics, User = User, Accuracy = Accuracy, Date = EndedAt, From 0f373acacb55be124e573144a8b112291ea82fdd Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 28 May 2020 20:08:45 +0900 Subject: [PATCH 1816/2376] Add test scene --- .../TestSceneTimeshiftResultsScreen.cs | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs new file mode 100644 index 0000000000..7f43aea56e --- /dev/null +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs @@ -0,0 +1,76 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using NUnit.Framework; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.Multiplayer; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Screens.Multi.Ranking; +using osu.Game.Tests.Beatmaps; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + public class TestSceneTimeshiftResultsScreen : ScreenTestScene + { + [Test] + public void TestShowResults() + { + var score = new TestScoreInfo(new OsuRuleset().RulesetInfo); + var roomScores = new List(); + + for (int i = 0; i < 10; i++) + { + roomScores.Add(new RoomScore + { + ID = i, + Accuracy = 0.9 - 0.01 * i, + EndedAt = DateTimeOffset.Now.Subtract(TimeSpan.FromHours(i)), + Passed = true, + Rank = ScoreRank.B, + MaxCombo = 999, + TotalScore = 999999 - i * 1000, + User = new User + { + Id = 2, + Username = $"peppy{i}", + CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + }, + Statistics = + { + { HitResult.Miss, 1 }, + { HitResult.Meh, 50 }, + { HitResult.Good, 100 }, + { HitResult.Great, 300 }, + } + }); + } + + AddStep("bind request handler", () => ((DummyAPIAccess)API).HandleRequest = request => + { + switch (request) + { + case GetRoomPlaylistScoresRequest r: + r.TriggerSuccess(roomScores); + break; + } + }); + + AddStep("load results", () => + { + LoadScreen(new TimeshiftResultsScreen(score, 1, new PlaylistItem + { + Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, + Ruleset = { Value = new OsuRuleset().RulesetInfo } + })); + }); + + AddWaitStep("wait for display", 10); + } + } +} From a606f41297931301e66225c6ada60cf5fa6b326b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 28 May 2020 22:25:00 +0900 Subject: [PATCH 1817/2376] Add button to open results --- .../TestSceneTimeshiftResultsScreen.cs | 14 ++++++-- .../Screens/Multi/Match/MatchSubScreen.cs | 33 +++++++++++++++++-- .../Multi/Ranking/TimeshiftResultsScreen.cs | 2 +- 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs index 7f43aea56e..d87a2e3408 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs @@ -19,9 +19,19 @@ namespace osu.Game.Tests.Visual.Multiplayer public class TestSceneTimeshiftResultsScreen : ScreenTestScene { [Test] - public void TestShowResults() + public void TestShowResultsWithScore() + { + createResults(new TestScoreInfo(new OsuRuleset().RulesetInfo)); + } + + [Test] + public void TestShowResultsNullScore() + { + createResults(null); + } + + private void createResults(ScoreInfo score) { - var score = new TestScoreInfo(new OsuRuleset().RulesetInfo); var roomScores = new List(); for (int i = 0; i < 10; i++) diff --git a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs index c37f51bcb4..01a90139c3 100644 --- a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs +++ b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -10,6 +11,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Game.Audio; using osu.Game.Beatmaps; +using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.Multiplayer; @@ -18,6 +20,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Screens.Multi.Components; using osu.Game.Screens.Multi.Match.Components; using osu.Game.Screens.Multi.Play; +using osu.Game.Screens.Multi.Ranking; using osu.Game.Screens.Select; using Footer = osu.Game.Screens.Multi.Match.Components.Footer; @@ -114,10 +117,29 @@ namespace osu.Game.Screens.Multi.Match { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Horizontal = 5 }, - Child = new OverlinedPlaylist(true) // Temporarily always allow selection + Child = new GridContainer { RelativeSizeAxes = Axes.Both, - SelectedItem = { BindTarget = SelectedItem } + Content = new[] + { + new Drawable[] + { + new OverlinedPlaylist(true) // Temporarily always allow selection + { + RelativeSizeAxes = Axes.Both, + SelectedItem = { BindTarget = SelectedItem } + } + }, + new Drawable[] + { + new TriangleButton + { + RelativeSizeAxes = Axes.X, + Text = "Show beatmap results", + Action = showBeatmapResults + } + } + } } }, new Container @@ -257,5 +279,12 @@ namespace osu.Game.Screens.Multi.Match break; } } + + private void showBeatmapResults() + { + Debug.Assert(roomId.Value != null); + + this.Push(new TimeshiftResultsScreen(null, roomId.Value.Value, SelectedItem.Value, false)); + } } } diff --git a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs index 60cffc06df..f2afe15d35 100644 --- a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs +++ b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs @@ -27,7 +27,7 @@ namespace osu.Game.Screens.Multi.Ranking protected override APIRequest FetchScores(Action> scoresCallback) { var req = new GetRoomPlaylistScoresRequest(roomId, playlistItem.ID); - req.Success += r => scoresCallback?.Invoke(r.Select(s => s.CreateScoreInfo(playlistItem))); + req.Success += r => scoresCallback?.Invoke(r.Where(s => s.ID != Score?.OnlineScoreID).Select(s => s.CreateScoreInfo(playlistItem))); return req; } } From 3731e76b10b99a1307e222355efe95df3a5efb2d Mon Sep 17 00:00:00 2001 From: Shivam Date: Thu, 28 May 2020 15:28:27 +0200 Subject: [PATCH 1818/2376] Move stable_config declaration, rename testscene --- ...thSelectScreens.cs => TestSceneStablePathSelectScreen.cs} | 0 osu.Game.Tournament/IPC/FileBasedIPC.cs | 5 +++-- osu.Game.Tournament/Models/StableInfo.cs | 2 -- osu.Game.Tournament/Screens/StablePathSelectScreen.cs | 2 +- osu.Game.Tournament/TournamentGameBase.cs | 4 ++-- 5 files changed, 6 insertions(+), 7 deletions(-) rename osu.Game.Tournament.Tests/Screens/{TestSceneStablePathSelectScreens.cs => TestSceneStablePathSelectScreen.cs} (100%) diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneStablePathSelectScreens.cs b/osu.Game.Tournament.Tests/Screens/TestSceneStablePathSelectScreen.cs similarity index 100% rename from osu.Game.Tournament.Tests/Screens/TestSceneStablePathSelectScreens.cs rename to osu.Game.Tournament.Tests/Screens/TestSceneStablePathSelectScreen.cs diff --git a/osu.Game.Tournament/IPC/FileBasedIPC.cs b/osu.Game.Tournament/IPC/FileBasedIPC.cs index 6a403c5a6a..8518b7f8da 100644 --- a/osu.Game.Tournament/IPC/FileBasedIPC.cs +++ b/osu.Game.Tournament/IPC/FileBasedIPC.cs @@ -39,7 +39,8 @@ namespace osu.Game.Tournament.IPC [Resolved] private StableInfo stableInfo { get; set; } - private const string STABLE_CONFIG = "tournament/stable.json"; + + public const string STABLE_CONFIG = "tournament/stable.json"; public Storage IPCStorage { get; private set; } @@ -166,6 +167,7 @@ namespace osu.Game.Tournament.IPC return stableInfo.StablePath.Value; string stableInstallPath = string.Empty; + try { List> stableFindMethods = new List> @@ -195,7 +197,6 @@ namespace osu.Game.Tournament.IPC } } - private void saveStableConfig(string path) { stableInfo.StablePath.Value = path; diff --git a/osu.Game.Tournament/Models/StableInfo.cs b/osu.Game.Tournament/Models/StableInfo.cs index 873e1c5e25..4818842151 100644 --- a/osu.Game.Tournament/Models/StableInfo.cs +++ b/osu.Game.Tournament/Models/StableInfo.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using Newtonsoft.Json; using osu.Framework.Bindables; namespace osu.Game.Tournament.Models @@ -14,6 +13,5 @@ namespace osu.Game.Tournament.Models public class StableInfo { public Bindable StablePath = new Bindable(string.Empty); - } } diff --git a/osu.Game.Tournament/Screens/StablePathSelectScreen.cs b/osu.Game.Tournament/Screens/StablePathSelectScreen.cs index d2c7225909..eace3c78d5 100644 --- a/osu.Game.Tournament/Screens/StablePathSelectScreen.cs +++ b/osu.Game.Tournament/Screens/StablePathSelectScreen.cs @@ -38,7 +38,7 @@ namespace osu.Game.Tournament.Screens [BackgroundDependencyLoader(true)] private void load(Storage storage, OsuColour colours) { - var initialPath = new DirectoryInfo(storage.GetFullPath(stableInfo.StablePath.Value ?? string.Empty)).Parent?.FullName; + var initialPath = new DirectoryInfo(storage.GetFullPath(stableInfo.StablePath.Value ?? string.Empty)).Parent?.FullName; AddRangeInternal(new Drawable[] { diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 7d7d4f84aa..dcfe646390 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -148,9 +148,9 @@ namespace osu.Game.Tournament if (stableInfo == null) stableInfo = new StableInfo(); - if (storage.Exists(StableInfo.STABLE_CONFIG)) + if (storage.Exists(FileBasedIPC.STABLE_CONFIG)) { - using (Stream stream = storage.GetStream(StableInfo.STABLE_CONFIG, FileAccess.Read, FileMode.Open)) + using (Stream stream = storage.GetStream(FileBasedIPC.STABLE_CONFIG, FileAccess.Read, FileMode.Open)) using (var sr = new StreamReader(stream)) { stableInfo = JsonConvert.DeserializeObject(sr.ReadToEnd()); From 46689a2fbc46281277feb6fe806b0f756e1bfddc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 May 2020 11:46:08 +0900 Subject: [PATCH 1819/2376] Tidy up and complete xmldoc for HitObjectComposer --- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 89 ++++++++++++++++----- osu.sln.DotSettings | 5 +- 2 files changed, 74 insertions(+), 20 deletions(-) diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 38576e02a0..c956439eb2 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -48,8 +48,6 @@ namespace osu.Game.Rulesets.Edit protected ComposeBlueprintContainer BlueprintContainer { get; private set; } - public override Playfield Playfield => drawableRulesetWrapper.Playfield; - private DrawableEditRulesetWrapper drawableRulesetWrapper; protected readonly Container LayerBelowRuleset = new Container { RelativeSizeAxes = Axes.Both }; @@ -61,7 +59,6 @@ namespace osu.Game.Rulesets.Edit protected HitObjectComposer(Ruleset ruleset) { Ruleset = ruleset; - RelativeSizeAxes = Axes.Both; } [BackgroundDependencyLoader] @@ -137,6 +134,49 @@ namespace osu.Game.Rulesets.Edit EditorBeatmap.SelectedHitObjects.CollectionChanged += selectionChanged; } + protected override void LoadComplete() + { + base.LoadComplete(); + + inputManager = GetContainingInputManager(); + } + + public override Playfield Playfield => drawableRulesetWrapper.Playfield; + + public override IEnumerable HitObjects => drawableRulesetWrapper.Playfield.AllHitObjects; + + public override bool CursorInPlacementArea => drawableRulesetWrapper.Playfield.ReceivePositionalInputAt(inputManager.CurrentState.Mouse.Position); + + /// + /// Defines all available composition tools, listed on the left side of the editor screen as button controls. + /// This should usually define one tool for each type used in the target ruleset. + /// + /// + /// A "select" tool is automatically added as the first tool. + /// + protected abstract IReadOnlyList CompositionTools { get; } + + /// + /// Construct a relevant blueprint container. This will manage hitobject selection/placement input handling and display logic. + /// + protected abstract ComposeBlueprintContainer CreateBlueprintContainer(); + + /// + /// Construct a drawable ruleset for the provided ruleset. + /// + /// + /// Can be overridden to add editor-specific logical changes to a 's standard . + /// For example, hit animations or judgement logic may be changed to give a better editor user experience. + /// + /// The ruleset used to construct its drawable counterpart. + /// The loaded beatmap. + /// The mods to be applied. + /// An editor-relevant . + protected virtual DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) + => (DrawableRuleset)ruleset.CreateDrawableRulesetWith(beatmap, mods); + + #region Tool selection logic + protected override bool OnKeyDown(KeyDownEvent e) { if (e.Key >= Key.Number1 && e.Key <= Key.Number9) @@ -153,13 +193,6 @@ namespace osu.Game.Rulesets.Edit return base.OnKeyDown(e); } - protected override void LoadComplete() - { - base.LoadComplete(); - - inputManager = GetContainingInputManager(); - } - private void selectionChanged(object sender, NotifyCollectionChangedEventArgs changedArgs) { if (EditorBeatmap.SelectedHitObjects.Any()) @@ -179,15 +212,9 @@ namespace osu.Game.Rulesets.Edit EditorBeatmap.SelectedHitObjects.Clear(); } - public override IEnumerable HitObjects => drawableRulesetWrapper.Playfield.AllHitObjects; + #endregion - public override bool CursorInPlacementArea => drawableRulesetWrapper.Playfield.ReceivePositionalInputAt(inputManager.CurrentState.Mouse.Position); - - protected abstract IReadOnlyList CompositionTools { get; } - - protected abstract ComposeBlueprintContainer CreateBlueprintContainer(); - - protected abstract DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null); + #region IPlacementHandler public void BeginPlacement(HitObject hitObject) { @@ -209,6 +236,17 @@ namespace osu.Game.Rulesets.Edit public void Delete(HitObject hitObject) => EditorBeatmap.Remove(hitObject); + #endregion + + #region IPositionSnapProvider + + /// + /// Retrieve the relevant at a specified screen-space position. + /// In cases where a ruleset doesn't require custom logic (due to nested playfields, for example) + /// this will return the ruleset's main playfield. + /// + /// The screen-space position to query. + /// The most relevant . protected virtual Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) => drawableRulesetWrapper.Playfield; public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) @@ -257,8 +295,14 @@ namespace osu.Game.Rulesets.Edit return DurationToDistance(referenceTime, snappedEndTime - referenceTime); } + + #endregion } + /// + /// A non-generic definition of a HitObject composer class. + /// Generally used to access certain methods without requiring a generic type for . + /// [Cached(typeof(HitObjectComposer))] [Cached(typeof(IPositionSnapProvider))] public abstract class HitObjectComposer : CompositeDrawable, IPositionSnapProvider @@ -268,10 +312,13 @@ namespace osu.Game.Rulesets.Edit RelativeSizeAxes = Axes.Both; } + /// + /// The target ruleset's playfield. + /// public abstract Playfield Playfield { get; } /// - /// All the s. + /// All s in currently loaded beatmap. /// public abstract IEnumerable HitObjects { get; } @@ -280,6 +327,8 @@ namespace osu.Game.Rulesets.Edit /// public abstract bool CursorInPlacementArea { get; } + #region IPositionSnapProvider + public abstract SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition); public abstract float GetBeatSnapDistanceAt(double referenceTime); @@ -291,5 +340,7 @@ namespace osu.Game.Rulesets.Edit public abstract double GetSnappedDurationFromDistance(double referenceTime, float distance); public abstract float GetSnappedDistanceFromDistance(double referenceTime, float distance); + + #endregion } } diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index e3b64c03b9..b9fc3de734 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -1,4 +1,4 @@ - + True True True @@ -905,14 +905,17 @@ private void load() True True True + True True True True True True True + True True True True + True True True From 8fa8c561e7bc438eb5c64473d70d50e6de4c4584 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 May 2020 12:20:50 +0900 Subject: [PATCH 1820/2376] Pass hitobjects as a parameter to CreateBlueprintContainer --- osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs | 4 +++- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 4 +++- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 6 ++++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index 10d344242c..7e2469a794 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -13,6 +13,7 @@ using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Screens.Edit.Compose.Components; @@ -88,7 +89,8 @@ namespace osu.Game.Rulesets.Mania.Edit return drawableRuleset; } - protected override ComposeBlueprintContainer CreateBlueprintContainer() => new ManiaBlueprintContainer(drawableRuleset.Playfield.AllHitObjects); + protected override ComposeBlueprintContainer CreateBlueprintContainer(IEnumerable hitObjects) + => new ManiaBlueprintContainer(hitObjects); protected override IReadOnlyList CompositionTools => new HitObjectCompositionTool[] { diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index de5c1e54d7..37019a7a05 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -13,6 +13,7 @@ using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.UI; using osu.Game.Screens.Edit.Compose.Components; @@ -46,7 +47,8 @@ namespace osu.Game.Rulesets.Osu.Edit EditorBeatmap.PlacementObject.ValueChanged += _ => updateDistanceSnapGrid(); } - protected override ComposeBlueprintContainer CreateBlueprintContainer() => new OsuBlueprintContainer(HitObjects); + protected override ComposeBlueprintContainer CreateBlueprintContainer(IEnumerable hitObjects) + => new OsuBlueprintContainer(hitObjects); private DistanceSnapGrid distanceSnapGrid; private Container distanceSnapGridContainer; diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index c956439eb2..8b9f531417 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -113,7 +113,7 @@ namespace osu.Game.Rulesets.Edit drawableRulesetWrapper, // layers above playfield drawableRulesetWrapper.CreatePlayfieldAdjustmentContainer() - .WithChild(BlueprintContainer = CreateBlueprintContainer()) + .WithChild(BlueprintContainer = CreateBlueprintContainer(HitObjects)) } } }, @@ -159,7 +159,9 @@ namespace osu.Game.Rulesets.Edit /// /// Construct a relevant blueprint container. This will manage hitobject selection/placement input handling and display logic. /// - protected abstract ComposeBlueprintContainer CreateBlueprintContainer(); + /// A live collection of all s in the editor beatmap. + protected virtual ComposeBlueprintContainer CreateBlueprintContainer(IEnumerable hitObjects) + => new ComposeBlueprintContainer(hitObjects); /// /// Construct a drawable ruleset for the provided ruleset. From f9883373bbd86eb0513968d6dd487b95f4ce759f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 May 2020 16:11:26 +0900 Subject: [PATCH 1821/2376] Flip direction to avoid breaking other usages --- osu.Game/Rulesets/Objects/Types/IHasDuration.cs | 16 +++++++++++++--- osu.Game/Rulesets/Objects/Types/IHasEndTime.cs | 16 +++++++++++++++- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Types/IHasDuration.cs b/osu.Game/Rulesets/Objects/Types/IHasDuration.cs index 2433f9597e..185fd5977b 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasDuration.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasDuration.cs @@ -8,17 +8,27 @@ namespace osu.Game.Rulesets.Objects.Types /// /// A HitObject that ends at a different time than its start time. /// - public interface IHasDuration +#pragma warning disable 618 + public interface IHasDuration : IHasEndTime +#pragma warning restore 618 { + double IHasEndTime.EndTime + { + get => EndTime; + set => Duration = (Duration - EndTime) + value; + } + + double IHasEndTime.Duration => Duration; + /// /// The time at which the HitObject ends. /// - double EndTime { get; } + new double EndTime { get; } /// /// The duration of the HitObject. /// [JsonIgnore] - double Duration { get; set; } + new double Duration { get; set; } } } diff --git a/osu.Game/Rulesets/Objects/Types/IHasEndTime.cs b/osu.Game/Rulesets/Objects/Types/IHasEndTime.cs index 7395223c7e..c3769c5909 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasEndTime.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasEndTime.cs @@ -2,11 +2,25 @@ // See the LICENCE file in the repository root for full licence text. using System; +using Newtonsoft.Json; namespace osu.Game.Rulesets.Objects.Types { + /// + /// A HitObject that ends at a different time than its start time. + /// [Obsolete("Use IHasDuration instead.")] // can be removed 20201126 - public interface IHasEndTime : IHasDuration + public interface IHasEndTime { + /// + /// The time at which the HitObject ends. + /// + [JsonIgnore] + double EndTime { get; set; } + + /// + /// The duration of the HitObject. + /// + double Duration { get; } } } From 7d4e60f05e01b77932ffac10ab5f5735a85a3015 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 23 May 2020 16:06:25 +0900 Subject: [PATCH 1822/2376] Add basic setup for TaikoHitObjectComposer --- .../TestSceneTaikoHitObjectComposer.cs | 58 +++++++++++++++++++ .../TaikoHitObjectComposer.cs | 34 +++++++++++ osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 3 + 3 files changed, 95 insertions(+) create mode 100644 osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoHitObjectComposer.cs create mode 100644 osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoHitObjectComposer.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoHitObjectComposer.cs new file mode 100644 index 0000000000..0fff08fab7 --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoHitObjectComposer.cs @@ -0,0 +1,58 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Taiko.Beatmaps; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Screens.Edit; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Taiko.Tests +{ + public class TestSceneTaikoHitObjectComposer : EditorClockTestScene + { + private TestComposer composer; + + [SetUp] + public void Setup() => Schedule(() => + { + BeatDivisor.Value = 8; + Clock.Seek(0); + + Child = composer = new TestComposer { RelativeSizeAxes = Axes.Both }; + }); + + [Test] + public void BasicTest() + { + } + + private class TestComposer : CompositeDrawable + { + [Cached(typeof(EditorBeatmap))] + [Cached(typeof(IBeatSnapProvider))] + public readonly EditorBeatmap EditorBeatmap; + + public readonly TaikoHitObjectComposer Composer; + + public TestComposer() + { + InternalChildren = new Drawable[] + { + EditorBeatmap = new EditorBeatmap(new TaikoBeatmap()) + { + BeatmapInfo = { Ruleset = new TaikoRuleset().RulesetInfo } + }, + Composer = new TaikoHitObjectComposer(new TaikoRuleset()) + }; + + for (int i = 0; i < 10; i++) + EditorBeatmap.Add(new Hit { StartTime = 125 * i }); + } + } + } +} diff --git a/osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.cs b/osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.cs new file mode 100644 index 0000000000..069517fe46 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.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.Collections.Generic; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Tools; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.UI; +using osu.Game.Rulesets.UI; +using osu.Game.Screens.Edit.Compose.Components; + +namespace osu.Game.Rulesets.Taiko +{ + public class TaikoHitObjectComposer : HitObjectComposer + { + private DrawableTaikoRuleset drawableRuleset; + + public TaikoHitObjectComposer(Ruleset ruleset) + : base(ruleset) + { + } + + protected override IReadOnlyList CompositionTools => new List(); + + protected override ComposeBlueprintContainer CreateBlueprintContainer() => new ComposeBlueprintContainer(drawableRuleset.Playfield.AllHitObjects); + + protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) + { + return drawableRuleset = new DrawableTaikoRuleset(ruleset, beatmap, mods); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 74d9e68ad3..7be16471b4 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -21,6 +21,7 @@ using osu.Game.Rulesets.Taiko.Difficulty; using osu.Game.Rulesets.Taiko.Scoring; using osu.Game.Scoring; using System; +using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Taiko.Skinning; using osu.Game.Skinning; @@ -144,6 +145,8 @@ namespace osu.Game.Rulesets.Taiko public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.RulesetTaiko }; + public override HitObjectComposer CreateHitObjectComposer() => new TaikoHitObjectComposer(this); + public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new TaikoDifficultyCalculator(this, beatmap); public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => new TaikoPerformanceCalculator(this, beatmap, score); From 90acba8c36ded4f5d6c3873d46c99e27a5ff9517 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 May 2020 19:23:36 +0900 Subject: [PATCH 1823/2376] Introduce initial placement blueprint logic --- .../TestSceneTaikoHitObjectComposer.cs | 8 +- .../TaikoHitObjectComposer.cs | 119 +++++++++++++++++- 2 files changed, 119 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoHitObjectComposer.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoHitObjectComposer.cs index 0fff08fab7..b5ee33fa8e 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoHitObjectComposer.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoHitObjectComposer.cs @@ -15,15 +15,13 @@ namespace osu.Game.Rulesets.Taiko.Tests { public class TestSceneTaikoHitObjectComposer : EditorClockTestScene { - private TestComposer composer; - [SetUp] public void Setup() => Schedule(() => { BeatDivisor.Value = 8; Clock.Seek(0); - Child = composer = new TestComposer { RelativeSizeAxes = Axes.Both }; + Child = new TestComposer { RelativeSizeAxes = Axes.Both }; }); [Test] @@ -37,8 +35,6 @@ namespace osu.Game.Rulesets.Taiko.Tests [Cached(typeof(IBeatSnapProvider))] public readonly EditorBeatmap EditorBeatmap; - public readonly TaikoHitObjectComposer Composer; - public TestComposer() { InternalChildren = new Drawable[] @@ -47,7 +43,7 @@ namespace osu.Game.Rulesets.Taiko.Tests { BeatmapInfo = { Ruleset = new TaikoRuleset().RulesetInfo } }, - Composer = new TaikoHitObjectComposer(new TaikoRuleset()) + new TaikoHitObjectComposer(new TaikoRuleset()) }; for (int i = 0; i < 10; i++) diff --git a/osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.cs b/osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.cs index 069517fe46..dfdc91f7dc 100644 --- a/osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.cs +++ b/osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.cs @@ -2,14 +2,22 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.UI; using osu.Game.Rulesets.UI; using osu.Game.Screens.Edit.Compose.Components; +using osuTK; +using osuTK.Graphics; +using osuTK.Input; namespace osu.Game.Rulesets.Taiko { @@ -22,13 +30,120 @@ namespace osu.Game.Rulesets.Taiko { } - protected override IReadOnlyList CompositionTools => new List(); + protected override IReadOnlyList CompositionTools => new[] + { + new HitCompositionTool() + }; - protected override ComposeBlueprintContainer CreateBlueprintContainer() => new ComposeBlueprintContainer(drawableRuleset.Playfield.AllHitObjects); + protected override ComposeBlueprintContainer CreateBlueprintContainer() => new TaikoBlueprintContainer(drawableRuleset.Playfield.AllHitObjects); protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) { return drawableRuleset = new DrawableTaikoRuleset(ruleset, beatmap, mods); } } + + public class TaikoBlueprintContainer : ComposeBlueprintContainer + { + public TaikoBlueprintContainer(IEnumerable hitObjects) + : base(hitObjects) + { + } + + public override OverlaySelectionBlueprint CreateBlueprintFor(DrawableHitObject hitObject) => + new TaikoSelectionBlueprint(hitObject); + } + + public class TaikoSelectionBlueprint : OverlaySelectionBlueprint + { + public TaikoSelectionBlueprint(DrawableHitObject hitObject) + : base(hitObject) + { + RelativeSizeAxes = Axes.None; + + AddInternal(new HitPiece { RelativeSizeAxes = Axes.Both }); + } + + protected override void Update() + { + base.Update(); + + // Move the rectangle to cover the hitobjects + var topLeft = new Vector2(float.MaxValue, float.MaxValue); + var bottomRight = new Vector2(float.MinValue, float.MinValue); + + topLeft = Vector2.ComponentMin(topLeft, Parent.ToLocalSpace(DrawableObject.ScreenSpaceDrawQuad.TopLeft)); + bottomRight = Vector2.ComponentMax(bottomRight, Parent.ToLocalSpace(DrawableObject.ScreenSpaceDrawQuad.BottomRight)); + + Size = bottomRight - topLeft; + Position = topLeft; + } + } + + public class HitCompositionTool : HitObjectCompositionTool + { + public HitCompositionTool() + : base(nameof(Hit)) + { + } + + public override PlacementBlueprint CreatePlacementBlueprint() => new HitPlacementBlueprint(); + } + + public class HitPlacementBlueprint : PlacementBlueprint + + { + private readonly HitPiece piece; + + public HitPlacementBlueprint() + : base(new Hit()) + { + InternalChild = piece = new HitPiece + { + Size = new Vector2(TaikoHitObject.DEFAULT_SIZE * TaikoPlayfield.DEFAULT_HEIGHT) + }; + } + + protected override bool OnMouseDown(MouseDownEvent e) + { + if (e.Button == MouseButton.Left) + { + EndPlacement(true); + return true; + } + + return base.OnMouseDown(e); + } + + public override void UpdatePosition(SnapResult snapResult) + { + piece.Position = ToLocalSpace(snapResult.ScreenSpacePosition); + base.UpdatePosition(snapResult); + } + } + + public class HitPiece : CompositeDrawable + { + public HitPiece() + { + Origin = Anchor.Centre; + + InternalChild = new CircularContainer + { + Masking = true, + BorderThickness = 10, + BorderColour = Color4.Yellow, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + AlwaysPresent = true, + Alpha = 0, + RelativeSizeAxes = Axes.Both + } + } + }; + } + } } From 4b1a2b5bc2d1481120d60700270c7b48436d853c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 May 2020 22:36:49 +0900 Subject: [PATCH 1824/2376] Fix offsets --- osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.cs b/osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.cs index dfdc91f7dc..c820b3eb92 100644 --- a/osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.cs +++ b/osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.cs @@ -61,7 +61,11 @@ namespace osu.Game.Rulesets.Taiko { RelativeSizeAxes = Axes.None; - AddInternal(new HitPiece { RelativeSizeAxes = Axes.Both }); + AddInternal(new HitPiece + { + RelativeSizeAxes = Axes.Both, + Origin = Anchor.TopLeft + }); } protected override void Update() From 3487c1fd1b9e1d53df72f60cece30f890c239637 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 May 2020 13:51:53 +0900 Subject: [PATCH 1825/2376] Add menus to mark as rim and strong --- .../TestSceneEditor.cs | 17 +++++ osu.Game.Rulesets.Taiko/Objects/Hit.cs | 10 ++- .../Objects/TaikoHitObject.cs | 9 ++- .../TaikoHitObjectComposer.cs | 70 +++++++++++++++++++ .../UserInterface/TernaryStateMenuItem.cs | 12 +--- 5 files changed, 105 insertions(+), 13 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko.Tests/TestSceneEditor.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneEditor.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneEditor.cs new file mode 100644 index 0000000000..089a7ad00b --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneEditor.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 NUnit.Framework; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Taiko.Tests +{ + [TestFixture] + public class TestSceneEditor : EditorTestScene + { + public TestSceneEditor() + : base(new TaikoRuleset()) + { + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Objects/Hit.cs b/osu.Game.Rulesets.Taiko/Objects/Hit.cs index 2aca701515..68cc8d0ead 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Hit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Hit.cs @@ -1,13 +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.Bindables; + namespace osu.Game.Rulesets.Taiko.Objects { public class Hit : TaikoHitObject { + public readonly Bindable TypeBindable = new Bindable(); + /// /// The that actuates this . /// - public HitType Type { get; set; } + public HitType Type + { + get => TypeBindable.Value; + set => TypeBindable.Value = value; + } } } diff --git a/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs index 206bfcfdb2..4de762ce30 100644 --- a/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Threading; +using osu.Framework.Bindables; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; @@ -27,11 +28,17 @@ namespace osu.Game.Rulesets.Taiko.Objects /// public const float DEFAULT_STRONG_SIZE = DEFAULT_SIZE * STRONG_SCALE; + public readonly Bindable IsStrongBindable = new BindableBool(); + /// /// Whether this HitObject is a "strong" type. /// Strong hit objects give more points for hitting the hit object with both keys. /// - public virtual bool IsStrong { get; set; } + public virtual bool IsStrong + { + get => IsStrongBindable.Value; + set => IsStrongBindable.Value = value; + } protected override void CreateNestedHitObjects(CancellationToken cancellationToken) { diff --git a/osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.cs b/osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.cs index c820b3eb92..beef2b6155 100644 --- a/osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.cs +++ b/osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.cs @@ -1,12 +1,16 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; +using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Game.Beatmaps; +using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Mods; @@ -50,10 +54,76 @@ namespace osu.Game.Rulesets.Taiko { } + protected override SelectionHandler CreateSelectionHandler() => new TaikoSelectionHandler(); + public override OverlaySelectionBlueprint CreateBlueprintFor(DrawableHitObject hitObject) => new TaikoSelectionBlueprint(hitObject); } + public class TaikoSelectionHandler : SelectionHandler + { + protected override IEnumerable GetContextMenuItemsForSelection(IEnumerable selection) + { + if (selection.All(s => s.HitObject is Hit)) + { + var hits = selection.Select(s => s.HitObject).OfType(); + + yield return new TernaryStateMenuItem("Rim", action: state => + { + foreach (var h in hits) + { + switch (state) + { + case TernaryState.True: + h.Type = HitType.Rim; + break; + + case TernaryState.False: + h.Type = HitType.Centre; + break; + } + } + }) + { + State = { Value = getTernaryState(hits, h => h.Type == HitType.Rim) } + }; + } + + if (selection.All(s => s.HitObject is TaikoHitObject)) + { + var hits = selection.Select(s => s.HitObject).OfType(); + + yield return new TernaryStateMenuItem("Strong", action: state => + { + foreach (var h in hits) + { + switch (state) + { + case TernaryState.True: + h.IsStrong = true; + break; + + case TernaryState.False: + h.IsStrong = false; + break; + } + } + }) + { + State = { Value = getTernaryState(hits, h => h.IsStrong) } + }; + } + } + + private TernaryState getTernaryState(IEnumerable selection, Func func) + { + if (selection.Any(func)) + return selection.All(func) ? TernaryState.True : TernaryState.Indeterminate; + + return TernaryState.False; + } + } + public class TaikoSelectionBlueprint : OverlaySelectionBlueprint { public TaikoSelectionBlueprint(DrawableHitObject hitObject) diff --git a/osu.Game/Graphics/UserInterface/TernaryStateMenuItem.cs b/osu.Game/Graphics/UserInterface/TernaryStateMenuItem.cs index 2d9e2106d4..acf4065f49 100644 --- a/osu.Game/Graphics/UserInterface/TernaryStateMenuItem.cs +++ b/osu.Game/Graphics/UserInterface/TernaryStateMenuItem.cs @@ -11,23 +11,13 @@ namespace osu.Game.Graphics.UserInterface /// public class TernaryStateMenuItem : StatefulMenuItem { - /// - /// Creates a new . - /// - /// The text to display. - /// The type of action which this performs. - public TernaryStateMenuItem(string text, MenuItemType type = MenuItemType.Standard) - : this(text, type, null) - { - } - /// /// Creates a new . /// /// The text to display. /// The type of action which this performs. /// A delegate to be invoked when this is pressed. - public TernaryStateMenuItem(string text, MenuItemType type, Action action) + public TernaryStateMenuItem(string text, MenuItemType type = MenuItemType.Standard, Action action = null) : this(text, getNextState, type, action) { } From 4e9631b54631655ee6d92742b14fb3c730d9a463 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 May 2020 14:43:38 +0900 Subject: [PATCH 1826/2376] Support HitType bindable changes --- .../Objects/Drawables/DrawableHit.cs | 24 ++++++++++++++++++- .../Drawables/DrawableTaikoHitObject.cs | 21 ++++++++++++---- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index 81b969eaf3..92ae7e0fd3 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -5,6 +5,8 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Audio; using osu.Game.Rulesets.Objects.Drawables; @@ -19,7 +21,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables /// /// A list of keys which can result in hits for this HitObject. /// - public TaikoAction[] HitActions { get; } + public TaikoAction[] HitActions { get; private set; } /// /// The action that caused this to be hit. @@ -34,15 +36,35 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private bool pressHandledThisFrame; + private Bindable type; + public DrawableHit(Hit hit) : base(hit) { FillMode = FillMode.Fit; + } + [BackgroundDependencyLoader] + private void load() + { + type = HitObject.TypeBindable.GetBoundCopy(); + type.BindValueChanged(_ => + { + updateType(); + RecreatePieces(); + }); + + updateType(); + } + + private void updateType() + { HitActions = HitObject.Type == HitType.Centre ? new[] { TaikoAction.LeftCentre, TaikoAction.RightCentre } : new[] { TaikoAction.LeftRim, TaikoAction.RightRim }; + + RecreatePieces(); } protected override SkinnableDrawable CreateMainPiece() => HitObject.Type == HitType.Centre diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs index 3ab09d4cbe..a3dfc9acc0 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs @@ -8,6 +8,7 @@ using osuTK; using System.Linq; using osu.Game.Audio; using System.Collections.Generic; +using osu.Framework.Allocation; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; using osu.Game.Rulesets.Objects; @@ -115,10 +116,10 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables public new TObject HitObject; - protected readonly Vector2 BaseSize; - protected readonly SkinnableDrawable MainPiece; + protected Vector2 BaseSize; + protected SkinnableDrawable MainPiece; - private readonly Container strongHitContainer; + private Container strongHitContainer; protected DrawableTaikoHitObject(TObject hitObject) : base(hitObject) @@ -129,11 +130,21 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables Origin = Anchor.Custom; RelativeSizeAxes = Axes.Both; + AddInternal(strongHitContainer = new Container()); + } + + [BackgroundDependencyLoader] + private void load() + { + RecreatePieces(); + } + + protected virtual void RecreatePieces() + { Size = BaseSize = new Vector2(HitObject.IsStrong ? TaikoHitObject.DEFAULT_STRONG_SIZE : TaikoHitObject.DEFAULT_SIZE); + Content.Clear(); Content.Add(MainPiece = CreateMainPiece()); - - AddInternal(strongHitContainer = new Container()); } protected override void AddNestedHitObject(DrawableHitObject hitObject) From 50fcd4149f42dae335f9e0eae0507c516a2c058e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 May 2020 14:54:12 +0900 Subject: [PATCH 1827/2376] Support Strong bindable changes --- .../Objects/Drawables/DrawableTaikoHitObject.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs index a3dfc9acc0..929cf8a937 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs @@ -9,6 +9,7 @@ using System.Linq; using osu.Game.Audio; using System.Collections.Generic; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; using osu.Game.Rulesets.Objects; @@ -119,7 +120,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected Vector2 BaseSize; protected SkinnableDrawable MainPiece; - private Container strongHitContainer; + private Bindable isStrong; + + private readonly Container strongHitContainer; protected DrawableTaikoHitObject(TObject hitObject) : base(hitObject) @@ -130,20 +133,22 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables Origin = Anchor.Custom; RelativeSizeAxes = Axes.Both; + AddInternal(strongHitContainer = new Container()); } [BackgroundDependencyLoader] private void load() { - RecreatePieces(); + isStrong = HitObject.IsStrongBindable.GetBoundCopy(); + isStrong.BindValueChanged(_ => RecreatePieces(), true); } protected virtual void RecreatePieces() { Size = BaseSize = new Vector2(HitObject.IsStrong ? TaikoHitObject.DEFAULT_STRONG_SIZE : TaikoHitObject.DEFAULT_SIZE); - Content.Clear(); + MainPiece?.Expire(); Content.Add(MainPiece = CreateMainPiece()); } From 910326623c8e24658565e63d95e77b707c244a4a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 May 2020 15:09:22 +0900 Subject: [PATCH 1828/2376] Place rim hits using right mosue for now --- .../TaikoHitObjectComposer.cs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.cs b/osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.cs index beef2b6155..5d7880278a 100644 --- a/osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.cs +++ b/osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.cs @@ -169,8 +169,10 @@ namespace osu.Game.Rulesets.Taiko { private readonly HitPiece piece; + private static Hit hit; + public HitPlacementBlueprint() - : base(new Hit()) + : base(hit = new Hit()) { InternalChild = piece = new HitPiece { @@ -180,13 +182,20 @@ namespace osu.Game.Rulesets.Taiko protected override bool OnMouseDown(MouseDownEvent e) { - if (e.Button == MouseButton.Left) + switch (e.Button) { - EndPlacement(true); - return true; + case MouseButton.Left: + hit.Type = HitType.Centre; + EndPlacement(true); + return true; + + case MouseButton.Right: + hit.Type = HitType.Rim; + EndPlacement(true); + return true; } - return base.OnMouseDown(e); + return false; } public override void UpdatePosition(SnapResult snapResult) From a2eec5d963dad4cbf4cda878eb7fefc36d3c5d91 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 May 2020 16:58:28 +0900 Subject: [PATCH 1829/2376] Fix strong bindable changes for DrumRolls --- .../Objects/Drawables/DrawableDrumRoll.cs | 17 +++++++++++------ osu.Game.Rulesets.Taiko/Objects/Swell.cs | 6 ------ .../Objects/TaikoHitObject.cs | 2 +- .../TaikoHitObjectComposer.cs | 2 ++ .../Edit/Compose/Components/SelectionHandler.cs | 8 ++++---- 5 files changed, 18 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index 5e731e5ad6..2c1c2d2bc1 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -48,12 +48,11 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables colourIdle = colours.YellowDark; colourEngaged = colours.YellowDarker; - updateColour(); - - Content.Add(tickContainer = new Container { RelativeSizeAxes = Axes.Both }); - - if (MainPiece.Drawable is IHasAccentColour accentMain) - accentMain.AccentColour = colourIdle; + Content.Add(tickContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Depth = float.MinValue + }); } protected override void LoadComplete() @@ -63,6 +62,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables OnNewResult += onNewResult; } + protected override void RecreatePieces() + { + base.RecreatePieces(); + updateColour(); + } + protected override void AddNestedHitObject(DrawableHitObject hitObject) { base.AddNestedHitObject(hitObject); diff --git a/osu.Game.Rulesets.Taiko/Objects/Swell.cs b/osu.Game.Rulesets.Taiko/Objects/Swell.cs index 390f8d1f3b..b4c09b884e 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Swell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Swell.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Threading; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Judgements; @@ -25,11 +24,6 @@ namespace osu.Game.Rulesets.Taiko.Objects /// public int RequiredHits = 10; - public override bool IsStrong - { - set => throw new NotSupportedException($"{nameof(Swell)} cannot be a strong hitobject."); - } - protected override void CreateNestedHitObjects(CancellationToken cancellationToken) { base.CreateNestedHitObjects(cancellationToken); diff --git a/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs index 4de762ce30..2922010001 100644 --- a/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Taiko.Objects /// Whether this HitObject is a "strong" type. /// Strong hit objects give more points for hitting the hit object with both keys. /// - public virtual bool IsStrong + public bool IsStrong { get => IsStrongBindable.Value; set => IsStrongBindable.Value = value; diff --git a/osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.cs b/osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.cs index 5d7880278a..802bb80fa3 100644 --- a/osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.cs +++ b/osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.cs @@ -107,6 +107,8 @@ namespace osu.Game.Rulesets.Taiko h.IsStrong = false; break; } + + EditorBeatmap?.UpdateHitObject(h); } }) { diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 7ab6340e07..38893f90a8 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -38,7 +38,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private Drawable outline; [Resolved(CanBeNull = true)] - private EditorBeatmap editorBeatmap { get; set; } + protected EditorBeatmap EditorBeatmap { get; private set; } [Resolved(CanBeNull = true)] private IEditorChangeHandler changeHandler { get; set; } @@ -117,7 +117,7 @@ namespace osu.Game.Screens.Edit.Compose.Components internal void HandleSelected(SelectionBlueprint blueprint) { selectedBlueprints.Add(blueprint); - editorBeatmap.SelectedHitObjects.Add(blueprint.HitObject); + EditorBeatmap.SelectedHitObjects.Add(blueprint.HitObject); UpdateVisibility(); } @@ -129,7 +129,7 @@ namespace osu.Game.Screens.Edit.Compose.Components internal void HandleDeselected(SelectionBlueprint blueprint) { selectedBlueprints.Remove(blueprint); - editorBeatmap.SelectedHitObjects.Remove(blueprint.HitObject); + EditorBeatmap.SelectedHitObjects.Remove(blueprint.HitObject); // We don't want to update visibility if > 0, since we may be deselecting blueprints during drag-selection if (selectedBlueprints.Count == 0) @@ -165,7 +165,7 @@ namespace osu.Game.Screens.Edit.Compose.Components changeHandler?.BeginChange(); foreach (var h in selectedBlueprints.ToList()) - editorBeatmap?.Remove(h.HitObject); + EditorBeatmap?.Remove(h.HitObject); changeHandler?.EndChange(); } From 280b0adb1dc532938d12e4fd966fb7be033030d2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 May 2020 17:44:47 +0900 Subject: [PATCH 1830/2376] Split out IHasPath from IHasCurve to better define hitobjects --- .../Beatmaps/CatchBeatmapConverter.cs | 2 +- .../Objects/JuiceStream.cs | 2 +- .../Legacy/DistanceObjectPatternGenerator.cs | 2 +- .../Beatmaps/OsuBeatmapConverter.cs | 2 +- osu.Game.Rulesets.Osu/Objects/Slider.cs | 2 +- .../Beatmaps/TaikoBeatmapConverter.cs | 2 +- osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs | 10 +---- .../Formats/LegacyBeatmapDecoderTest.cs | 2 +- .../Beatmaps/Formats/OsuJsonDecoderTest.cs | 2 +- .../Beatmaps/Formats/LegacyBeatmapEncoder.cs | 39 +++++++++++-------- .../Rulesets/Objects/Legacy/ConvertSlider.cs | 2 +- osu.Game/Rulesets/Objects/Types/IHasPath.cs | 13 +++++++ .../{IHasCurve.cs => IHasPathWithRepeats.cs} | 20 +++++----- 13 files changed, 57 insertions(+), 43 deletions(-) create mode 100644 osu.Game/Rulesets/Objects/Types/IHasPath.cs rename osu.Game/Rulesets/Objects/Types/{IHasCurve.cs => IHasPathWithRepeats.cs} (77%) diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs index 90a6e609f0..27a9b63e9a 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps switch (obj) { - case IHasCurve curveData: + case IHasPathWithRepeats curveData: return new JuiceStream { StartTime = obj.StartTime, diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index d32595c2e1..24090e233a 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -14,7 +14,7 @@ using osu.Game.Rulesets.Objects.Types; namespace osu.Game.Rulesets.Catch.Objects { - public class JuiceStream : CatchHitObject, IHasCurve + public class JuiceStream : CatchHitObject, IHasPathWithRepeats { /// /// Positional distance that results in a duration of one second, before any speed adjustments. diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs index d8d5b67c0e..1bd796511b 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs @@ -474,7 +474,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy /// private IList sampleInfoListAt(double time) { - if (!(HitObject is IHasCurve curveData)) + if (!(HitObject is IHasPathWithRepeats curveData)) return HitObject.Samples; double segmentTime = (EndTime - HitObject.StartTime) / spanCount; diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs index 147d74c929..060a3919bd 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs @@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps switch (original) { - case IHasCurve curveData: + case IHasPathWithRepeats curveData: return new Slider { StartTime = original.StartTime, diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 6ba0e1c6aa..713d1a61f8 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -17,7 +17,7 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects { - public class Slider : OsuHitObject, IHasCurve + public class Slider : OsuHitObject, IHasPathWithRepeats { public double EndTime { diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index d324441285..1a47be2282 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs @@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps if (!isForCurrentRuleset && tickSpacing > 0 && osuDuration < 2 * speedAdjustedBeatLength) { - List> allSamples = obj is IHasCurve curveData ? curveData.NodeSamples : new List>(new[] { samples }); + List> allSamples = obj is IHasPathWithRepeats curveData ? curveData.NodeSamples : new List>(new[] { samples }); int i = 0; diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs index 7b11bce520..5f52160be1 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs @@ -3,9 +3,7 @@ using osu.Game.Rulesets.Objects.Types; using System; -using System.Collections.Generic; using System.Threading; -using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Judgements; @@ -17,7 +15,7 @@ using osuTK; namespace osu.Game.Rulesets.Taiko.Objects { - public class DrumRoll : TaikoHitObject, IHasCurve + public class DrumRoll : TaikoHitObject, IHasPath { /// /// Drum roll distance that results in a duration of 1 speed-adjusted beat length. @@ -115,11 +113,7 @@ namespace osu.Game.Rulesets.Taiko.Objects double IHasDistance.Distance => Duration * Velocity; - int IHasRepeats.RepeatCount { get => 0; set { } } - - List> IHasRepeats.NodeSamples => new List>(); - - SliderPath IHasCurve.Path + SliderPath IHasPath.Path => new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(1) }, ((IHasDistance)this).Distance / TaikoBeatmapConverter.LEGACY_VELOCITY_MULTIPLIER); #endregion diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index acb30a6277..dab923d75b 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -365,7 +365,7 @@ namespace osu.Game.Tests.Beatmaps.Formats { var hitObjects = decoder.Decode(stream).HitObjects; - var curveData = hitObjects[0] as IHasCurve; + var curveData = hitObjects[0] as IHasPathWithRepeats; var positionData = hitObjects[0] as IHasPosition; Assert.IsNotNull(positionData); diff --git a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs index b034e66616..b4c78ce273 100644 --- a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs @@ -95,7 +95,7 @@ namespace osu.Game.Tests.Beatmaps.Formats { var beatmap = decodeAsJson(normal); - var curveData = beatmap.HitObjects[0] as IHasCurve; + var curveData = beatmap.HitObjects[0] as IHasPathWithRepeats; var positionData = beatmap.HitObjects[0] as IHasPosition; Assert.IsNotNull(positionData); diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 7727f25967..d7e83fa471 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -233,9 +233,9 @@ namespace osu.Game.Beatmaps.Formats writer.Write(FormattableString.Invariant($"{(int)getObjectType(hitObject)},")); writer.Write(FormattableString.Invariant($"{(int)toLegacyHitSoundType(hitObject.Samples)},")); - if (hitObject is IHasCurve curveData) + if (hitObject is IHasPathWithRepeats curveData) { - addCurveData(writer, curveData, position); + addPathData(writer, curveData, position); writer.Write(getSampleBank(hitObject.Samples, zeroBanks: true)); } else @@ -263,7 +263,7 @@ namespace osu.Game.Beatmaps.Formats switch (hitObject) { - case IHasCurve _: + case IHasPath _: type |= LegacyHitObjectType.Slider; break; @@ -282,13 +282,13 @@ namespace osu.Game.Beatmaps.Formats return type; } - private void addCurveData(TextWriter writer, IHasCurve curveData, Vector2 position) + private void addPathData(TextWriter writer, IHasPath pathData, Vector2 position) { PathType? lastType = null; - for (int i = 0; i < curveData.Path.ControlPoints.Count; i++) + for (int i = 0; i < pathData.Path.ControlPoints.Count; i++) { - PathControlPoint point = curveData.Path.ControlPoints[i]; + PathControlPoint point = pathData.Path.ControlPoints[i]; if (point.Type.Value != null) { @@ -325,23 +325,28 @@ namespace osu.Game.Beatmaps.Formats if (i != 0) { writer.Write(FormattableString.Invariant($"{position.X + point.Position.Value.X}:{position.Y + point.Position.Value.Y}")); - writer.Write(i != curveData.Path.ControlPoints.Count - 1 ? "|" : ","); + writer.Write(i != pathData.Path.ControlPoints.Count - 1 ? "|" : ","); } } - writer.Write(FormattableString.Invariant($"{curveData.RepeatCount + 1},")); - writer.Write(FormattableString.Invariant($"{curveData.Path.Distance},")); + var curveData = pathData as IHasPathWithRepeats; - for (int i = 0; i < curveData.NodeSamples.Count; i++) - { - writer.Write(FormattableString.Invariant($"{(int)toLegacyHitSoundType(curveData.NodeSamples[i])}")); - writer.Write(i != curveData.NodeSamples.Count - 1 ? "|" : ","); - } + writer.Write(FormattableString.Invariant($"{(curveData?.RepeatCount ?? 0) + 1},")); + writer.Write(FormattableString.Invariant($"{pathData.Path.Distance},")); - for (int i = 0; i < curveData.NodeSamples.Count; i++) + if (curveData != null) { - writer.Write(getSampleBank(curveData.NodeSamples[i], true)); - writer.Write(i != curveData.NodeSamples.Count - 1 ? "|" : ","); + for (int i = 0; i < curveData.NodeSamples.Count; i++) + { + writer.Write(FormattableString.Invariant($"{(int)toLegacyHitSoundType(curveData.NodeSamples[i])}")); + writer.Write(i != curveData.NodeSamples.Count - 1 ? "|" : ","); + } + + for (int i = 0; i < curveData.NodeSamples.Count; i++) + { + writer.Write(getSampleBank(curveData.NodeSamples[i], true)); + writer.Write(i != curveData.NodeSamples.Count - 1 ? "|" : ","); + } } } diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs index 924182b265..73192dc42e 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs @@ -9,7 +9,7 @@ using osu.Game.Beatmaps.ControlPoints; namespace osu.Game.Rulesets.Objects.Legacy { - internal abstract class ConvertSlider : ConvertHitObject, IHasCurve, IHasLegacyLastTickOffset + internal abstract class ConvertSlider : ConvertHitObject, IHasPathWithRepeats, IHasLegacyLastTickOffset { /// /// Scoring distance with a speed-adjusted beat length of 1 second. diff --git a/osu.Game/Rulesets/Objects/Types/IHasPath.cs b/osu.Game/Rulesets/Objects/Types/IHasPath.cs new file mode 100644 index 0000000000..567c24a4a2 --- /dev/null +++ b/osu.Game/Rulesets/Objects/Types/IHasPath.cs @@ -0,0 +1,13 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Rulesets.Objects.Types +{ + public interface IHasPath : IHasDistance + { + /// + /// The curve. + /// + SliderPath Path { get; } + } +} diff --git a/osu.Game/Rulesets/Objects/Types/IHasCurve.cs b/osu.Game/Rulesets/Objects/Types/IHasPathWithRepeats.cs similarity index 77% rename from osu.Game/Rulesets/Objects/Types/IHasCurve.cs rename to osu.Game/Rulesets/Objects/Types/IHasPathWithRepeats.cs index e98a888bd7..fba0fd7aff 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasCurve.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasPathWithRepeats.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osuTK; namespace osu.Game.Rulesets.Objects.Types @@ -8,15 +9,16 @@ namespace osu.Game.Rulesets.Objects.Types /// /// A HitObject that has a curve. /// - public interface IHasCurve : IHasDistance, IHasRepeats + public interface IHasPathWithRepeats : IHasPath, IHasRepeats { - /// - /// The curve. - /// - SliderPath Path { get; } } - public static class HasCurveExtensions + [Obsolete("Use IHasPathWithRepeats instead.")] // can be removed 20201126 + public interface IHasCurve : IHasPathWithRepeats + { + } + + public static class HasPathWithRepeatsExtensions { /// /// Computes the position on the curve relative to how much of the has been completed. @@ -24,7 +26,7 @@ namespace osu.Game.Rulesets.Objects.Types /// The curve. /// [0, 1] where 0 is the start time of the and 1 is the end time of the . /// The position on the curve. - public static Vector2 CurvePositionAt(this IHasCurve obj, double progress) + public static Vector2 CurvePositionAt(this IHasPathWithRepeats obj, double progress) => obj.Path.PositionAt(obj.ProgressAt(progress)); /// @@ -33,7 +35,7 @@ namespace osu.Game.Rulesets.Objects.Types /// The curve. /// [0, 1] where 0 is the start time of the and 1 is the end time of the . /// [0, 1] where 0 is the beginning of the curve and 1 is the end of the curve. - public static double ProgressAt(this IHasCurve obj, double progress) + public static double ProgressAt(this IHasPathWithRepeats obj, double progress) { double p = progress * obj.SpanCount() % 1; if (obj.SpanAt(progress) % 2 == 1) @@ -47,7 +49,7 @@ namespace osu.Game.Rulesets.Objects.Types /// The curve. /// [0, 1] where 0 is the beginning of the curve and 1 is the end of the curve. /// [0, SpanCount) where 0 is the first run. - public static int SpanAt(this IHasCurve obj, double progress) + public static int SpanAt(this IHasPathWithRepeats obj, double progress) => (int)(progress * obj.SpanCount()); } } From a953f9e422810198b1d431b52f4b365c912c4e84 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 May 2020 20:30:33 +0900 Subject: [PATCH 1831/2376] Add drum roll composition support --- .../TaikoHitObjectComposer.cs | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.cs b/osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.cs index 802bb80fa3..d233cd5e7f 100644 --- a/osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.cs +++ b/osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.cs @@ -34,9 +34,10 @@ namespace osu.Game.Rulesets.Taiko { } - protected override IReadOnlyList CompositionTools => new[] + protected override IReadOnlyList CompositionTools => new HitObjectCompositionTool[] { - new HitCompositionTool() + new HitCompositionTool(), + new DrumRollCompositionTool() }; protected override ComposeBlueprintContainer CreateBlueprintContainer() => new TaikoBlueprintContainer(drawableRuleset.Playfield.AllHitObjects); @@ -156,6 +157,26 @@ namespace osu.Game.Rulesets.Taiko } } + public class DrumRollCompositionTool : HitObjectCompositionTool + { + public DrumRollCompositionTool() + : base(nameof(DrumRoll)) + { + } + + public override PlacementBlueprint CreatePlacementBlueprint() => new DrumRollPlacementBlueprint(); + } + + public class DrumRollPlacementBlueprint : PlacementBlueprint + { + private static DrumRoll drumRoll; + + public DrumRollPlacementBlueprint() + : base(drumRoll = new DrumRoll()) + { + } + } + public class HitCompositionTool : HitObjectCompositionTool { public HitCompositionTool() From 534dccc0c384a0e33a6fbe49c1b3b2acc97d56b3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 May 2020 12:37:44 +0900 Subject: [PATCH 1832/2376] Move sett from EndTime to Duration --- osu.Game.Rulesets.Catch/Objects/JuiceStream.cs | 10 +++++----- osu.Game.Rulesets.Osu/Objects/Slider.cs | 8 ++++---- .../Objects/Drawables/DrawableSwell.cs | 2 +- .../Gameplay/TestSceneDrawableScrollingRuleset.cs | 6 +++--- .../Objects/Legacy/Catch/ConvertHitObjectParser.cs | 6 +++--- .../Objects/Legacy/Catch/ConvertSpinner.cs | 4 ++-- .../Objects/Legacy/ConvertHitObjectParser.cs | 14 +++++++------- osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs | 6 +++--- .../Objects/Legacy/Mania/ConvertHitObjectParser.cs | 8 ++++---- .../Rulesets/Objects/Legacy/Mania/ConvertHold.cs | 4 ++-- .../Objects/Legacy/Mania/ConvertSpinner.cs | 4 ++-- .../Objects/Legacy/Osu/ConvertHitObjectParser.cs | 6 +++--- .../Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs | 4 ++-- .../Objects/Legacy/Taiko/ConvertHitObjectParser.cs | 6 +++--- .../Objects/Legacy/Taiko/ConvertSpinner.cs | 4 ++-- osu.Game/Rulesets/Objects/Types/IHasEndTime.cs | 6 +++--- .../Timeline/TimelineHitObjectBlueprint.cs | 2 +- 17 files changed, 50 insertions(+), 50 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index 24090e233a..2c96ee2b19 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -115,15 +115,15 @@ namespace osu.Game.Rulesets.Catch.Objects } } - public double EndTime + public float EndX => X + this.CurvePositionAt(1).X / CatchPlayfield.BASE_WIDTH; + + public double Duration { - get => StartTime + this.SpanCount() * Path.Distance / Velocity; + get => this.SpanCount() * Path.Distance / Velocity; set => throw new System.NotSupportedException($"Adjust via {nameof(RepeatCount)} instead"); // can be implemented if/when needed. } - public float EndX => X + this.CurvePositionAt(1).X / CatchPlayfield.BASE_WIDTH; - - public double Duration => EndTime - StartTime; + public double EndTime => StartTime + Duration; private readonly SliderPath path = new SliderPath(); diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 713d1a61f8..705e88040f 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -19,14 +19,14 @@ namespace osu.Game.Rulesets.Osu.Objects { public class Slider : OsuHitObject, IHasPathWithRepeats { - public double EndTime + public double EndTime => StartTime + this.SpanCount() * Path.Distance / Velocity; + + public double Duration { - get => StartTime + this.SpanCount() * Path.Distance / Velocity; + get => EndTime - StartTime; set => throw new System.NotSupportedException($"Adjust via {nameof(RepeatCount)} instead"); // can be implemented if/when needed. } - public double Duration => EndTime - StartTime; - private readonly Cached endPositionCache = new Cached(); public override Vector2 EndPosition => endPositionCache.IsValid ? endPositionCache.Value : endPositionCache.Value = Position + this.CurvePositionAt(1); diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 32f7acadc8..7294587b10 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -237,7 +237,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables case ArmedState.Miss: case ArmedState.Hit: - using (BeginAbsoluteSequence(Time.Current, true)) + using (BeginDelayedSequence(HitObject.Duration, true)) { this.FadeOut(transition_duration, Easing.Out); bodyContainer.ScaleTo(1.4f, transition_duration); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs index b25b81c9af..08fd849fa6 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs @@ -281,7 +281,7 @@ namespace osu.Game.Tests.Visual.Gameplay yield return new TestHitObject { StartTime = original.StartTime, - EndTime = (original as IHasEndTime)?.EndTime ?? (original.StartTime + 100) + Duration = (original as IHasEndTime)?.Duration ?? 100 }; } } @@ -292,9 +292,9 @@ namespace osu.Game.Tests.Visual.Gameplay private class TestHitObject : ConvertHitObject, IHasEndTime { - public double EndTime { get; set; } + public double EndTime => StartTime + Duration; - public double Duration => EndTime - StartTime; + public double Duration { get; set; } } private class DrawableTestHitObject : DrawableHitObject diff --git a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs index 43e8d01297..c10c8dc30f 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs @@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch }; } - protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double endTime) + protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double duration) { // Convert spinners don't create the new combo themselves, but force the next non-spinner hitobject to create a new combo // Their combo offset is still added to that next hitobject's combo index @@ -65,11 +65,11 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch return new ConvertSpinner { - EndTime = endTime + Duration = duration }; } - protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double endTime) + protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double duration) { return null; } diff --git a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs index 9de311c9d7..4b0270064a 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs @@ -10,9 +10,9 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch /// internal sealed class ConvertSpinner : ConvertHitObject, IHasEndTime, IHasXPosition, IHasCombo { - public double EndTime { get; set; } + public double EndTime => StartTime + Duration; - public double Duration => EndTime - StartTime; + public double Duration { get; set; } public float X => 256; // Required for CatchBeatmapConverter diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 9a60a0a75c..d8d90fddfa 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -189,9 +189,9 @@ namespace osu.Game.Rulesets.Objects.Legacy } else if (type.HasFlag(LegacyHitObjectType.Spinner)) { - double endTime = Math.Max(startTime, Parsing.ParseDouble(split[5]) + Offset); + double duration = Math.Max(0, Parsing.ParseDouble(split[5]) + Offset - startTime); - result = CreateSpinner(new Vector2(512, 384) / 2, combo, comboOffset, endTime); + result = CreateSpinner(new Vector2(512, 384) / 2, combo, comboOffset, duration); if (split.Length > 6) readCustomSampleBanks(split[6], bankInfo); @@ -209,7 +209,7 @@ namespace osu.Game.Rulesets.Objects.Legacy readCustomSampleBanks(string.Join(":", ss.Skip(1)), bankInfo); } - result = CreateHold(pos, combo, comboOffset, endTime + Offset); + result = CreateHold(pos, combo, comboOffset, endTime + Offset - startTime); } if (result == null) @@ -321,9 +321,9 @@ namespace osu.Game.Rulesets.Objects.Legacy /// The position of the hit object. /// Whether the hit object creates a new combo. /// When starting a new combo, the offset of the new combo relative to the current one. - /// The spinner end time. + /// The spinner duration. /// The hit object. - protected abstract HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double endTime); + protected abstract HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double duration); /// /// Creates a legacy Hold-type hit object. @@ -331,8 +331,8 @@ namespace osu.Game.Rulesets.Objects.Legacy /// The position of the hit object. /// Whether the hit object creates a new combo. /// When starting a new combo, the offset of the new combo relative to the current one. - /// The hold end time. - protected abstract HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double endTime); + /// The hold end time. + protected abstract HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double duration); private List convertSoundType(LegacyHitSoundType type, SampleBankInfo bankInfo) { diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs index 73192dc42e..cd2f9f88b8 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs @@ -26,13 +26,13 @@ namespace osu.Game.Rulesets.Objects.Legacy public List> NodeSamples { get; set; } public int RepeatCount { get; set; } - public double EndTime + public double Duration { - get => StartTime + this.SpanCount() * Distance / Velocity; + get => this.SpanCount() * Distance / Velocity; set => throw new System.NotSupportedException($"Adjust via {nameof(RepeatCount)} instead"); // can be implemented if/when needed. } - public double Duration => EndTime - StartTime; + public double EndTime => StartTime + Duration; public double Velocity = 1; diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs index f94c4aaa75..bc64518f40 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs @@ -37,21 +37,21 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania }; } - protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double endTime) + protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double duration) { return new ConvertSpinner { X = position.X, - EndTime = endTime + Duration = duration }; } - protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double endTime) + protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double duration) { return new ConvertHold { X = position.X, - EndTime = endTime + Duration = duration }; } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs index 1d92d638dd..dcb66163e4 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs @@ -9,8 +9,8 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania { public float X { get; set; } - public double EndTime { get; set; } + public double Duration { get; set; } - public double Duration => EndTime - StartTime; + public double EndTime => StartTime + Duration; } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSpinner.cs index 7dc13e27cd..b731f7c8d8 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSpinner.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSpinner.cs @@ -10,9 +10,9 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania /// internal sealed class ConvertSpinner : ConvertHitObject, IHasEndTime, IHasXPosition { - public double EndTime { get; set; } + public double Duration { get; set; } - public double Duration => EndTime - StartTime; + public double EndTime => StartTime + Duration; public float X { get; set; } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs index b95ec703b6..75ecab0b8f 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs @@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu }; } - protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double endTime) + protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double duration) { // Convert spinners don't create the new combo themselves, but force the next non-spinner hitobject to create a new combo // Their combo offset is still added to that next hitobject's combo index @@ -66,11 +66,11 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu return new ConvertSpinner { Position = position, - EndTime = endTime + Duration = duration }; } - protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double endTime) + protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double duration) { return null; } diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs index 8b21aab411..a231237077 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs @@ -11,9 +11,9 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu /// internal sealed class ConvertSpinner : ConvertHitObject, IHasEndTime, IHasPosition, IHasCombo { - public double EndTime { get; set; } + public double Duration { get; set; } - public double Duration => EndTime - StartTime; + public double EndTime => StartTime + Duration; public Vector2 Position { get; set; } diff --git a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitObjectParser.cs index db65a61c90..13e3e84c6a 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitObjectParser.cs @@ -33,15 +33,15 @@ namespace osu.Game.Rulesets.Objects.Legacy.Taiko }; } - protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double endTime) + protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double duration) { return new ConvertSpinner { - EndTime = endTime + Duration = duration }; } - protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double endTime) + protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double duration) { return null; } diff --git a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSpinner.cs index 8e28487f2f..0976106ec4 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSpinner.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSpinner.cs @@ -10,8 +10,8 @@ namespace osu.Game.Rulesets.Objects.Legacy.Taiko /// internal sealed class ConvertSpinner : ConvertHitObject, IHasEndTime { - public double EndTime { get; set; } + public double Duration { get; set; } - public double Duration => EndTime - StartTime; + public double EndTime => StartTime + Duration; } } diff --git a/osu.Game/Rulesets/Objects/Types/IHasEndTime.cs b/osu.Game/Rulesets/Objects/Types/IHasEndTime.cs index bc7103c60d..5eb551e15c 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasEndTime.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasEndTime.cs @@ -13,12 +13,12 @@ namespace osu.Game.Rulesets.Objects.Types /// /// The time at which the HitObject ends. /// - [JsonIgnore] - double EndTime { get; set; } + double EndTime { get; } /// /// The duration of the HitObject. /// - double Duration { get; } + [JsonIgnore] + double Duration { get; set; } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index dd2f7a833e..d6fc17f358 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -296,7 +296,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (endTimeHitObject.EndTime == snappedTime) return; - endTimeHitObject.EndTime = snappedTime; + endTimeHitObject.Duration = snappedTime - hitObject.StartTime; break; } From dd7dbfd5488efddef6d56f897876ce6cba4eb32d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 May 2020 12:38:39 +0900 Subject: [PATCH 1833/2376] Rename to IHasDuration --- .../Beatmaps/CatchBeatmapConverter.cs | 2 +- .../Objects/BananaShower.cs | 2 +- .../TestSceneNotes.cs | 2 +- .../Beatmaps/ManiaBeatmapConverter.cs | 6 ++--- .../Legacy/EndTimeObjectPatternGenerator.cs | 2 +- osu.Game.Rulesets.Mania/Objects/HoldNote.cs | 2 +- .../Beatmaps/OsuBeatmapConverter.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs | 2 +- osu.Game.Rulesets.Osu/Objects/Spinner.cs | 2 +- .../Beatmaps/TaikoBeatmapConverter.cs | 2 +- osu.Game.Rulesets.Taiko/Objects/Swell.cs | 2 +- .../TestSceneDrawableScrollingRuleset.cs | 4 ++-- .../Beatmaps/Formats/LegacyBeatmapEncoder.cs | 6 ++--- osu.Game/Beatmaps/WorkingBeatmap.cs | 2 +- osu.Game/Rulesets/Objects/HitObject.cs | 4 ++-- .../Objects/Legacy/Catch/ConvertSpinner.cs | 2 +- .../Objects/Legacy/Mania/ConvertHold.cs | 2 +- .../Objects/Legacy/Mania/ConvertSpinner.cs | 2 +- .../Objects/Legacy/Osu/ConvertSpinner.cs | 2 +- .../Objects/Legacy/Taiko/ConvertSpinner.cs | 2 +- .../Rulesets/Objects/Types/IHasDistance.cs | 2 +- .../Rulesets/Objects/Types/IHasDuration.cs | 24 +++++++++++++++++++ .../Rulesets/Objects/Types/IHasEndTime.cs | 20 ++++------------ .../Rulesets/Objects/Types/IHasRepeats.cs | 2 +- .../Scrolling/ScrollingHitObjectContainer.cs | 2 +- .../Timeline/TimelineHitObjectBlueprint.cs | 4 ++-- 27 files changed, 60 insertions(+), 48 deletions(-) create mode 100644 osu.Game/Rulesets/Objects/Types/IHasDuration.cs diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs index 27a9b63e9a..0de2060e2d 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps LegacyLastTickOffset = (obj as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0 }.Yield(); - case IHasEndTime endTime: + case IHasDuration endTime: return new BananaShower { StartTime = obj.StartTime, diff --git a/osu.Game.Rulesets.Catch/Objects/BananaShower.cs b/osu.Game.Rulesets.Catch/Objects/BananaShower.cs index 3a0b5ace53..04a995c77e 100644 --- a/osu.Game.Rulesets.Catch/Objects/BananaShower.cs +++ b/osu.Game.Rulesets.Catch/Objects/BananaShower.cs @@ -7,7 +7,7 @@ using osu.Game.Rulesets.Objects.Types; namespace osu.Game.Rulesets.Catch.Objects { - public class BananaShower : CatchHitObject, IHasEndTime + public class BananaShower : CatchHitObject, IHasDuration { public override FruitVisualRepresentation VisualRepresentation => FruitVisualRepresentation.Banana; diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs index ea6a1e2e6a..dd5fd93710 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs @@ -156,7 +156,7 @@ namespace osu.Game.Rulesets.Mania.Tests foreach (var obj in content.OfType()) { - if (!(obj.HitObject is IHasEndTime endTime)) + if (!(obj.HitObject is IHasDuration endTime)) continue; foreach (var nested in obj.NestedHitObjects) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index 1c8116754f..32abf5e7f9 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -54,7 +54,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps } else { - float percentSliderOrSpinner = (float)beatmap.HitObjects.Count(h => h is IHasEndTime) / beatmap.HitObjects.Count; + float percentSliderOrSpinner = (float)beatmap.HitObjects.Count(h => h is IHasDuration) / beatmap.HitObjects.Count; if (percentSliderOrSpinner < 0.2) TargetColumns = 7; else if (percentSliderOrSpinner < 0.3 || roundedCircleSize >= 5) @@ -175,7 +175,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps break; } - case IHasEndTime endTimeData: + case IHasDuration endTimeData: { conversion = new EndTimeObjectPatternGenerator(Random, original, beatmap, originalBeatmap); @@ -231,7 +231,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps var pattern = new Pattern(); - if (HitObject is IHasEndTime endTimeData) + if (HitObject is IHasDuration endTimeData) { pattern.Add(new HoldNote { diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs index 907bed0d65..d5286a3779 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy public EndTimeObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, IBeatmap originalBeatmap) : base(random, hitObject, beatmap, new Pattern(), originalBeatmap) { - endTime = (HitObject as IHasEndTime)?.EndTime ?? 0; + endTime = (HitObject as IHasDuration)?.EndTime ?? 0; } public override IEnumerable Generate() diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs index e6f722a8a9..a100c9a58e 100644 --- a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Mania.Objects /// /// Represents a hit object which requires pressing, holding, and releasing a key. /// - public class HoldNote : ManiaHitObject, IHasEndTime + public class HoldNote : ManiaHitObject, IHasDuration { public double EndTime { diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs index 060a3919bd..fcad356a1c 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs @@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / beatmap.ControlPointInfo.DifficultyPointAt(original.StartTime).SpeedMultiplier : 1 }.Yield(); - case IHasEndTime endTimeData: + case IHasDuration endTimeData: return new Spinner { StartTime = original.StartTime, diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs index 7b1941b7f9..5d191119b9 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs @@ -70,7 +70,7 @@ namespace osu.Game.Rulesets.Osu.Mods break; // already hit or beyond the hittable end time. - if (h.IsHit || (h.HitObject is IHasEndTime hasEnd && time > hasEnd.EndTime)) + if (h.IsHit || (h.HitObject is IHasDuration hasEnd && time > hasEnd.EndTime)) continue; switch (h) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs b/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs index 297a0fea79..3cad52faeb 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs @@ -61,7 +61,7 @@ namespace osu.Game.Rulesets.Osu.Mods } // Keep wiggling sliders and spinners for their duration - if (!(osuObject is IHasEndTime endTime)) + if (!(osuObject is IHasDuration endTime)) return; amountWiggles = (int)(endTime.Duration / wiggle_duration); diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs index 0b8d03d118..418375c090 100644 --- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Spinner.cs @@ -11,7 +11,7 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects { - public class Spinner : OsuHitObject, IHasEndTime + public class Spinner : OsuHitObject, IHasDuration { public double EndTime { diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index 1a47be2282..78550ed270 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs @@ -150,7 +150,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps break; } - case IHasEndTime endTimeData: + case IHasDuration endTimeData: { double hitMultiplier = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty, 3, 5, 7.5) * swell_hit_multiplier; diff --git a/osu.Game.Rulesets.Taiko/Objects/Swell.cs b/osu.Game.Rulesets.Taiko/Objects/Swell.cs index b4c09b884e..eeae6e79f8 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Swell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Swell.cs @@ -9,7 +9,7 @@ using osu.Game.Rulesets.Taiko.Judgements; namespace osu.Game.Rulesets.Taiko.Objects { - public class Swell : TaikoHitObject, IHasEndTime + public class Swell : TaikoHitObject, IHasDuration { public double EndTime { diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs index 08fd849fa6..bd7e894cf8 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs @@ -281,7 +281,7 @@ namespace osu.Game.Tests.Visual.Gameplay yield return new TestHitObject { StartTime = original.StartTime, - Duration = (original as IHasEndTime)?.Duration ?? 100 + Duration = (original as IHasDuration)?.Duration ?? 100 }; } } @@ -290,7 +290,7 @@ namespace osu.Game.Tests.Visual.Gameplay #region HitObject - private class TestHitObject : ConvertHitObject, IHasEndTime + private class TestHitObject : ConvertHitObject, IHasDuration { public double EndTime => StartTime + Duration; diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index d7e83fa471..8c63ce6fcb 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -240,7 +240,7 @@ namespace osu.Game.Beatmaps.Formats } else { - if (hitObject is IHasEndTime) + if (hitObject is IHasDuration) addEndTimeData(writer, hitObject); writer.Write(getSampleBank(hitObject.Samples)); @@ -267,7 +267,7 @@ namespace osu.Game.Beatmaps.Formats type |= LegacyHitObjectType.Slider; break; - case IHasEndTime _: + case IHasDuration _: if (beatmap.BeatmapInfo.RulesetID == 3) type |= LegacyHitObjectType.Hold; else @@ -352,7 +352,7 @@ namespace osu.Game.Beatmaps.Formats private void addEndTimeData(TextWriter writer, HitObject hitObject) { - var endTimeData = (IHasEndTime)hitObject; + var endTimeData = (IHasDuration)hitObject; var type = getObjectType(hitObject); char suffix = ','; diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 8126311cbd..ac399e37c4 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -63,7 +63,7 @@ namespace osu.Game.Beatmaps length = emptyLength; break; - case IHasEndTime endTime: + case IHasDuration endTime: length = endTime.EndTime + excess_length; break; diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs index 6f9053d7cb..e2cc98813a 100644 --- a/osu.Game/Rulesets/Objects/HitObject.cs +++ b/osu.Game/Rulesets/Objects/HitObject.cs @@ -175,10 +175,10 @@ namespace osu.Game.Rulesets.Objects /// Returns the end time of this object. /// /// - /// This returns the where available, falling back to otherwise. + /// This returns the where available, falling back to otherwise. /// /// The object. /// The end time of this object. - public static double GetEndTime(this HitObject hitObject) => (hitObject as IHasEndTime)?.EndTime ?? hitObject.StartTime; + public static double GetEndTime(this HitObject hitObject) => (hitObject as IHasDuration)?.EndTime ?? hitObject.StartTime; } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs index 4b0270064a..014494ec54 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs @@ -8,7 +8,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch /// /// Legacy osu!catch Spinner-type, used for parsing Beatmaps. /// - internal sealed class ConvertSpinner : ConvertHitObject, IHasEndTime, IHasXPosition, IHasCombo + internal sealed class ConvertSpinner : ConvertHitObject, IHasDuration, IHasXPosition, IHasCombo { public double EndTime => StartTime + Duration; diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs index dcb66163e4..2fa4766c1d 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs @@ -5,7 +5,7 @@ using osu.Game.Rulesets.Objects.Types; namespace osu.Game.Rulesets.Objects.Legacy.Mania { - internal sealed class ConvertHold : ConvertHitObject, IHasXPosition, IHasEndTime + internal sealed class ConvertHold : ConvertHitObject, IHasXPosition, IHasDuration { public float X { get; set; } diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSpinner.cs index b731f7c8d8..c05aaceb9c 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSpinner.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSpinner.cs @@ -8,7 +8,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania /// /// Legacy osu!mania Spinner-type, used for parsing Beatmaps. /// - internal sealed class ConvertSpinner : ConvertHitObject, IHasEndTime, IHasXPosition + internal sealed class ConvertSpinner : ConvertHitObject, IHasDuration, IHasXPosition { public double Duration { get; set; } diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs index a231237077..e9e5ca8c94 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs @@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu /// /// Legacy osu! Spinner-type, used for parsing Beatmaps. /// - internal sealed class ConvertSpinner : ConvertHitObject, IHasEndTime, IHasPosition, IHasCombo + internal sealed class ConvertSpinner : ConvertHitObject, IHasDuration, IHasPosition, IHasCombo { public double Duration { get; set; } diff --git a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSpinner.cs index 0976106ec4..1d5ecb1ef3 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSpinner.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSpinner.cs @@ -8,7 +8,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Taiko /// /// Legacy osu!taiko Spinner-type, used for parsing Beatmaps. /// - internal sealed class ConvertSpinner : ConvertHitObject, IHasEndTime + internal sealed class ConvertSpinner : ConvertHitObject, IHasDuration { public double Duration { get; set; } diff --git a/osu.Game/Rulesets/Objects/Types/IHasDistance.cs b/osu.Game/Rulesets/Objects/Types/IHasDistance.cs index e7f552115e..b497ca5da3 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasDistance.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasDistance.cs @@ -6,7 +6,7 @@ namespace osu.Game.Rulesets.Objects.Types /// /// A HitObject that has a positional length. /// - public interface IHasDistance : IHasEndTime + public interface IHasDistance : IHasDuration { /// /// The positional length of the HitObject. diff --git a/osu.Game/Rulesets/Objects/Types/IHasDuration.cs b/osu.Game/Rulesets/Objects/Types/IHasDuration.cs new file mode 100644 index 0000000000..2433f9597e --- /dev/null +++ b/osu.Game/Rulesets/Objects/Types/IHasDuration.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 Newtonsoft.Json; + +namespace osu.Game.Rulesets.Objects.Types +{ + /// + /// A HitObject that ends at a different time than its start time. + /// + public interface IHasDuration + { + /// + /// The time at which the HitObject ends. + /// + double EndTime { get; } + + /// + /// The duration of the HitObject. + /// + [JsonIgnore] + double Duration { get; set; } + } +} diff --git a/osu.Game/Rulesets/Objects/Types/IHasEndTime.cs b/osu.Game/Rulesets/Objects/Types/IHasEndTime.cs index 5eb551e15c..7395223c7e 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasEndTime.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasEndTime.cs @@ -1,24 +1,12 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using Newtonsoft.Json; +using System; namespace osu.Game.Rulesets.Objects.Types { - /// - /// A HitObject that ends at a different time than its start time. - /// - public interface IHasEndTime + [Obsolete("Use IHasDuration instead.")] // can be removed 20201126 + public interface IHasEndTime : IHasDuration { - /// - /// The time at which the HitObject ends. - /// - double EndTime { get; } - - /// - /// The duration of the HitObject. - /// - [JsonIgnore] - double Duration { get; set; } } } diff --git a/osu.Game/Rulesets/Objects/Types/IHasRepeats.cs b/osu.Game/Rulesets/Objects/Types/IHasRepeats.cs index 256b1f3963..7a3fb16196 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasRepeats.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasRepeats.cs @@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Objects.Types /// /// A HitObject that spans some length. /// - public interface IHasRepeats : IHasEndTime + public interface IHasRepeats : IHasDuration { /// /// The amount of times the HitObject repeats. diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index c817d84d5c..0dc3324559 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -270,7 +270,7 @@ namespace osu.Game.Rulesets.UI.Scrolling // Cant use AddOnce() since the delegate is re-constructed every invocation private void computeInitialStateRecursive(DrawableHitObject hitObject) => hitObject.Schedule(() => { - if (hitObject.HitObject is IHasEndTime e) + if (hitObject.HitObject is IHasDuration e) { switch (direction.Value) { diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index d6fc17f358..b95b3842b3 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -72,7 +72,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline shadowComponents.Add(circle); - if (hitObject is IHasEndTime) + if (hitObject is IHasDuration) { DragBar dragBarUnderlay; Container extensionBar; @@ -290,7 +290,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline repeatHitObject.RepeatCount = proposedCount; break; - case IHasEndTime endTimeHitObject: + case IHasDuration endTimeHitObject: var snappedTime = Math.Max(hitObject.StartTime, beatSnapProvider.SnapTime(time)); if (endTimeHitObject.EndTime == snappedTime) From b2fad915898c445cfe8ef13888de52dc08d232d7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 May 2020 20:33:12 +0900 Subject: [PATCH 1834/2376] Add swell and drumroll blueprints --- .../TaikoHitObjectComposer.cs | 155 +++++++++++++++++- osu.Game/Rulesets/Edit/PlacementBlueprint.cs | 6 +- 2 files changed, 149 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.cs b/osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.cs index d233cd5e7f..2bb037815e 100644 --- a/osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.cs +++ b/osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.cs @@ -14,7 +14,9 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.UI; using osu.Game.Rulesets.UI; @@ -37,7 +39,8 @@ namespace osu.Game.Rulesets.Taiko protected override IReadOnlyList CompositionTools => new HitObjectCompositionTool[] { new HitCompositionTool(), - new DrumRollCompositionTool() + new DrumRollCompositionTool(), + new SwellCompositionTool() }; protected override ComposeBlueprintContainer CreateBlueprintContainer() => new TaikoBlueprintContainer(drawableRuleset.Playfield.AllHitObjects); @@ -157,6 +160,16 @@ namespace osu.Game.Rulesets.Taiko } } + public class SwellCompositionTool : HitObjectCompositionTool + { + public SwellCompositionTool() + : base(nameof(Swell)) + { + } + + public override PlacementBlueprint CreatePlacementBlueprint() => new SwellPlacementBlueprint(); + } + public class DrumRollCompositionTool : HitObjectCompositionTool { public DrumRollCompositionTool() @@ -167,16 +180,110 @@ namespace osu.Game.Rulesets.Taiko public override PlacementBlueprint CreatePlacementBlueprint() => new DrumRollPlacementBlueprint(); } - public class DrumRollPlacementBlueprint : PlacementBlueprint + public class SwellPlacementBlueprint : TaikoSpanPlacementBlueprint { - private static DrumRoll drumRoll; - - public DrumRollPlacementBlueprint() - : base(drumRoll = new DrumRoll()) + public SwellPlacementBlueprint() + : base(new Swell()) { } } + public class DrumRollPlacementBlueprint : TaikoSpanPlacementBlueprint + { + public DrumRollPlacementBlueprint() + : base(new DrumRoll()) + { + } + } + + public class TaikoSpanPlacementBlueprint : PlacementBlueprint + { + private readonly HitPiece headPiece; + private readonly HitPiece tailPiece; + + private readonly LengthPiece lengthPiece; + + private readonly IHasDuration spanPlacementObject; + + public TaikoSpanPlacementBlueprint(HitObject hitObject) + : base(hitObject) + + { + spanPlacementObject = hitObject as IHasDuration; + + RelativeSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + headPiece = new HitPiece + { + Size = new Vector2(TaikoHitObject.DEFAULT_SIZE * TaikoPlayfield.DEFAULT_HEIGHT) + }, + lengthPiece = new LengthPiece + { + Height = TaikoHitObject.DEFAULT_SIZE * TaikoPlayfield.DEFAULT_HEIGHT + }, + tailPiece = new HitPiece + { + Size = new Vector2(TaikoHitObject.DEFAULT_SIZE * TaikoPlayfield.DEFAULT_HEIGHT) + } + }; + } + + private double originalStartTime; + + protected override bool OnMouseDown(MouseDownEvent e) + { + if (e.Button != MouseButton.Left) + return false; + + BeginPlacement(true); + return true; + } + + protected override void OnMouseUp(MouseUpEvent e) + { + if (e.Button != MouseButton.Left) + return; + + base.OnMouseUp(e); + EndPlacement(true); + } + + public override void UpdatePosition(SnapResult result) + { + base.UpdatePosition(result); + + if (PlacementActive) + { + if (result.Time is double endTime) + { + if (endTime < originalStartTime) + { + HitObject.StartTime = endTime; + spanPlacementObject.Duration = Math.Abs(endTime - originalStartTime); + headPiece.Position = ToLocalSpace(result.ScreenSpacePosition); + lengthPiece.X = headPiece.X; + lengthPiece.Width = tailPiece.X - headPiece.X; + } + else + { + spanPlacementObject.Duration = Math.Abs(endTime - originalStartTime); + tailPiece.Position = ToLocalSpace(result.ScreenSpacePosition); + lengthPiece.Width = tailPiece.X - headPiece.X; + } + } + } + else + { + lengthPiece.Position = headPiece.Position = tailPiece.Position = ToLocalSpace(result.ScreenSpacePosition); + + if (result.Time is double startTime) + originalStartTime = HitObject.StartTime = startTime; + } + } + } + public class HitCompositionTool : HitObjectCompositionTool { public HitCompositionTool() @@ -221,10 +328,40 @@ namespace osu.Game.Rulesets.Taiko return false; } - public override void UpdatePosition(SnapResult snapResult) + public override void UpdatePosition(SnapResult result) { - piece.Position = ToLocalSpace(snapResult.ScreenSpacePosition); - base.UpdatePosition(snapResult); + piece.Position = ToLocalSpace(result.ScreenSpacePosition); + base.UpdatePosition(result); + } + } + + public class LengthPiece : CompositeDrawable + { + public LengthPiece() + { + Origin = Anchor.CentreLeft; + + InternalChild = new Container + { + Masking = true, + Colour = Color4.Yellow, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.X, + Height = 8, + }, + new Box + { + Origin = Anchor.BottomLeft, + Anchor = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + Height = 8, + } + } + }; } } diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index bb89ba8311..02d5955ae6 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -87,11 +87,11 @@ namespace osu.Game.Rulesets.Edit /// /// Updates the position of this to a new screen-space position. /// - /// The snap result information. - public virtual void UpdatePosition(SnapResult snapResult) + /// The snap result information. + public virtual void UpdatePosition(SnapResult result) { if (!PlacementActive) - HitObject.StartTime = snapResult.Time ?? EditorClock?.CurrentTime ?? Time.Current; + HitObject.StartTime = result.Time ?? EditorClock?.CurrentTime ?? Time.Current; } /// From 597f2848054977d21c2af3a68dff5df82a18c516 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 May 2020 11:46:08 +0900 Subject: [PATCH 1835/2376] Tidy up and complete xmldoc for HitObjectComposer --- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 89 ++++++++++++++++----- osu.sln.DotSettings | 5 +- 2 files changed, 74 insertions(+), 20 deletions(-) diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 38576e02a0..c956439eb2 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -48,8 +48,6 @@ namespace osu.Game.Rulesets.Edit protected ComposeBlueprintContainer BlueprintContainer { get; private set; } - public override Playfield Playfield => drawableRulesetWrapper.Playfield; - private DrawableEditRulesetWrapper drawableRulesetWrapper; protected readonly Container LayerBelowRuleset = new Container { RelativeSizeAxes = Axes.Both }; @@ -61,7 +59,6 @@ namespace osu.Game.Rulesets.Edit protected HitObjectComposer(Ruleset ruleset) { Ruleset = ruleset; - RelativeSizeAxes = Axes.Both; } [BackgroundDependencyLoader] @@ -137,6 +134,49 @@ namespace osu.Game.Rulesets.Edit EditorBeatmap.SelectedHitObjects.CollectionChanged += selectionChanged; } + protected override void LoadComplete() + { + base.LoadComplete(); + + inputManager = GetContainingInputManager(); + } + + public override Playfield Playfield => drawableRulesetWrapper.Playfield; + + public override IEnumerable HitObjects => drawableRulesetWrapper.Playfield.AllHitObjects; + + public override bool CursorInPlacementArea => drawableRulesetWrapper.Playfield.ReceivePositionalInputAt(inputManager.CurrentState.Mouse.Position); + + /// + /// Defines all available composition tools, listed on the left side of the editor screen as button controls. + /// This should usually define one tool for each type used in the target ruleset. + /// + /// + /// A "select" tool is automatically added as the first tool. + /// + protected abstract IReadOnlyList CompositionTools { get; } + + /// + /// Construct a relevant blueprint container. This will manage hitobject selection/placement input handling and display logic. + /// + protected abstract ComposeBlueprintContainer CreateBlueprintContainer(); + + /// + /// Construct a drawable ruleset for the provided ruleset. + /// + /// + /// Can be overridden to add editor-specific logical changes to a 's standard . + /// For example, hit animations or judgement logic may be changed to give a better editor user experience. + /// + /// The ruleset used to construct its drawable counterpart. + /// The loaded beatmap. + /// The mods to be applied. + /// An editor-relevant . + protected virtual DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) + => (DrawableRuleset)ruleset.CreateDrawableRulesetWith(beatmap, mods); + + #region Tool selection logic + protected override bool OnKeyDown(KeyDownEvent e) { if (e.Key >= Key.Number1 && e.Key <= Key.Number9) @@ -153,13 +193,6 @@ namespace osu.Game.Rulesets.Edit return base.OnKeyDown(e); } - protected override void LoadComplete() - { - base.LoadComplete(); - - inputManager = GetContainingInputManager(); - } - private void selectionChanged(object sender, NotifyCollectionChangedEventArgs changedArgs) { if (EditorBeatmap.SelectedHitObjects.Any()) @@ -179,15 +212,9 @@ namespace osu.Game.Rulesets.Edit EditorBeatmap.SelectedHitObjects.Clear(); } - public override IEnumerable HitObjects => drawableRulesetWrapper.Playfield.AllHitObjects; + #endregion - public override bool CursorInPlacementArea => drawableRulesetWrapper.Playfield.ReceivePositionalInputAt(inputManager.CurrentState.Mouse.Position); - - protected abstract IReadOnlyList CompositionTools { get; } - - protected abstract ComposeBlueprintContainer CreateBlueprintContainer(); - - protected abstract DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null); + #region IPlacementHandler public void BeginPlacement(HitObject hitObject) { @@ -209,6 +236,17 @@ namespace osu.Game.Rulesets.Edit public void Delete(HitObject hitObject) => EditorBeatmap.Remove(hitObject); + #endregion + + #region IPositionSnapProvider + + /// + /// Retrieve the relevant at a specified screen-space position. + /// In cases where a ruleset doesn't require custom logic (due to nested playfields, for example) + /// this will return the ruleset's main playfield. + /// + /// The screen-space position to query. + /// The most relevant . protected virtual Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) => drawableRulesetWrapper.Playfield; public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) @@ -257,8 +295,14 @@ namespace osu.Game.Rulesets.Edit return DurationToDistance(referenceTime, snappedEndTime - referenceTime); } + + #endregion } + /// + /// A non-generic definition of a HitObject composer class. + /// Generally used to access certain methods without requiring a generic type for . + /// [Cached(typeof(HitObjectComposer))] [Cached(typeof(IPositionSnapProvider))] public abstract class HitObjectComposer : CompositeDrawable, IPositionSnapProvider @@ -268,10 +312,13 @@ namespace osu.Game.Rulesets.Edit RelativeSizeAxes = Axes.Both; } + /// + /// The target ruleset's playfield. + /// public abstract Playfield Playfield { get; } /// - /// All the s. + /// All s in currently loaded beatmap. /// public abstract IEnumerable HitObjects { get; } @@ -280,6 +327,8 @@ namespace osu.Game.Rulesets.Edit /// public abstract bool CursorInPlacementArea { get; } + #region IPositionSnapProvider + public abstract SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition); public abstract float GetBeatSnapDistanceAt(double referenceTime); @@ -291,5 +340,7 @@ namespace osu.Game.Rulesets.Edit public abstract double GetSnappedDurationFromDistance(double referenceTime, float distance); public abstract float GetSnappedDistanceFromDistance(double referenceTime, float distance); + + #endregion } } diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index e3b64c03b9..b9fc3de734 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -1,4 +1,4 @@ - + True True True @@ -905,14 +905,17 @@ private void load() True True True + True True True True True True True + True True True True + True True True From 3e973c176f0ccf506b2b07c94c5bf317a94f0b34 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 May 2020 11:59:21 +0900 Subject: [PATCH 1836/2376] Remove unnecessary overrides --- osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.cs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.cs b/osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.cs index 2bb037815e..16111f4abf 100644 --- a/osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.cs +++ b/osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.cs @@ -9,17 +9,14 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; -using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; -using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.UI; -using osu.Game.Rulesets.UI; using osu.Game.Screens.Edit.Compose.Components; using osuTK; using osuTK.Graphics; @@ -29,8 +26,6 @@ namespace osu.Game.Rulesets.Taiko { public class TaikoHitObjectComposer : HitObjectComposer { - private DrawableTaikoRuleset drawableRuleset; - public TaikoHitObjectComposer(Ruleset ruleset) : base(ruleset) { @@ -43,12 +38,7 @@ namespace osu.Game.Rulesets.Taiko new SwellCompositionTool() }; - protected override ComposeBlueprintContainer CreateBlueprintContainer() => new TaikoBlueprintContainer(drawableRuleset.Playfield.AllHitObjects); - - protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) - { - return drawableRuleset = new DrawableTaikoRuleset(ruleset, beatmap, mods); - } + protected override ComposeBlueprintContainer CreateBlueprintContainer() => new TaikoBlueprintContainer(Playfield.AllHitObjects); } public class TaikoBlueprintContainer : ComposeBlueprintContainer From 590931b17cfaccafd3bf8f0db168f4082a1dd8be Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 May 2020 12:20:50 +0900 Subject: [PATCH 1837/2376] Pass hitobjects as a parameter to CreateBlueprintContainer --- osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs | 4 +++- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 4 +++- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 6 ++++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index 10d344242c..7e2469a794 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -13,6 +13,7 @@ using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Screens.Edit.Compose.Components; @@ -88,7 +89,8 @@ namespace osu.Game.Rulesets.Mania.Edit return drawableRuleset; } - protected override ComposeBlueprintContainer CreateBlueprintContainer() => new ManiaBlueprintContainer(drawableRuleset.Playfield.AllHitObjects); + protected override ComposeBlueprintContainer CreateBlueprintContainer(IEnumerable hitObjects) + => new ManiaBlueprintContainer(hitObjects); protected override IReadOnlyList CompositionTools => new HitObjectCompositionTool[] { diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index de5c1e54d7..37019a7a05 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -13,6 +13,7 @@ using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.UI; using osu.Game.Screens.Edit.Compose.Components; @@ -46,7 +47,8 @@ namespace osu.Game.Rulesets.Osu.Edit EditorBeatmap.PlacementObject.ValueChanged += _ => updateDistanceSnapGrid(); } - protected override ComposeBlueprintContainer CreateBlueprintContainer() => new OsuBlueprintContainer(HitObjects); + protected override ComposeBlueprintContainer CreateBlueprintContainer(IEnumerable hitObjects) + => new OsuBlueprintContainer(hitObjects); private DistanceSnapGrid distanceSnapGrid; private Container distanceSnapGridContainer; diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index c956439eb2..8b9f531417 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -113,7 +113,7 @@ namespace osu.Game.Rulesets.Edit drawableRulesetWrapper, // layers above playfield drawableRulesetWrapper.CreatePlayfieldAdjustmentContainer() - .WithChild(BlueprintContainer = CreateBlueprintContainer()) + .WithChild(BlueprintContainer = CreateBlueprintContainer(HitObjects)) } } }, @@ -159,7 +159,9 @@ namespace osu.Game.Rulesets.Edit /// /// Construct a relevant blueprint container. This will manage hitobject selection/placement input handling and display logic. /// - protected abstract ComposeBlueprintContainer CreateBlueprintContainer(); + /// A live collection of all s in the editor beatmap. + protected virtual ComposeBlueprintContainer CreateBlueprintContainer(IEnumerable hitObjects) + => new ComposeBlueprintContainer(hitObjects); /// /// Construct a drawable ruleset for the provided ruleset. From 7b52faa76d086ce98dd455e1475448e09c825870 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 May 2020 12:45:09 +0900 Subject: [PATCH 1838/2376] Update override --- osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.cs b/osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.cs index 16111f4abf..29e0e3a3c0 100644 --- a/osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.cs +++ b/osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.cs @@ -38,7 +38,8 @@ namespace osu.Game.Rulesets.Taiko new SwellCompositionTool() }; - protected override ComposeBlueprintContainer CreateBlueprintContainer() => new TaikoBlueprintContainer(Playfield.AllHitObjects); + protected override ComposeBlueprintContainer CreateBlueprintContainer(IEnumerable hitObjects) + => new TaikoBlueprintContainer(hitObjects); } public class TaikoBlueprintContainer : ComposeBlueprintContainer From 7f8f41715d31f74234e019b4de049c9e0acacc64 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 May 2020 13:15:43 +0900 Subject: [PATCH 1839/2376] Remove stray whitespace --- osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.cs b/osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.cs index 29e0e3a3c0..b34a8e75cc 100644 --- a/osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.cs +++ b/osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.cs @@ -286,7 +286,6 @@ namespace osu.Game.Rulesets.Taiko } public class HitPlacementBlueprint : PlacementBlueprint - { private readonly HitPiece piece; From 3b6619a3608e7d60c7c22b541478ec62c4442335 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 May 2020 16:11:26 +0900 Subject: [PATCH 1840/2376] Flip direction to avoid breaking other usages --- osu.Game/Rulesets/Objects/Types/IHasDuration.cs | 16 +++++++++++++--- osu.Game/Rulesets/Objects/Types/IHasEndTime.cs | 16 +++++++++++++++- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Types/IHasDuration.cs b/osu.Game/Rulesets/Objects/Types/IHasDuration.cs index 2433f9597e..185fd5977b 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasDuration.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasDuration.cs @@ -8,17 +8,27 @@ namespace osu.Game.Rulesets.Objects.Types /// /// A HitObject that ends at a different time than its start time. /// - public interface IHasDuration +#pragma warning disable 618 + public interface IHasDuration : IHasEndTime +#pragma warning restore 618 { + double IHasEndTime.EndTime + { + get => EndTime; + set => Duration = (Duration - EndTime) + value; + } + + double IHasEndTime.Duration => Duration; + /// /// The time at which the HitObject ends. /// - double EndTime { get; } + new double EndTime { get; } /// /// The duration of the HitObject. /// [JsonIgnore] - double Duration { get; set; } + new double Duration { get; set; } } } diff --git a/osu.Game/Rulesets/Objects/Types/IHasEndTime.cs b/osu.Game/Rulesets/Objects/Types/IHasEndTime.cs index 7395223c7e..c3769c5909 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasEndTime.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasEndTime.cs @@ -2,11 +2,25 @@ // See the LICENCE file in the repository root for full licence text. using System; +using Newtonsoft.Json; namespace osu.Game.Rulesets.Objects.Types { + /// + /// A HitObject that ends at a different time than its start time. + /// [Obsolete("Use IHasDuration instead.")] // can be removed 20201126 - public interface IHasEndTime : IHasDuration + public interface IHasEndTime { + /// + /// The time at which the HitObject ends. + /// + [JsonIgnore] + double EndTime { get; set; } + + /// + /// The duration of the HitObject. + /// + double Duration { get; } } } From da289c474e2ceba450437a20572fa7d7d2b902f6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 May 2020 16:40:10 +0900 Subject: [PATCH 1841/2376] Split files out --- .../TestSceneTaikoHitObjectComposer.cs | 1 + .../Blueprints/DrumRollPlacementBlueprint.cs | 12 + .../Edit/Blueprints/HitPiece.cs | 32 ++ .../Edit/Blueprints/HitPlacementBlueprint.cs | 49 +++ .../Edit/Blueprints/LengthPiece.cs | 37 ++ .../Blueprints/SwellPlacementBlueprint.cs | 12 + .../Blueprints/TaikoSpanPlacementBlueprint.cs | 101 +++++ .../Edit/DrumRollCompositionTool.cs | 17 + .../Edit/HitCompositionTool.cs | 17 + .../Edit/SwellCompositionTool.cs | 17 + .../Edit/TaikoBlueprintContainer.cs | 20 + .../Edit/TaikoHitObjectComposer.cs | 30 ++ .../Edit/TaikoSelectionBlueprint.cs | 41 ++ .../Edit/TaikoSelectionHandler.cs | 80 ++++ .../TaikoHitObjectComposer.cs | 382 ------------------ osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 1 + 16 files changed, 467 insertions(+), 382 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/Edit/Blueprints/DrumRollPlacementBlueprint.cs create mode 100644 osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPiece.cs create mode 100644 osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs create mode 100644 osu.Game.Rulesets.Taiko/Edit/Blueprints/LengthPiece.cs create mode 100644 osu.Game.Rulesets.Taiko/Edit/Blueprints/SwellPlacementBlueprint.cs create mode 100644 osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs create mode 100644 osu.Game.Rulesets.Taiko/Edit/DrumRollCompositionTool.cs create mode 100644 osu.Game.Rulesets.Taiko/Edit/HitCompositionTool.cs create mode 100644 osu.Game.Rulesets.Taiko/Edit/SwellCompositionTool.cs create mode 100644 osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs create mode 100644 osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs create mode 100644 osu.Game.Rulesets.Taiko/Edit/TaikoSelectionBlueprint.cs create mode 100644 osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs delete mode 100644 osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoHitObjectComposer.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoHitObjectComposer.cs index b5ee33fa8e..34d5fdf857 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoHitObjectComposer.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoHitObjectComposer.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Taiko.Beatmaps; +using osu.Game.Rulesets.Taiko.Edit; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Screens.Edit; using osu.Game.Tests.Visual; diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/DrumRollPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/DrumRollPlacementBlueprint.cs new file mode 100644 index 0000000000..2f086891a9 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/DrumRollPlacementBlueprint.cs @@ -0,0 +1,12 @@ +using osu.Game.Rulesets.Taiko.Objects; + +namespace osu.Game.Rulesets.Taiko.Edit.Blueprints +{ + public class DrumRollPlacementBlueprint : TaikoSpanPlacementBlueprint + { + public DrumRollPlacementBlueprint() + : base(new DrumRoll()) + { + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPiece.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPiece.cs new file mode 100644 index 0000000000..992fba1e29 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPiece.cs @@ -0,0 +1,32 @@ +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Taiko.Edit.Blueprints +{ + public class HitPiece : CompositeDrawable + { + public HitPiece() + { + Origin = Anchor.Centre; + + InternalChild = new CircularContainer + { + Masking = true, + BorderThickness = 10, + BorderColour = Color4.Yellow, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + AlwaysPresent = true, + Alpha = 0, + RelativeSizeAxes = Axes.Both + } + } + }; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs new file mode 100644 index 0000000000..c21aed32e8 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs @@ -0,0 +1,49 @@ +using osu.Framework.Input.Events; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.UI; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Rulesets.Taiko.Edit.Blueprints +{ + public class HitPlacementBlueprint : PlacementBlueprint + { + private readonly HitPiece piece; + + private static Hit hit; + + public HitPlacementBlueprint() + : base(hit = new Hit()) + { + InternalChild = piece = new HitPiece + { + Size = new Vector2(TaikoHitObject.DEFAULT_SIZE * TaikoPlayfield.DEFAULT_HEIGHT) + }; + } + + protected override bool OnMouseDown(MouseDownEvent e) + { + switch (e.Button) + { + case MouseButton.Left: + hit.Type = HitType.Centre; + EndPlacement(true); + return true; + + case MouseButton.Right: + hit.Type = HitType.Rim; + EndPlacement(true); + return true; + } + + return false; + } + + public override void UpdatePosition(SnapResult result) + { + piece.Position = ToLocalSpace(result.ScreenSpacePosition); + base.UpdatePosition(result); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/LengthPiece.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/LengthPiece.cs new file mode 100644 index 0000000000..2139a852b2 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/LengthPiece.cs @@ -0,0 +1,37 @@ +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Taiko.Edit.Blueprints +{ + public class LengthPiece : CompositeDrawable + { + public LengthPiece() + { + Origin = Anchor.CentreLeft; + + InternalChild = new Container + { + Masking = true, + Colour = Color4.Yellow, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.X, + Height = 8, + }, + new Box + { + Origin = Anchor.BottomLeft, + Anchor = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + Height = 8, + } + } + }; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/SwellPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/SwellPlacementBlueprint.cs new file mode 100644 index 0000000000..180bf26f34 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/SwellPlacementBlueprint.cs @@ -0,0 +1,12 @@ +using osu.Game.Rulesets.Taiko.Objects; + +namespace osu.Game.Rulesets.Taiko.Edit.Blueprints +{ + public class SwellPlacementBlueprint : TaikoSpanPlacementBlueprint + { + public SwellPlacementBlueprint() + : base(new Swell()) + { + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs new file mode 100644 index 0000000000..b08cc68225 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs @@ -0,0 +1,101 @@ +using System; +using osu.Framework.Graphics; +using osu.Framework.Input.Events; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.UI; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Rulesets.Taiko.Edit.Blueprints +{ + public class TaikoSpanPlacementBlueprint : PlacementBlueprint + { + private readonly HitPiece headPiece; + private readonly HitPiece tailPiece; + + private readonly LengthPiece lengthPiece; + + private readonly IHasDuration spanPlacementObject; + + public TaikoSpanPlacementBlueprint(HitObject hitObject) + : base(hitObject) + + { + spanPlacementObject = hitObject as IHasDuration; + + RelativeSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + headPiece = new HitPiece + { + Size = new Vector2(TaikoHitObject.DEFAULT_SIZE * TaikoPlayfield.DEFAULT_HEIGHT) + }, + lengthPiece = new LengthPiece + { + Height = TaikoHitObject.DEFAULT_SIZE * TaikoPlayfield.DEFAULT_HEIGHT + }, + tailPiece = new HitPiece + { + Size = new Vector2(TaikoHitObject.DEFAULT_SIZE * TaikoPlayfield.DEFAULT_HEIGHT) + } + }; + } + + private double originalStartTime; + + protected override bool OnMouseDown(MouseDownEvent e) + { + if (e.Button != MouseButton.Left) + return false; + + BeginPlacement(true); + return true; + } + + protected override void OnMouseUp(MouseUpEvent e) + { + if (e.Button != MouseButton.Left) + return; + + base.OnMouseUp(e); + EndPlacement(true); + } + + public override void UpdatePosition(SnapResult result) + { + base.UpdatePosition(result); + + if (PlacementActive) + { + if (result.Time is double endTime) + { + if (endTime < originalStartTime) + { + HitObject.StartTime = endTime; + spanPlacementObject.Duration = Math.Abs(endTime - originalStartTime); + headPiece.Position = ToLocalSpace(result.ScreenSpacePosition); + lengthPiece.X = headPiece.X; + lengthPiece.Width = tailPiece.X - headPiece.X; + } + else + { + spanPlacementObject.Duration = Math.Abs(endTime - originalStartTime); + tailPiece.Position = ToLocalSpace(result.ScreenSpacePosition); + lengthPiece.Width = tailPiece.X - headPiece.X; + } + } + } + else + { + lengthPiece.Position = headPiece.Position = tailPiece.Position = ToLocalSpace(result.ScreenSpacePosition); + + if (result.Time is double startTime) + originalStartTime = HitObject.StartTime = startTime; + } + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Edit/DrumRollCompositionTool.cs b/osu.Game.Rulesets.Taiko/Edit/DrumRollCompositionTool.cs new file mode 100644 index 0000000000..c17e22180a --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Edit/DrumRollCompositionTool.cs @@ -0,0 +1,17 @@ +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Tools; +using osu.Game.Rulesets.Taiko.Edit.Blueprints; +using osu.Game.Rulesets.Taiko.Objects; + +namespace osu.Game.Rulesets.Taiko.Edit +{ + public class DrumRollCompositionTool : HitObjectCompositionTool + { + public DrumRollCompositionTool() + : base(nameof(DrumRoll)) + { + } + + public override PlacementBlueprint CreatePlacementBlueprint() => new DrumRollPlacementBlueprint(); + } +} diff --git a/osu.Game.Rulesets.Taiko/Edit/HitCompositionTool.cs b/osu.Game.Rulesets.Taiko/Edit/HitCompositionTool.cs new file mode 100644 index 0000000000..7e8f245613 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Edit/HitCompositionTool.cs @@ -0,0 +1,17 @@ +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Tools; +using osu.Game.Rulesets.Taiko.Edit.Blueprints; +using osu.Game.Rulesets.Taiko.Objects; + +namespace osu.Game.Rulesets.Taiko.Edit +{ + public class HitCompositionTool : HitObjectCompositionTool + { + public HitCompositionTool() + : base(nameof(Hit)) + { + } + + public override PlacementBlueprint CreatePlacementBlueprint() => new HitPlacementBlueprint(); + } +} diff --git a/osu.Game.Rulesets.Taiko/Edit/SwellCompositionTool.cs b/osu.Game.Rulesets.Taiko/Edit/SwellCompositionTool.cs new file mode 100644 index 0000000000..aa96a397d0 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Edit/SwellCompositionTool.cs @@ -0,0 +1,17 @@ +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Tools; +using osu.Game.Rulesets.Taiko.Edit.Blueprints; +using osu.Game.Rulesets.Taiko.Objects; + +namespace osu.Game.Rulesets.Taiko.Edit +{ + public class SwellCompositionTool : HitObjectCompositionTool + { + public SwellCompositionTool() + : base(nameof(Swell)) + { + } + + public override PlacementBlueprint CreatePlacementBlueprint() => new SwellPlacementBlueprint(); + } +} diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs new file mode 100644 index 0000000000..b7cda04705 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Screens.Edit.Compose.Components; + +namespace osu.Game.Rulesets.Taiko.Edit +{ + public class TaikoBlueprintContainer : ComposeBlueprintContainer + { + public TaikoBlueprintContainer(IEnumerable hitObjects) + : base(hitObjects) + { + } + + protected override SelectionHandler CreateSelectionHandler() => new TaikoSelectionHandler(); + + public override OverlaySelectionBlueprint CreateBlueprintFor(DrawableHitObject hitObject) => + new TaikoSelectionBlueprint(hitObject); + } +} diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs new file mode 100644 index 0000000000..7ad40903d2 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs @@ -0,0 +1,30 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Tools; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Screens.Edit.Compose.Components; + +namespace osu.Game.Rulesets.Taiko.Edit +{ + public class TaikoHitObjectComposer : HitObjectComposer + { + public TaikoHitObjectComposer(Ruleset ruleset) + : base(ruleset) + { + } + + protected override IReadOnlyList CompositionTools => new HitObjectCompositionTool[] + { + new HitCompositionTool(), + new DrumRollCompositionTool(), + new SwellCompositionTool() + }; + + protected override ComposeBlueprintContainer CreateBlueprintContainer(IEnumerable hitObjects) + => new TaikoBlueprintContainer(hitObjects); + } +} diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionBlueprint.cs new file mode 100644 index 0000000000..e3797a5fa6 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionBlueprint.cs @@ -0,0 +1,41 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Taiko.Edit.Blueprints; +using osuTK; + +namespace osu.Game.Rulesets.Taiko.Edit +{ + public class TaikoSelectionBlueprint : OverlaySelectionBlueprint + { + public TaikoSelectionBlueprint(DrawableHitObject hitObject) + : base(hitObject) + { + RelativeSizeAxes = Axes.None; + + AddInternal(new HitPiece + { + RelativeSizeAxes = Axes.Both, + Origin = Anchor.TopLeft + }); + } + + protected override void Update() + { + base.Update(); + + // Move the rectangle to cover the hitobjects + var topLeft = new Vector2(float.MaxValue, float.MaxValue); + var bottomRight = new Vector2(float.MinValue, float.MinValue); + + topLeft = Vector2.ComponentMin(topLeft, Parent.ToLocalSpace(DrawableObject.ScreenSpaceDrawQuad.TopLeft)); + bottomRight = Vector2.ComponentMax(bottomRight, Parent.ToLocalSpace(DrawableObject.ScreenSpaceDrawQuad.BottomRight)); + + Size = bottomRight - topLeft; + Position = topLeft; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs new file mode 100644 index 0000000000..eebf6980fe --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs @@ -0,0 +1,80 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Screens.Edit.Compose.Components; + +namespace osu.Game.Rulesets.Taiko.Edit +{ + public class TaikoSelectionHandler : SelectionHandler + { + protected override IEnumerable GetContextMenuItemsForSelection(IEnumerable selection) + { + if (selection.All(s => s.HitObject is Hit)) + { + var hits = selection.Select(s => s.HitObject).OfType(); + + yield return new TernaryStateMenuItem("Rim", action: state => + { + foreach (var h in hits) + { + switch (state) + { + case TernaryState.True: + h.Type = HitType.Rim; + break; + + case TernaryState.False: + h.Type = HitType.Centre; + break; + } + } + }) + { + State = { Value = getTernaryState(hits, h => h.Type == HitType.Rim) } + }; + } + + if (selection.All(s => s.HitObject is TaikoHitObject)) + { + var hits = selection.Select(s => s.HitObject).OfType(); + + yield return new TernaryStateMenuItem("Strong", action: state => + { + foreach (var h in hits) + { + switch (state) + { + case TernaryState.True: + h.IsStrong = true; + break; + + case TernaryState.False: + h.IsStrong = false; + break; + } + + EditorBeatmap?.UpdateHitObject(h); + } + }) + { + State = { Value = getTernaryState(hits, h => h.IsStrong) } + }; + } + } + + private TernaryState getTernaryState(IEnumerable selection, Func func) + { + if (selection.Any(func)) + return selection.All(func) ? TernaryState.True : TernaryState.Indeterminate; + + return TernaryState.False; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.cs b/osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.cs deleted file mode 100644 index b34a8e75cc..0000000000 --- a/osu.Game.Rulesets.Taiko/TaikoHitObjectComposer.cs +++ /dev/null @@ -1,382 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Collections.Generic; -using System.Linq; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.UserInterface; -using osu.Framework.Input.Events; -using osu.Game.Graphics.UserInterface; -using osu.Game.Rulesets.Edit; -using osu.Game.Rulesets.Edit.Tools; -using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Objects.Types; -using osu.Game.Rulesets.Taiko.Objects; -using osu.Game.Rulesets.Taiko.UI; -using osu.Game.Screens.Edit.Compose.Components; -using osuTK; -using osuTK.Graphics; -using osuTK.Input; - -namespace osu.Game.Rulesets.Taiko -{ - public class TaikoHitObjectComposer : HitObjectComposer - { - public TaikoHitObjectComposer(Ruleset ruleset) - : base(ruleset) - { - } - - protected override IReadOnlyList CompositionTools => new HitObjectCompositionTool[] - { - new HitCompositionTool(), - new DrumRollCompositionTool(), - new SwellCompositionTool() - }; - - protected override ComposeBlueprintContainer CreateBlueprintContainer(IEnumerable hitObjects) - => new TaikoBlueprintContainer(hitObjects); - } - - public class TaikoBlueprintContainer : ComposeBlueprintContainer - { - public TaikoBlueprintContainer(IEnumerable hitObjects) - : base(hitObjects) - { - } - - protected override SelectionHandler CreateSelectionHandler() => new TaikoSelectionHandler(); - - public override OverlaySelectionBlueprint CreateBlueprintFor(DrawableHitObject hitObject) => - new TaikoSelectionBlueprint(hitObject); - } - - public class TaikoSelectionHandler : SelectionHandler - { - protected override IEnumerable GetContextMenuItemsForSelection(IEnumerable selection) - { - if (selection.All(s => s.HitObject is Hit)) - { - var hits = selection.Select(s => s.HitObject).OfType(); - - yield return new TernaryStateMenuItem("Rim", action: state => - { - foreach (var h in hits) - { - switch (state) - { - case TernaryState.True: - h.Type = HitType.Rim; - break; - - case TernaryState.False: - h.Type = HitType.Centre; - break; - } - } - }) - { - State = { Value = getTernaryState(hits, h => h.Type == HitType.Rim) } - }; - } - - if (selection.All(s => s.HitObject is TaikoHitObject)) - { - var hits = selection.Select(s => s.HitObject).OfType(); - - yield return new TernaryStateMenuItem("Strong", action: state => - { - foreach (var h in hits) - { - switch (state) - { - case TernaryState.True: - h.IsStrong = true; - break; - - case TernaryState.False: - h.IsStrong = false; - break; - } - - EditorBeatmap?.UpdateHitObject(h); - } - }) - { - State = { Value = getTernaryState(hits, h => h.IsStrong) } - }; - } - } - - private TernaryState getTernaryState(IEnumerable selection, Func func) - { - if (selection.Any(func)) - return selection.All(func) ? TernaryState.True : TernaryState.Indeterminate; - - return TernaryState.False; - } - } - - public class TaikoSelectionBlueprint : OverlaySelectionBlueprint - { - public TaikoSelectionBlueprint(DrawableHitObject hitObject) - : base(hitObject) - { - RelativeSizeAxes = Axes.None; - - AddInternal(new HitPiece - { - RelativeSizeAxes = Axes.Both, - Origin = Anchor.TopLeft - }); - } - - protected override void Update() - { - base.Update(); - - // Move the rectangle to cover the hitobjects - var topLeft = new Vector2(float.MaxValue, float.MaxValue); - var bottomRight = new Vector2(float.MinValue, float.MinValue); - - topLeft = Vector2.ComponentMin(topLeft, Parent.ToLocalSpace(DrawableObject.ScreenSpaceDrawQuad.TopLeft)); - bottomRight = Vector2.ComponentMax(bottomRight, Parent.ToLocalSpace(DrawableObject.ScreenSpaceDrawQuad.BottomRight)); - - Size = bottomRight - topLeft; - Position = topLeft; - } - } - - public class SwellCompositionTool : HitObjectCompositionTool - { - public SwellCompositionTool() - : base(nameof(Swell)) - { - } - - public override PlacementBlueprint CreatePlacementBlueprint() => new SwellPlacementBlueprint(); - } - - public class DrumRollCompositionTool : HitObjectCompositionTool - { - public DrumRollCompositionTool() - : base(nameof(DrumRoll)) - { - } - - public override PlacementBlueprint CreatePlacementBlueprint() => new DrumRollPlacementBlueprint(); - } - - public class SwellPlacementBlueprint : TaikoSpanPlacementBlueprint - { - public SwellPlacementBlueprint() - : base(new Swell()) - { - } - } - - public class DrumRollPlacementBlueprint : TaikoSpanPlacementBlueprint - { - public DrumRollPlacementBlueprint() - : base(new DrumRoll()) - { - } - } - - public class TaikoSpanPlacementBlueprint : PlacementBlueprint - { - private readonly HitPiece headPiece; - private readonly HitPiece tailPiece; - - private readonly LengthPiece lengthPiece; - - private readonly IHasDuration spanPlacementObject; - - public TaikoSpanPlacementBlueprint(HitObject hitObject) - : base(hitObject) - - { - spanPlacementObject = hitObject as IHasDuration; - - RelativeSizeAxes = Axes.Both; - - InternalChildren = new Drawable[] - { - headPiece = new HitPiece - { - Size = new Vector2(TaikoHitObject.DEFAULT_SIZE * TaikoPlayfield.DEFAULT_HEIGHT) - }, - lengthPiece = new LengthPiece - { - Height = TaikoHitObject.DEFAULT_SIZE * TaikoPlayfield.DEFAULT_HEIGHT - }, - tailPiece = new HitPiece - { - Size = new Vector2(TaikoHitObject.DEFAULT_SIZE * TaikoPlayfield.DEFAULT_HEIGHT) - } - }; - } - - private double originalStartTime; - - protected override bool OnMouseDown(MouseDownEvent e) - { - if (e.Button != MouseButton.Left) - return false; - - BeginPlacement(true); - return true; - } - - protected override void OnMouseUp(MouseUpEvent e) - { - if (e.Button != MouseButton.Left) - return; - - base.OnMouseUp(e); - EndPlacement(true); - } - - public override void UpdatePosition(SnapResult result) - { - base.UpdatePosition(result); - - if (PlacementActive) - { - if (result.Time is double endTime) - { - if (endTime < originalStartTime) - { - HitObject.StartTime = endTime; - spanPlacementObject.Duration = Math.Abs(endTime - originalStartTime); - headPiece.Position = ToLocalSpace(result.ScreenSpacePosition); - lengthPiece.X = headPiece.X; - lengthPiece.Width = tailPiece.X - headPiece.X; - } - else - { - spanPlacementObject.Duration = Math.Abs(endTime - originalStartTime); - tailPiece.Position = ToLocalSpace(result.ScreenSpacePosition); - lengthPiece.Width = tailPiece.X - headPiece.X; - } - } - } - else - { - lengthPiece.Position = headPiece.Position = tailPiece.Position = ToLocalSpace(result.ScreenSpacePosition); - - if (result.Time is double startTime) - originalStartTime = HitObject.StartTime = startTime; - } - } - } - - public class HitCompositionTool : HitObjectCompositionTool - { - public HitCompositionTool() - : base(nameof(Hit)) - { - } - - public override PlacementBlueprint CreatePlacementBlueprint() => new HitPlacementBlueprint(); - } - - public class HitPlacementBlueprint : PlacementBlueprint - { - private readonly HitPiece piece; - - private static Hit hit; - - public HitPlacementBlueprint() - : base(hit = new Hit()) - { - InternalChild = piece = new HitPiece - { - Size = new Vector2(TaikoHitObject.DEFAULT_SIZE * TaikoPlayfield.DEFAULT_HEIGHT) - }; - } - - protected override bool OnMouseDown(MouseDownEvent e) - { - switch (e.Button) - { - case MouseButton.Left: - hit.Type = HitType.Centre; - EndPlacement(true); - return true; - - case MouseButton.Right: - hit.Type = HitType.Rim; - EndPlacement(true); - return true; - } - - return false; - } - - public override void UpdatePosition(SnapResult result) - { - piece.Position = ToLocalSpace(result.ScreenSpacePosition); - base.UpdatePosition(result); - } - } - - public class LengthPiece : CompositeDrawable - { - public LengthPiece() - { - Origin = Anchor.CentreLeft; - - InternalChild = new Container - { - Masking = true, - Colour = Color4.Yellow, - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.X, - Height = 8, - }, - new Box - { - Origin = Anchor.BottomLeft, - Anchor = Anchor.BottomLeft, - RelativeSizeAxes = Axes.X, - Height = 8, - } - } - }; - } - } - - public class HitPiece : CompositeDrawable - { - public HitPiece() - { - Origin = Anchor.Centre; - - InternalChild = new CircularContainer - { - Masking = true, - BorderThickness = 10, - BorderColour = Color4.Yellow, - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Box - { - AlwaysPresent = true, - Alpha = 0, - RelativeSizeAxes = Axes.Both - } - } - }; - } - } -} diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 7be16471b4..4cdd1fbc24 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -22,6 +22,7 @@ using osu.Game.Rulesets.Taiko.Scoring; using osu.Game.Scoring; using System; using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Taiko.Edit; using osu.Game.Rulesets.Taiko.Skinning; using osu.Game.Skinning; From e0aae15c0a313549f29a610b0bc697a1fd2c9435 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 May 2020 16:40:23 +0900 Subject: [PATCH 1842/2376] Hard type incoming ruleset --- osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs index 7ad40903d2..cdc9672a8e 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs @@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Taiko.Edit { public class TaikoHitObjectComposer : HitObjectComposer { - public TaikoHitObjectComposer(Ruleset ruleset) + public TaikoHitObjectComposer(TaikoRuleset ruleset) : base(ruleset) { } From b068992a15f43237bfb3693e0fca4daa63922fd8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 May 2020 18:58:34 +0900 Subject: [PATCH 1843/2376] Add missing licence headers --- .../Edit/Blueprints/DrumRollPlacementBlueprint.cs | 3 +++ osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPiece.cs | 3 +++ .../Edit/Blueprints/HitPlacementBlueprint.cs | 3 +++ osu.Game.Rulesets.Taiko/Edit/Blueprints/LengthPiece.cs | 3 +++ .../Edit/Blueprints/SwellPlacementBlueprint.cs | 3 +++ .../Edit/Blueprints/TaikoSpanPlacementBlueprint.cs | 3 +++ osu.Game.Rulesets.Taiko/Edit/DrumRollCompositionTool.cs | 3 +++ osu.Game.Rulesets.Taiko/Edit/HitCompositionTool.cs | 3 +++ osu.Game.Rulesets.Taiko/Edit/SwellCompositionTool.cs | 3 +++ osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs | 3 +++ 10 files changed, 30 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/DrumRollPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/DrumRollPlacementBlueprint.cs index 2f086891a9..eb07ce7635 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/DrumRollPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/DrumRollPlacementBlueprint.cs @@ -1,3 +1,6 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Edit.Blueprints diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPiece.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPiece.cs index 992fba1e29..b02e3aa9ba 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPiece.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPiece.cs @@ -1,3 +1,6 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs index c21aed32e8..c5191ab241 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs @@ -1,3 +1,6 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + using osu.Framework.Input.Events; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Taiko.Objects; diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/LengthPiece.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/LengthPiece.cs index 2139a852b2..6b651fd739 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/LengthPiece.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/LengthPiece.cs @@ -1,3 +1,6 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/SwellPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/SwellPlacementBlueprint.cs index 180bf26f34..95fa82a0f2 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/SwellPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/SwellPlacementBlueprint.cs @@ -1,3 +1,6 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Edit.Blueprints diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs index b08cc68225..7f96b5a46e 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.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 osu.Framework.Graphics; using osu.Framework.Input.Events; diff --git a/osu.Game.Rulesets.Taiko/Edit/DrumRollCompositionTool.cs b/osu.Game.Rulesets.Taiko/Edit/DrumRollCompositionTool.cs index c17e22180a..bf77c76670 100644 --- a/osu.Game.Rulesets.Taiko/Edit/DrumRollCompositionTool.cs +++ b/osu.Game.Rulesets.Taiko/Edit/DrumRollCompositionTool.cs @@ -1,3 +1,6 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Taiko.Edit.Blueprints; diff --git a/osu.Game.Rulesets.Taiko/Edit/HitCompositionTool.cs b/osu.Game.Rulesets.Taiko/Edit/HitCompositionTool.cs index 7e8f245613..e877cf6240 100644 --- a/osu.Game.Rulesets.Taiko/Edit/HitCompositionTool.cs +++ b/osu.Game.Rulesets.Taiko/Edit/HitCompositionTool.cs @@ -1,3 +1,6 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Taiko.Edit.Blueprints; diff --git a/osu.Game.Rulesets.Taiko/Edit/SwellCompositionTool.cs b/osu.Game.Rulesets.Taiko/Edit/SwellCompositionTool.cs index aa96a397d0..a6191fcedc 100644 --- a/osu.Game.Rulesets.Taiko/Edit/SwellCompositionTool.cs +++ b/osu.Game.Rulesets.Taiko/Edit/SwellCompositionTool.cs @@ -1,3 +1,6 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Taiko.Edit.Blueprints; diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs index b7cda04705..36227b0798 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.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.Collections.Generic; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects.Drawables; From affad4724867f9cda7a5d707a1edca3943e0c0a0 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Fri, 29 May 2020 19:44:53 +0300 Subject: [PATCH 1844/2376] Fix genre/language search doesn't work --- .../Online/API/Requests/SearchBeatmapSetsRequest.cs | 13 ++++++++++--- .../BeatmapListing/BeatmapListingFilterControl.cs | 4 +++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs index 0c3272c7de..ce8b40aa30 100644 --- a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs +++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs @@ -27,7 +27,14 @@ namespace osu.Game.Online.API.Requests private string directionString => SortDirection == SortDirection.Descending ? @"desc" : @"asc"; - public SearchBeatmapSetsRequest(string query, RulesetInfo ruleset, Cursor cursor = null, SearchCategory searchCategory = SearchCategory.Any, SortCriteria sortCriteria = SortCriteria.Ranked, SortDirection sortDirection = SortDirection.Descending) + public SearchBeatmapSetsRequest(string query, + RulesetInfo ruleset, + Cursor cursor = null, + SearchCategory searchCategory = SearchCategory.Any, + SortCriteria sortCriteria = SortCriteria.Ranked, + SortDirection sortDirection = SortDirection.Descending, + SearchGenre genre = SearchGenre.Any, + SearchLanguage language = SearchLanguage.Any) { this.query = string.IsNullOrEmpty(query) ? string.Empty : System.Uri.EscapeDataString(query); this.ruleset = ruleset; @@ -36,8 +43,8 @@ namespace osu.Game.Online.API.Requests SearchCategory = searchCategory; SortCriteria = sortCriteria; SortDirection = sortDirection; - Genre = SearchGenre.Any; - Language = SearchLanguage.Any; + Genre = genre; + Language = language; } protected override WebRequest CreateWebRequest() diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs index 41c99d5d03..0ead5cc226 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs @@ -177,7 +177,9 @@ namespace osu.Game.Overlays.BeatmapListing lastResponse?.Cursor, searchControl.Category.Value, sortControl.Current.Value, - sortControl.SortDirection.Value); + sortControl.SortDirection.Value, + searchControl.Genre.Value, + searchControl.Language.Value); getSetsRequest.Success += response => { From 9aa54ed89e059349ca257fbb5b27e8ceee5f835e Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Fri, 29 May 2020 19:53:32 +0300 Subject: [PATCH 1845/2376] Fix serach control background never being updated --- .../Overlays/BeatmapListing/BeatmapListingFilterControl.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs index 0ead5cc226..494a0df8f8 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs @@ -188,6 +188,9 @@ namespace osu.Game.Overlays.BeatmapListing if (sets.Count == 0) noMoreResults = true; + if (CurrentPage == 0) + searchControl.BeatmapSet = sets.FirstOrDefault(); + lastResponse = response; getSetsRequest = null; From 11057cd6a83859744ee3057112eef5a8b3daefca Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Fri, 29 May 2020 21:43:31 +0300 Subject: [PATCH 1846/2376] CI fix --- osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs index ce8b40aa30..dde45b5aeb 100644 --- a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs +++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs @@ -27,7 +27,8 @@ namespace osu.Game.Online.API.Requests private string directionString => SortDirection == SortDirection.Descending ? @"desc" : @"asc"; - public SearchBeatmapSetsRequest(string query, + public SearchBeatmapSetsRequest( + string query, RulesetInfo ruleset, Cursor cursor = null, SearchCategory searchCategory = SearchCategory.Any, From 816f721f3dffb36ed0d6279237e9137bd791dba6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 30 May 2020 15:24:37 +0900 Subject: [PATCH 1847/2376] Move selection blueprint to correct namespace --- .../Edit/{ => Blueprints}/TaikoSelectionBlueprint.cs | 3 +-- osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) rename osu.Game.Rulesets.Taiko/Edit/{ => Blueprints}/TaikoSelectionBlueprint.cs (93%) diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSelectionBlueprint.cs similarity index 93% rename from osu.Game.Rulesets.Taiko/Edit/TaikoSelectionBlueprint.cs rename to osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSelectionBlueprint.cs index e3797a5fa6..62f69122cc 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSelectionBlueprint.cs @@ -4,10 +4,9 @@ using osu.Framework.Graphics; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Taiko.Edit.Blueprints; using osuTK; -namespace osu.Game.Rulesets.Taiko.Edit +namespace osu.Game.Rulesets.Taiko.Edit.Blueprints { public class TaikoSelectionBlueprint : OverlaySelectionBlueprint { diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs index 36227b0798..35227b3c64 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Taiko.Edit.Blueprints; using osu.Game.Screens.Edit.Compose.Components; namespace osu.Game.Rulesets.Taiko.Edit From 82fe99cf4a54b90a473d43855238f2aa5ae58d65 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 31 May 2020 02:18:07 +0300 Subject: [PATCH 1848/2376] Replace any potential usage of Environment.CurrentDirectory with a new RuntimeInfo.StartupDirectory Using `Environment.CurrentDirectory` for storing / reading files is dangerous as the current directory is mutable and can be changed when performing a certain operation (like opening solutions in roslyn type reference builder for example). --- osu.Desktop/Program.cs | 4 +--- .../NonVisual/CustomDataDirectoryTest.cs | 16 ++++++++-------- osu.Game.Tests/Resources/TestResources.cs | 5 +++-- osu.Game/Rulesets/RulesetStore.cs | 5 +++-- osu.Game/Tests/Visual/OsuTestScene.cs | 3 ++- 5 files changed, 17 insertions(+), 16 deletions(-) diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index bd91bcc933..285a813d97 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -33,13 +33,11 @@ namespace osu.Desktop if (args.Length > 0 && args[0].Contains('.')) // easy way to check for a file import in args { var importer = new ArchiveImportIPCChannel(host); - // Restore the cwd so relative paths given at the command line work correctly - Directory.SetCurrentDirectory(cwd); foreach (var file in args) { Console.WriteLine(@"Importing {0}", file); - if (!importer.ImportAsync(Path.GetFullPath(file)).Wait(3000)) + if (!importer.ImportAsync(Path.GetFullPath(file, cwd)).Wait(3000)) throw new TimeoutException(@"IPC took too long to send"); } diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs index 743c924bbd..1f6e92a535 100644 --- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using NUnit.Framework; +using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Framework.Platform; @@ -35,8 +36,7 @@ namespace osu.Game.Tests.NonVisual var osu = loadOsu(host); var storage = osu.Dependencies.Get(); - string defaultStorageLocation = Path.Combine(Environment.CurrentDirectory, "headless", nameof(TestDefaultDirectory)); - + string defaultStorageLocation = RuntimeInfo.StartupStorage.GetFullPath(Path.Combine("headless", nameof(TestDefaultDirectory))); Assert.That(storage.GetFullPath("."), Is.EqualTo(defaultStorageLocation)); } finally @@ -46,17 +46,17 @@ namespace osu.Game.Tests.NonVisual } } - private string customPath => Path.Combine(Environment.CurrentDirectory, "custom-path"); + private string customPath { get; } = RuntimeInfo.StartupStorage.GetFullPath("custom-path"); [Test] public void TestCustomDirectory() { using (var host = new HeadlessGameHost(nameof(TestCustomDirectory))) { - string headlessPrefix = Path.Combine("headless", nameof(TestCustomDirectory)); + string defaultStorageLocation = RuntimeInfo.StartupStorage.GetFullPath(Path.Combine("headless", nameof(TestCustomDirectory))); // need access before the game has constructed its own storage yet. - Storage storage = new DesktopStorage(headlessPrefix, host); + Storage storage = new DesktopStorage(defaultStorageLocation, host); // manual cleaning so we can prepare a config file. storage.DeleteDirectory(string.Empty); @@ -84,10 +84,10 @@ namespace osu.Game.Tests.NonVisual { using (var host = new HeadlessGameHost(nameof(TestSubDirectoryLookup))) { - string headlessPrefix = Path.Combine("headless", nameof(TestSubDirectoryLookup)); + string defaultStorageLocation = RuntimeInfo.StartupStorage.GetFullPath(Path.Combine("headless", nameof(TestSubDirectoryLookup))); // need access before the game has constructed its own storage yet. - Storage storage = new DesktopStorage(headlessPrefix, host); + Storage storage = new DesktopStorage(defaultStorageLocation, host); // manual cleaning so we can prepare a config file. storage.DeleteDirectory(string.Empty); @@ -136,7 +136,7 @@ namespace osu.Game.Tests.NonVisual // for testing nested files are not ignored (only top level) host.Storage.GetStorageForDirectory("test-nested").GetStorageForDirectory("cache"); - string defaultStorageLocation = Path.Combine(Environment.CurrentDirectory, "headless", nameof(TestMigration)); + string defaultStorageLocation = RuntimeInfo.StartupStorage.GetFullPath(Path.Combine("headless", nameof(TestMigration))); Assert.That(storage.GetFullPath("."), Is.EqualTo(defaultStorageLocation)); diff --git a/osu.Game.Tests/Resources/TestResources.cs b/osu.Game.Tests/Resources/TestResources.cs index 8b892fbb2f..33b580f68f 100644 --- a/osu.Game.Tests/Resources/TestResources.cs +++ b/osu.Game.Tests/Resources/TestResources.cs @@ -3,6 +3,7 @@ using System.IO; using NUnit.Framework; +using osu.Framework; using osu.Framework.IO.Stores; namespace osu.Game.Tests.Resources @@ -20,10 +21,10 @@ namespace osu.Game.Tests.Resources var temp = Path.GetTempFileName() + ".osz"; using (var stream = GetTestBeatmapStream(virtualTrack)) - using (var newFile = File.Create(temp)) + using (var newFile = RuntimeInfo.StartupStorage.GetStream(temp, FileAccess.Write)) stream.CopyTo(newFile); - Assert.IsTrue(File.Exists(temp)); + Assert.IsTrue(RuntimeInfo.StartupStorage.Exists(temp)); return temp; } } diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index b3026bf2b7..5c49141064 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; +using osu.Framework; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Database; @@ -153,14 +154,14 @@ namespace osu.Game.Rulesets { try { - string[] files = Directory.GetFiles(Environment.CurrentDirectory, $"{ruleset_library_prefix}.*.dll"); + var files = RuntimeInfo.StartupStorage.GetFiles($"{ruleset_library_prefix}.*.dll"); foreach (string file in files.Where(f => !Path.GetFileName(f).Contains("Tests"))) loadRulesetFromFile(file); } catch (Exception e) { - Logger.Error(e, $"Could not load rulesets from directory {Environment.CurrentDirectory}"); + Logger.Error(e, $"Could not load rulesets from directory {RuntimeInfo.StartupDirectory}"); } } diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index 5dc8714c07..2672022720 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; +using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Track; @@ -118,7 +119,7 @@ namespace osu.Game.Tests.Visual } } - localStorage = new Lazy(() => new NativeStorage($"{GetType().Name}-{Guid.NewGuid()}")); + localStorage = new Lazy(() => RuntimeInfo.StartupStorage.GetStorageForDirectory($"{GetType().Name}-{Guid.NewGuid()}")); } [Resolved] From b06017dbf16a868f353226c1bed6261bed79308c Mon Sep 17 00:00:00 2001 From: mcendu Date: Sun, 31 May 2020 11:28:54 +0800 Subject: [PATCH 1849/2376] supress horizontal scaling of left-and-right stages --- osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs index 7680526ac4..f177284399 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs @@ -52,10 +52,10 @@ namespace osu.Game.Rulesets.Mania.Skinning base.Update(); if (leftSprite?.Height > 0) - leftSprite.Scale = new Vector2(DrawHeight / leftSprite.Height); + leftSprite.Scale = new Vector2(1, DrawHeight / leftSprite.Height); if (rightSprite?.Height > 0) - rightSprite.Scale = new Vector2(DrawHeight / rightSprite.Height); + rightSprite.Scale = new Vector2(1, DrawHeight / rightSprite.Height); } } } From f2dadeeeb5bbd96c2ff9e62f3b09464eb1c7ffc5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 31 May 2020 13:50:42 +0900 Subject: [PATCH 1850/2376] Allow horizontal scroll on results screen when not hovering expanded panel --- osu.Game/Screens/Ranking/ScorePanelList.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index 18db3f2af4..1142297274 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Bindables; @@ -107,6 +108,9 @@ namespace osu.Game.Screens.Ranking // Find the panel corresponding to the new score. expandedPanel = flow.SingleOrDefault(p => p.Score == score.NewValue); + // handle horizontal scroll only when not hovering the expanded panel. + scroll.HandleScroll = () => expandedPanel?.IsHovered != true; + if (expandedPanel == null) return; @@ -166,6 +170,11 @@ namespace osu.Game.Screens.Ranking /// public float? InstantScrollTarget; + /// + /// Whether this container should handle scroll trigger events. + /// + public Func HandleScroll; + protected override void UpdateAfterChildren() { if (InstantScrollTarget != null) @@ -177,9 +186,9 @@ namespace osu.Game.Screens.Ranking base.UpdateAfterChildren(); } - public override bool HandlePositionalInput => false; + public override bool HandlePositionalInput => HandleScroll(); - public override bool HandleNonPositionalInput => false; + public override bool HandleNonPositionalInput => HandleScroll(); } } } From e43217f5797c4699511078fbe3e3550a85b5a7b1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 31 May 2020 20:01:13 +0900 Subject: [PATCH 1851/2376] Add test resources --- .../Resources/special-skin/mania-stage-left.png | Bin 0 -> 165 bytes .../Resources/special-skin/mania-stage-right.png | Bin 0 -> 899 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania-stage-left.png create mode 100644 osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania-stage-right.png diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania-stage-left.png b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania-stage-left.png new file mode 100644 index 0000000000000000000000000000000000000000..03ca371c4e26972982544c5b8f7b02a85da9a823 GIT binary patch literal 165 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61SBU+%rFB|oCO|{#S9F3${@^GvDCf{D9B#o z>Fdh=gqe+pPssgM@hqT_WQl7;iF1B#Zfaf$gL6@8Vo7R>LV0FMhJw4NZ$Nk>pEyvF zyr+v}h{pNkHS5=>EAW|^m>2+o;RFsh)1E8|D=7xCTxMCBnie0RQU*^~KbLh*2~7Y- CL?;CR literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania-stage-right.png b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania-stage-right.png new file mode 100644 index 0000000000000000000000000000000000000000..45b7be025591b85e6849824f181672905d129933 GIT binary patch literal 899 zcmZ`%OK;Oa5FY1IO4N$d6BHyYw?YLq-XwrTEITw!ATg>-6jCWI9B`9OWAG!gH^fMs zI20jqK%6;5NL)DdA0UCaqe%P;#E~l!?AobQT54-|W@oDM2f3eKxn)?okJ#UJ$W5jWM2romOJPeYQMhd6`KAGifb20Fl9?n0#3 zx#ck?2Jq5=#2B+pB~w?}7RkihJvc=z=jnPg0!6zSQfU)AsPv0_c*2zg-rmt793JK;hyD?z&F8uXqz z>tP`)qhq#lFTr$&yxmMdzx*tmJN_4`a>W_2_g!Taje r`qIzU@0rIC|JptGdXHOvF!Njj=r1MTeKD0y_QurP8|D4AM=$;YKmXm5 literal 0 HcmV?d00001 From 81b8898272fa1bf325afb85236bb0020be65667c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 31 May 2020 22:30:55 +0900 Subject: [PATCH 1852/2376] Fix incorrect type cast in encoder --- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index d7e83fa471..ab1b8aecfd 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -233,9 +233,9 @@ namespace osu.Game.Beatmaps.Formats writer.Write(FormattableString.Invariant($"{(int)getObjectType(hitObject)},")); writer.Write(FormattableString.Invariant($"{(int)toLegacyHitSoundType(hitObject.Samples)},")); - if (hitObject is IHasPathWithRepeats curveData) + if (hitObject is IHasPath path) { - addPathData(writer, curveData, position); + addPathData(writer, path, position); writer.Write(getSampleBank(hitObject.Samples, zeroBanks: true)); } else From 19be111da098a03227d645ee13f75b3b42b74814 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 31 May 2020 22:33:10 +0900 Subject: [PATCH 1853/2376] Move incorrect placed full stop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index c956439eb2..6dd6e6815b 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -171,7 +171,7 @@ namespace osu.Game.Rulesets.Edit /// The ruleset used to construct its drawable counterpart. /// The loaded beatmap. /// The mods to be applied. - /// An editor-relevant . + /// An editor-relevant . protected virtual DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) => (DrawableRuleset)ruleset.CreateDrawableRulesetWith(beatmap, mods); From e688033967bbd5000f0f515d8e63ccafa1ded00d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 31 May 2020 22:39:03 +0900 Subject: [PATCH 1854/2376] Fix incorrect xmldoc --- osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index d8d90fddfa..9e936c7717 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -331,7 +331,7 @@ namespace osu.Game.Rulesets.Objects.Legacy /// The position of the hit object. /// Whether the hit object creates a new combo. /// When starting a new combo, the offset of the new combo relative to the current one. - /// The hold end time. + /// The hold duration. protected abstract HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double duration); private List convertSoundType(LegacyHitSoundType type, SampleBankInfo bankInfo) From 0027f44bd0d0a099a4bd1ce1b5a053b3c771d1b3 Mon Sep 17 00:00:00 2001 From: Shivam Date: Sun, 31 May 2020 16:27:05 +0200 Subject: [PATCH 1855/2376] Moved stableInfo read to FileBasedIPC DI is also not needed anymore to access StableInfo, this goes through FileBasedIPC. Note: directory selector now always navigates to the osu! lazer base path. --- osu.Game.Tournament/IPC/FileBasedIPC.cs | 30 +++++++++++++++---- osu.Game.Tournament/Screens/SetupScreen.cs | 12 ++------ .../Screens/StablePathSelectScreen.cs | 10 ++----- osu.Game.Tournament/TournamentGameBase.cs | 19 ------------ 4 files changed, 31 insertions(+), 40 deletions(-) diff --git a/osu.Game.Tournament/IPC/FileBasedIPC.cs b/osu.Game.Tournament/IPC/FileBasedIPC.cs index 8518b7f8da..4ec9d2012a 100644 --- a/osu.Game.Tournament/IPC/FileBasedIPC.cs +++ b/osu.Game.Tournament/IPC/FileBasedIPC.cs @@ -37,8 +37,7 @@ namespace osu.Game.Tournament.IPC private int lastBeatmapId; private ScheduledDelegate scheduled; - [Resolved] - private StableInfo stableInfo { get; set; } + private StableInfo stableInfo; public const string STABLE_CONFIG = "tournament/stable.json"; @@ -161,9 +160,11 @@ namespace osu.Game.Tournament.IPC public static bool CheckExists(string p) => File.Exists(Path.Combine(p, "ipc.txt")); + public StableInfo GetStableInfo() => stableInfo; + private string findStablePath() { - if (!string.IsNullOrEmpty(stableInfo.StablePath.Value)) + if (!string.IsNullOrEmpty(readStableConfig())) return stableInfo.StablePath.Value; string stableInstallPath = string.Empty; @@ -184,7 +185,7 @@ namespace osu.Game.Tournament.IPC if (stableInstallPath != null) { - saveStableConfig(stableInstallPath); + SaveStableConfig(stableInstallPath); return stableInstallPath; } } @@ -197,7 +198,7 @@ namespace osu.Game.Tournament.IPC } } - private void saveStableConfig(string path) + public void SaveStableConfig(string path) { stableInfo.StablePath.Value = path; @@ -214,6 +215,25 @@ namespace osu.Game.Tournament.IPC } } + private string readStableConfig() + { + if (stableInfo == null) + stableInfo = new StableInfo(); + + if (tournamentStorage.Exists(FileBasedIPC.STABLE_CONFIG)) + { + using (Stream stream = tournamentStorage.GetStream(FileBasedIPC.STABLE_CONFIG, FileAccess.Read, FileMode.Open)) + using (var sr = new StreamReader(stream)) + { + stableInfo = JsonConvert.DeserializeObject(sr.ReadToEnd()); + } + + return stableInfo.StablePath.Value; + } + + return null; + } + private string findFromEnvVar() { try diff --git a/osu.Game.Tournament/Screens/SetupScreen.cs b/osu.Game.Tournament/Screens/SetupScreen.cs index 9f8f81aa80..da91fbba04 100644 --- a/osu.Game.Tournament/Screens/SetupScreen.cs +++ b/osu.Game.Tournament/Screens/SetupScreen.cs @@ -15,7 +15,6 @@ using osu.Game.Online.API; using osu.Game.Overlays; using osu.Game.Rulesets; using osu.Game.Tournament.IPC; -using osu.Framework.Platform; using osu.Game.Tournament.Models; using osuTK; using osuTK.Graphics; @@ -43,12 +42,6 @@ namespace osu.Game.Tournament.Screens private Bindable windowSize; - [Resolved] - private Storage storage { get; set; } - - [Resolved] - private StableInfo stableInfo { get; set; } - [BackgroundDependencyLoader] private void load(FrameworkConfigManager frameworkConfig) { @@ -73,6 +66,7 @@ namespace osu.Game.Tournament.Screens private void reload() { var fileBasedIpc = ipc as FileBasedIPC; + StableInfo stableInfo = fileBasedIpc?.GetStableInfo(); fillFlow.Children = new Drawable[] { new ActionableInfo @@ -81,13 +75,13 @@ namespace osu.Game.Tournament.Screens ButtonText = "Change source", Action = () => { - stableInfo.StablePath.BindValueChanged(_ => + stableInfo?.StablePath.BindValueChanged(_ => { Schedule(reload); }); sceneManager?.SetScreen(new StablePathSelectScreen()); }, - Value = fileBasedIpc?.IPCStorage?.GetFullPath(string.Empty) ?? "Not found", + Value = fileBasedIpc?.IPCStorage.GetFullPath(string.Empty) ?? "Not found", Failing = fileBasedIpc?.IPCStorage == null, Description = "The osu!stable installation which is currently being used as a data source. If a source is not found, make sure you have created an empty ipc.txt in your stable cutting-edge installation." }, diff --git a/osu.Game.Tournament/Screens/StablePathSelectScreen.cs b/osu.Game.Tournament/Screens/StablePathSelectScreen.cs index eace3c78d5..2e1f0180a9 100644 --- a/osu.Game.Tournament/Screens/StablePathSelectScreen.cs +++ b/osu.Game.Tournament/Screens/StablePathSelectScreen.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Logging; using osu.Framework.Platform; -using osu.Game.Tournament.Models; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -24,9 +23,6 @@ namespace osu.Game.Tournament.Screens { private DirectorySelector directorySelector; - [Resolved] - private StableInfo stableInfo { get; set; } - [Resolved] private MatchIPCInfo ipc { get; set; } @@ -38,7 +34,7 @@ namespace osu.Game.Tournament.Screens [BackgroundDependencyLoader(true)] private void load(Storage storage, OsuColour colours) { - var initialPath = new DirectoryInfo(storage.GetFullPath(stableInfo.StablePath.Value ?? string.Empty)).Parent?.FullName; + var initialPath = new DirectoryInfo(storage.GetFullPath(string.Empty)).Parent?.FullName; AddRangeInternal(new Drawable[] { @@ -131,7 +127,7 @@ namespace osu.Game.Tournament.Screens protected virtual void ChangePath(Storage storage) { var target = directorySelector.CurrentDirectory.Value.FullName; - stableInfo.StablePath.Value = target; + var fileBasedIpc = ipc as FileBasedIPC; Logger.Log($"Changing Stable CE location to {target}"); if (!FileBasedIPC.CheckExists(target)) @@ -143,7 +139,7 @@ namespace osu.Game.Tournament.Screens return; } - var fileBasedIpc = ipc as FileBasedIPC; + fileBasedIpc?.SaveStableConfig(target); fileBasedIpc?.LocateStableStorage(); sceneManager?.SetScreen(typeof(SetupScreen)); } diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index dcfe646390..85db9e61fb 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -43,7 +43,6 @@ namespace osu.Game.Tournament private Bindable windowSize; private FileBasedIPC ipc; - private StableInfo stableInfo; private Drawable heightWarning; @@ -72,7 +71,6 @@ namespace osu.Game.Tournament }), true); readBracket(); - readStableConfig(); ladder.CurrentMatch.Value = ladder.Matches.FirstOrDefault(p => p.Current.Value); @@ -143,23 +141,6 @@ namespace osu.Game.Tournament }); } - private void readStableConfig() - { - if (stableInfo == null) - stableInfo = new StableInfo(); - - if (storage.Exists(FileBasedIPC.STABLE_CONFIG)) - { - using (Stream stream = storage.GetStream(FileBasedIPC.STABLE_CONFIG, FileAccess.Read, FileMode.Open)) - using (var sr = new StreamReader(stream)) - { - stableInfo = JsonConvert.DeserializeObject(sr.ReadToEnd()); - } - } - - dependencies.Cache(stableInfo); - } - private void readBracket() { if (storage.Exists(bracket_filename)) From ce360a960f2d0d876a7c424baac0cd202edc336c Mon Sep 17 00:00:00 2001 From: Shivam Date: Sun, 31 May 2020 16:50:13 +0200 Subject: [PATCH 1856/2376] use GameHost's GetStorage instead of local storage This will now get the IPC Path again as the default path if one is present, else it will fall back to osu! lazer's base path. --- osu.Game.Tournament/Screens/StablePathSelectScreen.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/StablePathSelectScreen.cs b/osu.Game.Tournament/Screens/StablePathSelectScreen.cs index 2e1f0180a9..50db0afa66 100644 --- a/osu.Game.Tournament/Screens/StablePathSelectScreen.cs +++ b/osu.Game.Tournament/Screens/StablePathSelectScreen.cs @@ -31,10 +31,14 @@ namespace osu.Game.Tournament.Screens [Resolved(canBeNull: true)] private TournamentSceneManager sceneManager { get; set; } + [Resolved] + private GameHost host { get; set; } + [BackgroundDependencyLoader(true)] private void load(Storage storage, OsuColour colours) { - var initialPath = new DirectoryInfo(storage.GetFullPath(string.Empty)).Parent?.FullName; + var fileBasedIpc = ipc as FileBasedIPC; + var initialPath = new DirectoryInfo(host.GetStorage(fileBasedIpc?.GetStableInfo().StablePath.Value).GetFullPath(string.Empty) ?? storage.GetFullPath(string.Empty)).Parent?.FullName; AddRangeInternal(new Drawable[] { From 33d731644c092f7164687b1ceeed6ec3145bae4b Mon Sep 17 00:00:00 2001 From: Shivam Date: Sun, 31 May 2020 17:35:53 +0200 Subject: [PATCH 1857/2376] Fix test crashing: NullReferenceException --- osu.Game.Tournament/Screens/SetupScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/SetupScreen.cs b/osu.Game.Tournament/Screens/SetupScreen.cs index da91fbba04..19ac84dea3 100644 --- a/osu.Game.Tournament/Screens/SetupScreen.cs +++ b/osu.Game.Tournament/Screens/SetupScreen.cs @@ -81,7 +81,7 @@ namespace osu.Game.Tournament.Screens }); sceneManager?.SetScreen(new StablePathSelectScreen()); }, - Value = fileBasedIpc?.IPCStorage.GetFullPath(string.Empty) ?? "Not found", + Value = fileBasedIpc?.IPCStorage?.GetFullPath(string.Empty) ?? "Not found", Failing = fileBasedIpc?.IPCStorage == null, Description = "The osu!stable installation which is currently being used as a data source. If a source is not found, make sure you have created an empty ipc.txt in your stable cutting-edge installation." }, From 2c6887e610dec2babe5f1436b2406b31547b7a15 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 31 May 2020 19:49:03 +0300 Subject: [PATCH 1858/2376] Remove unnecessary use of and remove StartupStorage --- osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs | 10 +++++----- osu.Game/Rulesets/RulesetStore.cs | 2 +- osu.Game/Tests/Visual/OsuTestScene.cs | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs index 1f6e92a535..f3d54d876a 100644 --- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs @@ -36,7 +36,7 @@ namespace osu.Game.Tests.NonVisual var osu = loadOsu(host); var storage = osu.Dependencies.Get(); - string defaultStorageLocation = RuntimeInfo.StartupStorage.GetFullPath(Path.Combine("headless", nameof(TestDefaultDirectory))); + string defaultStorageLocation = Path.Combine(RuntimeInfo.StartupDirectory, "headless", nameof(TestDefaultDirectory)); Assert.That(storage.GetFullPath("."), Is.EqualTo(defaultStorageLocation)); } finally @@ -46,14 +46,14 @@ namespace osu.Game.Tests.NonVisual } } - private string customPath { get; } = RuntimeInfo.StartupStorage.GetFullPath("custom-path"); + private string customPath => Path.Combine(RuntimeInfo.StartupDirectory, "custom-path"); [Test] public void TestCustomDirectory() { using (var host = new HeadlessGameHost(nameof(TestCustomDirectory))) { - string defaultStorageLocation = RuntimeInfo.StartupStorage.GetFullPath(Path.Combine("headless", nameof(TestCustomDirectory))); + string defaultStorageLocation = Path.Combine(RuntimeInfo.StartupDirectory, "headless", nameof(TestCustomDirectory)); // need access before the game has constructed its own storage yet. Storage storage = new DesktopStorage(defaultStorageLocation, host); @@ -84,7 +84,7 @@ namespace osu.Game.Tests.NonVisual { using (var host = new HeadlessGameHost(nameof(TestSubDirectoryLookup))) { - string defaultStorageLocation = RuntimeInfo.StartupStorage.GetFullPath(Path.Combine("headless", nameof(TestSubDirectoryLookup))); + string defaultStorageLocation = Path.Combine(RuntimeInfo.StartupDirectory, "headless", nameof(TestSubDirectoryLookup)); // need access before the game has constructed its own storage yet. Storage storage = new DesktopStorage(defaultStorageLocation, host); @@ -136,7 +136,7 @@ namespace osu.Game.Tests.NonVisual // for testing nested files are not ignored (only top level) host.Storage.GetStorageForDirectory("test-nested").GetStorageForDirectory("cache"); - string defaultStorageLocation = RuntimeInfo.StartupStorage.GetFullPath(Path.Combine("headless", nameof(TestMigration))); + string defaultStorageLocation = Path.Combine(RuntimeInfo.StartupDirectory, "headless", nameof(TestMigration)); Assert.That(storage.GetFullPath("."), Is.EqualTo(defaultStorageLocation)); diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index 5c49141064..10b6edca8c 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -154,7 +154,7 @@ namespace osu.Game.Rulesets { try { - var files = RuntimeInfo.StartupStorage.GetFiles($"{ruleset_library_prefix}.*.dll"); + var files = Directory.GetFiles(Path.Combine(RuntimeInfo.StartupDirectory, $"{ruleset_library_prefix}.*.dll")); foreach (string file in files.Where(f => !Path.GetFileName(f).Contains("Tests"))) loadRulesetFromFile(file); diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index 2672022720..632d668a01 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -119,7 +119,7 @@ namespace osu.Game.Tests.Visual } } - localStorage = new Lazy(() => RuntimeInfo.StartupStorage.GetStorageForDirectory($"{GetType().Name}-{Guid.NewGuid()}")); + localStorage = new Lazy(() => new NativeStorage(Path.Combine(RuntimeInfo.StartupDirectory, $"{GetType().Name}-{Guid.NewGuid()}"))); } [Resolved] From 53b58910c3d0faacf969685c45ba5620e90d5917 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 1 Jun 2020 14:27:39 +0900 Subject: [PATCH 1859/2376] Invert interface definition --- osu.Game/Rulesets/Objects/Types/IHasPathWithRepeats.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Types/IHasPathWithRepeats.cs b/osu.Game/Rulesets/Objects/Types/IHasPathWithRepeats.cs index fba0fd7aff..342b0ec1f6 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasPathWithRepeats.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasPathWithRepeats.cs @@ -9,12 +9,14 @@ namespace osu.Game.Rulesets.Objects.Types /// /// A HitObject that has a curve. /// - public interface IHasPathWithRepeats : IHasPath, IHasRepeats +#pragma warning disable 618 + public interface IHasPathWithRepeats : IHasCurve +#pragma warning restore 618 { } [Obsolete("Use IHasPathWithRepeats instead.")] // can be removed 20201126 - public interface IHasCurve : IHasPathWithRepeats + public interface IHasCurve : IHasPath, IHasRepeats { } From cac6e93575890d31ea6e4276d93b3b4698ea440c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 1 Jun 2020 15:10:22 +0900 Subject: [PATCH 1860/2376] Restore original IHasCurve implementation --- .../Rulesets/Objects/Legacy/ConvertSlider.cs | 5 +- osu.Game/Rulesets/Objects/Types/IHasCurve.cs | 55 +++++++++++++++++++ .../Objects/Types/IHasPathWithRepeats.cs | 11 +--- 3 files changed, 61 insertions(+), 10 deletions(-) create mode 100644 osu.Game/Rulesets/Objects/Types/IHasCurve.cs diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs index 73192dc42e..c946e43df3 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs @@ -9,7 +9,10 @@ using osu.Game.Beatmaps.ControlPoints; namespace osu.Game.Rulesets.Objects.Legacy { - internal abstract class ConvertSlider : ConvertHitObject, IHasPathWithRepeats, IHasLegacyLastTickOffset + internal abstract class ConvertSlider : ConvertHitObject, IHasPathWithRepeats, IHasLegacyLastTickOffset, +#pragma warning disable 618 + IHasCurve +#pragma warning restore 618 { /// /// Scoring distance with a speed-adjusted beat length of 1 second. diff --git a/osu.Game/Rulesets/Objects/Types/IHasCurve.cs b/osu.Game/Rulesets/Objects/Types/IHasCurve.cs new file mode 100644 index 0000000000..26f50ffa31 --- /dev/null +++ b/osu.Game/Rulesets/Objects/Types/IHasCurve.cs @@ -0,0 +1,55 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osuTK; + +namespace osu.Game.Rulesets.Objects.Types +{ + [Obsolete("Use IHasPathWithRepeats instead.")] // can be removed 20201126 + public interface IHasCurve : IHasDistance, IHasRepeats + { + /// + /// The curve. + /// + SliderPath Path { get; } + } + +#pragma warning disable 618 + [Obsolete("Use IHasPathWithRepeats instead.")] // can be removed 20201126 + public static class HasCurveExtensions + { + /// + /// Computes the position on the curve relative to how much of the has been completed. + /// + /// The curve. + /// [0, 1] where 0 is the start time of the and 1 is the end time of the . + /// The position on the curve. + public static Vector2 CurvePositionAt(this IHasCurve obj, double progress) + => obj.Path.PositionAt(obj.ProgressAt(progress)); + + /// + /// Computes the progress along the curve relative to how much of the has been completed. + /// + /// The curve. + /// [0, 1] where 0 is the start time of the and 1 is the end time of the . + /// [0, 1] where 0 is the beginning of the curve and 1 is the end of the curve. + public static double ProgressAt(this IHasCurve obj, double progress) + { + double p = progress * obj.SpanCount() % 1; + if (obj.SpanAt(progress) % 2 == 1) + p = 1 - p; + return p; + } + + /// + /// Determines which span of the curve the progress point is on. + /// + /// The curve. + /// [0, 1] where 0 is the beginning of the curve and 1 is the end of the curve. + /// [0, SpanCount) where 0 is the first run. + public static int SpanAt(this IHasCurve obj, double progress) + => (int)(progress * obj.SpanCount()); + } +#pragma warning restore 618 +} diff --git a/osu.Game/Rulesets/Objects/Types/IHasPathWithRepeats.cs b/osu.Game/Rulesets/Objects/Types/IHasPathWithRepeats.cs index 342b0ec1f6..279946b44e 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasPathWithRepeats.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasPathWithRepeats.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using osuTK; namespace osu.Game.Rulesets.Objects.Types @@ -9,14 +8,8 @@ namespace osu.Game.Rulesets.Objects.Types /// /// A HitObject that has a curve. /// -#pragma warning disable 618 - public interface IHasPathWithRepeats : IHasCurve -#pragma warning restore 618 - { - } - - [Obsolete("Use IHasPathWithRepeats instead.")] // can be removed 20201126 - public interface IHasCurve : IHasPath, IHasRepeats + // ReSharper disable once RedundantExtendsListEntry + public interface IHasPathWithRepeats : IHasPath, IHasRepeats { } From 7a9ed78527d1f9c02d7e3509011cc6be5a8ec60b Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 1 Jun 2020 11:57:32 +0300 Subject: [PATCH 1861/2376] Remove missed leftover usages --- osu.Game.Tests/Resources/TestResources.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Resources/TestResources.cs b/osu.Game.Tests/Resources/TestResources.cs index 33b580f68f..1c264f66e0 100644 --- a/osu.Game.Tests/Resources/TestResources.cs +++ b/osu.Game.Tests/Resources/TestResources.cs @@ -18,14 +18,14 @@ namespace osu.Game.Tests.Resources public static string GetTestBeatmapForImport(bool virtualTrack = false) { - var temp = Path.GetTempFileName() + ".osz"; + var tempPath = Path.Combine(RuntimeInfo.StartupDirectory, Path.GetTempFileName() + ".osz"); using (var stream = GetTestBeatmapStream(virtualTrack)) - using (var newFile = RuntimeInfo.StartupStorage.GetStream(temp, FileAccess.Write)) + using (var newFile = File.Create(tempPath)) stream.CopyTo(newFile); - Assert.IsTrue(RuntimeInfo.StartupStorage.Exists(temp)); - return temp; + Assert.IsTrue(File.Exists(tempPath)); + return tempPath; } } } From fbd9ad411f85ceb5b8894701b830d5da8b6bac11 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 1 Jun 2020 09:05:46 +0000 Subject: [PATCH 1862/2376] Bump DiffPlex from 1.6.2 to 1.6.3 Bumps [DiffPlex](https://github.com/mmanela/diffplex) from 1.6.2 to 1.6.3. - [Release notes](https://github.com/mmanela/diffplex/releases) - [Commits](https://github.com/mmanela/diffplex/commits) Signed-off-by: dependabot-preview[bot] --- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 3d2a4f3081..305e4e0a92 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -19,7 +19,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 8a7f75b515..016f2ba35d 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -75,7 +75,7 @@ - + From e9b09373e784e50a9bd6c718656df9dc2dd371b1 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Mon, 1 Jun 2020 17:41:04 +0200 Subject: [PATCH 1863/2376] Fix crashing if selected ruleset doesn't have an autoplay mod. --- osu.Game/Rulesets/Ruleset.cs | 2 +- osu.Game/Screens/Select/PlaySongSelect.cs | 22 ++++++++++++++-------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index bee11accca..8f41e421a3 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -100,7 +100,7 @@ namespace osu.Game.Rulesets return value; } - public ModAutoplay GetAutoplayMod() => GetAllMods().OfType().First(); + public ModAutoplay GetAutoplayMod() => GetAllMods().OfType().FirstOrDefault(); public virtual ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => null; diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index 0a4c0e2085..71ab3715e0 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -49,8 +49,11 @@ namespace osu.Game.Screens.Select if (removeAutoModOnResume) { - var autoType = Ruleset.Value.CreateInstance().GetAutoplayMod().GetType(); - ModSelect.DeselectTypes(new[] { autoType }, true); + var autoType = Ruleset.Value.CreateInstance().GetAutoplayMod()?.GetType(); + + if (autoType != null) + ModSelect.DeselectTypes(new[] { autoType }, true); + removeAutoModOnResume = false; } } @@ -78,14 +81,17 @@ namespace osu.Game.Screens.Select if (GetContainingInputManager().CurrentState?.Keyboard.ControlPressed == true) { var auto = Ruleset.Value.CreateInstance().GetAutoplayMod(); - var autoType = auto.GetType(); + var autoType = auto?.GetType(); - var mods = Mods.Value; - - if (mods.All(m => m.GetType() != autoType)) + if (autoType != null) { - Mods.Value = mods.Append(auto).ToArray(); - removeAutoModOnResume = true; + var mods = Mods.Value; + + if (mods.All(m => m.GetType() != autoType)) + { + Mods.Value = mods.Append(auto).ToArray(); + removeAutoModOnResume = true; + } } } From fea5c8460a45026fbe667d780d484863437e804c Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 1 Jun 2020 22:50:24 +0200 Subject: [PATCH 1864/2376] Fixed path is empty exception Also converted method to property get, private set --- osu.Game.Tournament/IPC/FileBasedIPC.cs | 20 +++++++++---------- osu.Game.Tournament/Screens/SetupScreen.cs | 2 +- .../Screens/StablePathSelectScreen.cs | 7 ++++++- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tournament/IPC/FileBasedIPC.cs b/osu.Game.Tournament/IPC/FileBasedIPC.cs index 4ec9d2012a..44a010e506 100644 --- a/osu.Game.Tournament/IPC/FileBasedIPC.cs +++ b/osu.Game.Tournament/IPC/FileBasedIPC.cs @@ -37,7 +37,7 @@ namespace osu.Game.Tournament.IPC private int lastBeatmapId; private ScheduledDelegate scheduled; - private StableInfo stableInfo; + public StableInfo StableInfo { get; private set; } public const string STABLE_CONFIG = "tournament/stable.json"; @@ -160,12 +160,10 @@ namespace osu.Game.Tournament.IPC public static bool CheckExists(string p) => File.Exists(Path.Combine(p, "ipc.txt")); - public StableInfo GetStableInfo() => stableInfo; - private string findStablePath() { if (!string.IsNullOrEmpty(readStableConfig())) - return stableInfo.StablePath.Value; + return StableInfo.StablePath.Value; string stableInstallPath = string.Empty; @@ -186,7 +184,7 @@ namespace osu.Game.Tournament.IPC if (stableInstallPath != null) { SaveStableConfig(stableInstallPath); - return stableInstallPath; + return null; } } @@ -200,12 +198,12 @@ namespace osu.Game.Tournament.IPC public void SaveStableConfig(string path) { - stableInfo.StablePath.Value = path; + StableInfo.StablePath.Value = path; using (var stream = tournamentStorage.GetStream(STABLE_CONFIG, FileAccess.Write, FileMode.Create)) using (var sw = new StreamWriter(stream)) { - sw.Write(JsonConvert.SerializeObject(stableInfo, + sw.Write(JsonConvert.SerializeObject(StableInfo, new JsonSerializerSettings { Formatting = Formatting.Indented, @@ -217,18 +215,18 @@ namespace osu.Game.Tournament.IPC private string readStableConfig() { - if (stableInfo == null) - stableInfo = new StableInfo(); + if (StableInfo == null) + StableInfo = new StableInfo(); if (tournamentStorage.Exists(FileBasedIPC.STABLE_CONFIG)) { using (Stream stream = tournamentStorage.GetStream(FileBasedIPC.STABLE_CONFIG, FileAccess.Read, FileMode.Open)) using (var sr = new StreamReader(stream)) { - stableInfo = JsonConvert.DeserializeObject(sr.ReadToEnd()); + StableInfo = JsonConvert.DeserializeObject(sr.ReadToEnd()); } - return stableInfo.StablePath.Value; + return StableInfo.StablePath.Value; } return null; diff --git a/osu.Game.Tournament/Screens/SetupScreen.cs b/osu.Game.Tournament/Screens/SetupScreen.cs index 19ac84dea3..db7669184f 100644 --- a/osu.Game.Tournament/Screens/SetupScreen.cs +++ b/osu.Game.Tournament/Screens/SetupScreen.cs @@ -66,7 +66,7 @@ namespace osu.Game.Tournament.Screens private void reload() { var fileBasedIpc = ipc as FileBasedIPC; - StableInfo stableInfo = fileBasedIpc?.GetStableInfo(); + StableInfo stableInfo = fileBasedIpc?.StableInfo; fillFlow.Children = new Drawable[] { new ActionableInfo diff --git a/osu.Game.Tournament/Screens/StablePathSelectScreen.cs b/osu.Game.Tournament/Screens/StablePathSelectScreen.cs index 50db0afa66..fee2696c4c 100644 --- a/osu.Game.Tournament/Screens/StablePathSelectScreen.cs +++ b/osu.Game.Tournament/Screens/StablePathSelectScreen.cs @@ -38,7 +38,12 @@ namespace osu.Game.Tournament.Screens private void load(Storage storage, OsuColour colours) { var fileBasedIpc = ipc as FileBasedIPC; - var initialPath = new DirectoryInfo(host.GetStorage(fileBasedIpc?.GetStableInfo().StablePath.Value).GetFullPath(string.Empty) ?? storage.GetFullPath(string.Empty)).Parent?.FullName; + var initialPath = new DirectoryInfo(storage.GetFullPath(string.Empty)).Parent?.FullName; + + if (!string.IsNullOrEmpty(fileBasedIpc?.StableInfo.StablePath.Value)) + { + initialPath = new DirectoryInfo(host.GetStorage(fileBasedIpc.StableInfo.StablePath.Value).GetFullPath(string.Empty)).Parent?.FullName; + } AddRangeInternal(new Drawable[] { From 578c955658fb4846acb022b64d965896c9d0b897 Mon Sep 17 00:00:00 2001 From: Shivam Date: Tue, 2 Jun 2020 03:48:23 +0200 Subject: [PATCH 1865/2376] Add fallback intro screen --- .../Visual/Menus/TestSceneIntroFallback.cs | 15 +++++ osu.Game/Configuration/IntroSequence.cs | 1 + osu.Game/Screens/Loader.cs | 3 + osu.Game/Screens/Menu/IntroFallback.cs | 56 +++++++++++++++++++ 4 files changed, 75 insertions(+) create mode 100644 osu.Game.Tests/Visual/Menus/TestSceneIntroFallback.cs create mode 100644 osu.Game/Screens/Menu/IntroFallback.cs diff --git a/osu.Game.Tests/Visual/Menus/TestSceneIntroFallback.cs b/osu.Game.Tests/Visual/Menus/TestSceneIntroFallback.cs new file mode 100644 index 0000000000..cb32d6bf32 --- /dev/null +++ b/osu.Game.Tests/Visual/Menus/TestSceneIntroFallback.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 NUnit.Framework; +using osu.Framework.Screens; +using osu.Game.Screens.Menu; + +namespace osu.Game.Tests.Visual.Menus +{ + [TestFixture] + public class TestSceneIntroFallback : IntroTestScene + { + protected override IScreen CreateScreen() => new IntroFallback(); + } +} diff --git a/osu.Game/Configuration/IntroSequence.cs b/osu.Game/Configuration/IntroSequence.cs index 1ee7da8bac..24f8c0f048 100644 --- a/osu.Game/Configuration/IntroSequence.cs +++ b/osu.Game/Configuration/IntroSequence.cs @@ -6,6 +6,7 @@ namespace osu.Game.Configuration public enum IntroSequence { Circles, + Fallback, Triangles, Random } diff --git a/osu.Game/Screens/Loader.cs b/osu.Game/Screens/Loader.cs index a5b55a24e5..690868bd36 100644 --- a/osu.Game/Screens/Loader.cs +++ b/osu.Game/Screens/Loader.cs @@ -51,6 +51,9 @@ namespace osu.Game.Screens case IntroSequence.Circles: return new IntroCircles(); + case IntroSequence.Fallback: + return new IntroFallback(); + default: return new IntroTriangles(); } diff --git a/osu.Game/Screens/Menu/IntroFallback.cs b/osu.Game/Screens/Menu/IntroFallback.cs new file mode 100644 index 0000000000..bc01e9c502 --- /dev/null +++ b/osu.Game/Screens/Menu/IntroFallback.cs @@ -0,0 +1,56 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Screens; +using osu.Framework.Graphics; + +namespace osu.Game.Screens.Menu +{ + public class IntroFallback : IntroScreen + { + protected override string BeatmapHash => "64E00D7022195959BFA3109D09C2E2276C8F12F486B91FCF6175583E973B48F2"; + protected override string BeatmapFile => "welcome.osz"; + private const double delay_step_two = 3000; + + private SampleChannel welcome; + + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + if (MenuVoice.Value) + welcome = audio.Samples.Get(@"welcome"); + } + + protected override void LogoArriving(OsuLogo logo, bool resuming) + { + base.LogoArriving(logo, resuming); + + if (!resuming) + { + welcome?.Play(); + + Scheduler.AddDelayed(delegate + { + StartTrack(); + + PrepareMenuLoad(); + + Scheduler.AddDelayed(LoadMenu, 0); + }, delay_step_two); + + logo.ScaleTo(1); + logo.FadeIn(); + logo.PlayIntro(); + } + } + + public override void OnSuspending(IScreen next) + { + this.FadeOut(300); + base.OnSuspending(next); + } + } +} From 1ccdfd736429a43f913491a6d562086c97e5133d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Jun 2020 14:03:13 +0900 Subject: [PATCH 1866/2376] Pull playlist beatmap checksum from api --- osu.Game/Online/API/APIPlaylistBeatmap.cs | 23 +++++++++++++++++++ .../API/Requests/Responses/APIBeatmap.cs | 2 +- osu.Game/Online/Multiplayer/PlaylistItem.cs | 3 +-- 3 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 osu.Game/Online/API/APIPlaylistBeatmap.cs diff --git a/osu.Game/Online/API/APIPlaylistBeatmap.cs b/osu.Game/Online/API/APIPlaylistBeatmap.cs new file mode 100644 index 0000000000..4f7786e880 --- /dev/null +++ b/osu.Game/Online/API/APIPlaylistBeatmap.cs @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Newtonsoft.Json; +using osu.Game.Beatmaps; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Rulesets; + +namespace osu.Game.Online.API +{ + public class APIPlaylistBeatmap : APIBeatmap + { + [JsonProperty("checksum")] + public string Checksum { get; set; } + + public override BeatmapInfo ToBeatmap(RulesetStore rulesets) + { + var b = base.ToBeatmap(rulesets); + b.MD5Hash = Checksum; + return b; + } + } +} diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs index e023a2502f..ae65ac09b2 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 BeatmapInfo ToBeatmap(RulesetStore rulesets) + public virtual BeatmapInfo ToBeatmap(RulesetStore rulesets) { var set = BeatmapSet?.ToBeatmapSet(rulesets); diff --git a/osu.Game/Online/Multiplayer/PlaylistItem.cs b/osu.Game/Online/Multiplayer/PlaylistItem.cs index 9d6e8eb8e3..416091a1aa 100644 --- a/osu.Game/Online/Multiplayer/PlaylistItem.cs +++ b/osu.Game/Online/Multiplayer/PlaylistItem.cs @@ -7,7 +7,6 @@ using Newtonsoft.Json; using osu.Framework.Bindables; using osu.Game.Beatmaps; using osu.Game.Online.API; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -37,7 +36,7 @@ namespace osu.Game.Online.Multiplayer public readonly BindableList RequiredMods = new BindableList(); [JsonProperty("beatmap")] - private APIBeatmap apiBeatmap { get; set; } + private APIPlaylistBeatmap apiBeatmap { get; set; } private APIMod[] allowedModsBacking; From 68fbe9f4c144ad8be6be89286053fb08ccd5f50d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Jun 2020 14:03:50 +0900 Subject: [PATCH 1867/2376] Add checksum validation to the ready/start button --- .../Multi/Match/Components/ReadyButton.cs | 32 +++++++++---------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/osu.Game/Screens/Multi/Match/Components/ReadyButton.cs b/osu.Game/Screens/Multi/Match/Components/ReadyButton.cs index e1f86fcc97..a64f24dd7e 100644 --- a/osu.Game/Screens/Multi/Match/Components/ReadyButton.cs +++ b/osu.Game/Screens/Multi/Match/Components/ReadyButton.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using System.Linq.Expressions; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Beatmaps; @@ -52,24 +53,14 @@ namespace osu.Game.Screens.Multi.Match.Components private void updateSelectedItem(PlaylistItem item) { - hasBeatmap = false; - - int? beatmapId = SelectedItem.Value?.Beatmap.Value?.OnlineBeatmapID; - if (beatmapId == null) - return; - - hasBeatmap = beatmaps.QueryBeatmap(b => b.OnlineBeatmapID == beatmapId) != null; + hasBeatmap = findBeatmap(expr => beatmaps.QueryBeatmap(expr)); } private void beatmapUpdated(ValueChangedEvent> weakSet) { if (weakSet.NewValue.TryGetTarget(out var set)) { - int? beatmapId = SelectedItem.Value?.Beatmap.Value?.OnlineBeatmapID; - if (beatmapId == null) - return; - - if (set.Beatmaps.Any(b => b.OnlineBeatmapID == beatmapId)) + if (findBeatmap(expr => set.Beatmaps.AsQueryable().FirstOrDefault(expr))) Schedule(() => hasBeatmap = true); } } @@ -78,15 +69,22 @@ namespace osu.Game.Screens.Multi.Match.Components { if (weakSet.NewValue.TryGetTarget(out var set)) { - int? beatmapId = SelectedItem.Value?.Beatmap.Value?.OnlineBeatmapID; - if (beatmapId == null) - return; - - if (set.Beatmaps.Any(b => b.OnlineBeatmapID == beatmapId)) + if (findBeatmap(expr => set.Beatmaps.AsQueryable().FirstOrDefault(expr))) Schedule(() => hasBeatmap = false); } } + private bool findBeatmap(Func>, BeatmapInfo> expression) + { + int? beatmapId = SelectedItem.Value?.Beatmap.Value?.OnlineBeatmapID; + string checksum = SelectedItem.Value?.Beatmap.Value?.MD5Hash; + + if (beatmapId == null || checksum == null) + return false; + + return expression(b => b.OnlineBeatmapID == beatmapId && b.MD5Hash == checksum) != null; + } + protected override void Update() { base.Update(); From b41bb5a6824d8f4467b4178708cce88ace77011c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Jun 2020 14:04:00 +0900 Subject: [PATCH 1868/2376] Update databased MD5 hash on save --- osu.Game/Beatmaps/BeatmapManager.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index f626b45e42..e5907809f3 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -201,7 +201,9 @@ namespace osu.Game.Beatmaps using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) new LegacyBeatmapEncoder(beatmapContent).Encode(sw); - stream.Seek(0, SeekOrigin.Begin); + var attachedInfo = setInfo.Beatmaps.Single(b => b.ID == info.ID); + var md5Hash = stream.ComputeMD5Hash(); + attachedInfo.MD5Hash = md5Hash; UpdateFile(setInfo, setInfo.Files.Single(f => string.Equals(f.Filename, info.Path, StringComparison.OrdinalIgnoreCase)), stream); } From 17e91695e0cf982b76b34db1184aca8be4a7020b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Jun 2020 14:04:51 +0900 Subject: [PATCH 1869/2376] Add checksum validation to the panel download buttons --- .../Screens/Multi/DrawableRoomPlaylistItem.cs | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs index c024304856..414c1f5748 100644 --- a/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs @@ -188,7 +188,7 @@ namespace osu.Game.Screens.Multi X = -18, Children = new Drawable[] { - new PlaylistDownloadButton(item.Beatmap.Value.BeatmapSet) + new PlaylistDownloadButton(item) { Size = new Vector2(50, 30) }, @@ -212,9 +212,15 @@ namespace osu.Game.Screens.Multi private class PlaylistDownloadButton : BeatmapPanelDownloadButton { - public PlaylistDownloadButton(BeatmapSetInfo beatmapSet) - : base(beatmapSet) + private readonly PlaylistItem playlistItem; + + [Resolved] + private BeatmapManager beatmapManager { get; set; } + + public PlaylistDownloadButton(PlaylistItem playlistItem) + : base(playlistItem.Beatmap.Value.BeatmapSet) { + this.playlistItem = playlistItem; Alpha = 0; } @@ -223,11 +229,26 @@ namespace osu.Game.Screens.Multi base.LoadComplete(); State.BindValueChanged(stateChanged, true); + FinishTransforms(true); } private void stateChanged(ValueChangedEvent state) { - this.FadeTo(state.NewValue == DownloadState.LocallyAvailable ? 0 : 1, 500); + switch (state.NewValue) + { + case DownloadState.LocallyAvailable: + // Perform a local query of the beatmap by beatmap checksum, and reset the state if not matching. + if (beatmapManager.QueryBeatmap(b => b.MD5Hash == playlistItem.Beatmap.Value.MD5Hash) == null) + State.Value = DownloadState.NotDownloaded; + else + this.FadeTo(0, 500); + + break; + + default: + this.FadeTo(1, 500); + break; + } } } From 46a209540e65ee40bbbd4020128e4e01fbb91ce8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Jun 2020 14:28:48 +0900 Subject: [PATCH 1870/2376] 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 7ea1f3140b..8dcd2f1c6f 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 305e4e0a92..a70a263cdc 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -24,7 +24,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 016f2ba35d..9652f967f2 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From 90f9905ed0f142121d8e295f8df4873eee3682e7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Jun 2020 14:29:17 +0900 Subject: [PATCH 1871/2376] Update resourcse --- 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 8dcd2f1c6f..07be3ab0d2 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 a70a263cdc..4d6358575b 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -25,7 +25,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 9652f967f2..6b55fa51ff 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + From 3c85561cdce09b7531aac9ea14bd8a681c159d8c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Jun 2020 14:31:43 +0900 Subject: [PATCH 1872/2376] Add tests --- .../TestSceneDrawableRoomPlaylist.cs | 73 +++++++++++++++++++ osu.Game/Tests/Beatmaps/TestBeatmap.cs | 21 +++++- 2 files changed, 92 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index 5ef4dd6773..55b026eff6 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -4,12 +4,18 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Audio; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Platform; using osu.Framework.Testing; +using osu.Game.Beatmaps; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; +using osu.Game.Overlays; +using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.Multi; @@ -23,6 +29,18 @@ namespace osu.Game.Tests.Visual.Multiplayer { private TestPlaylist playlist; + private BeatmapManager manager; + private RulesetStore rulesets; + + [BackgroundDependencyLoader] + private void load(GameHost host, AudioManager audio) + { + Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); + Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, host, Beatmap.Default)); + + manager.Import(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo.BeatmapSet).Wait(); + } + [Test] public void TestNonEditableNonSelectable() { @@ -182,6 +200,28 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("click delete button", () => InputManager.Click(MouseButton.Left)); } + [Test] + public void TestDownloadButtonHiddenInitiallyWhenBeatmapExists() + { + createPlaylist(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo); + + AddAssert("download button hidden", () => !playlist.ChildrenOfType().Single().IsPresent); + } + + [Test] + public void TestDownloadButtonVisibleInitiallyWhenBeatmapDoesNotExist() + { + var byOnlineId = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo; + byOnlineId.BeatmapSet.OnlineBeatmapSetID = 1337; // Some random ID that does not exist locally. + + var byChecksum = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo; + byChecksum.MD5Hash = "1337"; // Some random checksum that does not exist locally. + + createPlaylist(byOnlineId, byChecksum); + + AddAssert("download buttons shown", () => playlist.ChildrenOfType().All(d => d.IsPresent)); + } + private void moveToItem(int index, Vector2? offset = null) => AddStep($"move mouse to item {index}", () => InputManager.MoveMouseTo(playlist.ChildrenOfType>().ElementAt(index), offset)); @@ -235,6 +275,39 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for items to load", () => playlist.ItemMap.Values.All(i => i.IsLoaded)); } + private void createPlaylist(params BeatmapInfo[] beatmaps) + { + AddStep("create playlist", () => + { + Child = playlist = new TestPlaylist(false, false) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(500, 300) + }; + + int index = 0; + + foreach (var b in beatmaps) + { + playlist.Items.Add(new PlaylistItem + { + ID = index++, + Beatmap = { Value = b }, + Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RequiredMods = + { + new OsuModHardRock(), + new OsuModDoubleTime(), + new OsuModAutoplay() + } + }); + } + }); + + AddUntilStep("wait for items to load", () => playlist.ItemMap.Values.All(i => i.IsLoaded)); + } + private class TestPlaylist : DrawableRoomPlaylist { public new IReadOnlyDictionary> ItemMap => base.ItemMap; diff --git a/osu.Game/Tests/Beatmaps/TestBeatmap.cs b/osu.Game/Tests/Beatmaps/TestBeatmap.cs index a7c84bf692..9fc20fd0f2 100644 --- a/osu.Game/Tests/Beatmaps/TestBeatmap.cs +++ b/osu.Game/Tests/Beatmaps/TestBeatmap.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 System; using System.Collections.Generic; using System.IO; using System.Text; +using osu.Framework.Extensions; using osu.Game.Beatmaps; using osu.Game.IO; using osu.Game.Rulesets; @@ -43,10 +45,25 @@ namespace osu.Game.Tests.Beatmaps private static Beatmap createTestBeatmap() { using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(test_beatmap_data))) - using (var reader = new LineBufferedReader(stream)) - return Decoder.GetDecoder(reader).Decode(reader); + { + using (var reader = new LineBufferedReader(stream)) + { + var b = Decoder.GetDecoder(reader).Decode(reader); + + b.BeatmapInfo.MD5Hash = test_beatmap_hash.Value.md5; + b.BeatmapInfo.Hash = test_beatmap_hash.Value.sha2; + + return b; + } + } } + private static readonly Lazy<(string md5, string sha2)> test_beatmap_hash = new Lazy<(string md5, string sha2)>(() => + { + using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(test_beatmap_data))) + return (stream.ComputeMD5Hash(), stream.ComputeSHA2Hash()); + }); + private const string test_beatmap_data = @"osu file format v14 [General] From 800c46f7524277b9aa8e9af5732bcfb06487a863 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Jun 2020 14:39:15 +0900 Subject: [PATCH 1873/2376] Fix test function override --- .../NonVisual/Skinning/LegacySkinTextureFallbackTest.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs b/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs index 867af9c1b8..69e66942ab 100644 --- a/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs +++ b/osu.Game.Tests/NonVisual/Skinning/LegacySkinTextureFallbackTest.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; +using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Game.Skinning; @@ -103,7 +104,7 @@ namespace osu.Game.Tests.NonVisual.Skinning Textures = fileNames.ToDictionary(fileName => fileName, fileName => new Texture(1, 1)); } - public override Texture Get(string name) => Textures.GetValueOrDefault(name); + public override Texture Get(string name, WrapMode wrapModeS, WrapMode wrapModeT) => Textures.GetValueOrDefault(name); } } } From 6c8c95677f65d707cc9ac657138f1274a393b1b7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Jun 2020 14:54:55 +0900 Subject: [PATCH 1874/2376] Fix incorrect usage of Directory.GetFiles --- osu.Game/Rulesets/RulesetStore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index 10b6edca8c..58a2ba056e 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -154,7 +154,7 @@ namespace osu.Game.Rulesets { try { - var files = Directory.GetFiles(Path.Combine(RuntimeInfo.StartupDirectory, $"{ruleset_library_prefix}.*.dll")); + var files = Directory.GetFiles(RuntimeInfo.StartupDirectory, $"{ruleset_library_prefix}.*.dll"); foreach (string file in files.Where(f => !Path.GetFileName(f).Contains("Tests"))) loadRulesetFromFile(file); From 70c84811ed00a37b438cea3941000bba12ed418d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Jun 2020 15:49:51 +0900 Subject: [PATCH 1875/2376] Revert incorrect change --- osu.Game.Tests/Resources/TestResources.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Tests/Resources/TestResources.cs b/osu.Game.Tests/Resources/TestResources.cs index 1c264f66e0..e882229570 100644 --- a/osu.Game.Tests/Resources/TestResources.cs +++ b/osu.Game.Tests/Resources/TestResources.cs @@ -3,7 +3,6 @@ using System.IO; using NUnit.Framework; -using osu.Framework; using osu.Framework.IO.Stores; namespace osu.Game.Tests.Resources @@ -18,7 +17,7 @@ namespace osu.Game.Tests.Resources public static string GetTestBeatmapForImport(bool virtualTrack = false) { - var tempPath = Path.Combine(RuntimeInfo.StartupDirectory, Path.GetTempFileName() + ".osz"); + var tempPath = Path.GetTempFileName() + ".osz"; using (var stream = GetTestBeatmapStream(virtualTrack)) using (var newFile = File.Create(tempPath)) From fac96f6dddf9dba724fa0b298444694310c508af Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Jun 2020 17:02:01 +0900 Subject: [PATCH 1876/2376] Fix match beatmap not updating after re-download --- .../Multiplayer/TestSceneMatchSubScreen.cs | 57 +++++++++++++++++-- .../Match/Components/MatchSettingsOverlay.cs | 2 +- .../Screens/Multi/Match/MatchSubScreen.cs | 13 +---- 3 files changed, 55 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs index d678d5a814..6154e646f8 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs @@ -5,7 +5,9 @@ using System; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Audio; using osu.Framework.Bindables; +using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; @@ -29,14 +31,20 @@ namespace osu.Game.Tests.Visual.Multiplayer [Cached(typeof(IRoomManager))] private readonly TestRoomManager roomManager = new TestRoomManager(); - [Resolved] - private BeatmapManager beatmaps { get; set; } - - [Resolved] - private RulesetStore rulesets { get; set; } + private BeatmapManager manager; + private RulesetStore rulesets; private TestMatchSubScreen match; + [BackgroundDependencyLoader] + private void load(GameHost host, AudioManager audio) + { + Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); + Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, host, Beatmap.Default)); + + manager.Import(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo.BeatmapSet).Wait(); + } + [SetUp] public void Setup() => Schedule(() => { @@ -75,10 +83,49 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("first playlist item selected", () => match.SelectedItem.Value == Room.Playlist[0]); } + [Test] + public void TestBeatmapUpdatedOnReImport() + { + BeatmapSetInfo importedSet = null; + + AddStep("import altered beatmap", () => + { + var beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo); + beatmap.BeatmapInfo.BaseDifficulty.CircleSize = 1; + + importedSet = manager.Import(beatmap.BeatmapInfo.BeatmapSet).Result; + }); + + AddStep("load room", () => + { + Room.Name.Value = "my awesome room"; + Room.Host.Value = new User { Id = 2, Username = "peppy" }; + Room.Playlist.Add(new PlaylistItem + { + Beatmap = { Value = importedSet.Beatmaps[0] }, + Ruleset = { Value = new OsuRuleset().RulesetInfo } + }); + }); + + AddStep("create room", () => + { + InputManager.MoveMouseTo(match.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("match has altered beatmap", () => match.Beatmap.Value.Beatmap.BeatmapInfo.BaseDifficulty.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); + } + private class TestMatchSubScreen : MatchSubScreen { public new Bindable SelectedItem => base.SelectedItem; + public new Bindable Beatmap => base.Beatmap; + public TestMatchSubScreen(Room room) : base(room) { diff --git a/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs b/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs index 54c4f8f7c7..49a0fc434b 100644 --- a/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs +++ b/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs @@ -433,7 +433,7 @@ namespace osu.Game.Screens.Multi.Match.Components } } - private class CreateRoomButton : TriangleButton + public class CreateRoomButton : TriangleButton { public CreateRoomButton() { diff --git a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs index e1d72d9600..bbfbaf81af 100644 --- a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs +++ b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs @@ -207,6 +207,8 @@ namespace osu.Game.Screens.Multi.Match Ruleset.Value = item.Ruleset.Value; } + private void beatmapUpdated(ValueChangedEvent> weakSet) => Schedule(updateWorkingBeatmap); + private void updateWorkingBeatmap() { var beatmap = SelectedItem.Value?.Beatmap.Value; @@ -217,17 +219,6 @@ namespace osu.Game.Screens.Multi.Match Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); } - private void beatmapUpdated(ValueChangedEvent> weakSet) - { - Schedule(() => - { - if (Beatmap.Value != beatmapManager.DefaultBeatmap) - return; - - updateWorkingBeatmap(); - }); - } - private void onStart() { switch (type.Value) From dfb9687fb5bfc47670ea6b017410e46c76c5905c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Jun 2020 17:22:09 +0900 Subject: [PATCH 1877/2376] Extract update into PreUpdate(), add test --- osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs | 3 +++ osu.Game/Beatmaps/BeatmapManager.cs | 17 +++++++++++++---- osu.Game/Database/ArchiveModelManager.cs | 10 ++++++++++ 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index 5eb11a3264..88bb39a521 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -602,6 +602,8 @@ namespace osu.Game.Tests.Beatmaps.IO Beatmap beatmapToUpdate = (Beatmap)manager.GetWorkingBeatmap(setToUpdate.Beatmaps.First(b => b.RulesetID == 0)).Beatmap; BeatmapSetFileInfo fileToUpdate = setToUpdate.Files.First(f => beatmapToUpdate.BeatmapInfo.Path.Contains(f.Filename)); + string oldMd5Hash = beatmapToUpdate.BeatmapInfo.MD5Hash; + using (var stream = new MemoryStream()) { using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true)) @@ -624,6 +626,7 @@ namespace osu.Game.Tests.Beatmaps.IO Beatmap updatedBeatmap = (Beatmap)manager.GetWorkingBeatmap(manager.QueryBeatmap(b => b.ID == beatmapToUpdate.BeatmapInfo.ID)).Beatmap; Assert.That(updatedBeatmap.HitObjects.Count, Is.EqualTo(1)); Assert.That(updatedBeatmap.HitObjects[0].StartTime, Is.EqualTo(5000)); + Assert.That(updatedBeatmap.BeatmapInfo.MD5Hash, Is.Not.EqualTo(oldMd5Hash)); } finally { diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index e5907809f3..668ac6ee10 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -201,10 +201,6 @@ namespace osu.Game.Beatmaps using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) new LegacyBeatmapEncoder(beatmapContent).Encode(sw); - var attachedInfo = setInfo.Beatmaps.Single(b => b.ID == info.ID); - var md5Hash = stream.ComputeMD5Hash(); - attachedInfo.MD5Hash = md5Hash; - UpdateFile(setInfo, setInfo.Files.Single(f => string.Equals(f.Filename, info.Path, StringComparison.OrdinalIgnoreCase)), stream); } @@ -213,6 +209,19 @@ namespace osu.Game.Beatmaps workingCache.Remove(working); } + protected override void PreUpdate(BeatmapSetInfo item) + { + base.PreUpdate(item); + + foreach (var info in item.Beatmaps) + { + var file = item.Files.FirstOrDefault(f => string.Equals(f.Filename, info.Path, StringComparison.OrdinalIgnoreCase))?.FileInfo.StoragePath; + + using (var stream = Files.Store.GetStream(file)) + info.MD5Hash = stream.ComputeMD5Hash(); + } + } + private readonly WeakList workingCache = new WeakList(); /// diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index ae55a7b14a..f7e81ae4bc 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -430,10 +430,20 @@ namespace osu.Game.Database { item.Hash = computeHash(item); + PreUpdate(item); + ModelStore.Update(item); } } + /// + /// Perform any final actions before the update to database executes. + /// + /// The that is being updated. + protected virtual void PreUpdate(TModel item) + { + } + /// /// Delete an item from the manager. /// Is a no-op for already deleted items. From 665530f1c3a9b43ac2925220ee41efc767919974 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Jun 2020 17:22:59 +0900 Subject: [PATCH 1878/2376] Remove excess newline --- .../Edit/Blueprints/TaikoSpanPlacementBlueprint.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs index 7f96b5a46e..a88eff13a1 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs @@ -25,7 +25,6 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints public TaikoSpanPlacementBlueprint(HitObject hitObject) : base(hitObject) - { spanPlacementObject = hitObject as IHasDuration; From 2aadb9deba79800cbbbc22ce0a960dd6c709cfe6 Mon Sep 17 00:00:00 2001 From: Shivam Date: Tue, 2 Jun 2020 11:04:56 +0200 Subject: [PATCH 1879/2376] Implement welcome and seeya samples --- osu.Game/Screens/Menu/IntroCircles.cs | 2 +- osu.Game/Screens/Menu/IntroFallback.cs | 16 ++++++++++++---- osu.Game/Screens/Menu/IntroScreen.cs | 4 ++-- osu.Game/Screens/Menu/IntroTriangles.cs | 2 +- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Menu/IntroCircles.cs b/osu.Game/Screens/Menu/IntroCircles.cs index aa9cee969c..08a170f606 100644 --- a/osu.Game/Screens/Menu/IntroCircles.cs +++ b/osu.Game/Screens/Menu/IntroCircles.cs @@ -24,7 +24,7 @@ namespace osu.Game.Screens.Menu private void load(AudioManager audio) { if (MenuVoice.Value) - welcome = audio.Samples.Get(@"welcome"); + welcome = audio.Samples.Get(@"Intro/welcome-lazer"); } protected override void LogoArriving(OsuLogo logo, bool resuming) diff --git a/osu.Game/Screens/Menu/IntroFallback.cs b/osu.Game/Screens/Menu/IntroFallback.cs index bc01e9c502..ea3c4fb040 100644 --- a/osu.Game/Screens/Menu/IntroFallback.cs +++ b/osu.Game/Screens/Menu/IntroFallback.cs @@ -13,15 +13,22 @@ namespace osu.Game.Screens.Menu { protected override string BeatmapHash => "64E00D7022195959BFA3109D09C2E2276C8F12F486B91FCF6175583E973B48F2"; protected override string BeatmapFile => "welcome.osz"; - private const double delay_step_two = 3000; + private const double delay_step_two = 2142; private SampleChannel welcome; + private SampleChannel pianoReverb; + [BackgroundDependencyLoader] private void load(AudioManager audio) { + seeya = audio.Samples.Get(@"Intro/seeya-fallback"); + if (MenuVoice.Value) - welcome = audio.Samples.Get(@"welcome"); + { + welcome = audio.Samples.Get(@"Intro/welcome-fallback"); + pianoReverb = audio.Samples.Get(@"Intro/welcome_piano"); + } } protected override void LogoArriving(OsuLogo logo, bool resuming) @@ -31,14 +38,15 @@ namespace osu.Game.Screens.Menu if (!resuming) { welcome?.Play(); - + pianoReverb?.Play(); Scheduler.AddDelayed(delegate { StartTrack(); PrepareMenuLoad(); - Scheduler.AddDelayed(LoadMenu, 0); + Scheduler.Add(LoadMenu); + }, delay_step_two); logo.ScaleTo(1); diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index 0d5f3d1142..20cd9671a0 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -49,7 +49,7 @@ namespace osu.Game.Screens.Menu private const int exit_delay = 3000; - private SampleChannel seeya; + protected SampleChannel seeya { get; set; } private LeasedBindable beatmap; @@ -67,7 +67,7 @@ namespace osu.Game.Screens.Menu MenuVoice = config.GetBindable(OsuSetting.MenuVoice); MenuMusic = config.GetBindable(OsuSetting.MenuMusic); - seeya = audio.Samples.Get(@"seeya"); + seeya = audio.Samples.Get(@"Intro/seeya-lazer"); BeatmapSetInfo setInfo = null; diff --git a/osu.Game/Screens/Menu/IntroTriangles.cs b/osu.Game/Screens/Menu/IntroTriangles.cs index 188a49c147..b44fea99e8 100644 --- a/osu.Game/Screens/Menu/IntroTriangles.cs +++ b/osu.Game/Screens/Menu/IntroTriangles.cs @@ -47,7 +47,7 @@ namespace osu.Game.Screens.Menu private void load() { if (MenuVoice.Value && !MenuMusic.Value) - welcome = audio.Samples.Get(@"welcome"); + welcome = audio.Samples.Get(@"Intro/welcome-lazer"); } protected override void LogoArriving(OsuLogo logo, bool resuming) From 3ae97c963454bd407a0132242fac4a309c61b7e0 Mon Sep 17 00:00:00 2001 From: Shivam Date: Tue, 2 Jun 2020 11:25:57 +0200 Subject: [PATCH 1880/2376] Change "Fallback" to "Welcome" visually --- osu.Game/Configuration/IntroSequence.cs | 2 +- osu.Game/Screens/Loader.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Configuration/IntroSequence.cs b/osu.Game/Configuration/IntroSequence.cs index 24f8c0f048..5672c44bbe 100644 --- a/osu.Game/Configuration/IntroSequence.cs +++ b/osu.Game/Configuration/IntroSequence.cs @@ -6,7 +6,7 @@ namespace osu.Game.Configuration public enum IntroSequence { Circles, - Fallback, + Welcome, Triangles, Random } diff --git a/osu.Game/Screens/Loader.cs b/osu.Game/Screens/Loader.cs index 690868bd36..9330226bda 100644 --- a/osu.Game/Screens/Loader.cs +++ b/osu.Game/Screens/Loader.cs @@ -51,7 +51,7 @@ namespace osu.Game.Screens case IntroSequence.Circles: return new IntroCircles(); - case IntroSequence.Fallback: + case IntroSequence.Welcome: return new IntroFallback(); default: From 828180ad9b3e6006e70c811e107d340ae18b603e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Jun 2020 19:29:22 +0900 Subject: [PATCH 1881/2376] Add default --- osu.Game.Tournament/Screens/SetupScreen.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/SetupScreen.cs b/osu.Game.Tournament/Screens/SetupScreen.cs index e1594de69e..2bd0bcf2f2 100644 --- a/osu.Game.Tournament/Screens/SetupScreen.cs +++ b/osu.Game.Tournament/Screens/SetupScreen.cs @@ -211,7 +211,8 @@ namespace osu.Game.Tournament.Screens private class ResolutionSelector : ActionableInfo { private const int minimum_window_height = 480; - private const int maximum_window_height = 2160; // 4k + private const int maximum_window_height = 2160; + public new Action Action; private OsuNumberBox numberBox; @@ -221,6 +222,7 @@ namespace osu.Game.Tournament.Screens var drawable = base.CreateComponent(); FlowContainer.Insert(-1, numberBox = new OsuNumberBox { + Text = "1080", Width = 100 }); From 78fddc895738ec770447f025bdc8a487bdd5c988 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Jun 2020 19:29:59 +0900 Subject: [PATCH 1882/2376] Make button match height --- osu.Game.Tournament/Screens/SetupScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/SetupScreen.cs b/osu.Game.Tournament/Screens/SetupScreen.cs index 2bd0bcf2f2..cf8eb8bd6c 100644 --- a/osu.Game.Tournament/Screens/SetupScreen.cs +++ b/osu.Game.Tournament/Screens/SetupScreen.cs @@ -199,7 +199,7 @@ namespace osu.Game.Tournament.Screens { button = new TriangleButton { - Size = new Vector2(100, 30), + Size = new Vector2(100, 40), Action = () => Action?.Invoke() } } From 19d73af90d2ca01faf33c320ee9015d5a218b2d5 Mon Sep 17 00:00:00 2001 From: Shivam Date: Tue, 2 Jun 2020 12:51:42 +0200 Subject: [PATCH 1883/2376] Implement basic intro sequence --- osu.Game/Screens/Menu/IntroFallback.cs | 65 +++++++++++++++++++++++--- osu.Game/Screens/Menu/IntroScreen.cs | 6 +-- 2 files changed, 62 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Menu/IntroFallback.cs b/osu.Game/Screens/Menu/IntroFallback.cs index ea3c4fb040..7c23f00d3f 100644 --- a/osu.Game/Screens/Menu/IntroFallback.cs +++ b/osu.Game/Screens/Menu/IntroFallback.cs @@ -1,11 +1,16 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; +using osuTK; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Screens; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; namespace osu.Game.Screens.Menu { @@ -22,8 +27,8 @@ namespace osu.Game.Screens.Menu [BackgroundDependencyLoader] private void load(AudioManager audio) { - seeya = audio.Samples.Get(@"Intro/seeya-fallback"); - + Seeya = audio.Samples.Get(@"Intro/seeya-fallback"); + if (MenuVoice.Value) { welcome = audio.Samples.Get(@"Intro/welcome-fallback"); @@ -45,13 +50,20 @@ namespace osu.Game.Screens.Menu PrepareMenuLoad(); + logo.ScaleTo(1); + logo.FadeIn(); + Scheduler.Add(LoadMenu); - }, delay_step_two); - logo.ScaleTo(1); - logo.FadeIn(); - logo.PlayIntro(); + LoadComponentAsync(new FallbackIntroSequence + { + RelativeSizeAxes = Axes.Both + }, t => + { + AddInternal(t); + t.Start(delay_step_two); + }); } } @@ -60,5 +72,46 @@ namespace osu.Game.Screens.Menu this.FadeOut(300); base.OnSuspending(next); } + + private class FallbackIntroSequence : Container + { + private OsuSpriteText welcomeText; + + [BackgroundDependencyLoader] + private void load() + { + Children = new Drawable[] + { + welcomeText = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "welcome", + Padding = new MarginPadding { Bottom = 10 }, + Font = OsuFont.GetFont(weight: FontWeight.Light, size: 42), + Alpha = 0, + Spacing = new Vector2(5), + }, + }; + } + + public void Start(double length) + { + if (Children.Any()) + { + // restart if we were already run previously. + FinishTransforms(true); + load(); + } + + double remainingTime() => length - TransformDelay; + + using (BeginDelayedSequence(250, true)) + { + welcomeText.FadeIn(700); + welcomeText.ScaleTo(welcomeText.Scale + new Vector2(0.5f), remainingTime(), Easing.Out); + } + } + } } } diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index 20cd9671a0..8588e2a41b 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -49,7 +49,7 @@ namespace osu.Game.Screens.Menu private const int exit_delay = 3000; - protected SampleChannel seeya { get; set; } + protected SampleChannel Seeya { get; set; } private LeasedBindable beatmap; @@ -67,7 +67,7 @@ namespace osu.Game.Screens.Menu MenuVoice = config.GetBindable(OsuSetting.MenuVoice); MenuMusic = config.GetBindable(OsuSetting.MenuMusic); - seeya = audio.Samples.Get(@"Intro/seeya-lazer"); + Seeya = audio.Samples.Get(@"Intro/seeya-lazer"); BeatmapSetInfo setInfo = null; @@ -103,7 +103,7 @@ namespace osu.Game.Screens.Menu double fadeOutTime = exit_delay; // we also handle the exit transition. if (MenuVoice.Value) - seeya.Play(); + Seeya.Play(); else fadeOutTime = 500; From 888b90b426f077a84e9b9e7e12fcba05858dbfde Mon Sep 17 00:00:00 2001 From: Shivam Date: Tue, 2 Jun 2020 13:14:50 +0200 Subject: [PATCH 1884/2376] Rename IntroFallback classes to IntroLegacy This commit also renames files accordingly with https://github.com/ppy/osu-resources/pull/103 --- ...{TestSceneIntroFallback.cs => TestSceneIntroLegacy.cs} | 4 ++-- osu.Game/Screens/Loader.cs | 2 +- osu.Game/Screens/Menu/IntroCircles.cs | 2 +- .../Screens/Menu/{IntroFallback.cs => IntroLegacy.cs} | 8 ++++---- osu.Game/Screens/Menu/IntroScreen.cs | 2 +- osu.Game/Screens/Menu/IntroTriangles.cs | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) rename osu.Game.Tests/Visual/Menus/{TestSceneIntroFallback.cs => TestSceneIntroLegacy.cs} (70%) rename osu.Game/Screens/Menu/{IntroFallback.cs => IntroLegacy.cs} (92%) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneIntroFallback.cs b/osu.Game.Tests/Visual/Menus/TestSceneIntroLegacy.cs similarity index 70% rename from osu.Game.Tests/Visual/Menus/TestSceneIntroFallback.cs rename to osu.Game.Tests/Visual/Menus/TestSceneIntroLegacy.cs index cb32d6bf32..7cb99467ad 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneIntroFallback.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneIntroLegacy.cs @@ -8,8 +8,8 @@ using osu.Game.Screens.Menu; namespace osu.Game.Tests.Visual.Menus { [TestFixture] - public class TestSceneIntroFallback : IntroTestScene + public class TestSceneIntroLegacy : IntroTestScene { - protected override IScreen CreateScreen() => new IntroFallback(); + protected override IScreen CreateScreen() => new IntroLegacy(); } } diff --git a/osu.Game/Screens/Loader.cs b/osu.Game/Screens/Loader.cs index 9330226bda..aa959e7d35 100644 --- a/osu.Game/Screens/Loader.cs +++ b/osu.Game/Screens/Loader.cs @@ -52,7 +52,7 @@ namespace osu.Game.Screens return new IntroCircles(); case IntroSequence.Welcome: - return new IntroFallback(); + return new IntroLegacy(); default: return new IntroTriangles(); diff --git a/osu.Game/Screens/Menu/IntroCircles.cs b/osu.Game/Screens/Menu/IntroCircles.cs index 08a170f606..113d496855 100644 --- a/osu.Game/Screens/Menu/IntroCircles.cs +++ b/osu.Game/Screens/Menu/IntroCircles.cs @@ -24,7 +24,7 @@ namespace osu.Game.Screens.Menu private void load(AudioManager audio) { if (MenuVoice.Value) - welcome = audio.Samples.Get(@"Intro/welcome-lazer"); + welcome = audio.Samples.Get(@"Intro/lazer/welcome"); } protected override void LogoArriving(OsuLogo logo, bool resuming) diff --git a/osu.Game/Screens/Menu/IntroFallback.cs b/osu.Game/Screens/Menu/IntroLegacy.cs similarity index 92% rename from osu.Game/Screens/Menu/IntroFallback.cs rename to osu.Game/Screens/Menu/IntroLegacy.cs index 7c23f00d3f..c1a360bca1 100644 --- a/osu.Game/Screens/Menu/IntroFallback.cs +++ b/osu.Game/Screens/Menu/IntroLegacy.cs @@ -14,7 +14,7 @@ using osu.Game.Graphics.Sprites; namespace osu.Game.Screens.Menu { - public class IntroFallback : IntroScreen + public class IntroLegacy : IntroScreen { protected override string BeatmapHash => "64E00D7022195959BFA3109D09C2E2276C8F12F486B91FCF6175583E973B48F2"; protected override string BeatmapFile => "welcome.osz"; @@ -27,12 +27,12 @@ namespace osu.Game.Screens.Menu [BackgroundDependencyLoader] private void load(AudioManager audio) { - Seeya = audio.Samples.Get(@"Intro/seeya-fallback"); + Seeya = audio.Samples.Get(@"Intro/legacy/seeya"); if (MenuVoice.Value) { - welcome = audio.Samples.Get(@"Intro/welcome-fallback"); - pianoReverb = audio.Samples.Get(@"Intro/welcome_piano"); + welcome = audio.Samples.Get(@"Intro/legacy/welcome"); + pianoReverb = audio.Samples.Get(@"Intro/legacy/welcome_piano"); } } diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index 8588e2a41b..d8769e3125 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -67,7 +67,7 @@ namespace osu.Game.Screens.Menu MenuVoice = config.GetBindable(OsuSetting.MenuVoice); MenuMusic = config.GetBindable(OsuSetting.MenuMusic); - Seeya = audio.Samples.Get(@"Intro/seeya-lazer"); + Seeya = audio.Samples.Get(@"Intro/lazer/seeya"); BeatmapSetInfo setInfo = null; diff --git a/osu.Game/Screens/Menu/IntroTriangles.cs b/osu.Game/Screens/Menu/IntroTriangles.cs index b44fea99e8..ef26038a6f 100644 --- a/osu.Game/Screens/Menu/IntroTriangles.cs +++ b/osu.Game/Screens/Menu/IntroTriangles.cs @@ -47,7 +47,7 @@ namespace osu.Game.Screens.Menu private void load() { if (MenuVoice.Value && !MenuMusic.Value) - welcome = audio.Samples.Get(@"Intro/welcome-lazer"); + welcome = audio.Samples.Get(@"Intro/lazer/welcome"); } protected override void LogoArriving(OsuLogo logo, bool resuming) From 3d78ec90ac879c6d064943629df1c2b2959a8dc1 Mon Sep 17 00:00:00 2001 From: Shivam Date: Tue, 2 Jun 2020 13:26:37 +0200 Subject: [PATCH 1885/2376] Rename legacy to welcome to match osu-resources --- osu.Game/Screens/Menu/IntroLegacy.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Menu/IntroLegacy.cs b/osu.Game/Screens/Menu/IntroLegacy.cs index c1a360bca1..3980a0cc8b 100644 --- a/osu.Game/Screens/Menu/IntroLegacy.cs +++ b/osu.Game/Screens/Menu/IntroLegacy.cs @@ -27,12 +27,12 @@ namespace osu.Game.Screens.Menu [BackgroundDependencyLoader] private void load(AudioManager audio) { - Seeya = audio.Samples.Get(@"Intro/legacy/seeya"); + Seeya = audio.Samples.Get(@"Intro/welcome/seeya"); if (MenuVoice.Value) { - welcome = audio.Samples.Get(@"Intro/legacy/welcome"); - pianoReverb = audio.Samples.Get(@"Intro/legacy/welcome_piano"); + welcome = audio.Samples.Get(@"Intro/welcome/welcome"); + pianoReverb = audio.Samples.Get(@"Intro/welcome/welcome_piano"); } } From f63c66396f7a2f1f462377ae4a3b6c4c9e4dce40 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Tue, 2 Jun 2020 13:32:52 +0200 Subject: [PATCH 1886/2376] Apply review suggestions. --- .../Visual/Gameplay/TestSceneReplay.cs | 2 +- osu.Game/Rulesets/Ruleset.cs | 2 ++ osu.Game/Screens/Select/PlaySongSelect.cs | 25 +++++++++++++------ 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs index 1908988739..3a71d4ca54 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs @@ -19,7 +19,7 @@ namespace osu.Game.Tests.Visual.Gameplay { var beatmap = Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo, Array.Empty()); - return new ScoreAccessibleReplayPlayer(ruleset.GetAutoplayMod().CreateReplayScore(beatmap)); + return new ScoreAccessibleReplayPlayer(ruleset.GetAutoplayMod()?.CreateReplayScore(beatmap)); } protected override void AddCheckSteps() diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 8f41e421a3..4f28607733 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -22,6 +22,7 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Skinning; using osu.Game.Users; +using JetBrains.Annotations; namespace osu.Game.Rulesets { @@ -100,6 +101,7 @@ namespace osu.Game.Rulesets return value; } + [CanBeNull] public ModAutoplay GetAutoplayMod() => GetAllMods().OfType().FirstOrDefault(); public virtual ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => null; diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index 71ab3715e0..a0201e696f 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -7,6 +7,8 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Framework.Screens; using osu.Game.Graphics; +using osu.Game.Overlays; +using osu.Game.Overlays.Notifications; using osu.Game.Scoring; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; @@ -20,6 +22,9 @@ namespace osu.Game.Screens.Select private bool removeAutoModOnResume; private OsuScreen player; + [Resolved] + private NotificationOverlay notifications { get; set; } + public override bool AllowExternalScreenChange => true; protected override UserActivity InitialActivity => new UserActivity.ChoosingBeatmap(); @@ -83,15 +88,21 @@ namespace osu.Game.Screens.Select var auto = Ruleset.Value.CreateInstance().GetAutoplayMod(); var autoType = auto?.GetType(); - if (autoType != null) - { - var mods = Mods.Value; + var mods = Mods.Value; - if (mods.All(m => m.GetType() != autoType)) + if (autoType == null) + { + notifications.Post(new SimpleNotification { - Mods.Value = mods.Append(auto).ToArray(); - removeAutoModOnResume = true; - } + Text = "The current ruleset doesn't have an autoplay mod avalaible!" + }); + return false; + } + + if (mods.All(m => m.GetType() != autoType)) + { + Mods.Value = mods.Append(auto).ToArray(); + removeAutoModOnResume = true; } } From 61f906d9c462279b89881f1083758f493eb34450 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Jun 2020 21:02:09 +0900 Subject: [PATCH 1887/2376] Fix span piece being incorrect in some drag scenarios --- .../Blueprints/TaikoSpanPlacementBlueprint.cs | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs index a88eff13a1..468d980b23 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs @@ -48,6 +48,7 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints } private double originalStartTime; + private Vector2 originalPosition; protected override bool OnMouseDown(MouseDownEvent e) { @@ -73,22 +74,25 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints if (PlacementActive) { - if (result.Time is double endTime) + if (result.Time is double dragTime) { - if (endTime < originalStartTime) + if (dragTime < originalStartTime) { - HitObject.StartTime = endTime; - spanPlacementObject.Duration = Math.Abs(endTime - originalStartTime); + HitObject.StartTime = dragTime; + spanPlacementObject.Duration = Math.Abs(dragTime - originalStartTime); headPiece.Position = ToLocalSpace(result.ScreenSpacePosition); - lengthPiece.X = headPiece.X; - lengthPiece.Width = tailPiece.X - headPiece.X; + tailPiece.Position = originalPosition; } else { - spanPlacementObject.Duration = Math.Abs(endTime - originalStartTime); + HitObject.StartTime = originalStartTime; + spanPlacementObject.Duration = Math.Abs(dragTime - originalStartTime); tailPiece.Position = ToLocalSpace(result.ScreenSpacePosition); - lengthPiece.Width = tailPiece.X - headPiece.X; + headPiece.Position = originalPosition; } + + lengthPiece.X = headPiece.X; + lengthPiece.Width = tailPiece.X - headPiece.X; } } else @@ -96,7 +100,10 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints lengthPiece.Position = headPiece.Position = tailPiece.Position = ToLocalSpace(result.ScreenSpacePosition); if (result.Time is double startTime) + { originalStartTime = HitObject.StartTime = startTime; + originalPosition = ToLocalSpace(result.ScreenSpacePosition); + } } } } From 275d95082a88f63e5bc0ba7be8d6efd4950fc30e Mon Sep 17 00:00:00 2001 From: Lucas A Date: Tue, 2 Jun 2020 16:01:01 +0200 Subject: [PATCH 1888/2376] Fix crash in testing environment. --- osu.Game/Screens/Select/PlaySongSelect.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index a0201e696f..2236aa4d72 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -22,7 +22,7 @@ namespace osu.Game.Screens.Select private bool removeAutoModOnResume; private OsuScreen player; - [Resolved] + [Resolved(CanBeNull = true)] private NotificationOverlay notifications { get; set; } public override bool AllowExternalScreenChange => true; @@ -92,7 +92,7 @@ namespace osu.Game.Screens.Select if (autoType == null) { - notifications.Post(new SimpleNotification + notifications?.Post(new SimpleNotification { Text = "The current ruleset doesn't have an autoplay mod avalaible!" }); From a7f8c5935dd611843ff92c9d4193a94281b16a98 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Jun 2020 23:36:56 +0900 Subject: [PATCH 1889/2376] Expose LowestSuccessfulHitResult() --- osu.Game/Rulesets/Scoring/HitWindows.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Scoring/HitWindows.cs b/osu.Game/Rulesets/Scoring/HitWindows.cs index 018b50bd3d..77acbd4137 100644 --- a/osu.Game/Rulesets/Scoring/HitWindows.cs +++ b/osu.Game/Rulesets/Scoring/HitWindows.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Scoring /// Retrieves the with the largest hit window that produces a successful hit. /// /// The lowest allowed successful . - protected HitResult LowestSuccessfulHitResult() + public HitResult LowestSuccessfulHitResult() { for (var result = HitResult.Meh; result <= HitResult.Perfect; ++result) { From e98f51923a7c242cae1ec275726d2a18a82dd48b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Jun 2020 23:38:24 +0900 Subject: [PATCH 1890/2376] Add timing distribution to OsuScoreProcessor --- .../Scoring/OsuScoreProcessor.cs | 76 +++++++++++++++++++ osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 12 +++ 2 files changed, 88 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs index 79a6ea7e92..83339bd061 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs @@ -1,17 +1,93 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; namespace osu.Game.Rulesets.Osu.Scoring { public class OsuScoreProcessor : ScoreProcessor { + /// + /// The number of bins on each side of the timing distribution. + /// + private const int timing_distribution_bins = 25; + + /// + /// The total number of bins in the timing distribution, including bins on both sides and the centre bin at 0. + /// + private const int total_timing_distribution_bins = timing_distribution_bins * 2 + 1; + + /// + /// The centre bin, with a timing distribution very close to/at 0. + /// + private const int timing_distribution_centre_bin_index = timing_distribution_bins; + + private TimingDistribution timingDistribution; + + public override void ApplyBeatmap(IBeatmap beatmap) + { + var hitWindows = CreateHitWindows(); + hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty); + + timingDistribution = new TimingDistribution(total_timing_distribution_bins, hitWindows.WindowFor(hitWindows.LowestSuccessfulHitResult()) / timing_distribution_bins); + + base.ApplyBeatmap(beatmap); + } + + protected override void OnResultApplied(JudgementResult result) + { + base.OnResultApplied(result); + + if (result.IsHit) + { + int binOffset = (int)(result.TimeOffset / timingDistribution.BinSize); + timingDistribution.Bins[timing_distribution_centre_bin_index + binOffset]++; + } + } + + protected override void OnResultReverted(JudgementResult result) + { + base.OnResultReverted(result); + + if (result.IsHit) + { + int binOffset = (int)(result.TimeOffset / timingDistribution.BinSize); + timingDistribution.Bins[timing_distribution_centre_bin_index + binOffset]--; + } + } + + public override void PopulateScore(ScoreInfo score) + { + base.PopulateScore(score); + } + + protected override void Reset(bool storeResults) + { + base.Reset(storeResults); + + timingDistribution.Bins.AsSpan().Clear(); + } + protected override JudgementResult CreateResult(HitObject hitObject, Judgement judgement) => new OsuJudgementResult(hitObject, judgement); public override HitWindows CreateHitWindows() => new OsuHitWindows(); } + + public class TimingDistribution + { + public readonly int[] Bins; + public readonly double BinSize; + + public TimingDistribution(int binCount, double binSize) + { + Bins = new int[binCount]; + BinSize = binSize; + } + } } diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 1f40f44dce..619547aef4 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -129,6 +129,12 @@ namespace osu.Game.Rulesets.Scoring } updateScore(); + + OnResultApplied(result); + } + + protected virtual void OnResultApplied(JudgementResult result) + { } protected sealed override void RevertResultInternal(JudgementResult result) @@ -154,6 +160,12 @@ namespace osu.Game.Rulesets.Scoring } updateScore(); + + OnResultReverted(result); + } + + protected virtual void OnResultReverted(JudgementResult result) + { } private void updateScore() From c7c94eb3fdd6c25363c9379bc9c881a407952171 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Jun 2020 23:38:50 +0900 Subject: [PATCH 1891/2376] Initial implementation of timing distribution graph --- .../TestSceneTimingDistributionGraph.cs | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs b/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs new file mode 100644 index 0000000000..456ac19383 --- /dev/null +++ b/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs @@ -0,0 +1,103 @@ +// 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.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Rulesets.Osu.Scoring; +using osuTK; + +namespace osu.Game.Tests.Visual.Ranking +{ + public class TestSceneTimingDistributionGraph : OsuTestScene + { + public TestSceneTimingDistributionGraph() + { + Add(new TimingDistributionGraph(createNormalDistribution()) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(300, 100) + }); + } + + private TimingDistribution createNormalDistribution() + { + var distribution = new TimingDistribution(51, 5); + + // We create an approximately-normal distribution of 51 elements by using the 13th binomial row (14 initial elements) and subdividing the inner values twice. + var row = new List { 1 }; + for (int i = 0; i < 13; i++) + row.Add(row[i] * (13 - i) / (i + 1)); + + // Each subdivision yields 2n-1 total elements, so first subdivision will contain 27 elements, and the second will contain 53 elements. + for (int div = 0; div < 2; div++) + { + var newRow = new List { 1 }; + + for (int i = 0; i < row.Count - 1; i++) + { + newRow.Add((row[i] + row[i + 1]) / 2); + newRow.Add(row[i + 1]); + } + + row = newRow; + } + + // After the subdivisions take place, we're left with 53 values which we use the inner 51 of. + for (int i = 1; i < row.Count - 1; i++) + distribution.Bins[i - 1] = row[i]; + + return distribution; + } + } + + public class TimingDistributionGraph : CompositeDrawable + { + private readonly TimingDistribution distribution; + + public TimingDistributionGraph(TimingDistribution distribution) + { + this.distribution = distribution; + } + + [BackgroundDependencyLoader] + private void load() + { + int maxCount = distribution.Bins.Max(); + + var bars = new Drawable[distribution.Bins.Length]; + for (int i = 0; i < bars.Length; i++) + bars[i] = new Bar { Height = (float)distribution.Bins[i] / maxCount }; + + InternalChild = new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] { bars } + }; + } + + private class Bar : CompositeDrawable + { + public Bar() + { + Anchor = Anchor.BottomCentre; + Origin = Anchor.BottomCentre; + + RelativeSizeAxes = Axes.Both; + + Padding = new MarginPadding { Horizontal = 1 }; + + InternalChild = new Circle + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#66FFCC") + }; + } + } + } +} From dc41e74e1912ea9a49a641ab091ed536f6f5e38a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Jun 2020 23:47:18 +0900 Subject: [PATCH 1892/2376] Fix cursor trail not displaying --- osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs index 37df5ec540..9bcb3abc63 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs @@ -237,6 +237,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor { Position = new Vector2(part.Position.X - size.X / 2, part.Position.Y + size.Y / 2), TexturePosition = textureRect.BottomLeft, + TextureRect = new Vector4(0, 0, 1, 1), Colour = DrawColourInfo.Colour.BottomLeft.Linear, Time = part.Time }); @@ -245,6 +246,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor { Position = new Vector2(part.Position.X + size.X / 2, part.Position.Y + size.Y / 2), TexturePosition = textureRect.BottomRight, + TextureRect = new Vector4(0, 0, 1, 1), Colour = DrawColourInfo.Colour.BottomRight.Linear, Time = part.Time }); @@ -253,6 +255,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor { Position = new Vector2(part.Position.X + size.X / 2, part.Position.Y - size.Y / 2), TexturePosition = textureRect.TopRight, + TextureRect = new Vector4(0, 0, 1, 1), Colour = DrawColourInfo.Colour.TopRight.Linear, Time = part.Time }); @@ -261,6 +264,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor { Position = new Vector2(part.Position.X - size.X / 2, part.Position.Y - size.Y / 2), TexturePosition = textureRect.TopLeft, + TextureRect = new Vector4(0, 0, 1, 1), Colour = DrawColourInfo.Colour.TopLeft.Linear, Time = part.Time }); @@ -290,6 +294,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor [VertexMember(2, VertexAttribPointerType.Float)] public Vector2 TexturePosition; + [VertexMember(4, VertexAttribPointerType.Float)] + public Vector4 TextureRect; + [VertexMember(1, VertexAttribPointerType.Float)] public float Time; From a2fdf9448394f451c2243e0e7c6ebd2ba72db94e Mon Sep 17 00:00:00 2001 From: Power Maker <42269909+power9maker@users.noreply.github.com> Date: Tue, 2 Jun 2020 20:55:21 +0200 Subject: [PATCH 1893/2376] Add cursor rotation on right mouse button --- osu.Game/Graphics/Cursor/MenuCursor.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/osu.Game/Graphics/Cursor/MenuCursor.cs b/osu.Game/Graphics/Cursor/MenuCursor.cs index 580177d17a..740c809afc 100644 --- a/osu.Game/Graphics/Cursor/MenuCursor.cs +++ b/osu.Game/Graphics/Cursor/MenuCursor.cs @@ -83,10 +83,13 @@ namespace osu.Game.Graphics.Cursor activeCursor.AdditiveLayer.FadeInFromZero(800, Easing.OutQuint); } - if (e.Button == MouseButton.Left && cursorRotate.Value) + if ((e.Button == MouseButton.Left || e.Button == MouseButton.Right) && cursorRotate.Value) { - dragRotationState = DragRotationState.DragStarted; - positionMouseDown = e.MousePosition; + if(!(dragRotationState == DragRotationState.Rotating)) + { + positionMouseDown = e.MousePosition; + dragRotationState = DragRotationState.DragStarted; + } } return base.OnMouseDown(e); @@ -94,13 +97,13 @@ namespace osu.Game.Graphics.Cursor protected override void OnMouseUp(MouseUpEvent e) { - if (!e.IsPressed(MouseButton.Left) && !e.IsPressed(MouseButton.Right)) + if (!e.IsPressed(MouseButton.Left) && !e.IsPressed(MouseButton.Middle) && !e.IsPressed(MouseButton.Right)) { activeCursor.AdditiveLayer.FadeOutFromOne(500, Easing.OutQuint); activeCursor.ScaleTo(1, 500, Easing.OutElastic); } - if (e.Button == MouseButton.Left) + if (!e.IsPressed(MouseButton.Left) && !e.IsPressed(MouseButton.Right)) { if (dragRotationState == DragRotationState.Rotating) activeCursor.RotateTo(0, 600 * (1 + Math.Abs(activeCursor.Rotation / 720)), Easing.OutElasticHalf); From 85d0c04e61222d9734297f9365bb22fcfec6514a Mon Sep 17 00:00:00 2001 From: Power Maker <42269909+power9maker@users.noreply.github.com> Date: Tue, 2 Jun 2020 20:57:02 +0200 Subject: [PATCH 1894/2376] Add cursor rotation on right mouse button --- osu.Game/Graphics/Cursor/MenuCursor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Cursor/MenuCursor.cs b/osu.Game/Graphics/Cursor/MenuCursor.cs index 740c809afc..c92304b2d2 100644 --- a/osu.Game/Graphics/Cursor/MenuCursor.cs +++ b/osu.Game/Graphics/Cursor/MenuCursor.cs @@ -87,8 +87,8 @@ namespace osu.Game.Graphics.Cursor { if(!(dragRotationState == DragRotationState.Rotating)) { - positionMouseDown = e.MousePosition; dragRotationState = DragRotationState.DragStarted; + positionMouseDown = e.MousePosition; } } From 4ebc1d3721f50e758eb473465b90edcc7271c75f Mon Sep 17 00:00:00 2001 From: Shivam Date: Tue, 2 Jun 2020 21:06:22 +0200 Subject: [PATCH 1895/2376] Add original sprite and visualiser Notes: This is using a modified version of welcome.osz to facilitate the visualiser and the animation of the sprite is not accurate. --- ...ntroLegacy.cs => TestSceneIntroWelcome.cs} | 4 +- osu.Game/Screens/Loader.cs | 2 +- osu.Game/Screens/Menu/IntroLegacy.cs | 117 -------------- osu.Game/Screens/Menu/IntroWelcome.cs | 152 ++++++++++++++++++ 4 files changed, 155 insertions(+), 120 deletions(-) rename osu.Game.Tests/Visual/Menus/{TestSceneIntroLegacy.cs => TestSceneIntroWelcome.cs} (70%) delete mode 100644 osu.Game/Screens/Menu/IntroLegacy.cs create mode 100644 osu.Game/Screens/Menu/IntroWelcome.cs diff --git a/osu.Game.Tests/Visual/Menus/TestSceneIntroLegacy.cs b/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs similarity index 70% rename from osu.Game.Tests/Visual/Menus/TestSceneIntroLegacy.cs rename to osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs index 7cb99467ad..905f17ef0b 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneIntroLegacy.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs @@ -8,8 +8,8 @@ using osu.Game.Screens.Menu; namespace osu.Game.Tests.Visual.Menus { [TestFixture] - public class TestSceneIntroLegacy : IntroTestScene + public class TestSceneIntroWelcome : IntroTestScene { - protected override IScreen CreateScreen() => new IntroLegacy(); + protected override IScreen CreateScreen() => new IntroWelcome(); } } diff --git a/osu.Game/Screens/Loader.cs b/osu.Game/Screens/Loader.cs index aa959e7d35..0bfabdaa15 100644 --- a/osu.Game/Screens/Loader.cs +++ b/osu.Game/Screens/Loader.cs @@ -52,7 +52,7 @@ namespace osu.Game.Screens return new IntroCircles(); case IntroSequence.Welcome: - return new IntroLegacy(); + return new IntroWelcome(); default: return new IntroTriangles(); diff --git a/osu.Game/Screens/Menu/IntroLegacy.cs b/osu.Game/Screens/Menu/IntroLegacy.cs deleted file mode 100644 index 3980a0cc8b..0000000000 --- a/osu.Game/Screens/Menu/IntroLegacy.cs +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Linq; -using osuTK; -using osu.Framework.Allocation; -using osu.Framework.Audio; -using osu.Framework.Audio.Sample; -using osu.Framework.Screens; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; - -namespace osu.Game.Screens.Menu -{ - public class IntroLegacy : IntroScreen - { - protected override string BeatmapHash => "64E00D7022195959BFA3109D09C2E2276C8F12F486B91FCF6175583E973B48F2"; - protected override string BeatmapFile => "welcome.osz"; - private const double delay_step_two = 2142; - - private SampleChannel welcome; - - private SampleChannel pianoReverb; - - [BackgroundDependencyLoader] - private void load(AudioManager audio) - { - Seeya = audio.Samples.Get(@"Intro/welcome/seeya"); - - if (MenuVoice.Value) - { - welcome = audio.Samples.Get(@"Intro/welcome/welcome"); - pianoReverb = audio.Samples.Get(@"Intro/welcome/welcome_piano"); - } - } - - protected override void LogoArriving(OsuLogo logo, bool resuming) - { - base.LogoArriving(logo, resuming); - - if (!resuming) - { - welcome?.Play(); - pianoReverb?.Play(); - Scheduler.AddDelayed(delegate - { - StartTrack(); - - PrepareMenuLoad(); - - logo.ScaleTo(1); - logo.FadeIn(); - - Scheduler.Add(LoadMenu); - }, delay_step_two); - - LoadComponentAsync(new FallbackIntroSequence - { - RelativeSizeAxes = Axes.Both - }, t => - { - AddInternal(t); - t.Start(delay_step_two); - }); - } - } - - public override void OnSuspending(IScreen next) - { - this.FadeOut(300); - base.OnSuspending(next); - } - - private class FallbackIntroSequence : Container - { - private OsuSpriteText welcomeText; - - [BackgroundDependencyLoader] - private void load() - { - Children = new Drawable[] - { - welcomeText = new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Text = "welcome", - Padding = new MarginPadding { Bottom = 10 }, - Font = OsuFont.GetFont(weight: FontWeight.Light, size: 42), - Alpha = 0, - Spacing = new Vector2(5), - }, - }; - } - - public void Start(double length) - { - if (Children.Any()) - { - // restart if we were already run previously. - FinishTransforms(true); - load(); - } - - double remainingTime() => length - TransformDelay; - - using (BeginDelayedSequence(250, true)) - { - welcomeText.FadeIn(700); - welcomeText.ScaleTo(welcomeText.Scale + new Vector2(0.5f), remainingTime(), Easing.Out); - } - } - } - } -} diff --git a/osu.Game/Screens/Menu/IntroWelcome.cs b/osu.Game/Screens/Menu/IntroWelcome.cs new file mode 100644 index 0000000000..fbed0bf654 --- /dev/null +++ b/osu.Game/Screens/Menu/IntroWelcome.cs @@ -0,0 +1,152 @@ +// 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.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Screens; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osuTK.Graphics; + +namespace osu.Game.Screens.Menu +{ + public class IntroWelcome : IntroScreen + { + protected override string BeatmapHash => "64E00D7022195959BFA3109D09C2E2276C8F12F486B91FCF6175583E973B48F2"; + protected override string BeatmapFile => "welcome.osz"; + private const double delay_step_two = 2142; + private SampleChannel welcome; + + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + Seeya = audio.Samples.Get(@"Intro/welcome/seeya"); + + if (MenuVoice.Value) + welcome = audio.Samples.Get(@"Intro/welcome/welcome"); + } + + protected override void LogoArriving(OsuLogo logo, bool resuming) + { + base.LogoArriving(logo, resuming); + + if (!resuming) + { + welcome?.Play(); + StartTrack(); + Scheduler.AddDelayed(delegate + { + PrepareMenuLoad(); + + logo.ScaleTo(1); + logo.FadeIn(); + + Scheduler.Add(LoadMenu); + }, delay_step_two); + + LoadComponentAsync(new WelcomeIntroSequence + { + RelativeSizeAxes = Axes.Both + }, AddInternal); + } + } + + public override void OnSuspending(IScreen next) + { + this.FadeOut(300); + base.OnSuspending(next); + } + + private class WelcomeIntroSequence : Container + { + private Sprite welcomeText; + private LogoVisualisation visualizer; + private Container elementContainer; + private Container circleContainer; + private Circle blackCircle; + + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + Origin = Anchor.Centre; + Anchor = Anchor.Centre; + Children = new Drawable[] + { + new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + elementContainer = new Container + { + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + visualizer = new LogoVisualisation + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Alpha = 0.5f, + AccentColour = Color4.Blue, + Size = new Vector2(0.96f) + }, + circleContainer = new Container + { + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + blackCircle = new Circle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(480), + Colour = Color4.Black + } + } + } + } + } + } + }, + welcomeText = new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(0.5f), + Texture = textures.Get(@"Welcome/welcome_text@2x") + }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + double remainingTime() => delay_step_two - TransformDelay; + + using (BeginDelayedSequence(250, true)) + { + welcomeText.FadeIn(700); + welcomeText.ScaleTo(welcomeText.Scale + new Vector2(0.5f), remainingTime(), Easing.Out).OnComplete(_ => + { + elementContainer.Remove(visualizer); + circleContainer.Remove(blackCircle); + elementContainer.Remove(circleContainer); + Remove(welcomeText); + visualizer.Dispose(); + blackCircle.Dispose(); + welcomeText.Dispose(); + }); + } + } + } + } +} From b79773cdb17524bc7fae1da25016cfe2ff90ac46 Mon Sep 17 00:00:00 2001 From: Shivam Date: Tue, 2 Jun 2020 21:50:50 +0200 Subject: [PATCH 1896/2376] Modify LogoVisualisation to allow color changes Also change the color from blue to dark blue --- osu.Game/Screens/Menu/IntroWelcome.cs | 3 ++- osu.Game/Screens/Menu/LogoVisualisation.cs | 8 ++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Menu/IntroWelcome.cs b/osu.Game/Screens/Menu/IntroWelcome.cs index fbed0bf654..7c60048d1c 100644 --- a/osu.Game/Screens/Menu/IntroWelcome.cs +++ b/osu.Game/Screens/Menu/IntroWelcome.cs @@ -95,7 +95,8 @@ namespace osu.Game.Screens.Menu Anchor = Anchor.Centre, Origin = Anchor.Centre, Alpha = 0.5f, - AccentColour = Color4.Blue, + isIntro = true, + AccentColour = Color4.DarkBlue, Size = new Vector2(0.96f) }, circleContainer = new Container diff --git a/osu.Game/Screens/Menu/LogoVisualisation.cs b/osu.Game/Screens/Menu/LogoVisualisation.cs index 0db7f2a2dc..0e77d8d171 100644 --- a/osu.Game/Screens/Menu/LogoVisualisation.cs +++ b/osu.Game/Screens/Menu/LogoVisualisation.cs @@ -70,6 +70,7 @@ namespace osu.Game.Screens.Menu private IShader shader; private readonly Texture texture; + public bool isIntro = false; private Bindable user; private Bindable skin; @@ -88,8 +89,11 @@ namespace osu.Game.Screens.Menu user = api.LocalUser.GetBoundCopy(); skin = skinManager.CurrentSkin.GetBoundCopy(); - user.ValueChanged += _ => updateColour(); - skin.BindValueChanged(_ => updateColour(), true); + if (!isIntro) + { + user.ValueChanged += _ => updateColour(); + skin.BindValueChanged(_ => updateColour(), true); + } } private void updateAmplitudes() From 9cd66dcdef26d39bd771f375b8a27fa25ddcb6bc Mon Sep 17 00:00:00 2001 From: Shivam Date: Tue, 2 Jun 2020 21:54:39 +0200 Subject: [PATCH 1897/2376] Fix styling error --- osu.Game/Screens/Menu/IntroWelcome.cs | 2 +- osu.Game/Screens/Menu/LogoVisualisation.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Menu/IntroWelcome.cs b/osu.Game/Screens/Menu/IntroWelcome.cs index 7c60048d1c..9f9012cb2b 100644 --- a/osu.Game/Screens/Menu/IntroWelcome.cs +++ b/osu.Game/Screens/Menu/IntroWelcome.cs @@ -95,7 +95,7 @@ namespace osu.Game.Screens.Menu Anchor = Anchor.Centre, Origin = Anchor.Centre, Alpha = 0.5f, - isIntro = true, + IsIntro = true, AccentColour = Color4.DarkBlue, Size = new Vector2(0.96f) }, diff --git a/osu.Game/Screens/Menu/LogoVisualisation.cs b/osu.Game/Screens/Menu/LogoVisualisation.cs index 0e77d8d171..c72b3a6576 100644 --- a/osu.Game/Screens/Menu/LogoVisualisation.cs +++ b/osu.Game/Screens/Menu/LogoVisualisation.cs @@ -70,7 +70,7 @@ namespace osu.Game.Screens.Menu private IShader shader; private readonly Texture texture; - public bool isIntro = false; + public bool IsIntro = false; private Bindable user; private Bindable skin; @@ -89,7 +89,7 @@ namespace osu.Game.Screens.Menu user = api.LocalUser.GetBoundCopy(); skin = skinManager.CurrentSkin.GetBoundCopy(); - if (!isIntro) + if (!IsIntro) { user.ValueChanged += _ => updateColour(); skin.BindValueChanged(_ => updateColour(), true); From fa4d13a22b68440e885288f57157cfb2d3466007 Mon Sep 17 00:00:00 2001 From: Power Maker Date: Tue, 2 Jun 2020 22:25:25 +0200 Subject: [PATCH 1898/2376] Fixed whitespace --- osu.Game/Graphics/Cursor/MenuCursor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Cursor/MenuCursor.cs b/osu.Game/Graphics/Cursor/MenuCursor.cs index c92304b2d2..1aa7b68d1a 100644 --- a/osu.Game/Graphics/Cursor/MenuCursor.cs +++ b/osu.Game/Graphics/Cursor/MenuCursor.cs @@ -85,7 +85,7 @@ namespace osu.Game.Graphics.Cursor if ((e.Button == MouseButton.Left || e.Button == MouseButton.Right) && cursorRotate.Value) { - if(!(dragRotationState == DragRotationState.Rotating)) + if (!(dragRotationState == DragRotationState.Rotating)) { dragRotationState = DragRotationState.DragStarted; positionMouseDown = e.MousePosition; From 40e64eed475b7c09de60643671035e5cd0ec9967 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Jun 2020 23:15:14 +0200 Subject: [PATCH 1899/2376] Add contributing guidelines --- CONTRIBUTING.md | 121 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..441521f9ef --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,121 @@ +# Contributing Guidelines + +Thank you for showing interest in the development of osu!lazer! We aim to provide a good collaborating environment for everyone involved, and as such have decided to list some of the most important things to keep in mind in the process. The guidelines below have been chosen based on past experience. + +These are not "official rules" *per se*, but following them will help everyone deal with things in the most efficient manner. + +## Table of contents + +1. [I would like to submit an issue!](#i-would-like-to-submit-an-issue) +2. [I would like to submit a pull request!](#i-would-like-to-submit-a-pull-request) + +## I would like to submit an issue! + +When it comes to issues, bug reports and feature suggestions are welcomed, although please keep in mind that at any point in time, hundreds of issues are open, which vary in severity and the amount of time needed to address them. As such it's not uncommon for issues to remain unresolved for a long time or even closed outright if they are deemed not important enough to fix in the foreseeable future. Issues that are required to "go live" or otherwise achieve parity with stable are prioritised the most. + +* **Before submitting an issue, try searching existing issues first.** + + For housekeeping purposes, we close issues that overlap with or duplicate other pre-existing issues - you can help us not have to do that by searching existing issues yourself first. The issue search box, as well as the issue tag system, are tools you can use to check if an issue has been reported before. + +* **When submitting a bug report, please try to include as much detail as possible.** + + Bugs are not equal - some of them will be reproducible every time on pretty much all hardware, while others will be hard to track down due to being specific to particular hardware or even somewhat random in nature. As such, providing as much detail as possible when reporting a bug is hugely appreciated. A good starting set of information contains of: + + * the in-game logs, which are located at: + * `%AppData%/osu/logs` (on Windows), + * `~/.local/share/osu/logs` (on Linux and macOS), + * `Android/Data/sh.ppy.osulazer/logs` (on Android), + * on iOS they can be obtained by connecting your device to your desktop and copying the `logs` directory from the app's own document storage using iTunes, + * your system specifications (including the operating system and platform you are playing on), + * a reproduction scenario (list of steps you have performed leading up to the occurrence of the bug), + * a video or picture of the bug, if at all possible. + +* **Provide more information when asked to do so.** + + Sometimes when a bug is more elusive or complicated, none of the information listed above will pinpoint a concrete cause of the problem. In this case we will most likely ask you for additional info, such as a Windows Event Log dump or a copy of your local lazer database (`client.db`). Providing that information is beneficial to both parties - we can track down the problem better, and hopefully fix it for you at some point once we know where it is! + +* **When submitting a feature proposal, please describe it in the most understandable way you can.** + + Communicating your idea for a feature can often be hard, and we would like to avoid any misunderstandings. As such, please try to explain your idea in a short, but understandable manner - it's best to avoid jargon or terms and references that could be considered obscure. A mock-up picture (doesn't have to be good!) of the feature can also go a long way in explaining. + +* **Refrain from posting "+1" comments.** + + If an issue has already been created, saying that you also experience it without providing any additional details doesn't really help us in any way. To express support for a proposal or indicate that you are also affected by a particular bug, you can use comment reactions instead. + +* **Refrain from asking if an issue has been resolved yet.** + + As mentioned above, the issue tracker has hundreds of issues open at any given time. Currently the game is being worked on by two members of the core team, and a handful of outside contributors who offer their free time to help out. As such, it can happen that an issue gets placed on the backburner due to being less important; generally posting a comment demanding its resolution some months or years after it is reported is not very likely to increase its priority. + +* **Avoid long discussions about non-development topics.** + + GitHub is mostly a developer space, and as such isn't really fit for lengthened discussions about gameplay mechanics (which might not even be in any way confirmed for the final release) and similar non-technical matters. Such matters are probably best addressed at the osu! forums. + +## I would like to submit a pull request! + +We also welcome pull requests from unaffiliated contributors. The issue tracker should provide plenty of issues that you can work on; we also mark issues that we think would be good for newcomers with the [`good-first-issue`](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+label%3Agood-first-issue) label. + +However, do keep in mind that the core team is committed to bringing osu!lazer up to par with stable first and foremost, so depending on what your contribution concerns, it might not be merged and released right away. Our approach to managing issues and their priorities is described [in the wiki](https://github.com/ppy/osu/wiki/Project-management). + +Here are some key things to note before jumping in: + +* **Make sure you are comfortable with C\# and your development environment.** + + While we are accepting of all kinds of contributions, we also have a certain quality standard we'd like to uphold and limited time to review your code. Therefore, we would like to avoid providing entry-level advice, and as such if you're not very familiar with C\# as a programming language, we'd recommend that you start off with a few personal projects to get acquainted with the language's syntax, toolchain and principles of object-oriented programming first. + +* **Make sure you are familiar with git and the pull request workflow.** + + [git](https://git-scm.com/) is a distributed version control system that might not be very intuitive at the beginning if you're not familiar with version control. In particular, projects using git have a particular workflow for submitting code changes, which is called the pull request workflow. + + To make things run more smoothly, we recommend that you look up some online resources to familiarise yourself with the git vocabulary and commands, and practice working with forks and submitting pull requests at your own pace. A high-level overview of the process can be found in [this article by GitHub](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/proposing-changes-to-your-work-with-pull-requests). + +* **Make sure to submit pull requests off of a topic branch.** + + As described in the article linked in the previous point, topic branches help you parallelise your work and separate it from the main `master` branch, and additionally are easier for maintainers to work with. Working with multiple `master` branches across many remotes is difficult to keep track of, and it's easy to make a mistake and push to the wrong `master` branch by accident. + +* **Refrain from making changes through the GitHub web interface.** + + Even though GitHub provides an option to edit code or replace files in the repository using the web interface, we strongly discourage using it in most scenarios. Editing files this way is inefficient and likely to introduce whitespace or file encoding changes that make it more difficult to review the code. + + Code written through the web interface will also very likely be questioned outright by the reviewers, as it is likely that it has not been properly tested or that it will fail continuous integration checks. We strongly encourage using an IDE like [Visual Studio](https://visualstudio.microsoft.com/), [Visual Studio Code](https://code.visualstudio.com/) or [JetBrains Rider](https://www.jetbrains.com/rider/) instead. + +* **Add tests for your code whenever possible.** + + Automated tests are an essential part of a quality and reliable codebase. They help to make the code more maintainable by ensuring it is safe to reorganise (or refactor) the code in various ways, and also prevent regressions - bugs that resurface after having been fixed at some point in the past. If it is viable, please put in the time to add tests, so that the changes you make can last for a (hopefully) very long time. + +* **Run tests before opening a pull request.** + + Tying into the previous point, sometimes changes in one part of the codebase can result in unpredictable changes in behaviour in other pieces of the code. This is why it is best to always try to run tests before opening a PR. + + Continuous integration will always run the tests for you (and us), too, but it is best not to rely on it, as there might be many builds queued at any time. Running tests on your own will help you be more certain that at the point of clicking the "Create pull request" button, your changes are as ready as can be. + +* **Run code style analysis before opening a pull request.** + + As part of continuous integration, we also run code style analysis, which is supposed to make sure that your code is formatted the same way as all the pre-existing code in the repository. The reason we enforce a particular code style everywhere is to make sure the codebase is consistent in that regard - having one whitespace convention in one place and another one elsewhere causes disorganisation. + +* **Make sure to keep the *Allow edits from maintainers* check box checked.** + + To speed up the merging process, collaborators and team members will sometimes want to push changes to your branch themselves, to make minor code style adjustments or to otherwise refactor the code without having to describe how they'd like the code to look like in painstaking detail. Having the *Allow edits from maintainers* check box checked lets them do that; without it they are forced to report issues back to you and wait for you to address them. + +* **Refrain from continually merging the master branch back to the PR.** + + Unless there are merge conflicts that need resolution, there is no need to keep merging `master` back to a branch over and over again. One of the maintainers will merge `master` themselves before merging the PR itself anyway, and continual merge commits can cause CI to get overwhelmed due to queueing up too many builds. + +* **Refrain from force-pushing to the PR branch.** + + Force-pushing should be avoided, as it can lead to accidentally overwriting a maintainer's changes or CI building wrong commits. We value all history in the project, so there is no need to squash or amend commits in most cases. + + The cases in which force-pushing is warranted are very rare (such as accidentally leaking sensitive info in one of the files committed, adding unrelated files, or mis-merging a dependent PR). + +* **Be patient when waiting for the code to be reviewed and merged.** + + As much as we'd like to review all contributions as fast as possible, our time is limited, as team members have to work on their own tasks in addition to reviewing code. As such, work needs to be prioritised, and it can unfortunately take weeks or months for your PR to be merged, depending on how important it is deemed to be. + +* **Don't mistake criticism of code for criticism of your person.** + + As mentioned before, we are highly committed to quality when it comes to the lazer project. This means that contributions from less experienced community members can take multiple rounds of review to get to a mergeable state. We try our utmost best to never conflate a person with the code they authored, and to keep the discussion focused on the code at all times. Please consider our comments and requests a learning experience, and don't treat it as a personal attack. + +* **Feel free to reach out for help.** + + If you're uncertain about some part of the codebase or some inner workings of the game and framework, please reach out either by leaving a comment in the relevant issue or PR thread, or by posting a message in the [development Discord server](https://discord.gg/ppy). We will try to help you as much as we can. + + When it comes to which form of communication is best, GitHub generally lends better to longer-form discussions, while Discord is better for snappy call-and-response answers. Use your best discretion when deciding, and try to keep a single discussion in one place instead of moving back and forth. From f4f84ede6a2ff88b3f0cf7517c89a59ef309ed20 Mon Sep 17 00:00:00 2001 From: Shane Woolcock Date: Wed, 3 Jun 2020 10:43:16 +0930 Subject: [PATCH 1900/2376] Fix results screen crashing for beatmaps with no online ID --- osu.Game/Screens/Ranking/ResultsScreen.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index fbb9b95478..145ba93573 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -140,14 +140,17 @@ namespace osu.Game.Screens.Ranking { base.LoadComplete(); - var req = FetchScores(scores => Schedule(() => + if (Score.Beatmap.OnlineBeatmapID != null) { - foreach (var s in scores) - panels.AddScore(s); - })); + var req = FetchScores(scores => Schedule(() => + { + foreach (var s in scores) + panels.AddScore(s); + })); - if (req != null) - api.Queue(req); + if (req != null) + api.Queue(req); + } } /// From 90213d079d33713d8729739675910ebe1cfdda24 Mon Sep 17 00:00:00 2001 From: Shane Woolcock Date: Wed, 3 Jun 2020 10:48:27 +0930 Subject: [PATCH 1901/2376] Include submission status in check --- osu.Game/Screens/Ranking/ResultsScreen.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 145ba93573..25c8205c30 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Screens; +using osu.Game.Beatmaps; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; @@ -140,7 +141,7 @@ namespace osu.Game.Screens.Ranking { base.LoadComplete(); - if (Score.Beatmap.OnlineBeatmapID != null) + if (Score.Beatmap.OnlineBeatmapID != null && Score.Beatmap.Status > BeatmapSetOnlineStatus.Pending) { var req = FetchScores(scores => Schedule(() => { From 96e3c6e8e888d47069cc636c3049178cae09733b Mon Sep 17 00:00:00 2001 From: Shane Woolcock Date: Wed, 3 Jun 2020 11:36:47 +0930 Subject: [PATCH 1902/2376] Move check to SoloResultsScreen --- osu.Game/Screens/Ranking/ResultsScreen.cs | 15 ++++++--------- osu.Game/Screens/Ranking/SoloResultsScreen.cs | 4 ++++ 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 25c8205c30..aff0540652 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -141,17 +141,14 @@ namespace osu.Game.Screens.Ranking { base.LoadComplete(); - if (Score.Beatmap.OnlineBeatmapID != null && Score.Beatmap.Status > BeatmapSetOnlineStatus.Pending) + var req = FetchScores(scores => Schedule(() => { - var req = FetchScores(scores => Schedule(() => - { - foreach (var s in scores) - panels.AddScore(s); - })); + foreach (var s in scores) + panels.AddScore(s); + })); - if (req != null) - api.Queue(req); - } + if (req != null) + api.Queue(req); } /// diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs index 3ae723683a..9cf2e6757a 100644 --- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs +++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Rulesets; @@ -24,6 +25,9 @@ namespace osu.Game.Screens.Ranking protected override APIRequest FetchScores(Action> scoresCallback) { + if (Score.Beatmap.OnlineBeatmapID == null || Score.Beatmap.Status <= BeatmapSetOnlineStatus.Pending) + return null; + var req = new GetScoresRequest(Score.Beatmap, Score.Ruleset); req.Success += r => scoresCallback?.Invoke(r.Scores.Where(s => s.OnlineScoreID != Score.OnlineScoreID).Select(s => s.CreateScoreInfo(rulesets))); return req; From 0d5a2cf96d089038615d8f30572ae4b7e9b7c7ed Mon Sep 17 00:00:00 2001 From: Shane Woolcock Date: Wed, 3 Jun 2020 11:36:59 +0930 Subject: [PATCH 1903/2376] Add unit tests --- .../Visual/Ranking/TestSceneResultsScreen.cs | 41 ++++++++++++++++--- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 242766ad4b..125aa0a1e7 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -36,12 +36,14 @@ namespace osu.Game.Tests.Visual.Ranking Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo); } - private TestSoloResults createResultsScreen() => new TestSoloResults(new TestScoreInfo(new OsuRuleset().RulesetInfo)); + private TestResultsScreen createResultsScreen() => new TestResultsScreen(new TestScoreInfo(new OsuRuleset().RulesetInfo)); + + private UnrankedSoloResultsScreen createUnrankedSoloResultsScreen() => new UnrankedSoloResultsScreen(new TestScoreInfo(new OsuRuleset().RulesetInfo)); [Test] public void ResultsWithoutPlayer() { - TestSoloResults screen = null; + TestResultsScreen screen = null; OsuScreenStack stack; AddStep("load results", () => @@ -60,13 +62,23 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void ResultsWithPlayer() { - TestSoloResults screen = null; + TestResultsScreen screen = null; AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen())); AddUntilStep("wait for loaded", () => screen.IsLoaded); AddAssert("retry overlay present", () => screen.RetryOverlay != null); } + [Test] + public void ResultsForUnranked() + { + UnrankedSoloResultsScreen screen = null; + + AddStep("load results", () => Child = new TestResultsContainer(screen = createUnrankedSoloResultsScreen())); + AddUntilStep("wait for loaded", () => screen.IsLoaded); + AddAssert("retry overlay present", () => screen.RetryOverlay != null); + } + private class TestResultsContainer : Container { [Cached(typeof(Player))] @@ -86,11 +98,11 @@ namespace osu.Game.Tests.Visual.Ranking } } - private class TestSoloResults : ResultsScreen + private class TestResultsScreen : ResultsScreen { public HotkeyRetryOverlay RetryOverlay; - public TestSoloResults(ScoreInfo score) + public TestResultsScreen(ScoreInfo score) : base(score) { } @@ -102,5 +114,24 @@ namespace osu.Game.Tests.Visual.Ranking RetryOverlay = InternalChildren.OfType().SingleOrDefault(); } } + + private class UnrankedSoloResultsScreen : SoloResultsScreen + { + public HotkeyRetryOverlay RetryOverlay; + + public UnrankedSoloResultsScreen(ScoreInfo score) + : base(score) + { + Score.Beatmap.OnlineBeatmapID = 0; + Score.Beatmap.Status = BeatmapSetOnlineStatus.Pending; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + RetryOverlay = InternalChildren.OfType().SingleOrDefault(); + } + } } } From b174daa94a5f7968ac9ef31b50257ecf6a96698d Mon Sep 17 00:00:00 2001 From: Shane Woolcock Date: Wed, 3 Jun 2020 11:58:56 +0930 Subject: [PATCH 1904/2376] Remove unused using --- osu.Game/Screens/Ranking/ResultsScreen.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index aff0540652..fbb9b95478 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -10,7 +10,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Screens; -using osu.Game.Beatmaps; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; From 13622eff1f12e8338f96ff5005ccbc2a9e12b8f6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 3 Jun 2020 12:54:07 +0900 Subject: [PATCH 1905/2376] Fix response value --- .../Multiplayer/TestSceneTimeshiftResultsScreen.cs | 2 +- .../Online/API/Requests/GetRoomPlaylistScoresRequest.cs | 9 ++++++++- osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs index d87a2e3408..8559e7e2f4 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs @@ -66,7 +66,7 @@ namespace osu.Game.Tests.Visual.Multiplayer switch (request) { case GetRoomPlaylistScoresRequest r: - r.TriggerSuccess(roomScores); + r.TriggerSuccess(new RoomPlaylistScores { Scores = roomScores }); break; } }); diff --git a/osu.Game/Online/API/Requests/GetRoomPlaylistScoresRequest.cs b/osu.Game/Online/API/Requests/GetRoomPlaylistScoresRequest.cs index dd7f80fd46..38f852870b 100644 --- a/osu.Game/Online/API/Requests/GetRoomPlaylistScoresRequest.cs +++ b/osu.Game/Online/API/Requests/GetRoomPlaylistScoresRequest.cs @@ -2,10 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using Newtonsoft.Json; namespace osu.Game.Online.API.Requests { - public class GetRoomPlaylistScoresRequest : APIRequest> + public class GetRoomPlaylistScoresRequest : APIRequest { private readonly int roomId; private readonly int playlistItemId; @@ -18,4 +19,10 @@ namespace osu.Game.Online.API.Requests protected override string Target => $@"rooms/{roomId}/playlist/{playlistItemId}/scores"; } + + public class RoomPlaylistScores + { + [JsonProperty("scores")] + public List Scores { get; set; } + } } diff --git a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs index f2afe15d35..d95cee2ab8 100644 --- a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs +++ b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs @@ -27,7 +27,7 @@ namespace osu.Game.Screens.Multi.Ranking protected override APIRequest FetchScores(Action> scoresCallback) { var req = new GetRoomPlaylistScoresRequest(roomId, playlistItem.ID); - req.Success += r => scoresCallback?.Invoke(r.Where(s => s.ID != Score?.OnlineScoreID).Select(s => s.CreateScoreInfo(playlistItem))); + req.Success += r => scoresCallback?.Invoke(r.Scores.Where(s => s.ID != Score?.OnlineScoreID).Select(s => s.CreateScoreInfo(playlistItem))); return req; } } From 22f4e9012c58e96b8b03f5e9bb4b9639efa83c34 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 3 Jun 2020 12:54:16 +0900 Subject: [PATCH 1906/2376] Remove temporary code --- osu.Game/Screens/Multi/Match/MatchSubScreen.cs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs index 01a90139c3..3609be2dfe 100644 --- a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs +++ b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs @@ -13,7 +13,6 @@ using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; -using osu.Game.Online.API.Requests; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.GameTypes; using osu.Game.Rulesets.Mods; @@ -212,17 +211,6 @@ namespace osu.Game.Screens.Multi.Match managerAdded = beatmapManager.ItemAdded.GetBoundCopy(); managerAdded.BindValueChanged(beatmapAdded); - - if (roomId.Value != null) - { - var req = new GetRoomPlaylistScoresRequest(roomId.Value.Value, playlist[0].ID); - - req.Success += scores => - { - }; - - api.Queue(req); - } } public override bool OnExiting(IScreen next) From 74875f9b629715a1e5a13e65704af277fb2c8c35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 3 Jun 2020 06:47:10 +0200 Subject: [PATCH 1907/2376] Apply review suggestions & other cleanups --- CONTRIBUTING.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 441521f9ef..331534ad73 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,21 +11,21 @@ These are not "official rules" *per se*, but following them will help everyone d ## I would like to submit an issue! -When it comes to issues, bug reports and feature suggestions are welcomed, although please keep in mind that at any point in time, hundreds of issues are open, which vary in severity and the amount of time needed to address them. As such it's not uncommon for issues to remain unresolved for a long time or even closed outright if they are deemed not important enough to fix in the foreseeable future. Issues that are required to "go live" or otherwise achieve parity with stable are prioritised the most. +Issues, bug reports and feature suggestions are welcomed, though please keep in mind that at any point in time, hundreds of issues are open, which vary in severity and the amount of time needed to address them. As such it's not uncommon for issues to remain unresolved for a long time or even closed outright if they are deemed not important enough to fix in the foreseeable future. Issues that are required to "go live" or otherwise achieve parity with stable are prioritised the most. * **Before submitting an issue, try searching existing issues first.** - For housekeeping purposes, we close issues that overlap with or duplicate other pre-existing issues - you can help us not have to do that by searching existing issues yourself first. The issue search box, as well as the issue tag system, are tools you can use to check if an issue has been reported before. + For housekeeping purposes, we close issues that overlap with or duplicate other pre-existing issues - you can help us not to have to do that by searching existing issues yourself first. The issue search box, as well as the issue tag system, are tools you can use to check if an issue has been reported before. * **When submitting a bug report, please try to include as much detail as possible.** - Bugs are not equal - some of them will be reproducible every time on pretty much all hardware, while others will be hard to track down due to being specific to particular hardware or even somewhat random in nature. As such, providing as much detail as possible when reporting a bug is hugely appreciated. A good starting set of information contains of: + Bugs are not equal - some of them will be reproducible every time on pretty much all hardware, while others will be hard to track down due to being specific to particular hardware or even somewhat random in nature. As such, providing as much detail as possible when reporting a bug is hugely appreciated. A good starting set of information consists of: * the in-game logs, which are located at: * `%AppData%/osu/logs` (on Windows), * `~/.local/share/osu/logs` (on Linux and macOS), * `Android/Data/sh.ppy.osulazer/logs` (on Android), - * on iOS they can be obtained by connecting your device to your desktop and copying the `logs` directory from the app's own document storage using iTunes, + * on iOS they can be obtained by connecting your device to your desktop and [copying the `logs` directory from the app's own document storage using iTunes](https://support.apple.com/en-us/HT201301#copy-to-computer), * your system specifications (including the operating system and platform you are playing on), * a reproduction scenario (list of steps you have performed leading up to the occurrence of the bug), * a video or picture of the bug, if at all possible. From 1992a3db546126f536ddc9974cd5074bff5f4876 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Jun 2020 15:50:00 +0900 Subject: [PATCH 1908/2376] Fix redundant override showing up in build warnings --- osu.Game/Rulesets/Objects/SliderEventGenerator.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs index 6df0041e7a..d8c6da86f9 100644 --- a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs +++ b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs @@ -11,6 +11,7 @@ namespace osu.Game.Rulesets.Objects public static class SliderEventGenerator { [Obsolete("Use the overload with cancellation support instead.")] // can be removed 20201115 + // ReSharper disable once RedundantOverload.Global public static IEnumerable Generate(double startTime, double spanDuration, double velocity, double tickDistance, double totalDistance, int spanCount, double? legacyLastTickOffset) { From 5f1d44a2bef173fc2745e0e1212672db4e3b5d86 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Jun 2020 15:52:47 +0900 Subject: [PATCH 1909/2376] Update inspectcode / CodeFileSanity versions used in CI --- build/InspectCode.cake | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build/InspectCode.cake b/build/InspectCode.cake index 2e7a1d1b28..c8f4f37c94 100644 --- a/build/InspectCode.cake +++ b/build/InspectCode.cake @@ -1,5 +1,5 @@ -#addin "nuget:?package=CodeFileSanity&version=0.0.33" -#addin "nuget:?package=JetBrains.ReSharper.CommandLineTools&version=2019.3.2" +#addin "nuget:?package=CodeFileSanity&version=0.0.36" +#addin "nuget:?package=JetBrains.ReSharper.CommandLineTools&version=2020.1.3" #tool "nuget:?package=NVika.MSBuild&version=1.0.1" var nVikaToolPath = GetFiles("./tools/NVika.MSBuild.*/tools/NVika.exe").First(); From 3c07defa1a48a4a778d7e21b9fe9e8fc9bd1abe5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 3 Jun 2020 15:57:01 +0900 Subject: [PATCH 1910/2376] Push to main multiplayer screen instead --- osu.Game/Screens/Multi/Match/MatchSubScreen.cs | 8 ++++---- osu.Game/Screens/Multi/Multiplayer.cs | 14 -------------- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs index 3609be2dfe..b0717d3d28 100644 --- a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs +++ b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs @@ -20,6 +20,7 @@ using osu.Game.Screens.Multi.Components; using osu.Game.Screens.Multi.Match.Components; using osu.Game.Screens.Multi.Play; using osu.Game.Screens.Multi.Ranking; +using osu.Game.Screens.Play; using osu.Game.Screens.Select; using Footer = osu.Game.Screens.Multi.Match.Components.Footer; @@ -260,10 +261,10 @@ namespace osu.Game.Screens.Multi.Match { default: case GameTypeTimeshift _: - multiplayer?.Start(() => new TimeshiftPlayer(SelectedItem.Value) + multiplayer?.Push(new PlayerLoader(() => new TimeshiftPlayer(SelectedItem.Value) { Exited = () => leaderboardChatDisplay.RefreshScores() - }); + })); break; } } @@ -271,8 +272,7 @@ namespace osu.Game.Screens.Multi.Match private void showBeatmapResults() { Debug.Assert(roomId.Value != null); - - this.Push(new TimeshiftResultsScreen(null, roomId.Value.Value, SelectedItem.Value, false)); + multiplayer?.Push(new TimeshiftResultsScreen(null, roomId.Value.Value, SelectedItem.Value, false)); } } } diff --git a/osu.Game/Screens/Multi/Multiplayer.cs b/osu.Game/Screens/Multi/Multiplayer.cs index 863a28609b..e724152e08 100644 --- a/osu.Game/Screens/Multi/Multiplayer.cs +++ b/osu.Game/Screens/Multi/Multiplayer.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -24,7 +23,6 @@ using osu.Game.Screens.Multi.Lounge; using osu.Game.Screens.Multi.Lounge.Components; using osu.Game.Screens.Multi.Match; using osu.Game.Screens.Multi.Match.Components; -using osu.Game.Screens.Play; using osuTK; namespace osu.Game.Screens.Multi @@ -197,18 +195,6 @@ namespace osu.Game.Screens.Multi Logger.Log($"Polling adjusted (listing: {roomManager.TimeBetweenListingPolls}, selection: {roomManager.TimeBetweenSelectionPolls})"); } - /// - /// Push a to the main screen stack to begin gameplay. - /// Generally called from a via DI resolution. - /// - public void Start(Func player) - { - if (!this.IsCurrentScreen()) - return; - - this.Push(new PlayerLoader(player)); - } - public void APIStateChanged(IAPIProvider api, APIState state) { if (state != APIState.Online) From f3b514964894fd38c69cff189d59f71b04b2fe82 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 3 Jun 2020 16:48:44 +0900 Subject: [PATCH 1911/2376] Move some suggestions to warnings, resolve issues --- osu.Desktop/Updater/SquirrelUpdateManager.cs | 2 +- osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs | 3 +-- .../Drawables/Connections/FollowPointConnection.cs | 3 +-- osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs | 4 ++-- osu.Game.Tests/Scores/IO/ImportScoreTest.cs | 7 ++----- osu.Game.Tournament.Tests/LadderTestScene.cs | 3 +-- osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs | 3 +-- osu.Game.Tournament/TournamentGameBase.cs | 7 ++----- osu.Game/Beatmaps/BeatmapManager.cs | 3 +-- osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs | 3 +-- osu.Game/Online/Chat/ChannelManager.cs | 9 +++------ osu.Game/Online/Chat/StandAloneChatDisplay.cs | 3 +-- osu.Game/OsuGameBase.cs | 9 ++++----- osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs | 3 +-- osu.Game/Rulesets/Objects/HitObject.cs | 3 +-- osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 3 +-- osu.Game/Scoring/ScoreInfo.cs | 8 ++------ osu.Game/Screens/Edit/Timing/Section.cs | 2 +- .../Screens/Multi/Lounge/Components/FilterControl.cs | 3 +-- osu.Game/Screens/Select/BeatmapCarousel.cs | 5 +---- osu.Game/Users/Drawables/DrawableAvatar.cs | 2 +- osu.sln.DotSettings | 4 ++++ 22 files changed, 34 insertions(+), 58 deletions(-) diff --git a/osu.Desktop/Updater/SquirrelUpdateManager.cs b/osu.Desktop/Updater/SquirrelUpdateManager.cs index ade8460dd7..dd50b05c75 100644 --- a/osu.Desktop/Updater/SquirrelUpdateManager.cs +++ b/osu.Desktop/Updater/SquirrelUpdateManager.cs @@ -48,7 +48,7 @@ namespace osu.Desktop.Updater try { - if (updateManager == null) updateManager = await UpdateManager.GitHubUpdateManager(@"https://github.com/ppy/osu", @"osulazer", null, null, true); + updateManager ??= await UpdateManager.GitHubUpdateManager(@"https://github.com/ppy/osu", @"osulazer", null, null, true); var info = await updateManager.CheckForUpdate(!useDeltaPatching); if (info.ReleasesToApply.Count == 0) diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs index 5cd2f1f581..918ed77683 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs @@ -35,8 +35,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills { var catchCurrent = (CatchDifficultyHitObject)current; - if (lastPlayerPosition == null) - lastPlayerPosition = catchCurrent.LastNormalizedPosition; + lastPlayerPosition ??= catchCurrent.LastNormalizedPosition; float playerPosition = Math.Clamp( lastPlayerPosition.Value, diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs index 8a0ef22c4a..2c41e6b0e9 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs @@ -135,8 +135,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections fp.Alpha = 0; fp.Scale = new Vector2(1.5f * osuEnd.Scale); - if (firstTransformStartTime == null) - firstTransformStartTime = fadeInTime; + firstTransformStartTime ??= fadeInTime; fp.AnimationStartTime = fadeInTime; diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index 5eb11a3264..195fec6278 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -175,7 +175,7 @@ namespace osu.Game.Tests.Beatmaps.IO var breakTemp = TestResources.GetTestBeatmapForImport(); MemoryStream brokenOsu = new MemoryStream(); - MemoryStream brokenOsz = new MemoryStream(File.ReadAllBytes(breakTemp)); + MemoryStream brokenOsz = new MemoryStream(await File.ReadAllBytesAsync(breakTemp)); File.Delete(breakTemp); @@ -522,7 +522,7 @@ namespace osu.Game.Tests.Beatmaps.IO using (var resourceForkFile = File.CreateText(resourceForkFilePath)) { - resourceForkFile.WriteLine("adding content so that it's not empty"); + await resourceForkFile.WriteLineAsync("adding content so that it's not empty"); } try diff --git a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs index 90bf419644..57f0d7e957 100644 --- a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs +++ b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs @@ -183,11 +183,8 @@ namespace osu.Game.Tests.Scores.IO { var beatmapManager = osu.Dependencies.Get(); - if (score.Beatmap == null) - score.Beatmap = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First(); - - if (score.Ruleset == null) - score.Ruleset = new OsuRuleset().RulesetInfo; + score.Beatmap ??= beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First(); + score.Ruleset ??= new OsuRuleset().RulesetInfo; var scoreManager = osu.Dependencies.Get(); await scoreManager.Import(score, archive); diff --git a/osu.Game.Tournament.Tests/LadderTestScene.cs b/osu.Game.Tournament.Tests/LadderTestScene.cs index b962d035ab..2f4373679c 100644 --- a/osu.Game.Tournament.Tests/LadderTestScene.cs +++ b/osu.Game.Tournament.Tests/LadderTestScene.cs @@ -24,8 +24,7 @@ namespace osu.Game.Tournament.Tests [BackgroundDependencyLoader] private void load() { - if (Ladder.Ruleset.Value == null) - Ladder.Ruleset.Value = rulesetStore.AvailableRulesets.First(); + Ladder.Ruleset.Value ??= rulesetStore.AvailableRulesets.First(); Ruleset.BindTo(Ladder.Ruleset); } diff --git a/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs b/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs index 8be66ff98c..e10154b722 100644 --- a/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs +++ b/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs @@ -47,8 +47,7 @@ namespace osu.Game.Tournament.Screens.Drawings this.storage = storage; - if (TeamList == null) - TeamList = new StorageBackedTeamList(storage); + TeamList ??= new StorageBackedTeamList(storage); if (!TeamList.Teams.Any()) { diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 85db9e61fb..928c6deb3c 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -150,11 +150,8 @@ namespace osu.Game.Tournament ladder = JsonConvert.DeserializeObject(sr.ReadToEnd()); } - if (ladder == null) - ladder = new LadderInfo(); - - if (ladder.Ruleset.Value == null) - ladder.Ruleset.Value = RulesetStore.AvailableRulesets.First(); + ladder ??= new LadderInfo(); + ladder.Ruleset.Value ??= RulesetStore.AvailableRulesets.First(); Ruleset.BindTo(ladder.Ruleset); diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index f626b45e42..0785f9ef0e 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -239,8 +239,7 @@ namespace osu.Game.Beatmaps if (working == null) { - if (beatmapInfo.Metadata == null) - beatmapInfo.Metadata = beatmapInfo.BeatmapSet.Metadata; + beatmapInfo.Metadata ??= beatmapInfo.BeatmapSet.Metadata; workingCache.Add(working = new BeatmapManagerWorkingBeatmap(Files.Store, new LargeTextureStore(host?.CreateTextureLoaderStore(Files.Store)), beatmapInfo, audioManager)); diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 388abf4648..be5cd78dc8 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -425,8 +425,7 @@ namespace osu.Game.Beatmaps.Formats private void handleHitObject(string line) { // If the ruleset wasn't specified, assume the osu!standard ruleset. - if (parser == null) - parser = new Rulesets.Objects.Legacy.Osu.ConvertHitObjectParser(getOffsetTime(), FormatVersion); + parser ??= new Rulesets.Objects.Legacy.Osu.ConvertHitObjectParser(getOffsetTime(), FormatVersion); var obj = parser.Parse(line); if (obj != null) diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index 53872ddcba..90d4c2a03b 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -114,8 +114,7 @@ namespace osu.Game.Online.Chat /// An optional target channel. If null, will be used. public void PostMessage(string text, bool isAction = false, Channel target = null) { - if (target == null) - target = CurrentChannel.Value; + target ??= CurrentChannel.Value; if (target == null) return; @@ -198,8 +197,7 @@ namespace osu.Game.Online.Chat /// An optional target channel. If null, will be used. public void PostCommand(string text, Channel target = null) { - if (target == null) - target = CurrentChannel.Value; + target ??= CurrentChannel.Value; if (target == null) return; @@ -378,8 +376,7 @@ namespace osu.Game.Online.Chat } } - if (CurrentChannel.Value == null) - CurrentChannel.Value = channel; + CurrentChannel.Value ??= channel; return channel; } diff --git a/osu.Game/Online/Chat/StandAloneChatDisplay.cs b/osu.Game/Online/Chat/StandAloneChatDisplay.cs index 4fbeac1db9..f8810c778f 100644 --- a/osu.Game/Online/Chat/StandAloneChatDisplay.cs +++ b/osu.Game/Online/Chat/StandAloneChatDisplay.cs @@ -73,8 +73,7 @@ namespace osu.Game.Online.Chat [BackgroundDependencyLoader(true)] private void load(ChannelManager manager) { - if (ChannelManager == null) - ChannelManager = manager; + ChannelManager ??= manager; } protected virtual StandAloneDrawableChannel CreateDrawableChannel(Channel channel) => diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 5e44562144..3e7311092e 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -164,7 +164,7 @@ namespace osu.Game dependencies.Cache(SkinManager = new SkinManager(Storage, contextFactory, Host, Audio, new NamespacedResourceStore(Resources, "Skins/Legacy"))); dependencies.CacheAs(SkinManager); - if (API == null) API = new APIAccess(LocalConfig); + API ??= new APIAccess(LocalConfig); dependencies.CacheAs(API); @@ -311,11 +311,10 @@ namespace osu.Game { base.SetHost(host); - if (Storage == null) // may be non-null for certain tests - Storage = new OsuStorage(host); + // may be non-null for certain tests + Storage ??= new OsuStorage(host); - if (LocalConfig == null) - LocalConfig = new OsuConfigManager(Storage); + LocalConfig ??= new OsuConfigManager(Storage); } private readonly List fileImporters = new List(); diff --git a/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs b/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs index a72f182450..cb6abb7cc6 100644 --- a/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs +++ b/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs @@ -68,8 +68,7 @@ namespace osu.Game.Overlays.Chat.Tabs if (!Items.Contains(channel)) AddItem(channel); - if (Current.Value == null) - Current.Value = channel; + Current.Value ??= channel; } /// diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs index e2cc98813a..1d60b266e3 100644 --- a/osu.Game/Rulesets/Objects/HitObject.cs +++ b/osu.Game/Rulesets/Objects/HitObject.cs @@ -133,8 +133,7 @@ namespace osu.Game.Rulesets.Objects { Kiai = controlPointInfo.EffectPointAt(StartTime + control_point_leniency).KiaiMode; - if (HitWindows == null) - HitWindows = CreateHitWindows(); + HitWindows ??= CreateHitWindows(); HitWindows?.SetDifficulty(difficulty.OverallDifficulty); } diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index bc9401a095..d574991fa0 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -181,8 +181,7 @@ namespace osu.Game.Rulesets.UI private void setClock() { // in case a parent gameplay clock isn't available, just use the parent clock. - if (parentGameplayClock == null) - parentGameplayClock = Clock; + parentGameplayClock ??= Clock; Clock = GameplayClock; ProcessCustomClock = false; diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index a40f436a6e..7b37c267bc 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -115,9 +115,7 @@ namespace osu.Game.Scoring get => User?.Username; set { - if (User == null) - User = new User(); - + User ??= new User(); User.Username = value; } } @@ -129,9 +127,7 @@ namespace osu.Game.Scoring get => User?.Id ?? 1; set { - if (User == null) - User = new User(); - + User ??= new User(); User.Id = value ?? 1; } } diff --git a/osu.Game/Screens/Edit/Timing/Section.cs b/osu.Game/Screens/Edit/Timing/Section.cs index ccf1582486..603fb77f31 100644 --- a/osu.Game/Screens/Edit/Timing/Section.cs +++ b/osu.Game/Screens/Edit/Timing/Section.cs @@ -57,7 +57,7 @@ namespace osu.Game.Screens.Edit.Timing { checkbox = new OsuCheckbox { - LabelText = typeof(T).Name.Replace(typeof(ControlPoint).Name, string.Empty) + LabelText = typeof(T).Name.Replace(nameof(Beatmaps.ControlPoints.ControlPoint), string.Empty) } } }, diff --git a/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs b/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs index 300418441e..2742ef3404 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs @@ -34,8 +34,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components [BackgroundDependencyLoader] private void load() { - if (filter == null) - filter = new Bindable(); + filter ??= new Bindable(); } protected override void LoadComplete() diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 2d714d1794..e174c46610 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -607,10 +607,7 @@ namespace osu.Game.Screens.Select // todo: remove the need for this. foreach (var b in beatmapSet.Beatmaps) - { - if (b.Metadata == null) - b.Metadata = beatmapSet.Metadata; - } + b.Metadata ??= beatmapSet.Metadata; var set = new CarouselBeatmapSet(beatmapSet) { diff --git a/osu.Game/Users/Drawables/DrawableAvatar.cs b/osu.Game/Users/Drawables/DrawableAvatar.cs index 09750c5bfe..42d2dbb1c6 100644 --- a/osu.Game/Users/Drawables/DrawableAvatar.cs +++ b/osu.Game/Users/Drawables/DrawableAvatar.cs @@ -43,7 +43,7 @@ namespace osu.Game.Users.Drawables Texture texture = null; if (user != null && user.Id > 1) texture = textures.Get($@"https://a.ppy.sh/{user.Id}"); - if (texture == null) texture = textures.Get(@"Online/avatar-guest"); + texture ??= textures.Get(@"Online/avatar-guest"); ClickableArea clickableArea; Add(clickableArea = new ClickableArea diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index b9fc3de734..6e8ecb42d6 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -60,6 +60,7 @@ WARNING WARNING WARNING + WARNING WARNING WARNING WARNING @@ -105,6 +106,8 @@ HINT WARNING WARNING + WARNING + WARNING WARNING WARNING WARNING @@ -222,6 +225,7 @@ WARNING WARNING WARNING + WARNING True WARNING From 8aa8d2c88011adafa5b84ac0adaa7ff88eddda35 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 3 Jun 2020 16:59:37 +0900 Subject: [PATCH 1912/2376] Resolve NREs --- osu.Desktop/OsuGameDesktop.cs | 2 +- osu.Game.Tests/Chat/MessageFormatterTests.cs | 10 +++++----- .../Visual/UserInterface/TestSceneOsuIcon.cs | 2 +- .../Converters/TypedListConverter.cs | 18 +++++++++++++++--- osu.Game/Online/API/APIAccess.cs | 2 +- 5 files changed, 23 insertions(+), 11 deletions(-) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index 9351e17419..5f74883803 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -59,7 +59,7 @@ namespace osu.Desktop try { using (RegistryKey key = Registry.ClassesRoot.OpenSubKey("osu")) - stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty).ToString().Split('"')[1].Replace("osu!.exe", ""); + stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty).ToString()?.Split('"')[1].Replace("osu!.exe", ""); if (checkExists(stableInstallPath)) return stableInstallPath; diff --git a/osu.Game.Tests/Chat/MessageFormatterTests.cs b/osu.Game.Tests/Chat/MessageFormatterTests.cs index fbb0416c45..d1a859c84b 100644 --- a/osu.Game.Tests/Chat/MessageFormatterTests.cs +++ b/osu.Game.Tests/Chat/MessageFormatterTests.cs @@ -428,23 +428,23 @@ namespace osu.Game.Tests.Chat Assert.AreEqual(5, result.Links.Count); Link f = result.Links.Find(l => l.Url == "https://osu.ppy.sh/wiki/wiki links"); - Assert.AreEqual(44, f.Index); + Assert.AreEqual(44, f!.Index); Assert.AreEqual(10, f.Length); f = result.Links.Find(l => l.Url == "http://www.simple-test.com"); - Assert.AreEqual(10, f.Index); + Assert.AreEqual(10, f!.Index); Assert.AreEqual(11, f.Length); f = result.Links.Find(l => l.Url == "http://google.com"); - Assert.AreEqual(97, f.Index); + Assert.AreEqual(97, f!.Index); Assert.AreEqual(4, f.Length); f = result.Links.Find(l => l.Url == "https://osu.ppy.sh"); - Assert.AreEqual(78, f.Index); + Assert.AreEqual(78, f!.Index); Assert.AreEqual(18, f.Length); f = result.Links.Find(l => l.Url == "\uD83D\uDE12"); - Assert.AreEqual(101, f.Index); + Assert.AreEqual(101, f!.Index); Assert.AreEqual(3, f.Length); } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuIcon.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuIcon.cs index 061039b297..246eb119e8 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuIcon.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuIcon.cs @@ -45,7 +45,7 @@ namespace osu.Game.Tests.Visual.UserInterface }); foreach (var p in typeof(OsuIcon).GetProperties(BindingFlags.Public | BindingFlags.Static)) - flow.Add(new Icon($"{nameof(OsuIcon)}.{p.Name}", (IconUsage)p.GetValue(null))); + flow.Add(new Icon($"{nameof(OsuIcon)}.{p.Name}", (IconUsage)p.GetValue(null)!)); AddStep("toggle shadows", () => flow.Children.ForEach(i => i.SpriteIcon.Shadow = !i.SpriteIcon.Shadow)); AddStep("change icons", () => flow.Children.ForEach(i => i.SpriteIcon.Icon = new IconUsage((char)(i.SpriteIcon.Icon.Icon + 1)))); diff --git a/osu.Game/IO/Serialization/Converters/TypedListConverter.cs b/osu.Game/IO/Serialization/Converters/TypedListConverter.cs index f98fa05821..ddfdf08f52 100644 --- a/osu.Game/IO/Serialization/Converters/TypedListConverter.cs +++ b/osu.Game/IO/Serialization/Converters/TypedListConverter.cs @@ -41,13 +41,25 @@ namespace osu.Game.IO.Serialization.Converters var list = new List(); var obj = JObject.Load(reader); - var lookupTable = serializer.Deserialize>(obj["$lookup_table"].CreateReader()); - foreach (var tok in obj["$items"]) + if (!obj.TryGetValue("$lookup_table", out var lookupTableToken) || lookupTableToken == null) + return list; + + var lookupTable = serializer.Deserialize>(lookupTableToken.CreateReader()); + if (lookupTable == null) + return list; + + if (!obj.TryGetValue("$items", out var itemsToken) || itemsToken == null) + return list; + + foreach (var tok in itemsToken) { var itemReader = tok.CreateReader(); - var typeName = lookupTable[(int)tok["$type"]]; + if (!obj.TryGetValue("$type", out var typeToken) || typeToken == null) + throw new JsonException("Expected $type token."); + + var typeName = lookupTable[(int)typeToken]; var instance = (T)Activator.CreateInstance(Type.GetType(typeName)); serializer.Populate(itemReader, instance); diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 4945f7f185..f9e2da9af8 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -250,7 +250,7 @@ namespace osu.Game.Online.API { try { - return JObject.Parse(req.GetResponseString()).SelectToken("form_error", true).ToObject(); + return JObject.Parse(req.GetResponseString()).SelectToken("form_error", true)!.ToObject(); } catch { From 86a4664d9ba73522ae51320686f0f60082521a2e Mon Sep 17 00:00:00 2001 From: Power Maker Date: Wed, 3 Jun 2020 10:03:39 +0200 Subject: [PATCH 1913/2376] Add method for checking if cursor should rotate --- osu.Game/Graphics/Cursor/MenuCursor.cs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/osu.Game/Graphics/Cursor/MenuCursor.cs b/osu.Game/Graphics/Cursor/MenuCursor.cs index 1aa7b68d1a..e0b39ac311 100644 --- a/osu.Game/Graphics/Cursor/MenuCursor.cs +++ b/osu.Game/Graphics/Cursor/MenuCursor.cs @@ -83,8 +83,9 @@ namespace osu.Game.Graphics.Cursor activeCursor.AdditiveLayer.FadeInFromZero(800, Easing.OutQuint); } - if ((e.Button == MouseButton.Left || e.Button == MouseButton.Right) && cursorRotate.Value) + if (shouldRotate(e) && cursorRotate.Value) { + // if cursor is already rotating don't reset its rotate origin if (!(dragRotationState == DragRotationState.Rotating)) { dragRotationState = DragRotationState.DragStarted; @@ -97,13 +98,14 @@ namespace osu.Game.Graphics.Cursor protected override void OnMouseUp(MouseUpEvent e) { - if (!e.IsPressed(MouseButton.Left) && !e.IsPressed(MouseButton.Middle) && !e.IsPressed(MouseButton.Right)) + // cursor should go back to original size when none of main buttons are pressed + if (!(e.IsPressed(MouseButton.Left) || e.IsPressed(MouseButton.Middle) || e.IsPressed(MouseButton.Right))) { activeCursor.AdditiveLayer.FadeOutFromOne(500, Easing.OutQuint); activeCursor.ScaleTo(1, 500, Easing.OutElastic); } - if (!e.IsPressed(MouseButton.Left) && !e.IsPressed(MouseButton.Right)) + if (!shouldRotate(e)) { if (dragRotationState == DragRotationState.Rotating) activeCursor.RotateTo(0, 600 * (1 + Math.Abs(activeCursor.Rotation / 720)), Easing.OutElasticHalf); @@ -125,6 +127,14 @@ namespace osu.Game.Graphics.Cursor activeCursor.ScaleTo(0.6f, 250, Easing.In); } + private static bool shouldRotate(MouseEvent e) + { + if (e.IsPressed(MouseButton.Left) || e.IsPressed(MouseButton.Right)) + return true; + else + return false; + } + public class Cursor : Container { private Container cursorContainer; From 1ba3f0ac14dd2d293bf453972f26ebc3db98c544 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Jun 2020 17:31:55 +0900 Subject: [PATCH 1914/2376] Fix chat history not being loaded for multiplayer matches --- osu.Game/Online/Chat/ChannelManager.cs | 51 ++++++++++++++------------ 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index 53872ddcba..6812052eeb 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -93,12 +93,6 @@ namespace osu.Game.Online.Chat { if (!(e.NewValue is ChannelSelectorTabItem.ChannelSelectorTabChannel)) JoinChannel(e.NewValue); - - if (e.NewValue?.MessagesLoaded == false) - { - // let's fetch a small number of messages to bring us up-to-date with the backlog. - fetchInitalMessages(e.NewValue); - } } /// @@ -240,7 +234,6 @@ namespace osu.Game.Online.Chat } JoinChannel(channel); - CurrentChannel.Value = channel; break; case "help": @@ -275,7 +268,7 @@ namespace osu.Game.Online.Chat // join any channels classified as "defaults" if (joinDefaults && defaultChannels.Any(c => c.Equals(channel.Name, StringComparison.OrdinalIgnoreCase))) - JoinChannel(ch); + joinChannel(ch); } }; req.Failure += error => @@ -296,7 +289,7 @@ namespace osu.Game.Online.Chat /// The channel private void fetchInitalMessages(Channel channel) { - if (channel.Id <= 0) return; + if (channel.Id <= 0 || channel.MessagesLoaded) return; var fetchInitialMsgReq = new GetMessagesRequest(channel); fetchInitialMsgReq.Success += messages => @@ -351,9 +344,10 @@ namespace osu.Game.Online.Chat /// Joins a channel if it has not already been joined. /// /// The channel to join. - /// Whether the channel has already been joined server-side. Will skip a join request. /// The joined channel. Note that this may not match the parameter channel as it is a backed object. - public Channel JoinChannel(Channel channel, bool alreadyJoined = false) + public Channel JoinChannel(Channel channel) => joinChannel(channel, true); + + private Channel joinChannel(Channel channel, bool fetchInitialMessages = false) { if (channel == null) return null; @@ -362,21 +356,29 @@ namespace osu.Game.Online.Chat // ensure we are joined to the channel if (!channel.Joined.Value) { - if (alreadyJoined) - channel.Joined.Value = true; - else + switch (channel.Type) { - switch (channel.Type) - { - case ChannelType.Public: - var req = new JoinChannelRequest(channel, api.LocalUser.Value); - req.Success += () => JoinChannel(channel, true); - req.Failure += ex => LeaveChannel(channel); - api.Queue(req); - return channel; - } + case ChannelType.Private: + // can't do this yet. + break; + + default: + var req = new JoinChannelRequest(channel, api.LocalUser.Value); + req.Success += () => + { + channel.Joined.Value = true; + joinChannel(channel, fetchInitialMessages); + }; + req.Failure += ex => LeaveChannel(channel); + api.Queue(req); + return channel; } } + else + { + if (fetchInitialMessages) + fetchInitalMessages(channel); + } if (CurrentChannel.Value == null) CurrentChannel.Value = channel; @@ -420,7 +422,8 @@ namespace osu.Game.Online.Chat foreach (var channel in updates.Presence) { // we received this from the server so should mark the channel already joined. - JoinChannel(channel, true); + channel.Joined.Value = true; + joinChannel(channel); } //todo: handle left channels From 092f5b6521c1617d363d9c953a01438e4b38607e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 3 Jun 2020 17:41:05 +0900 Subject: [PATCH 1915/2376] Fix incorrect reference + simplify --- .../Serialization/Converters/TypedListConverter.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/IO/Serialization/Converters/TypedListConverter.cs b/osu.Game/IO/Serialization/Converters/TypedListConverter.cs index ddfdf08f52..50b28ea74b 100644 --- a/osu.Game/IO/Serialization/Converters/TypedListConverter.cs +++ b/osu.Game/IO/Serialization/Converters/TypedListConverter.cs @@ -42,24 +42,24 @@ namespace osu.Game.IO.Serialization.Converters var obj = JObject.Load(reader); - if (!obj.TryGetValue("$lookup_table", out var lookupTableToken) || lookupTableToken == null) + if (obj["$lookup_table"] == null) return list; - var lookupTable = serializer.Deserialize>(lookupTableToken.CreateReader()); + var lookupTable = serializer.Deserialize>(obj["$lookup_table"].CreateReader()); if (lookupTable == null) return list; - if (!obj.TryGetValue("$items", out var itemsToken) || itemsToken == null) + if (obj["$items"] == null) return list; - foreach (var tok in itemsToken) + foreach (var tok in obj["$items"]) { var itemReader = tok.CreateReader(); - if (!obj.TryGetValue("$type", out var typeToken) || typeToken == null) + if (tok["$type"] == null) throw new JsonException("Expected $type token."); - var typeName = lookupTable[(int)typeToken]; + var typeName = lookupTable[(int)tok["$type"]]; var instance = (T)Activator.CreateInstance(Type.GetType(typeName)); serializer.Populate(itemReader, instance); From c0881e14ab176ac500e9308607b37f1368ac8d78 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 3 Jun 2020 17:44:14 +0900 Subject: [PATCH 1916/2376] Add "struct can be made readonly" inspection --- osu.sln.DotSettings | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index 6e8ecb42d6..85be2077be 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -199,6 +199,7 @@ WARNING WARNING HINT + WARNING DO_NOT_SHOW DO_NOT_SHOW DO_NOT_SHOW From 3c7e5a5b42832d2d3b801b6baf09481b47a583e6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Jun 2020 18:00:31 +0900 Subject: [PATCH 1917/2376] Fix ChannelManager not being loaded in tests --- osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index 05b33e4386..0025a26baf 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -246,7 +246,12 @@ namespace osu.Game.Tests.Visual.Online { ((BindableList)ChannelManager.AvailableChannels).AddRange(channels); - Child = ChatOverlay = new TestChatOverlay { RelativeSizeAxes = Axes.Both, }; + InternalChildren = new Drawable[] + { + ChannelManager, + ChatOverlay = new TestChatOverlay { RelativeSizeAxes = Axes.Both, }, + }; + ChatOverlay.Show(); } } From c155ab83399a51bdab44a48d5805463c8520318a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Jun 2020 18:03:10 +0900 Subject: [PATCH 1918/2376] Check filenames and timestamps before reusing an already imported model --- osu.Game/Beatmaps/BeatmapManager.cs | 4 ++-- osu.Game/Database/ArchiveModelManager.cs | 22 +++++++++++++++++++--- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index f626b45e42..e7cef13c68 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -258,9 +258,9 @@ namespace osu.Game.Beatmaps /// 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 CanUndelete(BeatmapSetInfo existing, BeatmapSetInfo import) + protected override bool CanReuseExisting(BeatmapSetInfo existing, BeatmapSetInfo import) { - if (!base.CanUndelete(existing, import)) + if (!base.CanReuseExisting(existing, import)) return false; var existingIds = existing.Beatmaps.Select(b => b.OnlineBeatmapID).OrderBy(i => i); diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index ae55a7b14a..4d7d3e96e6 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -332,7 +332,7 @@ namespace osu.Game.Database if (existing != null) { - if (CanUndelete(existing, item)) + if (CanReuseExisting(existing, item)) { Undelete(existing); LogForModel(item, $"Found existing {HumanisedModelName} for {item} (ID {existing.ID}) – skipping import."); @@ -660,13 +660,29 @@ namespace osu.Game.Database protected TModel CheckForExisting(TModel model) => model.Hash == null ? null : ModelStore.ConsumableItems.FirstOrDefault(b => b.Hash == model.Hash); /// - /// After an existing is found during an import process, the default behaviour is to restore the existing + /// After an existing is found during an import process, the default behaviour is to use/restore the existing /// item and skip the import. This method allows changing that behaviour. /// /// The existing model. /// The newly imported model. /// Whether the existing model should be restored and used. Returning false will delete the existing and force a re-import. - protected virtual bool CanUndelete(TModel existing, TModel import) => true; + protected virtual bool CanReuseExisting(TModel existing, TModel import) => + getFilenames(existing.Files).SequenceEqual(getFilenames(import.Files)) && + // poor-man's (cheap) equality comparison, avoiding hashing unnecessarily. + // can switch to full hash checks on a per-case basis (or for all) if we decide this is not a performance issue. + getTimestamps(existing.Files).SequenceEqual(getTimestamps(import.Files)); + + private IEnumerable getFilenames(List files) + { + foreach (var f in files.OrderBy(f => f.Filename)) + yield return f.Filename; + } + + private IEnumerable getTimestamps(List files) + { + foreach (var f in files.OrderBy(f => f.Filename)) + yield return File.GetLastWriteTimeUtc(Files.Storage.GetFullPath(f.FileInfo.StoragePath)).ToFileTime(); + } private DbSet queryModel() => ContextFactory.Get().Set(); From 012933545eccd4510dc3c0a70b040610dad0bf8d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Jun 2020 18:30:27 +0900 Subject: [PATCH 1919/2376] Add test coverage --- .../Beatmaps/IO/ImportBeatmapTest.cs | 161 ++++++++++++++++++ osu.Game/Database/ArchiveModelManager.cs | 2 +- 2 files changed, 162 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index 5eb11a3264..12c9c92e90 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -12,6 +12,7 @@ using NUnit.Framework; using osu.Framework.Platform; using osu.Game.IPC; using osu.Framework.Allocation; +using osu.Framework.Extensions; using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; @@ -22,6 +23,7 @@ using SharpCompress.Archives; using SharpCompress.Archives.Zip; using SharpCompress.Common; using SharpCompress.Writers.Zip; +using FileInfo = System.IO.FileInfo; namespace osu.Game.Tests.Beatmaps.IO { @@ -93,6 +95,165 @@ namespace osu.Game.Tests.Beatmaps.IO } } + [Test] + public async Task TestImportThenImportWithReZip() + { + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportThenImportWithNewerTimestamp))) + { + try + { + var osu = loadOsu(host); + + var temp = TestResources.GetTestBeatmapForImport(); + + string extractedFolder = $"{temp}_extracted"; + Directory.CreateDirectory(extractedFolder); + + try + { + var imported = await LoadOszIntoOsu(osu); + + string hashBefore = hashFile(temp); + + using (var zip = ZipArchive.Open(temp)) + zip.WriteToDirectory(extractedFolder); + + using (var zip = ZipArchive.Create()) + { + zip.AddAllFromDirectory(extractedFolder); + zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate)); + } + + // zip files differ because different compression or encoder. + Assert.AreNotEqual(hashBefore, hashFile(temp)); + + var importedSecondTime = await osu.Dependencies.Get().Import(temp); + + 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); + } + finally + { + Directory.Delete(extractedFolder, true); + } + } + finally + { + host.Exit(); + } + } + } + + private string hashFile(string filename) + { + using (var s = File.OpenRead(filename)) + return s.ComputeMD5Hash(); + } + + [Test] + public async Task TestImportThenImportWithNewerTimestamp() + { + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportThenImportWithNewerTimestamp))) + { + try + { + var osu = loadOsu(host); + + var temp = TestResources.GetTestBeatmapForImport(); + + string extractedFolder = $"{temp}_extracted"; + Directory.CreateDirectory(extractedFolder); + + try + { + var imported = await LoadOszIntoOsu(osu); + + using (var zip = ZipArchive.Open(temp)) + zip.WriteToDirectory(extractedFolder); + + // change timestamp + new FileInfo(Directory.GetFiles(extractedFolder).First()).LastWriteTime = DateTime.Now; + + using (var zip = ZipArchive.Create()) + { + zip.AddAllFromDirectory(extractedFolder); + zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate)); + } + + var importedSecondTime = await osu.Dependencies.Get().Import(temp); + + 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); + } + finally + { + Directory.Delete(extractedFolder, true); + } + } + finally + { + host.Exit(); + } + } + } + + [Test] + public async Task TestImportThenImportWithDifferentFilename() + { + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportThenImportWithDifferentFilename))) + { + try + { + var osu = loadOsu(host); + + var temp = TestResources.GetTestBeatmapForImport(); + + string extractedFolder = $"{temp}_extracted"; + Directory.CreateDirectory(extractedFolder); + + try + { + var imported = await LoadOszIntoOsu(osu); + + using (var zip = ZipArchive.Open(temp)) + zip.WriteToDirectory(extractedFolder); + + // change filename + var firstFile = new FileInfo(Directory.GetFiles(extractedFolder).First()); + firstFile.MoveTo(Path.Combine(firstFile.DirectoryName, $"{firstFile.Name}-changed{firstFile.Extension}")); + + using (var zip = ZipArchive.Create()) + { + zip.AddAllFromDirectory(extractedFolder); + zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate)); + } + + var importedSecondTime = await osu.Dependencies.Get().Import(temp); + + 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); + } + finally + { + Directory.Delete(extractedFolder, true); + } + } + finally + { + host.Exit(); + } + } + } + [Test] public async Task TestImportCorruptThenImport() { diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 4d7d3e96e6..5ca9423de2 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -276,7 +276,7 @@ namespace osu.Game.Database // for now, concatenate all .osu files in the set to create a unique hash. MemoryStream hashable = new MemoryStream(); - foreach (TFileModel file in item.Files.Where(f => HashableFileTypes.Any(f.Filename.EndsWith))) + foreach (TFileModel file in item.Files.Where(f => HashableFileTypes.Any(f.Filename.EndsWith)).OrderBy(f => f.Filename)) { using (Stream s = Files.Store.GetStream(file.FileInfo.StoragePath)) s.CopyTo(hashable); From d002c0c03fbbbc2463edb9f9e1a8ee9b031a3ca0 Mon Sep 17 00:00:00 2001 From: Shivam Date: Wed, 3 Jun 2020 11:39:08 +0200 Subject: [PATCH 1920/2376] Revert piano reverb to a separate sample --- osu.Game/Screens/Menu/IntroWelcome.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/IntroWelcome.cs b/osu.Game/Screens/Menu/IntroWelcome.cs index 9f9012cb2b..7019e1f1a6 100644 --- a/osu.Game/Screens/Menu/IntroWelcome.cs +++ b/osu.Game/Screens/Menu/IntroWelcome.cs @@ -21,6 +21,7 @@ namespace osu.Game.Screens.Menu protected override string BeatmapFile => "welcome.osz"; private const double delay_step_two = 2142; private SampleChannel welcome; + private SampleChannel pianoReverb; [BackgroundDependencyLoader] private void load(AudioManager audio) @@ -28,7 +29,10 @@ namespace osu.Game.Screens.Menu Seeya = audio.Samples.Get(@"Intro/welcome/seeya"); if (MenuVoice.Value) + { welcome = audio.Samples.Get(@"Intro/welcome/welcome"); + pianoReverb = audio.Samples.Get(@"Intro/welcome/welcome_piano"); + } } protected override void LogoArriving(OsuLogo logo, bool resuming) @@ -38,9 +42,11 @@ namespace osu.Game.Screens.Menu if (!resuming) { welcome?.Play(); - StartTrack(); + pianoReverb?.Play(); + Scheduler.AddDelayed(delegate { + StartTrack(); PrepareMenuLoad(); logo.ScaleTo(1); From 6133c7d74784d0f848add452953ebf56aa39c0a2 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 3 Jun 2020 18:51:02 +0900 Subject: [PATCH 1921/2376] Change suggestion to warning --- osu.sln.DotSettings | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index 85be2077be..85d5fce29a 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -141,6 +141,7 @@ WARNING WARNING WARNING + WARNING WARNING WARNING WARNING From 25160dc220d9f2f0bde4125f0bafd7446e3ad354 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Jun 2020 19:15:52 +0900 Subject: [PATCH 1922/2376] Fix test name --- osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index 12c9c92e90..12f06059f7 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -98,7 +98,7 @@ namespace osu.Game.Tests.Beatmaps.IO [Test] public async Task TestImportThenImportWithReZip() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportThenImportWithNewerTimestamp))) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportThenImportWithReZip))) { try { From f6d9f0597b970c9411c623392ffea35a8bcc0fe4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Jun 2020 21:28:29 +0900 Subject: [PATCH 1923/2376] Add implicit join logic for multiplayer rooms --- osu.Game/Online/Chat/ChannelManager.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index 6812052eeb..b17e0812da 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -358,6 +358,13 @@ namespace osu.Game.Online.Chat { switch (channel.Type) { + case ChannelType.Multiplayer: + // join is implicit. happens when you join a multiplayer game. + // this will probably change in the future. + channel.Joined.Value = true; + joinChannel(channel, fetchInitialMessages); + return channel; + case ChannelType.Private: // can't do this yet. break; From 5ed3cd205f068d572caddc0ae01aa7ab8a580a6a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Jun 2020 22:35:01 +0900 Subject: [PATCH 1924/2376] Simplify reuse check using FileInfo IDs --- .../Beatmaps/IO/ImportBeatmapTest.cs | 9 +++++---- osu.Game/Database/ArchiveModelManager.cs | 20 +++++++++---------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index 12f06059f7..9b34eece5f 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -154,9 +154,9 @@ namespace osu.Game.Tests.Beatmaps.IO } [Test] - public async Task TestImportThenImportWithNewerTimestamp() + public async Task TestImportThenImportWithChangedFile() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportThenImportWithNewerTimestamp))) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportThenImportWithChangedFile))) { try { @@ -174,8 +174,9 @@ namespace osu.Game.Tests.Beatmaps.IO using (var zip = ZipArchive.Open(temp)) zip.WriteToDirectory(extractedFolder); - // change timestamp - new FileInfo(Directory.GetFiles(extractedFolder).First()).LastWriteTime = DateTime.Now; + // arbitrary write to non-hashed file + using (var sw = new FileInfo(Directory.GetFiles(extractedFolder, "*.mp3").First()).AppendText()) + sw.WriteLine("text"); using (var zip = ZipArchive.Create()) { diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 5ca9423de2..0fe8dd1268 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -667,10 +667,16 @@ namespace osu.Game.Database /// The newly imported model. /// Whether the existing model should be restored and used. Returning false will delete the existing and force a re-import. protected virtual bool CanReuseExisting(TModel existing, TModel import) => - getFilenames(existing.Files).SequenceEqual(getFilenames(import.Files)) && - // poor-man's (cheap) equality comparison, avoiding hashing unnecessarily. - // can switch to full hash checks on a per-case basis (or for all) if we decide this is not a performance issue. - getTimestamps(existing.Files).SequenceEqual(getTimestamps(import.Files)); + // for the best or worst, we copy and import files of a new import before checking whether + // it is a duplicate. so to check if anything has changed, we can just compare all FileInfo IDs. + getIDs(existing.Files).SequenceEqual(getIDs(import.Files)) && + getFilenames(existing.Files).SequenceEqual(getFilenames(import.Files)); + + private IEnumerable getIDs(List files) + { + foreach (var f in files.OrderBy(f => f.Filename)) + yield return f.FileInfo.ID; + } private IEnumerable getFilenames(List files) { @@ -678,12 +684,6 @@ namespace osu.Game.Database yield return f.Filename; } - private IEnumerable getTimestamps(List files) - { - foreach (var f in files.OrderBy(f => f.Filename)) - yield return File.GetLastWriteTimeUtc(Files.Storage.GetFullPath(f.FileInfo.StoragePath)).ToFileTime(); - } - private DbSet queryModel() => ContextFactory.Get().Set(); protected virtual string HumanisedModelName => $"{typeof(TModel).Name.Replace("Info", "").ToLower()}"; From 66ec2afe5cdcd9eb77adf5015966e5dcb652c1d1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Jun 2020 23:38:40 +0900 Subject: [PATCH 1925/2376] Remove broken import test --- .../Beatmaps/IO/ImportBeatmapTest.cs | 33 +------------------ 1 file changed, 1 insertion(+), 32 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index 9b34eece5f..546bf758c1 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; @@ -374,37 +374,6 @@ namespace osu.Game.Tests.Beatmaps.IO } } - [Test] - public async Task TestImportThenImportDifferentHash() - { - // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportThenImportDifferentHash))) - { - try - { - var osu = loadOsu(host); - var manager = osu.Dependencies.Get(); - - var imported = await LoadOszIntoOsu(osu); - - imported.Hash += "-changed"; - manager.Update(imported); - - var importedSecondTime = await LoadOszIntoOsu(osu); - - Assert.IsTrue(imported.ID != importedSecondTime.ID); - Assert.IsTrue(imported.Beatmaps.First().ID < importedSecondTime.Beatmaps.First().ID); - - // only one beatmap will exist as the online set ID matched, causing purging of the first import. - checkBeatmapSetCount(osu, 1); - } - finally - { - host.Exit(); - } - } - } - [Test] public async Task TestImportThenDeleteThenImport() { From 89d973416a1f9807b0d44bdb519c1c846ae5816d Mon Sep 17 00:00:00 2001 From: Power Maker <42269909+power9maker@users.noreply.github.com> Date: Wed, 3 Jun 2020 20:35:44 +0200 Subject: [PATCH 1926/2376] Simplify shouldRotate method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Graphics/Cursor/MenuCursor.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/osu.Game/Graphics/Cursor/MenuCursor.cs b/osu.Game/Graphics/Cursor/MenuCursor.cs index e0b39ac311..40735d6de0 100644 --- a/osu.Game/Graphics/Cursor/MenuCursor.cs +++ b/osu.Game/Graphics/Cursor/MenuCursor.cs @@ -127,13 +127,7 @@ namespace osu.Game.Graphics.Cursor activeCursor.ScaleTo(0.6f, 250, Easing.In); } - private static bool shouldRotate(MouseEvent e) - { - if (e.IsPressed(MouseButton.Left) || e.IsPressed(MouseButton.Right)) - return true; - else - return false; - } + private static bool shouldRotate(MouseEvent e) => e.IsPressed(MouseButton.Left) || e.IsPressed(MouseButton.Right); public class Cursor : Container { From 3fa02a5782e95b0b92362ac83ad15ae6e1ae5caa Mon Sep 17 00:00:00 2001 From: Power Maker Date: Wed, 3 Jun 2020 20:43:47 +0200 Subject: [PATCH 1927/2376] Add method for any mouse button pressed. --- osu.Game/Graphics/Cursor/MenuCursor.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game/Graphics/Cursor/MenuCursor.cs b/osu.Game/Graphics/Cursor/MenuCursor.cs index 40735d6de0..ad413f187a 100644 --- a/osu.Game/Graphics/Cursor/MenuCursor.cs +++ b/osu.Game/Graphics/Cursor/MenuCursor.cs @@ -15,6 +15,7 @@ using osu.Framework.Graphics.Textures; using osu.Framework.Input.Events; using osuTK.Input; using osu.Framework.Utils; +using osu.Game.Screens.Multi.Components; namespace osu.Game.Graphics.Cursor { @@ -83,7 +84,7 @@ namespace osu.Game.Graphics.Cursor activeCursor.AdditiveLayer.FadeInFromZero(800, Easing.OutQuint); } - if (shouldRotate(e) && cursorRotate.Value) + if (shouldRotateCursor(e) && cursorRotate.Value) { // if cursor is already rotating don't reset its rotate origin if (!(dragRotationState == DragRotationState.Rotating)) @@ -99,13 +100,13 @@ namespace osu.Game.Graphics.Cursor protected override void OnMouseUp(MouseUpEvent e) { // cursor should go back to original size when none of main buttons are pressed - if (!(e.IsPressed(MouseButton.Left) || e.IsPressed(MouseButton.Middle) || e.IsPressed(MouseButton.Right))) + if (!anyMouseButtonPressed(e)) { activeCursor.AdditiveLayer.FadeOutFromOne(500, Easing.OutQuint); activeCursor.ScaleTo(1, 500, Easing.OutElastic); } - if (!shouldRotate(e)) + if (!shouldRotateCursor(e)) { if (dragRotationState == DragRotationState.Rotating) activeCursor.RotateTo(0, 600 * (1 + Math.Abs(activeCursor.Rotation / 720)), Easing.OutElasticHalf); @@ -127,7 +128,9 @@ namespace osu.Game.Graphics.Cursor activeCursor.ScaleTo(0.6f, 250, Easing.In); } - private static bool shouldRotate(MouseEvent e) => e.IsPressed(MouseButton.Left) || e.IsPressed(MouseButton.Right); + private static bool shouldRotateCursor(MouseEvent e) => e.IsPressed(MouseButton.Left) || e.IsPressed(MouseButton.Right); + + private static bool anyMouseButtonPressed(MouseEvent e) => e.IsPressed(MouseButton.Left) || e.IsPressed(MouseButton.Middle) || e.IsPressed(MouseButton.Right); public class Cursor : Container { From eb15fc0bf9c4280de88ce215b98a4c8f4a36cdfa Mon Sep 17 00:00:00 2001 From: Power Maker Date: Wed, 3 Jun 2020 20:46:24 +0200 Subject: [PATCH 1928/2376] Remove unnecessary comment --- osu.Game/Graphics/Cursor/MenuCursor.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Graphics/Cursor/MenuCursor.cs b/osu.Game/Graphics/Cursor/MenuCursor.cs index ad413f187a..33715ad7f1 100644 --- a/osu.Game/Graphics/Cursor/MenuCursor.cs +++ b/osu.Game/Graphics/Cursor/MenuCursor.cs @@ -99,7 +99,6 @@ namespace osu.Game.Graphics.Cursor protected override void OnMouseUp(MouseUpEvent e) { - // cursor should go back to original size when none of main buttons are pressed if (!anyMouseButtonPressed(e)) { activeCursor.AdditiveLayer.FadeOutFromOne(500, Easing.OutQuint); From 747ecd5ab23aaf5d625e6daee39d7cda2c6d826b Mon Sep 17 00:00:00 2001 From: Power Maker Date: Wed, 3 Jun 2020 20:50:37 +0200 Subject: [PATCH 1929/2376] Rename method to avoid confusion --- osu.Game/Graphics/Cursor/MenuCursor.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/Cursor/MenuCursor.cs b/osu.Game/Graphics/Cursor/MenuCursor.cs index 33715ad7f1..df3eabe7c6 100644 --- a/osu.Game/Graphics/Cursor/MenuCursor.cs +++ b/osu.Game/Graphics/Cursor/MenuCursor.cs @@ -99,7 +99,7 @@ namespace osu.Game.Graphics.Cursor protected override void OnMouseUp(MouseUpEvent e) { - if (!anyMouseButtonPressed(e)) + if (!anyMainButtonPressed(e)) { activeCursor.AdditiveLayer.FadeOutFromOne(500, Easing.OutQuint); activeCursor.ScaleTo(1, 500, Easing.OutElastic); @@ -129,7 +129,7 @@ namespace osu.Game.Graphics.Cursor private static bool shouldRotateCursor(MouseEvent e) => e.IsPressed(MouseButton.Left) || e.IsPressed(MouseButton.Right); - private static bool anyMouseButtonPressed(MouseEvent e) => e.IsPressed(MouseButton.Left) || e.IsPressed(MouseButton.Middle) || e.IsPressed(MouseButton.Right); + private static bool anyMainButtonPressed(MouseEvent e) => e.IsPressed(MouseButton.Left) || e.IsPressed(MouseButton.Middle) || e.IsPressed(MouseButton.Right); public class Cursor : Container { From ff220b2ebeece677fe1836fd0124f9a9939407de Mon Sep 17 00:00:00 2001 From: Power Maker Date: Wed, 3 Jun 2020 21:13:11 +0200 Subject: [PATCH 1930/2376] Remove unnecessary using statement. --- osu.Game/Graphics/Cursor/MenuCursor.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Graphics/Cursor/MenuCursor.cs b/osu.Game/Graphics/Cursor/MenuCursor.cs index df3eabe7c6..f4a16c7727 100644 --- a/osu.Game/Graphics/Cursor/MenuCursor.cs +++ b/osu.Game/Graphics/Cursor/MenuCursor.cs @@ -15,7 +15,6 @@ using osu.Framework.Graphics.Textures; using osu.Framework.Input.Events; using osuTK.Input; using osu.Framework.Utils; -using osu.Game.Screens.Multi.Components; namespace osu.Game.Graphics.Cursor { From 939a76b08f32af92c6d425d7ff8003ad736d3126 Mon Sep 17 00:00:00 2001 From: Power Maker Date: Wed, 3 Jun 2020 21:42:23 +0200 Subject: [PATCH 1931/2376] Simplify negative equality expression --- osu.Game/Graphics/Cursor/MenuCursor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Cursor/MenuCursor.cs b/osu.Game/Graphics/Cursor/MenuCursor.cs index f4a16c7727..507d218fb8 100644 --- a/osu.Game/Graphics/Cursor/MenuCursor.cs +++ b/osu.Game/Graphics/Cursor/MenuCursor.cs @@ -86,7 +86,7 @@ namespace osu.Game.Graphics.Cursor if (shouldRotateCursor(e) && cursorRotate.Value) { // if cursor is already rotating don't reset its rotate origin - if (!(dragRotationState == DragRotationState.Rotating)) + if (dragRotationState != DragRotationState.Rotating) { dragRotationState = DragRotationState.DragStarted; positionMouseDown = e.MousePosition; From 611f64fd364525be3f98c553ce9501a4c3505291 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 3 Jun 2020 23:23:56 +0300 Subject: [PATCH 1932/2376] Add base ready-made abstract scene for osu! mod tests --- osu.Game.Rulesets.Osu.Tests/Mods/OsuModTestScene.cs | 12 ++++++++++++ .../Mods/TestSceneOsuModDifficultyAdjust.cs | 5 +---- .../Mods/TestSceneOsuModDoubleTime.cs | 5 +---- .../Mods/TestSceneOsuModHidden.cs | 5 +---- 4 files changed, 15 insertions(+), 12 deletions(-) create mode 100644 osu.Game.Rulesets.Osu.Tests/Mods/OsuModTestScene.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/OsuModTestScene.cs b/osu.Game.Rulesets.Osu.Tests/Mods/OsuModTestScene.cs new file mode 100644 index 0000000000..7697f46160 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Mods/OsuModTestScene.cs @@ -0,0 +1,12 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Osu.Tests.Mods +{ + public class OsuModTestScene : ModTestScene + { + protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs index 7c396054f1..49c1fe8540 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs @@ -9,14 +9,11 @@ using osu.Framework.Utils; using osu.Game.Graphics.Containers; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects.Drawables; -using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Osu.Tests.Mods { - public class TestSceneOsuModDifficultyAdjust : ModTestScene + public class TestSceneOsuModDifficultyAdjust : OsuModTestScene { - protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); - [Test] public void TestNoAdjustment() => CreateModTest(new ModTestData { diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDoubleTime.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDoubleTime.cs index 94ef6140e9..335ef31019 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDoubleTime.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDoubleTime.cs @@ -4,14 +4,11 @@ using NUnit.Framework; using osu.Framework.Utils; using osu.Game.Rulesets.Osu.Mods; -using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Osu.Tests.Mods { - public class TestSceneOsuModDoubleTime : ModTestScene + public class TestSceneOsuModDoubleTime : OsuModTestScene { - protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); - [TestCase(0.5)] [TestCase(1.01)] [TestCase(1.5)] diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs index 8ef2240c66..40f1c4a52f 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs @@ -8,15 +8,12 @@ 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.Tests.Visual; using osuTK; namespace osu.Game.Rulesets.Osu.Tests.Mods { - public class TestSceneOsuModHidden : ModTestScene + public class TestSceneOsuModHidden : OsuModTestScene { - protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); - [Test] public void TestDefaultBeatmapTest() => CreateModTest(new ModTestData { From 11da045d8cc54111157e76a9f91e6ae93a7b2c3d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 3 Jun 2020 23:43:18 +0300 Subject: [PATCH 1933/2376] Reorder declaration position of ruleset-creation methods Should be recognized as a normal protected method in its declaring class. --- .../TestSceneHyperDash.cs | 1 - osu.Game/Tests/Visual/PlayerTestScene.cs | 17 +++++++------- osu.Game/Tests/Visual/SkinnableTestScene.cs | 23 +++++++++++-------- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs index 83a6dc3d07..a0dcb86d57 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs @@ -16,7 +16,6 @@ namespace osu.Game.Rulesets.Catch.Tests [TestFixture] public class TestSceneHyperDash : TestSceneCatchPlayer { - protected override bool Autoplay => true; [Test] diff --git a/osu.Game/Tests/Visual/PlayerTestScene.cs b/osu.Game/Tests/Visual/PlayerTestScene.cs index 53abf83e72..d663848bbf 100644 --- a/osu.Game/Tests/Visual/PlayerTestScene.cs +++ b/osu.Game/Tests/Visual/PlayerTestScene.cs @@ -24,15 +24,6 @@ namespace osu.Game.Tests.Visual protected OsuConfigManager LocalConfig; - /// - /// Creates the ruleset for setting up the component. - /// - [NotNull] - protected abstract Ruleset CreatePlayerRuleset(); - - protected sealed override Ruleset CreateRuleset() => CreatePlayerRuleset(); - - [NotNull] private readonly Ruleset ruleset; protected PlayerTestScene() @@ -97,6 +88,14 @@ namespace osu.Game.Tests.Visual LoadScreen(Player); } + /// + /// Creates the ruleset for setting up the component. + /// + [NotNull] + protected abstract Ruleset CreatePlayerRuleset(); + + protected sealed override Ruleset CreateRuleset() => CreatePlayerRuleset(); + protected virtual TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(false, false); } } diff --git a/osu.Game/Tests/Visual/SkinnableTestScene.cs b/osu.Game/Tests/Visual/SkinnableTestScene.cs index 98164031b0..41147d3768 100644 --- a/osu.Game/Tests/Visual/SkinnableTestScene.cs +++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs @@ -23,27 +23,24 @@ namespace osu.Game.Tests.Visual { public abstract class SkinnableTestScene : OsuGridTestScene { + private readonly Ruleset ruleset; + private Skin metricsSkin; private Skin defaultSkin; private Skin specialSkin; private Skin oldSkin; - /// - /// Creates the ruleset for adding the ruleset-specific skin transforming component. - /// - [NotNull] - protected abstract Ruleset CreateRulesetForSkinProvider(); - - protected sealed override Ruleset CreateRuleset() => CreateRulesetForSkinProvider(); - protected SkinnableTestScene() : base(2, 3) { + ruleset = CreateRulesetForSkinProvider(); } [BackgroundDependencyLoader] private void load(AudioManager audio, SkinManager skinManager) { + Ruleset.Value = ruleset.RulesetInfo; + var dllStore = new DllResourceStore(DynamicCompilationOriginal.GetType().Assembly); metricsSkin = new TestLegacySkin(new SkinInfo { Name = "metrics-skin" }, new NamespacedResourceStore(dllStore, "Resources/metrics_skin"), audio, true); @@ -113,7 +110,7 @@ namespace osu.Game.Tests.Visual { new OutlineBox { Alpha = autoSize ? 1 : 0 }, mainProvider.WithChild( - new SkinProvidingContainer(CreateRulesetForSkinProvider().CreateLegacySkinProvider(mainProvider, beatmap)) + new SkinProvidingContainer(ruleset.CreateLegacySkinProvider(mainProvider, beatmap)) { Child = created, RelativeSizeAxes = !autoSize ? Axes.Both : Axes.None, @@ -126,6 +123,14 @@ namespace osu.Game.Tests.Visual }; } + /// + /// Creates the ruleset for adding the corresponding skin transforming component. + /// + [NotNull] + protected abstract Ruleset CreateRulesetForSkinProvider(); + + protected sealed override Ruleset CreateRuleset() => CreateRulesetForSkinProvider(); + protected virtual IBeatmap CreateBeatmapForSkinProvider() => CreateWorkingBeatmap(Ruleset.Value).GetPlayableBeatmap(Ruleset.Value); private class OutlineBox : CompositeDrawable From 136e10086acef397193487e11597623f2867f05b Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 4 Jun 2020 00:37:06 +0300 Subject: [PATCH 1934/2376] Set the ruleset bindable value at the BDL for its subclasses usages There are test scenes using current value of ruleset bindable on their BDL (example in TestSceneSliderSnaking's BDL) --- osu.Game/Tests/Visual/PlayerTestScene.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Tests/Visual/PlayerTestScene.cs b/osu.Game/Tests/Visual/PlayerTestScene.cs index d663848bbf..1e267726e0 100644 --- a/osu.Game/Tests/Visual/PlayerTestScene.cs +++ b/osu.Game/Tests/Visual/PlayerTestScene.cs @@ -34,6 +34,8 @@ namespace osu.Game.Tests.Visual [BackgroundDependencyLoader] private void load() { + Ruleset.Value = ruleset.RulesetInfo; + Dependencies.Cache(LocalConfig = new OsuConfigManager(LocalStorage)); LocalConfig.GetBindable(OsuSetting.DimLevel).Value = 1.0; } @@ -67,7 +69,6 @@ namespace osu.Game.Tests.Visual var beatmap = CreateBeatmap(ruleset.RulesetInfo); Beatmap.Value = CreateWorkingBeatmap(beatmap); - Ruleset.Value = ruleset.RulesetInfo; SelectedMods.Value = Array.Empty(); if (!AllowFail) From bbad70c3f0101fed3121bb894f28b3d6884a1322 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 4 Jun 2020 00:40:24 +0300 Subject: [PATCH 1935/2376] Fix mod perfect test scenes failing due to null ruleset provided Just a workaround for now, a better fix may be to put the test data creation in an action that is guaranteed to be invoked after the test scene has fully loaded (all dependencies would've been resolved by then). --- osu.Game/Tests/Visual/ModPerfectTestScene.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Tests/Visual/ModPerfectTestScene.cs b/osu.Game/Tests/Visual/ModPerfectTestScene.cs index bfd540093b..93b38a149c 100644 --- a/osu.Game/Tests/Visual/ModPerfectTestScene.cs +++ b/osu.Game/Tests/Visual/ModPerfectTestScene.cs @@ -22,7 +22,7 @@ namespace osu.Game.Tests.Visual Mod = mod, Beatmap = new Beatmap { - BeatmapInfo = { Ruleset = Ruleset.Value }, + BeatmapInfo = { Ruleset = CreatePlayerRuleset().RulesetInfo }, HitObjects = { testData.HitObject } }, Autoplay = !shouldMiss, From c2fd2b861642a6fcac8412891d0365104c6ed6b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 3 Jun 2020 23:20:43 +0200 Subject: [PATCH 1936/2376] Add notes about draft PRs & pushing --- CONTRIBUTING.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 331534ad73..9666f249e2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -92,6 +92,16 @@ Here are some key things to note before jumping in: As part of continuous integration, we also run code style analysis, which is supposed to make sure that your code is formatted the same way as all the pre-existing code in the repository. The reason we enforce a particular code style everywhere is to make sure the codebase is consistent in that regard - having one whitespace convention in one place and another one elsewhere causes disorganisation. +* **Make sure that the pull request is complete before opening it.** + + Whether it's fixing a bug or implementing new functionality, it's best that you make sure that the change you want to submit as a pull request is as complete as it can be before clicking the *Create pull request* button. Having to track if a pull request is ready for review or not places additional burden on reviewers. + + Draft pull requests are an option, but use them sparingly and within reason. They are best suited to discuss code changes that cannot be easily described in natural language or have a potential large impact on the future direction of the project. When in doubt, don't open drafts unless a maintainer asks you to do so. + +* **Only push code when it's ready.** + + As an extension of the above, when making changes to an already-open PR, please try to only push changes you are reasonably certain of. Pushing after every commit causes the continuous integration build queue to grow in size, slowing down work and taking up time that could be spent verifying other changes. + * **Make sure to keep the *Allow edits from maintainers* check box checked.** To speed up the merging process, collaborators and team members will sometimes want to push changes to your branch themselves, to make minor code style adjustments or to otherwise refactor the code without having to describe how they'd like the code to look like in painstaking detail. Having the *Allow edits from maintainers* check box checked lets them do that; without it they are forced to report issues back to you and wait for you to address them. From ddf5282d0e24d798a157d2c9704f8b30a7944731 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 3 Jun 2020 23:33:49 +0200 Subject: [PATCH 1937/2376] Move items from README.md to contributing guidelines --- CONTRIBUTING.md | 8 +++++++- README.md | 6 ------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9666f249e2..6c327f01b3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -52,7 +52,7 @@ Issues, bug reports and feature suggestions are welcomed, though please keep in ## I would like to submit a pull request! -We also welcome pull requests from unaffiliated contributors. The issue tracker should provide plenty of issues that you can work on; we also mark issues that we think would be good for newcomers with the [`good-first-issue`](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+label%3Agood-first-issue) label. +We also welcome pull requests from unaffiliated contributors. The [issue tracker](https://github.com/ppy/osu/issues) should provide plenty of issues that you can work on; we also mark issues that we think would be good for newcomers with the [`good-first-issue`](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+label%3Agood-first-issue) label. However, do keep in mind that the core team is committed to bringing osu!lazer up to par with stable first and foremost, so depending on what your contribution concerns, it might not be merged and released right away. Our approach to managing issues and their priorities is described [in the wiki](https://github.com/ppy/osu/wiki/Project-management). @@ -62,12 +62,18 @@ Here are some key things to note before jumping in: While we are accepting of all kinds of contributions, we also have a certain quality standard we'd like to uphold and limited time to review your code. Therefore, we would like to avoid providing entry-level advice, and as such if you're not very familiar with C\# as a programming language, we'd recommend that you start off with a few personal projects to get acquainted with the language's syntax, toolchain and principles of object-oriented programming first. + In addition, please take the time to take a look at and get acquainted with the [development and testing](https://github.com/ppy/osu-framework/wiki/Development-and-Testing) procedure we have set up. + * **Make sure you are familiar with git and the pull request workflow.** [git](https://git-scm.com/) is a distributed version control system that might not be very intuitive at the beginning if you're not familiar with version control. In particular, projects using git have a particular workflow for submitting code changes, which is called the pull request workflow. To make things run more smoothly, we recommend that you look up some online resources to familiarise yourself with the git vocabulary and commands, and practice working with forks and submitting pull requests at your own pace. A high-level overview of the process can be found in [this article by GitHub](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/proposing-changes-to-your-work-with-pull-requests). +* **Double-check designs before starting work on new functionality.** + + When implementing new features, keep in mind that we already have a lot of the UI designed. If you wish to work on something with the intention of having it included in the official distribution, please open an issue for discussion and we will give you what you need from a design perspective to proceed. If you want to make *changes* to the design, we recommend you open an issue with your intentions before spending too much time to ensure no effort is wasted. + * **Make sure to submit pull requests off of a topic branch.** As described in the article linked in the previous point, topic branches help you parallelise your work and separate it from the main `master` branch, and additionally are easier for maintainers to work with. Working with multiple `master` branches across many remotes is difficult to keep track of, and it's easy to make a mistake and push to the wrong `master` branch by accident. diff --git a/README.md b/README.md index 336bf33f7e..9e1cc20c8b 100644 --- a/README.md +++ b/README.md @@ -93,12 +93,6 @@ JetBrains ReSharper InspectCode is also used for wider rule sets. You can run it ## Contributing -We welcome all contributions, but keep in mind that we already have a lot of the UI designed. If you wish to work on something with the intention of having it included in the official distribution, please open an issue for discussion and we will give you what you need from a design perspective to proceed. If you want to make *changes* to the design, we recommend you open an issue with your intentions before spending too much time to ensure no effort is wasted. - -If you're unsure of what you can help with, check out the [list of open issues](https://github.com/ppy/osu/issues) (especially those with the ["good first issue"](https://github.com/ppy/osu/issues?q=is%3Aopen+label%3Agood-first-issue+sort%3Aupdated-desc) label). - -Before starting, please make sure you are familiar with the [development and testing](https://github.com/ppy/osu-framework/wiki/Development-and-Testing) procedure we have set up. New component development, and where possible, bug fixing and debugging existing components **should always be done under VisualTests**. - Note that while we already have certain standards in place, nothing is set in stone. If you have an issue with the way code is structured, with any libraries we are using, or with any processes involved with contributing, *please* bring it up. We welcome all feedback so we can make contributing to this project as painless as possible. For those interested, we love to reward quality contributions via [bounties](https://docs.google.com/spreadsheets/d/1jNXfj_S3Pb5PErA-czDdC9DUu4IgUbe1Lt8E7CYUJuE/view?&rm=minimal#gid=523803337), paid out via PayPal or osu!supporter tags. Don't hesitate to [request a bounty](https://docs.google.com/forms/d/e/1FAIpQLSet_8iFAgPMG526pBZ2Kic6HSh7XPM3fE8xPcnWNkMzINDdYg/viewform) for your work on this project. From af3daaaeafd294a740d9586ee56e13858b75b5e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 3 Jun 2020 23:39:29 +0200 Subject: [PATCH 1938/2376] Add reference to contributing guidelines in README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 9e1cc20c8b..dc3ee63844 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,8 @@ JetBrains ReSharper InspectCode is also used for wider rule sets. You can run it ## Contributing +When it comes to contributing to the project, the two main things you can do to help out are reporting issues and submitting pull requests. Based on past experiences, we have prepared a [list of contributing guidelines](CONTRIBUTING.md) that should hopefully ease you into our collaboration process and answer the most frequently-asked questions. + Note that while we already have certain standards in place, nothing is set in stone. If you have an issue with the way code is structured, with any libraries we are using, or with any processes involved with contributing, *please* bring it up. We welcome all feedback so we can make contributing to this project as painless as possible. For those interested, we love to reward quality contributions via [bounties](https://docs.google.com/spreadsheets/d/1jNXfj_S3Pb5PErA-czDdC9DUu4IgUbe1Lt8E7CYUJuE/view?&rm=minimal#gid=523803337), paid out via PayPal or osu!supporter tags. Don't hesitate to [request a bounty](https://docs.google.com/forms/d/e/1FAIpQLSet_8iFAgPMG526pBZ2Kic6HSh7XPM3fE8xPcnWNkMzINDdYg/viewform) for your work on this project. From c72592c52ce200cd1500d261f349d72caa99dd41 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 4 Jun 2020 00:44:28 +0300 Subject: [PATCH 1939/2376] Remove bindable-disabling logic and don't tie immediately to CreateRuleset() --- osu.Game/Tests/Visual/OsuTestScene.cs | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index 1b0dff162b..88bd087215 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -72,22 +72,7 @@ namespace osu.Game.Tests.Visual Beatmap.SetDefault(); Ruleset = Dependencies.Ruleset; - - var definedRuleset = CreateRuleset()?.RulesetInfo; - - if (definedRuleset != null) - { - // re-enable the bindable in case it was disabled. - // happens when restarting current test scene. - Ruleset.Disabled = false; - - // Set global ruleset bindable to the ruleset defined - // for this test scene and disallow changing it. - Ruleset.Value = definedRuleset; - Ruleset.Disabled = true; - } - else - Ruleset.SetDefault(); + Ruleset.SetDefault(); SelectedMods = Dependencies.Mods; SelectedMods.SetDefault(); @@ -145,7 +130,7 @@ namespace osu.Game.Tests.Visual /// Creates the ruleset to be used for this test scene. /// /// - /// When testing against ruleset-specific components, this method must be overriden to their ruleset. + /// When testing against ruleset-specific components, this method must be overriden to their corresponding ruleset. /// [CanBeNull] protected virtual Ruleset CreateRuleset() => null; From 741fa201492c41b916744315f28593f1e3c57cd9 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 4 Jun 2020 00:47:10 +0300 Subject: [PATCH 1940/2376] Use CreateRuleset() for editor test scenes as well --- .../TestSceneEditor.cs | 3 ++- .../TestSceneEditor.cs | 5 +---- .../TestSceneEditor.cs | 5 +---- .../Editing/TestSceneEditorChangeStates.cs | 8 +++----- osu.Game/Tests/Visual/EditorTestScene.cs | 19 +++++++++++-------- 5 files changed, 18 insertions(+), 22 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneEditor.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneEditor.cs index 7ed886be49..3b9c03b86a 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneEditor.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneEditor.cs @@ -15,8 +15,9 @@ namespace osu.Game.Rulesets.Mania.Tests { private readonly Bindable direction = new Bindable(); + protected override Ruleset CreateEditorRuleset() => new ManiaRuleset(); + public TestSceneEditor() - : base(new ManiaRuleset()) { AddStep("upwards scroll", () => direction.Value = ManiaScrollingDirection.Up); AddStep("downwards scroll", () => direction.Value = ManiaScrollingDirection.Down); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneEditor.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneEditor.cs index 4aca34bf64..9239034a53 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneEditor.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneEditor.cs @@ -9,9 +9,6 @@ namespace osu.Game.Rulesets.Osu.Tests [TestFixture] public class TestSceneEditor : EditorTestScene { - public TestSceneEditor() - : base(new OsuRuleset()) - { - } + protected override Ruleset CreateEditorRuleset() => new OsuRuleset(); } } diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneEditor.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneEditor.cs index 089a7ad00b..411fe08bcf 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneEditor.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneEditor.cs @@ -9,9 +9,6 @@ namespace osu.Game.Rulesets.Taiko.Tests [TestFixture] public class TestSceneEditor : EditorTestScene { - public TestSceneEditor() - : base(new TaikoRuleset()) - { - } + protected override Ruleset CreateEditorRuleset() => new TaikoRuleset(); } } diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorChangeStates.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorChangeStates.cs index 20862e9cac..293a6e6869 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorChangeStates.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorChangeStates.cs @@ -4,6 +4,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Testing; +using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Objects; @@ -13,13 +14,10 @@ namespace osu.Game.Tests.Visual.Editing { public class TestSceneEditorChangeStates : EditorTestScene { - public TestSceneEditorChangeStates() - : base(new OsuRuleset()) - { - } - private EditorBeatmap editorBeatmap; + protected override Ruleset CreateEditorRuleset() => new OsuRuleset(); + public override void SetUpSteps() { base.SetUpSteps(); diff --git a/osu.Game/Tests/Visual/EditorTestScene.cs b/osu.Game/Tests/Visual/EditorTestScene.cs index 2f6e6fb599..4f9a5b53b8 100644 --- a/osu.Game/Tests/Visual/EditorTestScene.cs +++ b/osu.Game/Tests/Visual/EditorTestScene.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Testing; using osu.Game.Rulesets; @@ -15,17 +16,11 @@ namespace osu.Game.Tests.Visual { protected Editor Editor { get; private set; } - private readonly Ruleset ruleset; - - protected EditorTestScene(Ruleset ruleset) - { - this.ruleset = ruleset; - } - [BackgroundDependencyLoader] private void load() { - Beatmap.Value = CreateWorkingBeatmap(ruleset.RulesetInfo); + Ruleset.Value = CreateEditorRuleset().RulesetInfo; + Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value); } public override void SetUpSteps() @@ -37,6 +32,14 @@ namespace osu.Game.Tests.Visual && Editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); } + /// + /// Creates the ruleset for providing a corresponding beatmap to load the editor on. + /// + [NotNull] + protected abstract Ruleset CreateEditorRuleset(); + + protected sealed override Ruleset CreateRuleset() => CreateEditorRuleset(); + protected virtual Editor CreateEditor() => new Editor(); } } From 7e5db5e933aea2b39dbf7faf769f5e1ffba9322b Mon Sep 17 00:00:00 2001 From: Shivam Date: Wed, 3 Jun 2020 23:49:06 +0200 Subject: [PATCH 1941/2376] Apply review suggestions --- .../Components/IPCErrorDialog.cs | 2 +- osu.Game.Tournament/IPC/FileBasedIPC.cs | 22 ++++++++++++------- .../Screens/StablePathSelectScreen.cs | 4 +--- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tournament/Components/IPCErrorDialog.cs b/osu.Game.Tournament/Components/IPCErrorDialog.cs index 07fd0ac973..dc039cd3bc 100644 --- a/osu.Game.Tournament/Components/IPCErrorDialog.cs +++ b/osu.Game.Tournament/Components/IPCErrorDialog.cs @@ -18,7 +18,7 @@ namespace osu.Game.Tournament.Components new PopupDialogOkButton { Text = @"Alright.", - Action = () => { Expire(); } + Action = () => Expire() } }; } diff --git a/osu.Game.Tournament/IPC/FileBasedIPC.cs b/osu.Game.Tournament/IPC/FileBasedIPC.cs index 44a010e506..aad44cd385 100644 --- a/osu.Game.Tournament/IPC/FileBasedIPC.cs +++ b/osu.Game.Tournament/IPC/FileBasedIPC.cs @@ -158,7 +158,7 @@ namespace osu.Game.Tournament.IPC return IPCStorage; } - public static bool CheckExists(string p) => File.Exists(Path.Combine(p, "ipc.txt")); + private static bool ipcFileExistsInDirectory(string p) => File.Exists(Path.Combine(p, "ipc.txt")); private string findStablePath() { @@ -183,8 +183,8 @@ namespace osu.Game.Tournament.IPC if (stableInstallPath != null) { - SaveStableConfig(stableInstallPath); - return null; + SetIPCLocation(stableInstallPath); + return stableInstallPath; } } @@ -196,8 +196,11 @@ namespace osu.Game.Tournament.IPC } } - public void SaveStableConfig(string path) + public bool SetIPCLocation(string path) { + if (!ipcFileExistsInDirectory(path)) + return false; + StableInfo.StablePath.Value = path; using (var stream = tournamentStorage.GetStream(STABLE_CONFIG, FileAccess.Write, FileMode.Create)) @@ -211,6 +214,9 @@ namespace osu.Game.Tournament.IPC DefaultValueHandling = DefaultValueHandling.Ignore, })); } + + LocateStableStorage(); + return true; } private string readStableConfig() @@ -239,7 +245,7 @@ namespace osu.Game.Tournament.IPC Logger.Log("Trying to find stable with environment variables"); string stableInstallPath = Environment.GetEnvironmentVariable("OSU_STABLE_PATH"); - if (CheckExists(stableInstallPath)) + if (ipcFileExistsInDirectory(stableInstallPath)) return stableInstallPath; } catch @@ -254,7 +260,7 @@ namespace osu.Game.Tournament.IPC Logger.Log("Trying to find stable in %LOCALAPPDATA%"); string stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"osu!"); - if (CheckExists(stableInstallPath)) + if (ipcFileExistsInDirectory(stableInstallPath)) return stableInstallPath; return null; @@ -265,7 +271,7 @@ namespace osu.Game.Tournament.IPC Logger.Log("Trying to find stable in dotfolders"); string stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".osu"); - if (CheckExists(stableInstallPath)) + if (ipcFileExistsInDirectory(stableInstallPath)) return stableInstallPath; return null; @@ -280,7 +286,7 @@ namespace osu.Game.Tournament.IPC using (RegistryKey key = Registry.ClassesRoot.OpenSubKey("osu")) stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty).ToString().Split('"')[1].Replace("osu!.exe", ""); - if (CheckExists(stableInstallPath)) + if (ipcFileExistsInDirectory(stableInstallPath)) return stableInstallPath; return null; diff --git a/osu.Game.Tournament/Screens/StablePathSelectScreen.cs b/osu.Game.Tournament/Screens/StablePathSelectScreen.cs index fee2696c4c..ad0c06e4f9 100644 --- a/osu.Game.Tournament/Screens/StablePathSelectScreen.cs +++ b/osu.Game.Tournament/Screens/StablePathSelectScreen.cs @@ -139,7 +139,7 @@ namespace osu.Game.Tournament.Screens var fileBasedIpc = ipc as FileBasedIPC; Logger.Log($"Changing Stable CE location to {target}"); - if (!FileBasedIPC.CheckExists(target)) + if (!fileBasedIpc?.SetIPCLocation(target) ?? false) { overlay = new DialogOverlay(); overlay.Push(new IPCErrorDialog("This is an invalid IPC Directory", "Select a directory that contains an osu! stable cutting edge installation and make sure it has an empty ipc.txt file in it.")); @@ -148,8 +148,6 @@ namespace osu.Game.Tournament.Screens return; } - fileBasedIpc?.SaveStableConfig(target); - fileBasedIpc?.LocateStableStorage(); sceneManager?.SetScreen(typeof(SetupScreen)); } From 9920911390833ffb35fddffaaa62803ebf92ecd1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 Jun 2020 17:20:08 +0900 Subject: [PATCH 1942/2376] Fix tournament displayed beatmap potentially being out of order on quick changes --- osu.Game.Tournament/IPC/FileBasedIPC.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tournament/IPC/FileBasedIPC.cs b/osu.Game.Tournament/IPC/FileBasedIPC.cs index 53ba597a7e..de4d482d13 100644 --- a/osu.Game.Tournament/IPC/FileBasedIPC.cs +++ b/osu.Game.Tournament/IPC/FileBasedIPC.cs @@ -34,6 +34,7 @@ namespace osu.Game.Tournament.IPC private int lastBeatmapId; private ScheduledDelegate scheduled; + private GetBeatmapRequest beatmapLookupRequest; public Storage Storage { get; private set; } @@ -77,6 +78,8 @@ namespace osu.Game.Tournament.IPC if (lastBeatmapId != beatmapId) { + beatmapLookupRequest?.Cancel(); + lastBeatmapId = beatmapId; var existing = ladder.CurrentMatch.Value?.Round.Value?.Beatmaps.FirstOrDefault(b => b.ID == beatmapId && b.BeatmapInfo != null); @@ -85,9 +88,9 @@ namespace osu.Game.Tournament.IPC Beatmap.Value = existing.BeatmapInfo; else { - var req = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = beatmapId }); - req.Success += b => Beatmap.Value = b.ToBeatmap(Rulesets); - API.Queue(req); + beatmapLookupRequest = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = beatmapId }); + beatmapLookupRequest.Success += b => Beatmap.Value = b.ToBeatmap(Rulesets); + API.Queue(beatmapLookupRequest); } } From 5d7bb8cb4e9e44eeb8a504f7d8e43f9046003aca Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 4 Jun 2020 21:33:38 +0900 Subject: [PATCH 1943/2376] Change format of date on score panel --- osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index fd8ac33aef..81d5d113ae 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -211,7 +211,7 @@ namespace osu.Game.Screens.Ranking.Expanded Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Font = OsuFont.GetFont(size: 10, weight: FontWeight.SemiBold), - Text = $"Played on {score.Date.ToLocalTime():g}" + Text = $"Played on {score.Date.ToLocalTime():d MMMM yyyy HH:mm}" } } }; From afcefe01bf74177240c59e70b3ea2b87745a4223 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 4 Jun 2020 21:48:55 +0900 Subject: [PATCH 1944/2376] Fix score panel not receiving input in some places --- osu.Game/Screens/Ranking/ScorePanel.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Screens/Ranking/ScorePanel.cs b/osu.Game/Screens/Ranking/ScorePanel.cs index a99b48e8f0..65fb901c89 100644 --- a/osu.Game/Screens/Ranking/ScorePanel.cs +++ b/osu.Game/Screens/Ranking/ScorePanel.cs @@ -243,5 +243,10 @@ namespace osu.Game.Screens.Ranking return true; } + + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) + => base.ReceivePositionalInputAt(screenSpacePos) + || topLayerContainer.ReceivePositionalInputAt(screenSpacePos) + || middleLayerContainer.ReceivePositionalInputAt(screenSpacePos); } } From 9c1542f8979637fcea83b327b5252f2fae8933a1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 4 Jun 2020 22:17:00 +0900 Subject: [PATCH 1945/2376] Fix crash when pressing clear button twice --- .../Settings/TestSceneKeyBindingPanel.cs | 45 ++++++++++++++++++- osu.Game/Overlays/KeyBinding/KeyBindingRow.cs | 5 ++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs index 745820696a..3d335995ac 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs @@ -1,13 +1,19 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Diagnostics; +using System.Linq; using NUnit.Framework; +using osu.Framework.Testing; +using osu.Framework.Threading; using osu.Game.Overlays; +using osu.Game.Overlays.KeyBinding; +using osuTK.Input; namespace osu.Game.Tests.Visual.Settings { [TestFixture] - public class TestSceneKeyBindingPanel : OsuTestScene + public class TestSceneKeyBindingPanel : OsuManualInputManagerTestScene { private readonly KeyBindingPanel panel; @@ -21,5 +27,42 @@ namespace osu.Game.Tests.Visual.Settings base.LoadComplete(); panel.Show(); } + + [Test] + public void TestClickTwiceOnClearButton() + { + KeyBindingRow firstRow = null; + + AddStep("click first row", () => + { + firstRow = panel.ChildrenOfType().First(); + InputManager.MoveMouseTo(firstRow); + InputManager.Click(MouseButton.Left); + }); + + AddStep("schedule button clicks", () => + { + var clearButton = firstRow.ChildrenOfType().Single(); + + InputManager.MoveMouseTo(clearButton); + + int buttonClicks = 0; + ScheduledDelegate clickDelegate = null; + + clickDelegate = Scheduler.AddDelayed(() => + { + InputManager.PressButton(MouseButton.Left); + InputManager.ReleaseButton(MouseButton.Left); + + if (++buttonClicks == 2) + { + // ReSharper disable once AccessToModifiedClosure + Debug.Assert(clickDelegate != null); + // ReSharper disable once AccessToModifiedClosure + clickDelegate.Cancel(); + } + }, 0, true); + }); + } } } diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs index 01d5991d3e..eafb4572ca 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs @@ -274,6 +274,9 @@ namespace osu.Game.Overlays.KeyBinding private void clear() { + if (bindTarget == null) + return; + bindTarget.UpdateKeyCombination(InputKey.None); finalise(); } @@ -333,7 +336,7 @@ namespace osu.Game.Overlays.KeyBinding } } - private class ClearButton : TriangleButton + public class ClearButton : TriangleButton { public ClearButton() { From 6b88141e58b6d3863b1aeb9db41d39225cd00bda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 4 Jun 2020 21:30:59 +0200 Subject: [PATCH 1946/2376] Add mania sample conversion test --- .../ManiaBeatmapSampleConversionTest.cs | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 osu.Game.Rulesets.Mania.Tests/ManiaBeatmapSampleConversionTest.cs diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapSampleConversionTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapSampleConversionTest.cs new file mode 100644 index 0000000000..dbf1cf5f72 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapSampleConversionTest.cs @@ -0,0 +1,72 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Utils; +using osu.Game.Audio; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Objects; +using osu.Game.Tests.Beatmaps; + +namespace osu.Game.Rulesets.Mania.Tests +{ + [TestFixture] + public class ManiaBeatmapSampleConversionTest : BeatmapConversionTest, SampleConvertValue> + { + protected override string ResourceAssembly => "osu.Game.Rulesets.Mania"; + + public void Test(string name) => base.Test(name); + + protected override IEnumerable CreateConvertValue(HitObject hitObject) + { + yield return new SampleConvertValue + { + StartTime = hitObject.StartTime, + EndTime = hitObject.GetEndTime(), + Column = ((ManiaHitObject)hitObject).Column, + NodeSamples = getSampleNames((hitObject as HoldNote)?.NodeSamples) + }; + } + + private IList> getSampleNames(List> hitSampleInfo) + => hitSampleInfo?.Select(samples => + (IList)samples.Select(sample => sample.LookupNames.First()).ToList()) + .ToList(); + + protected override Ruleset CreateRuleset() => new ManiaRuleset(); + } + + public struct SampleConvertValue : IEquatable + { + /// + /// A sane value to account for osu!stable using ints everywhere. + /// + private const float conversion_lenience = 2; + + public double StartTime; + public double EndTime; + public int Column; + public IList> NodeSamples; + + public bool Equals(SampleConvertValue other) + => Precision.AlmostEquals(StartTime, other.StartTime, conversion_lenience) + && Precision.AlmostEquals(EndTime, other.EndTime, conversion_lenience) + && samplesEqual(NodeSamples, other.NodeSamples); + + private static bool samplesEqual(ICollection> first, ICollection> second) + { + if (first == null && second == null) + return true; + + // both items can't be null now, so if any single one is, then they're not equal + if (first == null || second == null) + return false; + + return first.Count == second.Count + && first.Zip(second).All(samples => samples.First.SequenceEqual(samples.Second)); + } + } +} From 35544ede50069851ff7cfa0fecdf141fe94345db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 4 Jun 2020 21:54:19 +0200 Subject: [PATCH 1947/2376] Add failing test cases --- .../ManiaBeatmapSampleConversionTest.cs | 2 ++ .../convert-samples-expected-conversion.json | 30 +++++++++++++++++++ .../Testing/Beatmaps/convert-samples.osu | 16 ++++++++++ .../mania-samples-expected-conversion.json | 25 ++++++++++++++++ .../Testing/Beatmaps/mania-samples.osu | 19 ++++++++++++ 5 files changed, 92 insertions(+) create mode 100644 osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples-expected-conversion.json create mode 100644 osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples.osu create mode 100644 osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples-expected-conversion.json create mode 100644 osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples.osu diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapSampleConversionTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapSampleConversionTest.cs index dbf1cf5f72..2f6918d263 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapSampleConversionTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapSampleConversionTest.cs @@ -18,6 +18,8 @@ namespace osu.Game.Rulesets.Mania.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Mania"; + [TestCase("convert-samples")] + [TestCase("mania-samples")] public void Test(string name) => base.Test(name); protected override IEnumerable CreateConvertValue(HitObject hitObject) diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples-expected-conversion.json b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples-expected-conversion.json new file mode 100644 index 0000000000..b8ce85eef5 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples-expected-conversion.json @@ -0,0 +1,30 @@ +{ + "Mappings": [{ + "StartTime": 1000.0, + "Objects": [{ + "StartTime": 1000.0, + "EndTime": 2750.0, + "Column": 1, + "NodeSamples": [ + ["normal-hitnormal"], + ["soft-hitnormal"], + ["drum-hitnormal"] + ] + }, { + "StartTime": 1875.0, + "EndTime": 2750.0, + "Column": 0, + "NodeSamples": [ + ["soft-hitnormal"], + ["drum-hitnormal"] + ] + }] + }, { + "StartTime": 3750.0, + "Objects": [{ + "StartTime": 3750.0, + "EndTime": 3750.0, + "Column": 3 + }] + }] +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples.osu b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples.osu new file mode 100644 index 0000000000..16b73992d2 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples.osu @@ -0,0 +1,16 @@ +osu file format v14 + +[Difficulty] +HPDrainRate:5 +CircleSize:5 +OverallDifficulty:5 +ApproachRate:5 +SliderMultiplier:1.4 +SliderTickRate:1 + +[TimingPoints] +0,500,4,1,0,100,1,0 + +[HitObjects] +88,99,1000,6,0,L|306:259,2,245,0|0|0,1:0|2:0|3:0,0:0:0:0: +259,118,3750,1,0,0:0:0:0: diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples-expected-conversion.json b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples-expected-conversion.json new file mode 100644 index 0000000000..e22540614d --- /dev/null +++ b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples-expected-conversion.json @@ -0,0 +1,25 @@ +{ + "Mappings": [{ + "StartTime": 500.0, + "Objects": [{ + "StartTime": 500.0, + "EndTime": 1500.0, + "Column": 0, + "NodeSamples": [ + ["normal-hitnormal"], + [] + ] + }] + }, { + "StartTime": 2000.0, + "Objects": [{ + "StartTime": 2000.0, + "EndTime": 3000.0, + "Column": 2, + "NodeSamples": [ + ["drum-hitnormal"], + [] + ] + }] + }] +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples.osu b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples.osu new file mode 100644 index 0000000000..7c75b45e5f --- /dev/null +++ b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples.osu @@ -0,0 +1,19 @@ +osu file format v14 + +[General] +Mode: 3 + +[Difficulty] +HPDrainRate:5 +CircleSize:5 +OverallDifficulty:5 +ApproachRate:5 +SliderMultiplier:1.4 +SliderTickRate:1 + +[TimingPoints] +0,500,4,1,0,100,1,0 + +[HitObjects] +51,192,500,128,0,1500:1:0:0:0: +256,192,2000,128,0,3000:3:0:0:0: From ac019bddd61b798f86c0f9e545a5d1e45de5a746 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 4 Jun 2020 22:28:55 +0200 Subject: [PATCH 1948/2376] Only play samples at start of hold note in mania maps --- .../Beatmaps/ManiaBeatmapConverter.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index 32abf5e7f9..b025ac7992 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -6,6 +6,7 @@ using System; using System.Linq; using System.Collections.Generic; using osu.Framework.Utils; +using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; @@ -239,7 +240,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps Duration = endTimeData.Duration, Column = column, Samples = HitObject.Samples, - NodeSamples = (HitObject as IHasRepeats)?.NodeSamples + NodeSamples = (HitObject as IHasRepeats)?.NodeSamples ?? defaultNodeSamples }); } else if (HitObject is IHasXPosition) @@ -254,6 +255,16 @@ namespace osu.Game.Rulesets.Mania.Beatmaps return pattern; } + + /// + /// osu!mania-specific beatmaps in stable only play samples at the start of the hold note. + /// + private List> defaultNodeSamples + => new List> + { + HitObject.Samples, + new List() + }; } } } From c4cae006aa800e78f29b46dfcdccda0220838a60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 4 Jun 2020 22:47:14 +0200 Subject: [PATCH 1949/2376] Correctly slice node sample list when converting --- .../Legacy/DistanceObjectPatternGenerator.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs index 1bd796511b..b49b881656 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs @@ -472,15 +472,21 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy /// /// The time to retrieve the sample info list from. /// - private IList sampleInfoListAt(double time) + private IList sampleInfoListAt(double time) => nodeSamplesAt(time)?.First() ?? HitObject.Samples; + + /// + /// Retrieves the list of node samples that occur at time greater than or equal to . + /// + /// The time to retrieve node samples at. + private IEnumerable> nodeSamplesAt(double time) { if (!(HitObject is IHasPathWithRepeats curveData)) - return HitObject.Samples; + return null; double segmentTime = (EndTime - HitObject.StartTime) / spanCount; int index = (int)(segmentTime == 0 ? 0 : (time - HitObject.StartTime) / segmentTime); - return curveData.NodeSamples[index]; + return curveData.NodeSamples.Skip(index); } /// @@ -511,7 +517,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy Duration = endTime - startTime, Column = column, Samples = HitObject.Samples, - NodeSamples = (HitObject as IHasRepeats)?.NodeSamples + NodeSamples = nodeSamplesAt(startTime)?.ToList() }; } From 4c6116e6e7c9aa1300430954ef4e6cbcc793f39b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 4 Jun 2020 23:50:58 +0200 Subject: [PATCH 1950/2376] Fix compilation failure in Android test project --- .../ManiaBeatmapSampleConversionTest.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapSampleConversionTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapSampleConversionTest.cs index 2f6918d263..d8f87195d1 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapSampleConversionTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapSampleConversionTest.cs @@ -58,17 +58,19 @@ namespace osu.Game.Rulesets.Mania.Tests && Precision.AlmostEquals(EndTime, other.EndTime, conversion_lenience) && samplesEqual(NodeSamples, other.NodeSamples); - private static bool samplesEqual(ICollection> first, ICollection> second) + private static bool samplesEqual(ICollection> firstSampleList, ICollection> secondSampleList) { - if (first == null && second == null) + if (firstSampleList == null && secondSampleList == null) return true; // both items can't be null now, so if any single one is, then they're not equal - if (first == null || second == null) + if (firstSampleList == null || secondSampleList == null) return false; - return first.Count == second.Count - && first.Zip(second).All(samples => samples.First.SequenceEqual(samples.Second)); + return firstSampleList.Count == secondSampleList.Count + // cannot use .Zip() without the selector function as it doesn't compile in android test project + && firstSampleList.Zip(secondSampleList, (first, second) => (first, second)) + .All(samples => samples.first.SequenceEqual(samples.second)); } } } From 896177801a57e0bd2161309d05775be4d0d087bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 5 Jun 2020 00:07:27 +0200 Subject: [PATCH 1951/2376] Avoid creating copies of node samples every time --- .../Patterns/Legacy/DistanceObjectPatternGenerator.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs index b49b881656..9fbdf58e21 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs @@ -478,7 +478,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy /// Retrieves the list of node samples that occur at time greater than or equal to . /// /// The time to retrieve node samples at. - private IEnumerable> nodeSamplesAt(double time) + private List> nodeSamplesAt(double time) { if (!(HitObject is IHasPathWithRepeats curveData)) return null; @@ -486,7 +486,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy double segmentTime = (EndTime - HitObject.StartTime) / spanCount; int index = (int)(segmentTime == 0 ? 0 : (time - HitObject.StartTime) / segmentTime); - return curveData.NodeSamples.Skip(index); + + // avoid slicing the list & creating copies, if at all possible. + return index == 0 ? curveData.NodeSamples : curveData.NodeSamples.Skip(index).ToList(); } /// @@ -517,7 +519,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy Duration = endTime - startTime, Column = column, Samples = HitObject.Samples, - NodeSamples = nodeSamplesAt(startTime)?.ToList() + NodeSamples = nodeSamplesAt(startTime) }; } From c6c88a901ceaab95cab924c42b82e9723a601c30 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 5 Jun 2020 06:42:46 +0300 Subject: [PATCH 1952/2376] Add text box sample playback logic in OsuTextBox Moved from osu!framework. --- osu.Game/Graphics/UserInterface/OsuTextBox.cs | 57 ++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterface/OsuTextBox.cs b/osu.Game/Graphics/UserInterface/OsuTextBox.cs index 6f440d8138..f749326b0e 100644 --- a/osu.Game/Graphics/UserInterface/OsuTextBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuTextBox.cs @@ -1,7 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Audio.Track; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; @@ -11,6 +14,7 @@ using osuTK.Graphics; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; +using osu.Framework.Utils; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics.Containers; using osuTK; @@ -19,6 +23,18 @@ namespace osu.Game.Graphics.UserInterface { public class OsuTextBox : BasicTextBox { + private readonly SampleChannel[] textAddedSamples = new SampleChannel[4]; + private SampleChannel capsTextAddedSample; + private SampleChannel textRemovedSample; + private SampleChannel textCommittedSample; + private SampleChannel caretMovedSample; + + /// + /// Whether to allow playing a different sample when inserting upper case text. + /// If set to false, same sample will be played for both letter cases. + /// + protected virtual bool AllowUpperCaseSamples => true; + protected override float LeftRightPadding => 10; protected override float CaretWidth => 3; @@ -41,15 +57,54 @@ namespace osu.Game.Graphics.UserInterface } [BackgroundDependencyLoader] - private void load(OsuColour colour) + private void load(OsuColour colour, AudioManager audio) { BackgroundUnfocused = Color4.Black.Opacity(0.5f); BackgroundFocused = OsuColour.Gray(0.3f).Opacity(0.8f); BackgroundCommit = BorderColour = colour.Yellow; + + for (int i = 0; i < textAddedSamples.Length; i++) + textAddedSamples[i] = audio.Samples.Get($@"Keyboard/key-press-{1 + i}"); + + capsTextAddedSample = audio.Samples.Get(@"Keyboard/key-caps"); + textRemovedSample = audio.Samples.Get(@"Keyboard/key-delete"); + textCommittedSample = audio.Samples.Get(@"Keyboard/key-confirm"); + caretMovedSample = audio.Samples.Get(@"Keyboard/key-movement"); } protected override Color4 SelectionColour => new Color4(249, 90, 255, 255); + protected override void OnTextAdded(string added) + { + base.OnTextAdded(added); + + if (added.Any(char.IsUpper) && AllowUpperCaseSamples) + capsTextAddedSample?.Play(); + else + textAddedSamples[RNG.Next(0, 3)]?.Play(); + } + + protected override void OnTextRemoved(string removed) + { + base.OnTextRemoved(removed); + + textRemovedSample?.Play(); + } + + protected override void OnTextCommitted(bool textChanged) + { + base.OnTextCommitted(textChanged); + + textCommittedSample?.Play(); + } + + protected override void OnCaretMoved(bool selecting) + { + base.OnCaretMoved(selecting); + + caretMovedSample?.Play(); + } + protected override void OnFocus(FocusEvent e) { BorderThickness = 3; From 178bbf16d180483397555d5d2137194deaa31fea Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 5 Jun 2020 06:44:41 +0300 Subject: [PATCH 1953/2376] Fix password text boxes having distinguishable key sounds Closes https://github.com/ppy/osu-framework/issues/3280 --- osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs b/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs index 0c82a869f8..11867cf103 100644 --- a/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs @@ -24,6 +24,8 @@ namespace osu.Game.Graphics.UserInterface Child = new PasswordMaskChar(CalculatedTextSize), }; + protected override bool AllowUpperCaseSamples => false; + protected override bool AllowClipboardExport => false; private readonly CapsWarning warning; From 495f89ddaebdb30558b571dd20346dce9ef04245 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 5 Jun 2020 06:45:42 +0300 Subject: [PATCH 1954/2376] Expand number text box test scene to one holding all OsuTextBox's types --- .../UserInterface/TestSceneNumberBox.cs | 48 ----------- .../UserInterface/TestSceneOsuTextBox.cs | 80 +++++++++++++++++++ 2 files changed, 80 insertions(+), 48 deletions(-) delete mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneNumberBox.cs create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneOsuTextBox.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNumberBox.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNumberBox.cs deleted file mode 100644 index 97a3f62b2d..0000000000 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneNumberBox.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Graphics.UserInterface; - -namespace osu.Game.Tests.Visual.UserInterface -{ - [TestFixture] - public class TestSceneNumberBox : OsuTestScene - { - private OsuNumberBox numberBox; - - [BackgroundDependencyLoader] - private void load() - { - Child = new Container - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.X, - Padding = new MarginPadding { Horizontal = 250 }, - Child = numberBox = new OsuNumberBox - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.X, - PlaceholderText = "Insert numbers here" - } - }; - - clearInput(); - AddStep("enter numbers", () => numberBox.Text = "987654321"); - expectedValue("987654321"); - clearInput(); - AddStep("enter text + single number", () => numberBox.Text = "1 hello 2 world 3"); - expectedValue("123"); - clearInput(); - } - - private void clearInput() => AddStep("clear input", () => numberBox.Text = null); - - private void expectedValue(string value) => AddAssert("expect number", () => numberBox.Text == value); - } -} diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuTextBox.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuTextBox.cs new file mode 100644 index 0000000000..756928d3ec --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuTextBox.cs @@ -0,0 +1,80 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics.UserInterface; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public class TestSceneOsuTextBox : OsuTestScene + { + private readonly OsuNumberBox numberBox; + + public TestSceneOsuTextBox() + { + Child = new Container + { + Masking = true, + CornerRadius = 10f, + AutoSizeAxes = Axes.Both, + Padding = new MarginPadding(15f), + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.DarkSlateGray, + Alpha = 0.75f, + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Padding = new MarginPadding(50f), + Spacing = new Vector2(0f, 50f), + Children = new[] + { + new OsuTextBox + { + Width = 500f, + PlaceholderText = "Normal textbox", + }, + new OsuPasswordTextBox + { + Width = 500f, + PlaceholderText = "Password textbox", + }, + numberBox = new OsuNumberBox + { + Width = 500f, + PlaceholderText = "Number textbox" + } + } + } + } + }; + } + + [Test] + public void TestNumberBox() + { + clearTextbox(numberBox); + AddStep("enter numbers", () => numberBox.Text = "987654321"); + expectedValue(numberBox, "987654321"); + + clearTextbox(numberBox); + AddStep("enter text + single number", () => numberBox.Text = "1 hello 2 world 3"); + expectedValue(numberBox, "123"); + + clearTextbox(numberBox); + } + + private void clearTextbox(OsuTextBox textBox) => AddStep("clear textbox", () => textBox.Text = null); + private void expectedValue(OsuTextBox textBox, string value) => AddAssert("expected textbox value", () => textBox.Text == value); + } +} From 0107e9ba16deb94cd8f04c5dcf36b7ca2a781adc Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 5 Jun 2020 19:18:00 +0900 Subject: [PATCH 1955/2376] Change lookups to use SingleOrDefault() --- osu.Game/Beatmaps/BeatmapManager.cs | 2 +- osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 668ac6ee10..1f92d5461f 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -215,7 +215,7 @@ namespace osu.Game.Beatmaps foreach (var info in item.Beatmaps) { - var file = item.Files.FirstOrDefault(f => string.Equals(f.Filename, info.Path, StringComparison.OrdinalIgnoreCase))?.FileInfo.StoragePath; + var file = item.Files.SingleOrDefault(f => string.Equals(f.Filename, info.Path, StringComparison.OrdinalIgnoreCase))?.FileInfo.StoragePath; using (var stream = Files.Store.GetStream(file)) info.MD5Hash = stream.ComputeMD5Hash(); diff --git a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs index e62a9bb39d..39c5ccab27 100644 --- a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs @@ -42,7 +42,7 @@ namespace osu.Game.Beatmaps } } - private string getPathForFile(string filename) => BeatmapSetInfo.Files.FirstOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase))?.FileInfo.StoragePath; + private string getPathForFile(string filename) => BeatmapSetInfo.Files.SingleOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase))?.FileInfo.StoragePath; private TextureStore textureStore; From bb89114b70eb820096ae3dc1b371c0d3ce8c05c7 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 5 Jun 2020 20:52:27 +0900 Subject: [PATCH 1956/2376] Show a loading spinner on multiplayer lounge loads --- .../TestSceneLoungeRoomsContainer.cs | 3 ++ .../TestSceneMatchSettingsOverlay.cs | 2 ++ .../Multiplayer/TestSceneMatchSubScreen.cs | 2 ++ osu.Game/Screens/Multi/IRoomManager.cs | 5 +++ .../Screens/Multi/Lounge/LoungeSubScreen.cs | 33 +++++++++++++++++-- osu.Game/Screens/Multi/RoomManager.cs | 14 +++++++- 6 files changed, 56 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs index 77b41c89b0..83f2297bd2 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs @@ -141,6 +141,9 @@ namespace osu.Game.Tests.Visual.Multiplayer } public readonly BindableList Rooms = new BindableList(); + + public Bindable InitialRoomsReceived { get; } = new Bindable(true); + IBindableList IRoomManager.Rooms => Rooms; public void CreateRoom(Room room, Action onSuccess = null, Action onError = null) => Rooms.Add(room); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs index 34c6940552..fdc20dc477 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs @@ -133,6 +133,8 @@ namespace osu.Game.Tests.Visual.Multiplayer remove { } } + public Bindable InitialRoomsReceived { get; } = new Bindable(true); + public IBindableList Rooms { get; } = null; public void CreateRoom(Room room, Action onSuccess = null, Action onError = null) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs index d678d5a814..9d0c159549 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs @@ -93,6 +93,8 @@ namespace osu.Game.Tests.Visual.Multiplayer remove => throw new NotImplementedException(); } + public Bindable InitialRoomsReceived { get; } = new Bindable(true); + public IBindableList Rooms { get; } = new BindableList(); public void CreateRoom(Room room, Action onSuccess = null, Action onError = null) diff --git a/osu.Game/Screens/Multi/IRoomManager.cs b/osu.Game/Screens/Multi/IRoomManager.cs index f6c979851e..bf75843c3e 100644 --- a/osu.Game/Screens/Multi/IRoomManager.cs +++ b/osu.Game/Screens/Multi/IRoomManager.cs @@ -14,6 +14,11 @@ namespace osu.Game.Screens.Multi /// event Action RoomsUpdated; + /// + /// Whether an initial listing of rooms has been received. + /// + Bindable InitialRoomsReceived { get; } + /// /// All the active s. /// diff --git a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs index 7c10f0f975..d4b6a3b79f 100644 --- a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs @@ -22,12 +22,16 @@ namespace osu.Game.Screens.Multi.Lounge protected readonly FilterControl Filter; + private readonly Bindable initialRoomsReceived = new Bindable(); + private readonly Container content; private readonly LoadingLayer loadingLayer; [Resolved] private Bindable selectedRoom { get; set; } + private bool joiningRoom; + public LoungeSubScreen() { SearchContainer searchContainer; @@ -73,6 +77,14 @@ namespace osu.Game.Screens.Multi.Lounge }; } + protected override void LoadComplete() + { + base.LoadComplete(); + + initialRoomsReceived.BindTo(RoomManager.InitialRoomsReceived); + initialRoomsReceived.BindValueChanged(onInitialRoomsReceivedChanged, true); + } + protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); @@ -126,12 +138,29 @@ namespace osu.Game.Screens.Multi.Lounge private void joinRequested(Room room) { - loadingLayer.Show(); + joiningRoom = true; + updateLoadingLayer(); + RoomManager?.JoinRoom(room, r => { Open(room); + joiningRoom = false; + updateLoadingLayer(); + }, _ => + { + joiningRoom = false; + updateLoadingLayer(); + }); + } + + private void onInitialRoomsReceivedChanged(ValueChangedEvent received) => updateLoadingLayer(); + + private void updateLoadingLayer() + { + if (joiningRoom || !initialRoomsReceived.Value) + loadingLayer.Show(); + else loadingLayer.Hide(); - }, _ => loadingLayer.Hide()); } /// diff --git a/osu.Game/Screens/Multi/RoomManager.cs b/osu.Game/Screens/Multi/RoomManager.cs index ad461af57f..4d6ac46c84 100644 --- a/osu.Game/Screens/Multi/RoomManager.cs +++ b/osu.Game/Screens/Multi/RoomManager.cs @@ -25,6 +25,9 @@ namespace osu.Game.Screens.Multi public event Action RoomsUpdated; private readonly BindableList rooms = new BindableList(); + + public Bindable InitialRoomsReceived { get; } = new Bindable(); + public IBindableList Rooms => rooms; public double TimeBetweenListingPolls @@ -62,7 +65,11 @@ namespace osu.Game.Screens.Multi InternalChildren = new Drawable[] { - listingPollingComponent = new ListingPollingComponent { RoomsReceived = onListingReceived }, + listingPollingComponent = new ListingPollingComponent + { + InitialRoomsReceived = { BindTarget = InitialRoomsReceived }, + RoomsReceived = onListingReceived + }, selectionPollingComponent = new SelectionPollingComponent { RoomReceived = onSelectedRoomReceived } }; } @@ -262,6 +269,8 @@ namespace osu.Game.Screens.Multi { public Action> RoomsReceived; + public readonly Bindable InitialRoomsReceived = new Bindable(); + [Resolved] private IAPIProvider api { get; set; } @@ -273,6 +282,8 @@ namespace osu.Game.Screens.Multi { currentFilter.BindValueChanged(_ => { + InitialRoomsReceived.Value = false; + if (IsLoaded) PollImmediately(); }); @@ -292,6 +303,7 @@ namespace osu.Game.Screens.Multi pollReq.Success += result => { + InitialRoomsReceived.Value = true; RoomsReceived?.Invoke(result); tcs.SetResult(true); }; From 0f78af7252a08179cce65ee47d9bbe3acbf70c00 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 6 Jun 2020 19:19:30 +0300 Subject: [PATCH 1957/2376] Remove unnecessary disabled check I have a bad memory here, til. --- osu.Game/Tests/Visual/OsuTestScene.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index 88bd087215..e5d5442074 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -146,8 +146,7 @@ namespace osu.Game.Tests.Visual [BackgroundDependencyLoader] private void load(RulesetStore rulesets) { - if (!Ruleset.Disabled) - Ruleset.Value = rulesets.AvailableRulesets.First(); + Ruleset.Value = rulesets.AvailableRulesets.First(); } protected override void Dispose(bool isDisposing) From efd5e144103cfb7a06563674f23d712017550dd2 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 6 Jun 2020 19:20:06 +0300 Subject: [PATCH 1958/2376] Clarify why ruleset bindable must be set at the BDL of any base test scene --- osu.Game/Tests/Visual/PlayerTestScene.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Tests/Visual/PlayerTestScene.cs b/osu.Game/Tests/Visual/PlayerTestScene.cs index 1e267726e0..05b1eea6b3 100644 --- a/osu.Game/Tests/Visual/PlayerTestScene.cs +++ b/osu.Game/Tests/Visual/PlayerTestScene.cs @@ -34,6 +34,8 @@ namespace osu.Game.Tests.Visual [BackgroundDependencyLoader] private void load() { + // There are test scenes using current value of the ruleset bindable + // on their BDLs (example in TestSceneSliderSnaking's BDL) Ruleset.Value = ruleset.RulesetInfo; Dependencies.Cache(LocalConfig = new OsuConfigManager(LocalStorage)); From 101604e741c70ffa92f0b10c39191d687749316b Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Mon, 8 Jun 2020 00:39:33 +0200 Subject: [PATCH 1959/2376] Redesign classes and generally improve code --- osu.Desktop/Updater/SquirrelUpdateManager.cs | 20 +++------ .../Sections/General/UpdateSettings.cs | 14 +++--- osu.Game/Updater/SimpleUpdateManager.cs | 15 +------ osu.Game/Updater/UpdateManager.cs | 43 +++++++++++++------ 4 files changed, 43 insertions(+), 49 deletions(-) diff --git a/osu.Desktop/Updater/SquirrelUpdateManager.cs b/osu.Desktop/Updater/SquirrelUpdateManager.cs index 5c553f18f4..c55917fb5f 100644 --- a/osu.Desktop/Updater/SquirrelUpdateManager.cs +++ b/osu.Desktop/Updater/SquirrelUpdateManager.cs @@ -22,33 +22,25 @@ namespace osu.Desktop.Updater { public class SquirrelUpdateManager : osu.Game.Updater.UpdateManager { - public override bool CanCheckForUpdate => true; - private UpdateManager updateManager; private NotificationOverlay notificationOverlay; - private OsuGameBase gameBase; public Task PrepareUpdateAsync() => UpdateManager.RestartAppWhenExited(); private static readonly Logger logger = Logger.GetLogger("updater"); [BackgroundDependencyLoader] - private void load(NotificationOverlay notification, OsuGameBase game) + private void load(NotificationOverlay notification) { - gameBase = game; notificationOverlay = notification; Splat.Locator.CurrentMutable.Register(() => new SquirrelLogger(), typeof(Splat.ILogger)); - CheckForUpdate(); + Schedule(() => Task.Run(CheckForUpdateAsync)); } - public override void CheckForUpdate() - { - if (gameBase.IsDeployedBuild) - Schedule(() => Task.Run(() => checkForUpdateAsync())); - } + protected override async Task InternalCheckForUpdateAsync() => await checkForUpdateAsync(); - private async void checkForUpdateAsync(bool useDeltaPatching = true, UpdateProgressNotification notification = null) + private async Task checkForUpdateAsync(bool useDeltaPatching = true, UpdateProgressNotification notification = null) { // should we schedule a retry on completion of this check? bool scheduleRecheck = true; @@ -90,7 +82,7 @@ namespace osu.Desktop.Updater // could fail if deltas are unavailable for full update path (https://github.com/Squirrel/Squirrel.Windows/issues/959) // try again without deltas. - checkForUpdateAsync(false, notification); + await checkForUpdateAsync(false, notification); scheduleRecheck = false; } else @@ -109,7 +101,7 @@ namespace osu.Desktop.Updater if (scheduleRecheck) { // check again in 30 minutes. - Scheduler.AddDelayed(() => checkForUpdateAsync(), 60000 * 30); + Scheduler.AddDelayed(async () => await checkForUpdateAsync(), 60000 * 30); } } } diff --git a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs index 62d1ef162f..4a2a50885e 100644 --- a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Threading.Tasks; using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Platform; @@ -28,15 +29,12 @@ namespace osu.Game.Overlays.Settings.Sections.General }); // We should only display the button for UpdateManagers that do check for updates - if (updateManager?.CanCheckForUpdate == true) + Add(new SettingsButton { - Add(new SettingsButton - { - Text = "Check for updates", - Action = updateManager.CheckForUpdate, - Enabled = { Value = game.IsDeployedBuild } - }); - } + Text = "Check for updates", + Action = () => Schedule(() => Task.Run(updateManager.CheckForUpdateAsync)), + Enabled = { Value = updateManager.CanCheckForUpdate } + }); if (RuntimeInfo.IsDesktop) { diff --git a/osu.Game/Updater/SimpleUpdateManager.cs b/osu.Game/Updater/SimpleUpdateManager.cs index d4e8aed5ae..78d27ab754 100644 --- a/osu.Game/Updater/SimpleUpdateManager.cs +++ b/osu.Game/Updater/SimpleUpdateManager.cs @@ -19,31 +19,20 @@ namespace osu.Game.Updater /// public class SimpleUpdateManager : UpdateManager { - public override bool CanCheckForUpdate => true; - private string version; [Resolved] private GameHost host { get; set; } - private OsuGameBase gameBase; - [BackgroundDependencyLoader] private void load(OsuGameBase game) { - gameBase = game; version = game.Version; - CheckForUpdate(); + Schedule(() => Task.Run(CheckForUpdateAsync)); } - public override void CheckForUpdate() - { - if (gameBase.IsDeployedBuild) - Schedule(() => Task.Run(checkForUpdateAsync)); - } - - private async void checkForUpdateAsync() + protected override async Task InternalCheckForUpdateAsync() { try { diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs index 41bbfb76a5..abe21f08a4 100644 --- a/osu.Game/Updater/UpdateManager.cs +++ b/osu.Game/Updater/UpdateManager.cs @@ -1,10 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; -using osu.Framework.Logging; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Overlays; @@ -18,9 +18,11 @@ namespace osu.Game.Updater public class UpdateManager : CompositeDrawable { /// - /// Whether this UpdateManager is capable of checking for updates. + /// Whether this UpdateManager should be or is capable of checking for updates. /// - public virtual bool CanCheckForUpdate => false; + public bool CanCheckForUpdate => game.IsDeployedBuild; + + private string lastVersion; [Resolved] private OsuConfigManager config { get; set; } @@ -35,24 +37,37 @@ namespace osu.Game.Updater { base.LoadComplete(); - var version = game.Version; - var lastVersion = config.Get(OsuSetting.Version); + Schedule(() => Task.Run(CheckForUpdateAsync)); - if (game.IsDeployedBuild && version != lastVersion) + // debug / local compilations will reset to a non-release string. + // can be useful to check when an install has transitioned between release and otherwise (see OsuConfigManager's migrations). + config.Set(OsuSetting.Version, game.Version); + } + + public async Task CheckForUpdateAsync() + { + if (!CanCheckForUpdate) + return; + + await InternalCheckForUpdateAsync(); + } + + protected virtual Task InternalCheckForUpdateAsync() + { + // Query last version only *once*, so the user can re-check for updates, in case they closed the notification or else. + lastVersion ??= config.Get(OsuSetting.Version); + + var version = game.Version; + + if (version != lastVersion) { // only show a notification if we've previously saved a version to the config file (ie. not the first run). if (!string.IsNullOrEmpty(lastVersion)) Notifications.Post(new UpdateCompleteNotification(version)); } - // debug / local compilations will reset to a non-release string. - // can be useful to check when an install has transitioned between release and otherwise (see OsuConfigManager's migrations). - config.Set(OsuSetting.Version, version); - } - - public virtual void CheckForUpdate() - { - Logger.Log("CheckForUpdate was called on the base class (UpdateManager)", LoggingTarget.Information); + // we aren't doing any async in this method, so we return a completed task instead. + return Task.CompletedTask; } private class UpdateCompleteNotification : SimpleNotification From 72ada020a2515a1ed839fecbeb0af52c3ce86abc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 8 Jun 2020 13:42:16 +0900 Subject: [PATCH 1960/2376] Don't attempt to use virtual track for intro sequence clock --- osu.Game/Screens/Menu/IntroTriangles.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/IntroTriangles.cs b/osu.Game/Screens/Menu/IntroTriangles.cs index 188a49c147..cb05dcc932 100644 --- a/osu.Game/Screens/Menu/IntroTriangles.cs +++ b/osu.Game/Screens/Menu/IntroTriangles.cs @@ -7,6 +7,7 @@ using System.IO; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; +using osu.Framework.Audio.Track; using osu.Framework.Screens; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -61,7 +62,7 @@ namespace osu.Game.Screens.Menu LoadComponentAsync(new TrianglesIntroSequence(logo, background) { RelativeSizeAxes = Axes.Both, - Clock = new FramedClock(MenuMusic.Value ? Track : null), + Clock = new FramedClock(MenuMusic.Value && !(Track is TrackVirtual) ? Track : null), LoadMenu = LoadMenu }, t => { From dfed27bd4633e5b2d1268f8851fec97a698d61e6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 8 Jun 2020 14:24:21 +0900 Subject: [PATCH 1961/2376] Add back stream seeking for sanity --- osu.Game/Beatmaps/BeatmapManager.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index f11e94e63d..4e3714a582 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -201,6 +201,8 @@ namespace osu.Game.Beatmaps using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) new LegacyBeatmapEncoder(beatmapContent).Encode(sw); + stream.Seek(0, SeekOrigin.Begin); + UpdateFile(setInfo, setInfo.Files.Single(f => string.Equals(f.Filename, info.Path, StringComparison.OrdinalIgnoreCase)), stream); } From 443977aa8d71071a7566a4be643ffea72b77fee1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 8 Jun 2020 14:40:17 +0900 Subject: [PATCH 1962/2376] Remove PreUpdate, update hash in Save() --- osu.Game/Beatmaps/BeatmapManager.cs | 22 ++++++++-------------- osu.Game/Database/ArchiveModelManager.cs | 11 ----------- 2 files changed, 8 insertions(+), 25 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 4e3714a582..cbcdf51551 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -203,7 +203,14 @@ namespace osu.Game.Beatmaps stream.Seek(0, SeekOrigin.Begin); - UpdateFile(setInfo, setInfo.Files.Single(f => string.Equals(f.Filename, info.Path, StringComparison.OrdinalIgnoreCase)), stream); + using (ContextFactory.GetForWrite()) + { + var beatmapInfo = setInfo.Beatmaps.Single(b => b.ID == info.ID); + beatmapInfo.MD5Hash = stream.ComputeMD5Hash(); + + stream.Seek(0, SeekOrigin.Begin); + UpdateFile(setInfo, setInfo.Files.Single(f => string.Equals(f.Filename, info.Path, StringComparison.OrdinalIgnoreCase)), stream); + } } var working = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == info.ID); @@ -211,19 +218,6 @@ namespace osu.Game.Beatmaps workingCache.Remove(working); } - protected override void PreUpdate(BeatmapSetInfo item) - { - base.PreUpdate(item); - - foreach (var info in item.Beatmaps) - { - var file = item.Files.SingleOrDefault(f => string.Equals(f.Filename, info.Path, StringComparison.OrdinalIgnoreCase))?.FileInfo.StoragePath; - - using (var stream = Files.Store.GetStream(file)) - info.MD5Hash = stream.ComputeMD5Hash(); - } - } - private readonly WeakList workingCache = new WeakList(); /// diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index b9479af623..915d980d24 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -429,21 +429,10 @@ namespace osu.Game.Database using (ContextFactory.GetForWrite()) { item.Hash = computeHash(item); - - PreUpdate(item); - ModelStore.Update(item); } } - /// - /// Perform any final actions before the update to database executes. - /// - /// The that is being updated. - protected virtual void PreUpdate(TModel item) - { - } - /// /// Delete an item from the manager. /// Is a no-op for already deleted items. From 63003757c4fee77ca055861cb0538019509138a9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 8 Jun 2020 14:48:26 +0900 Subject: [PATCH 1963/2376] Remove WorkingBeatmap cache when deleting or updating a beatmap --- osu.Game/Beatmaps/BeatmapManager.cs | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index e7cef13c68..73e4c119e4 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -79,6 +79,8 @@ namespace osu.Game.Beatmaps 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; onlineLookupQueue = new BeatmapOnlineLookupQueue(api, storage); } @@ -206,9 +208,7 @@ namespace osu.Game.Beatmaps UpdateFile(setInfo, setInfo.Files.Single(f => string.Equals(f.Filename, info.Path, StringComparison.OrdinalIgnoreCase)), stream); } - var working = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == info.ID); - if (working != null) - workingCache.Remove(working); + removeWorkingCache(info); } private readonly WeakList workingCache = new WeakList(); @@ -410,6 +410,24 @@ 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); + } + } + public void Dispose() { onlineLookupQueue?.Dispose(); From dd61d6ed04f47aa77739e974b29949a898d79c74 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 8 Jun 2020 14:48:42 +0900 Subject: [PATCH 1964/2376] Attempt to reimport intro if a bad state is detected --- osu.Game/Screens/Menu/IntroScreen.cs | 62 ++++++++++++++++--------- osu.Game/Screens/Menu/IntroTriangles.cs | 5 +- 2 files changed, 43 insertions(+), 24 deletions(-) diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index 0d5f3d1142..b99d8ae9d1 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -41,9 +41,9 @@ namespace osu.Game.Screens.Menu protected IBindable MenuMusic { get; private set; } - private WorkingBeatmap introBeatmap; + private WorkingBeatmap initialBeatmap; - protected Track Track { get; private set; } + protected Track Track => initialBeatmap?.Track; private readonly BindableDouble exitingVolumeFade = new BindableDouble(1); @@ -58,6 +58,11 @@ namespace osu.Game.Screens.Menu [Resolved] private AudioManager audio { get; set; } + /// + /// Whether the is provided by osu! resources, rather than a user beatmap. + /// + protected bool UsingThemedIntro { get; private set; } + [BackgroundDependencyLoader] private void load(OsuConfigManager config, SkinManager skinManager, BeatmapManager beatmaps, Framework.Game game) { @@ -71,29 +76,45 @@ namespace osu.Game.Screens.Menu BeatmapSetInfo setInfo = null; + // if the user has requested not to play theme music, we should attempt to find a random beatmap from their collection. if (!MenuMusic.Value) { var sets = beatmaps.GetAllUsableBeatmapSets(IncludedDetails.Minimal); + if (sets.Count > 0) - setInfo = beatmaps.QueryBeatmapSet(s => s.ID == sets[RNG.Next(0, sets.Count - 1)].ID); - } - - if (setInfo == null) - { - setInfo = beatmaps.QueryBeatmapSet(b => b.Hash == BeatmapHash); - - if (setInfo == null) { - // we need to import the default menu background beatmap - setInfo = beatmaps.Import(new ZipArchiveReader(game.Resources.GetStream($"Tracks/{BeatmapFile}"), BeatmapFile)).Result; - - setInfo.Protected = true; - beatmaps.Update(setInfo); + setInfo = beatmaps.QueryBeatmapSet(s => s.ID == sets[RNG.Next(0, sets.Count - 1)].ID); + initialBeatmap = beatmaps.GetWorkingBeatmap(setInfo.Beatmaps[0]); } } - introBeatmap = beatmaps.GetWorkingBeatmap(setInfo.Beatmaps[0]); - Track = introBeatmap.Track; + // we generally want a song to be playing on startup, so use the intro music even if a user has specified not to if no other track is available. + if (setInfo == null) + { + if (!loadThemedIntro()) + { + // 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); + + loadThemedIntro(); + } + } + + bool loadThemedIntro() + { + setInfo = beatmaps.QueryBeatmapSet(b => b.Hash == BeatmapHash); + + if (setInfo != null) + { + initialBeatmap = beatmaps.GetWorkingBeatmap(setInfo.Beatmaps[0]); + UsingThemedIntro = !(Track is TrackVirtual); + } + + return UsingThemedIntro; + } } public override void OnResuming(IScreen last) @@ -119,7 +140,7 @@ namespace osu.Game.Screens.Menu public override void OnSuspending(IScreen next) { base.OnSuspending(next); - Track = null; + initialBeatmap = null; } protected override BackgroundScreen CreateBackground() => new BackgroundScreenBlack(); @@ -127,7 +148,7 @@ namespace osu.Game.Screens.Menu protected void StartTrack() { // Only start the current track if it is the menu music. A beatmap's track is started when entering the Main Menu. - if (MenuMusic.Value) + if (UsingThemedIntro) Track.Restart(); } @@ -141,8 +162,7 @@ namespace osu.Game.Screens.Menu if (!resuming) { - beatmap.Value = introBeatmap; - introBeatmap = null; + beatmap.Value = initialBeatmap; logo.MoveTo(new Vector2(0.5f)); logo.ScaleTo(Vector2.One); diff --git a/osu.Game/Screens/Menu/IntroTriangles.cs b/osu.Game/Screens/Menu/IntroTriangles.cs index cb05dcc932..225ad02ec4 100644 --- a/osu.Game/Screens/Menu/IntroTriangles.cs +++ b/osu.Game/Screens/Menu/IntroTriangles.cs @@ -7,7 +7,6 @@ using System.IO; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; -using osu.Framework.Audio.Track; using osu.Framework.Screens; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -47,7 +46,7 @@ namespace osu.Game.Screens.Menu [BackgroundDependencyLoader] private void load() { - if (MenuVoice.Value && !MenuMusic.Value) + if (MenuVoice.Value && !UsingThemedIntro) welcome = audio.Samples.Get(@"welcome"); } @@ -62,7 +61,7 @@ namespace osu.Game.Screens.Menu LoadComponentAsync(new TrianglesIntroSequence(logo, background) { RelativeSizeAxes = Axes.Both, - Clock = new FramedClock(MenuMusic.Value && !(Track is TrackVirtual) ? Track : null), + Clock = new FramedClock(UsingThemedIntro ? Track : null), LoadMenu = LoadMenu }, t => { From 712fd6a944cc957bcff10721451ffe613c7180c0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 8 Jun 2020 17:49:45 +0900 Subject: [PATCH 1965/2376] Fetch existing private message channels on re-joining --- .../API/Requests/CreateChannelRequest.cs | 34 +++++++++++++++++++ .../API/Requests/Responses/APIChatChannel.cs | 18 ++++++++++ osu.Game/Online/Chat/Channel.cs | 3 +- osu.Game/Online/Chat/ChannelManager.cs | 29 ++++++++++------ 4 files changed, 73 insertions(+), 11 deletions(-) create mode 100644 osu.Game/Online/API/Requests/CreateChannelRequest.cs create mode 100644 osu.Game/Online/API/Requests/Responses/APIChatChannel.cs diff --git a/osu.Game/Online/API/Requests/CreateChannelRequest.cs b/osu.Game/Online/API/Requests/CreateChannelRequest.cs new file mode 100644 index 0000000000..42cb201969 --- /dev/null +++ b/osu.Game/Online/API/Requests/CreateChannelRequest.cs @@ -0,0 +1,34 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using System.Net.Http; +using osu.Framework.IO.Network; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Chat; + +namespace osu.Game.Online.API.Requests +{ + public class CreateChannelRequest : APIRequest + { + private readonly Channel channel; + + public CreateChannelRequest(Channel channel) + { + this.channel = channel; + } + + protected override WebRequest CreateWebRequest() + { + var req = base.CreateWebRequest(); + req.Method = HttpMethod.Post; + + req.AddParameter("type", $"{ChannelType.PM}"); + req.AddParameter("target_id", $"{channel.Users.First().Id}"); + + return req; + } + + protected override string Target => @"chat/channels"; + } +} diff --git a/osu.Game/Online/API/Requests/Responses/APIChatChannel.cs b/osu.Game/Online/API/Requests/Responses/APIChatChannel.cs new file mode 100644 index 0000000000..fc3b2a8e31 --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/APIChatChannel.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 Newtonsoft.Json; +using osu.Game.Online.Chat; + +namespace osu.Game.Online.API.Requests.Responses +{ + public class APIChatChannel + { + [JsonProperty(@"channel_id")] + public int? ChannelID { get; set; } + + [JsonProperty(@"recent_messages")] + public List RecentMessages { get; set; } + } +} diff --git a/osu.Game/Online/Chat/Channel.cs b/osu.Game/Online/Chat/Channel.cs index dbb2da5c03..8c1e1ad128 100644 --- a/osu.Game/Online/Chat/Channel.cs +++ b/osu.Game/Online/Chat/Channel.cs @@ -84,7 +84,8 @@ namespace osu.Game.Online.Chat public long? LastReadId; /// - /// Signalles if the current user joined this channel or not. Defaults to false. + /// Signals if the current user joined this channel or not. Defaults to false. + /// Note that this does not guarantee a join has completed. Check Id > 0 for confirmation. /// public Bindable Joined = new Bindable(); diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index b17e0812da..9350887feb 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -86,7 +86,7 @@ namespace osu.Game.Online.Chat return; CurrentChannel.Value = JoinedChannels.FirstOrDefault(c => c.Type == ChannelType.PM && c.Users.Count == 1 && c.Users.Any(u => u.Id == user.Id)) - ?? new Channel(user); + ?? JoinChannel(new Channel(user)); } private void currentChannelChanged(ValueChangedEvent e) @@ -140,7 +140,7 @@ namespace osu.Game.Online.Chat target.AddLocalEcho(message); // if this is a PM and the first message, we need to do a special request to create the PM channel - if (target.Type == ChannelType.PM && !target.Joined.Value) + if (target.Type == ChannelType.PM && target.Id == 0) { var createNewPrivateMessageRequest = new CreateNewPrivateMessageRequest(target.Users.First(), message); @@ -356,26 +356,35 @@ namespace osu.Game.Online.Chat // ensure we are joined to the channel if (!channel.Joined.Value) { + channel.Joined.Value = true; + switch (channel.Type) { case ChannelType.Multiplayer: // join is implicit. happens when you join a multiplayer game. // this will probably change in the future. - channel.Joined.Value = true; joinChannel(channel, fetchInitialMessages); return channel; - case ChannelType.Private: - // can't do this yet. + case ChannelType.PM: + var createRequest = new CreateChannelRequest(channel); + createRequest.Success += resChannel => + { + if (resChannel.ChannelID.HasValue) + { + channel.Id = resChannel.ChannelID.Value; + + handleChannelMessages(resChannel.RecentMessages); + channel.MessagesLoaded = true; // this will mark the channel as having received messages even if there were none. + } + }; + + api.Queue(createRequest); break; default: var req = new JoinChannelRequest(channel, api.LocalUser.Value); - req.Success += () => - { - channel.Joined.Value = true; - joinChannel(channel, fetchInitialMessages); - }; + req.Success += () => joinChannel(channel, fetchInitialMessages); req.Failure += ex => LeaveChannel(channel); api.Queue(req); return channel; From ff555c41c6b667ebd91ba46f166cbd247ffeece3 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2020 08:57:44 +0000 Subject: [PATCH 1966/2376] Bump Sentry from 2.1.1 to 2.1.3 Bumps [Sentry](https://github.com/getsentry/sentry-dotnet) from 2.1.1 to 2.1.3. - [Release notes](https://github.com/getsentry/sentry-dotnet/releases) - [Commits](https://github.com/getsentry/sentry-dotnet/compare/2.1.1...2.1.3) Signed-off-by: dependabot-preview[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 4d6358575b..c41d0a0cf6 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -26,7 +26,7 @@ - + From bbf8864f1478d609fe2eb7184cbd303e0cc9a14b Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2020 09:45:31 +0000 Subject: [PATCH 1967/2376] Bump Microsoft.Build.Traversal from 2.0.34 to 2.0.48 Bumps [Microsoft.Build.Traversal](https://github.com/Microsoft/MSBuildSdks) from 2.0.34 to 2.0.48. - [Release notes](https://github.com/Microsoft/MSBuildSdks/releases) - [Changelog](https://github.com/microsoft/MSBuildSdks/blob/master/RELEASE.md) - [Commits](https://github.com/Microsoft/MSBuildSdks/compare/Microsoft.Build.Traversal.2.0.34...Microsoft.Build.Traversal.2.0.48) Signed-off-by: dependabot-preview[bot] --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index 6c793a3f1d..bdb90eb0e9 100644 --- a/global.json +++ b/global.json @@ -5,6 +5,6 @@ "version": "3.1.100" }, "msbuild-sdks": { - "Microsoft.Build.Traversal": "2.0.34" + "Microsoft.Build.Traversal": "2.0.48" } } \ No newline at end of file From e0c94304c79637c86e9304e0471ce37bb139f223 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2020 09:45:31 +0000 Subject: [PATCH 1968/2376] Bump Humanizer from 2.8.11 to 2.8.26 Bumps [Humanizer](https://github.com/Humanizr/Humanizer) from 2.8.11 to 2.8.26. - [Release notes](https://github.com/Humanizr/Humanizer/releases) - [Changelog](https://github.com/Humanizr/Humanizer/blob/master/release_notes.md) - [Commits](https://github.com/Humanizr/Humanizer/compare/v2.8.11...v2.8.26) Signed-off-by: dependabot-preview[bot] --- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index c41d0a0cf6..8213719c01 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -20,7 +20,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 6b55fa51ff..fd13455c63 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -76,7 +76,7 @@ - + From f80cdeac5ce2e6820dd3ba4cb0c6d6530e08105c Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 8 Jun 2020 15:31:30 +0200 Subject: [PATCH 1969/2376] Change transforms to roughly match fallback visually --- osu.Game/Screens/Menu/IntroWelcome.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Menu/IntroWelcome.cs b/osu.Game/Screens/Menu/IntroWelcome.cs index 7019e1f1a6..8110b973f6 100644 --- a/osu.Game/Screens/Menu/IntroWelcome.cs +++ b/osu.Game/Screens/Menu/IntroWelcome.cs @@ -127,7 +127,10 @@ namespace osu.Game.Screens.Menu { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Scale = new Vector2(0.5f), + Scale = new Vector2(0.3f), + Width = 750, + Height = 78, + Alpha = 0, Texture = textures.Get(@"Welcome/welcome_text@2x") }, }; @@ -139,10 +142,11 @@ namespace osu.Game.Screens.Menu double remainingTime() => delay_step_two - TransformDelay; - using (BeginDelayedSequence(250, true)) + using (BeginDelayedSequence(0, true)) { - welcomeText.FadeIn(700); - welcomeText.ScaleTo(welcomeText.Scale + new Vector2(0.5f), remainingTime(), Easing.Out).OnComplete(_ => + welcomeText.ResizeHeightTo(welcomeText.Height*2, 500, Easing.In); + welcomeText.FadeIn(remainingTime()); + welcomeText.ScaleTo(welcomeText.Scale + new Vector2(0.1f), remainingTime(), Easing.Out).OnComplete(_ => { elementContainer.Remove(visualizer); circleContainer.Remove(blackCircle); From 8a021e0beb39a897816d8da99983eb9de6e4b419 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 8 Jun 2020 22:35:01 +0900 Subject: [PATCH 1970/2376] Use save method in test --- .../Beatmaps/IO/ImportBeatmapTest.cs | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index 55368f6676..249a8caba9 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -1,11 +1,10 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.IO; using System.Collections.Generic; using System.Linq; -using System.Text; using System.Threading; using System.Threading.Tasks; using NUnit.Framework; @@ -15,7 +14,6 @@ using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Logging; using osu.Game.Beatmaps; -using osu.Game.Beatmaps.Formats; using osu.Game.IO; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Tests.Resources; @@ -730,25 +728,17 @@ namespace osu.Game.Tests.Beatmaps.IO await osu.Dependencies.Get().Import(temp); BeatmapSetInfo setToUpdate = manager.GetAllUsableBeatmapSets()[0]; + + var beatmapInfo = setToUpdate.Beatmaps.First(b => b.RulesetID == 0); Beatmap beatmapToUpdate = (Beatmap)manager.GetWorkingBeatmap(setToUpdate.Beatmaps.First(b => b.RulesetID == 0)).Beatmap; BeatmapSetFileInfo fileToUpdate = setToUpdate.Files.First(f => beatmapToUpdate.BeatmapInfo.Path.Contains(f.Filename)); string oldMd5Hash = beatmapToUpdate.BeatmapInfo.MD5Hash; - using (var stream = new MemoryStream()) - { - using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true)) - { - beatmapToUpdate.HitObjects.Clear(); - beatmapToUpdate.HitObjects.Add(new HitCircle { StartTime = 5000 }); + beatmapToUpdate.HitObjects.Clear(); + beatmapToUpdate.HitObjects.Add(new HitCircle { StartTime = 5000 }); - new LegacyBeatmapEncoder(beatmapToUpdate).Encode(writer); - } - - stream.Seek(0, SeekOrigin.Begin); - - manager.UpdateFile(setToUpdate, fileToUpdate, stream); - } + manager.Save(beatmapInfo, beatmapToUpdate); // Check that the old file reference has been removed Assert.That(manager.QueryBeatmapSet(s => s.ID == setToUpdate.ID).Files.All(f => f.ID != fileToUpdate.ID)); From 229a40e6e36ffcba060dc4a2f8594a9f5f6eda60 Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 8 Jun 2020 15:39:15 +0200 Subject: [PATCH 1971/2376] Code formatting fixed Somehow slipped through after pushing --- osu.Game/Screens/Menu/IntroWelcome.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/IntroWelcome.cs b/osu.Game/Screens/Menu/IntroWelcome.cs index 8110b973f6..34be0b6a9f 100644 --- a/osu.Game/Screens/Menu/IntroWelcome.cs +++ b/osu.Game/Screens/Menu/IntroWelcome.cs @@ -144,7 +144,7 @@ namespace osu.Game.Screens.Menu using (BeginDelayedSequence(0, true)) { - welcomeText.ResizeHeightTo(welcomeText.Height*2, 500, Easing.In); + welcomeText.ResizeHeightTo(welcomeText.Height * 2, 500, Easing.In); welcomeText.FadeIn(remainingTime()); welcomeText.ScaleTo(welcomeText.Scale + new Vector2(0.1f), remainingTime(), Easing.Out).OnComplete(_ => { From e821d787b42993865b165f309d843cea5ae2bd38 Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 8 Jun 2020 20:13:02 +0200 Subject: [PATCH 1972/2376] Implement suggested changes Note: LogoVisualisation is likely going to be needed in a separate PR to conform to the review. --- osu.Game/Screens/Menu/IntroWelcome.cs | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/Menu/IntroWelcome.cs b/osu.Game/Screens/Menu/IntroWelcome.cs index 34be0b6a9f..4534107ae8 100644 --- a/osu.Game/Screens/Menu/IntroWelcome.cs +++ b/osu.Game/Screens/Menu/IntroWelcome.cs @@ -44,7 +44,7 @@ namespace osu.Game.Screens.Menu welcome?.Play(); pianoReverb?.Play(); - Scheduler.AddDelayed(delegate + Scheduler.AddDelayed(() => { StartTrack(); PrepareMenuLoad(); @@ -146,16 +146,7 @@ namespace osu.Game.Screens.Menu { welcomeText.ResizeHeightTo(welcomeText.Height * 2, 500, Easing.In); welcomeText.FadeIn(remainingTime()); - welcomeText.ScaleTo(welcomeText.Scale + new Vector2(0.1f), remainingTime(), Easing.Out).OnComplete(_ => - { - elementContainer.Remove(visualizer); - circleContainer.Remove(blackCircle); - elementContainer.Remove(circleContainer); - Remove(welcomeText); - visualizer.Dispose(); - blackCircle.Dispose(); - welcomeText.Dispose(); - }); + welcomeText.ScaleTo(welcomeText.Scale + new Vector2(0.1f), remainingTime(), Easing.Out).OnComplete(_ => Expire()); } } } From 2a5e96002548e306ffe0837747029d0f3f62a4f1 Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 8 Jun 2020 21:15:51 +0200 Subject: [PATCH 1973/2376] Move user and skin specific settings to a subclass --- .../Screens/Menu/BasicLogoVisualisation.cs | 229 ++++++++++++++++++ osu.Game/Screens/Menu/LogoVisualisation.cs | 216 +---------------- 2 files changed, 231 insertions(+), 214 deletions(-) create mode 100644 osu.Game/Screens/Menu/BasicLogoVisualisation.cs diff --git a/osu.Game/Screens/Menu/BasicLogoVisualisation.cs b/osu.Game/Screens/Menu/BasicLogoVisualisation.cs new file mode 100644 index 0000000000..ab86c38cb4 --- /dev/null +++ b/osu.Game/Screens/Menu/BasicLogoVisualisation.cs @@ -0,0 +1,229 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osuTK; +using osuTK.Graphics; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Batches; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.OpenGL.Vertices; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.Shaders; +using osu.Framework.Graphics.Textures; +using osu.Game.Beatmaps; +using osu.Game.Graphics; +using System; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Utils; + +namespace osu.Game.Screens.Menu +{ + public class BasicLogoVisualisation : Drawable, IHasAccentColour + { + private readonly IBindable beatmap = new Bindable(); + + /// + /// The number of bars to jump each update iteration. + /// + private const int index_change = 5; + + /// + /// The maximum length of each bar in the visualiser. Will be reduced when kiai is not activated. + /// + private const float bar_length = 600; + + /// + /// The number of bars in one rotation of the visualiser. + /// + private const int bars_per_visualiser = 200; + + /// + /// How many times we should stretch around the circumference (overlapping overselves). + /// + private const float visualiser_rounds = 5; + + /// + /// How much should each bar go down each millisecond (based on a full bar). + /// + private const float decay_per_milisecond = 0.0024f; + + /// + /// Number of milliseconds between each amplitude update. + /// + private const float time_between_updates = 50; + + /// + /// The minimum amplitude to show a bar. + /// + private const float amplitude_dead_zone = 1f / bar_length; + + private int indexOffset; + + public Color4 AccentColour { get; set; } + + private readonly float[] frequencyAmplitudes = new float[256]; + + private IShader shader; + private readonly Texture texture; + + public BasicLogoVisualisation() + { + texture = Texture.WhitePixel; + Blending = BlendingParameters.Additive; + } + + [BackgroundDependencyLoader] + private void load(ShaderManager shaders, IBindable beatmap) + { + this.beatmap.BindTo(beatmap); + shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED); + } + + private void updateAmplitudes() + { + var track = beatmap.Value.TrackLoaded ? beatmap.Value.Track : null; + var effect = beatmap.Value.BeatmapLoaded ? beatmap.Value.Beatmap?.ControlPointInfo.EffectPointAt(track?.CurrentTime ?? Time.Current) : null; + + float[] temporalAmplitudes = track?.CurrentAmplitudes.FrequencyAmplitudes; + + for (int i = 0; i < bars_per_visualiser; i++) + { + if (track?.IsRunning ?? false) + { + float targetAmplitude = (temporalAmplitudes?[(i + indexOffset) % bars_per_visualiser] ?? 0) * (effect?.KiaiMode == true ? 1 : 0.5f); + if (targetAmplitude > frequencyAmplitudes[i]) + frequencyAmplitudes[i] = targetAmplitude; + } + else + { + int index = (i + index_change) % bars_per_visualiser; + if (frequencyAmplitudes[index] > frequencyAmplitudes[i]) + frequencyAmplitudes[i] = frequencyAmplitudes[index]; + } + } + + indexOffset = (indexOffset + index_change) % bars_per_visualiser; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + var delayed = Scheduler.AddDelayed(updateAmplitudes, time_between_updates, true); + delayed.PerformRepeatCatchUpExecutions = false; + } + + protected override void Update() + { + base.Update(); + + float decayFactor = (float)Time.Elapsed * decay_per_milisecond; + + for (int i = 0; i < bars_per_visualiser; i++) + { + //3% of extra bar length to make it a little faster when bar is almost at it's minimum + frequencyAmplitudes[i] -= decayFactor * (frequencyAmplitudes[i] + 0.03f); + if (frequencyAmplitudes[i] < 0) + frequencyAmplitudes[i] = 0; + } + + Invalidate(Invalidation.DrawNode); + } + + protected override DrawNode CreateDrawNode() => new VisualisationDrawNode(this); + + private class VisualisationDrawNode : DrawNode + { + protected new BasicLogoVisualisation Source => (BasicLogoVisualisation)base.Source; + + private IShader shader; + private Texture texture; + + // Assuming the logo is a circle, we don't need a second dimension. + private float size; + + private Color4 colour; + private float[] audioData; + + private readonly QuadBatch vertexBatch = new QuadBatch(100, 10); + + public VisualisationDrawNode(BasicLogoVisualisation source) + : base(source) + { + } + + public override void ApplyState() + { + base.ApplyState(); + + shader = Source.shader; + texture = Source.texture; + size = Source.DrawSize.X; + colour = Source.AccentColour; + audioData = Source.frequencyAmplitudes; + } + + public override void Draw(Action vertexAction) + { + base.Draw(vertexAction); + + shader.Bind(); + + Vector2 inflation = DrawInfo.MatrixInverse.ExtractScale().Xy; + + ColourInfo colourInfo = DrawColourInfo.Colour; + colourInfo.ApplyChild(colour); + + if (audioData != null) + { + for (int j = 0; j < visualiser_rounds; j++) + { + for (int i = 0; i < bars_per_visualiser; i++) + { + if (audioData[i] < amplitude_dead_zone) + continue; + + float rotation = MathUtils.DegreesToRadians(i / (float)bars_per_visualiser * 360 + j * 360 / visualiser_rounds); + float rotationCos = MathF.Cos(rotation); + float rotationSin = MathF.Sin(rotation); + // taking the cos and sin to the 0..1 range + var barPosition = new Vector2(rotationCos / 2 + 0.5f, rotationSin / 2 + 0.5f) * size; + + var barSize = new Vector2(size * MathF.Sqrt(2 * (1 - MathF.Cos(MathUtils.DegreesToRadians(360f / bars_per_visualiser)))) / 2f, bar_length * audioData[i]); + // The distance between the position and the sides of the bar. + var bottomOffset = new Vector2(-rotationSin * barSize.X / 2, rotationCos * barSize.X / 2); + // The distance between the bottom side of the bar and the top side. + var amplitudeOffset = new Vector2(rotationCos * barSize.Y, rotationSin * barSize.Y); + + var rectangle = new Quad( + Vector2Extensions.Transform(barPosition - bottomOffset, DrawInfo.Matrix), + Vector2Extensions.Transform(barPosition - bottomOffset + amplitudeOffset, DrawInfo.Matrix), + Vector2Extensions.Transform(barPosition + bottomOffset, DrawInfo.Matrix), + Vector2Extensions.Transform(barPosition + bottomOffset + amplitudeOffset, DrawInfo.Matrix) + ); + + DrawQuad( + texture, + rectangle, + colourInfo, + null, + vertexBatch.AddAction, + // barSize by itself will make it smooth more in the X axis than in the Y axis, this reverts that. + Vector2.Divide(inflation, barSize.Yx)); + } + } + } + + shader.Unbind(); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + vertexBatch.Dispose(); + } + } + } +} diff --git a/osu.Game/Screens/Menu/LogoVisualisation.cs b/osu.Game/Screens/Menu/LogoVisualisation.cs index 0db7f2a2dc..e893ef91bb 100644 --- a/osu.Game/Screens/Menu/LogoVisualisation.cs +++ b/osu.Game/Screens/Menu/LogoVisualisation.cs @@ -1,90 +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 osuTK; using osuTK.Graphics; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Batches; -using osu.Framework.Graphics.Colour; -using osu.Framework.Graphics.OpenGL.Vertices; -using osu.Framework.Graphics.Primitives; -using osu.Framework.Graphics.Shaders; -using osu.Framework.Graphics.Textures; -using osu.Game.Beatmaps; -using osu.Game.Graphics; using osu.Game.Skinning; using osu.Game.Online.API; using osu.Game.Users; -using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Utils; namespace osu.Game.Screens.Menu { - public class LogoVisualisation : Drawable, IHasAccentColour + public class LogoVisualisation : BasicLogoVisualisation { - private readonly IBindable beatmap = new Bindable(); - - /// - /// The number of bars to jump each update iteration. - /// - private const int index_change = 5; - - /// - /// The maximum length of each bar in the visualiser. Will be reduced when kiai is not activated. - /// - private const float bar_length = 600; - - /// - /// The number of bars in one rotation of the visualiser. - /// - private const int bars_per_visualiser = 200; - - /// - /// How many times we should stretch around the circumference (overlapping overselves). - /// - private const float visualiser_rounds = 5; - - /// - /// How much should each bar go down each millisecond (based on a full bar). - /// - private const float decay_per_milisecond = 0.0024f; - - /// - /// Number of milliseconds between each amplitude update. - /// - private const float time_between_updates = 50; - - /// - /// The minimum amplitude to show a bar. - /// - private const float amplitude_dead_zone = 1f / bar_length; - - private int indexOffset; - - public Color4 AccentColour { get; set; } - - private readonly float[] frequencyAmplitudes = new float[256]; - - private IShader shader; - private readonly Texture texture; - private Bindable user; private Bindable skin; - public LogoVisualisation() - { - texture = Texture.WhitePixel; - Blending = BlendingParameters.Additive; - } - [BackgroundDependencyLoader] - private void load(ShaderManager shaders, IBindable beatmap, IAPIProvider api, SkinManager skinManager) + private void load(IAPIProvider api, SkinManager skinManager) { - this.beatmap.BindTo(beatmap); - shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED); user = api.LocalUser.GetBoundCopy(); skin = skinManager.CurrentSkin.GetBoundCopy(); @@ -92,32 +26,6 @@ namespace osu.Game.Screens.Menu skin.BindValueChanged(_ => updateColour(), true); } - private void updateAmplitudes() - { - var track = beatmap.Value.TrackLoaded ? beatmap.Value.Track : null; - var effect = beatmap.Value.BeatmapLoaded ? beatmap.Value.Beatmap?.ControlPointInfo.EffectPointAt(track?.CurrentTime ?? Time.Current) : null; - - float[] temporalAmplitudes = track?.CurrentAmplitudes.FrequencyAmplitudes; - - for (int i = 0; i < bars_per_visualiser; i++) - { - if (track?.IsRunning ?? false) - { - float targetAmplitude = (temporalAmplitudes?[(i + indexOffset) % bars_per_visualiser] ?? 0) * (effect?.KiaiMode == true ? 1 : 0.5f); - if (targetAmplitude > frequencyAmplitudes[i]) - frequencyAmplitudes[i] = targetAmplitude; - } - else - { - int index = (i + index_change) % bars_per_visualiser; - if (frequencyAmplitudes[index] > frequencyAmplitudes[i]) - frequencyAmplitudes[i] = frequencyAmplitudes[index]; - } - } - - indexOffset = (indexOffset + index_change) % bars_per_visualiser; - } - private void updateColour() { Color4 defaultColour = Color4.White.Opacity(0.2f); @@ -127,125 +35,5 @@ namespace osu.Game.Screens.Menu else AccentColour = defaultColour; } - - protected override void LoadComplete() - { - base.LoadComplete(); - - var delayed = Scheduler.AddDelayed(updateAmplitudes, time_between_updates, true); - delayed.PerformRepeatCatchUpExecutions = false; - } - - protected override void Update() - { - base.Update(); - - float decayFactor = (float)Time.Elapsed * decay_per_milisecond; - - for (int i = 0; i < bars_per_visualiser; i++) - { - //3% of extra bar length to make it a little faster when bar is almost at it's minimum - frequencyAmplitudes[i] -= decayFactor * (frequencyAmplitudes[i] + 0.03f); - if (frequencyAmplitudes[i] < 0) - frequencyAmplitudes[i] = 0; - } - - Invalidate(Invalidation.DrawNode); - } - - protected override DrawNode CreateDrawNode() => new VisualisationDrawNode(this); - - private class VisualisationDrawNode : DrawNode - { - protected new LogoVisualisation Source => (LogoVisualisation)base.Source; - - private IShader shader; - private Texture texture; - - // Assuming the logo is a circle, we don't need a second dimension. - private float size; - - private Color4 colour; - private float[] audioData; - - private readonly QuadBatch vertexBatch = new QuadBatch(100, 10); - - public VisualisationDrawNode(LogoVisualisation source) - : base(source) - { - } - - public override void ApplyState() - { - base.ApplyState(); - - shader = Source.shader; - texture = Source.texture; - size = Source.DrawSize.X; - colour = Source.AccentColour; - audioData = Source.frequencyAmplitudes; - } - - public override void Draw(Action vertexAction) - { - base.Draw(vertexAction); - - shader.Bind(); - - Vector2 inflation = DrawInfo.MatrixInverse.ExtractScale().Xy; - - ColourInfo colourInfo = DrawColourInfo.Colour; - colourInfo.ApplyChild(colour); - - if (audioData != null) - { - for (int j = 0; j < visualiser_rounds; j++) - { - for (int i = 0; i < bars_per_visualiser; i++) - { - if (audioData[i] < amplitude_dead_zone) - continue; - - float rotation = MathUtils.DegreesToRadians(i / (float)bars_per_visualiser * 360 + j * 360 / visualiser_rounds); - float rotationCos = MathF.Cos(rotation); - float rotationSin = MathF.Sin(rotation); - // taking the cos and sin to the 0..1 range - var barPosition = new Vector2(rotationCos / 2 + 0.5f, rotationSin / 2 + 0.5f) * size; - - var barSize = new Vector2(size * MathF.Sqrt(2 * (1 - MathF.Cos(MathUtils.DegreesToRadians(360f / bars_per_visualiser)))) / 2f, bar_length * audioData[i]); - // The distance between the position and the sides of the bar. - var bottomOffset = new Vector2(-rotationSin * barSize.X / 2, rotationCos * barSize.X / 2); - // The distance between the bottom side of the bar and the top side. - var amplitudeOffset = new Vector2(rotationCos * barSize.Y, rotationSin * barSize.Y); - - var rectangle = new Quad( - Vector2Extensions.Transform(barPosition - bottomOffset, DrawInfo.Matrix), - Vector2Extensions.Transform(barPosition - bottomOffset + amplitudeOffset, DrawInfo.Matrix), - Vector2Extensions.Transform(barPosition + bottomOffset, DrawInfo.Matrix), - Vector2Extensions.Transform(barPosition + bottomOffset + amplitudeOffset, DrawInfo.Matrix) - ); - - DrawQuad( - texture, - rectangle, - colourInfo, - null, - vertexBatch.AddAction, - // barSize by itself will make it smooth more in the X axis than in the Y axis, this reverts that. - Vector2.Divide(inflation, barSize.Yx)); - } - } - } - - shader.Unbind(); - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - vertexBatch.Dispose(); - } - } } } From d52e3f938637e26aa46e643d57ee6ed4eb25cacd Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 8 Jun 2020 21:26:48 +0200 Subject: [PATCH 1974/2376] Removed logovisualisation changes Now depends on https://github.com/ppy/osu/pull/9236 for accent color changes to apply --- osu.Game/Screens/Menu/IntroWelcome.cs | 1 - osu.Game/Screens/Menu/LogoVisualisation.cs | 8 ++------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Menu/IntroWelcome.cs b/osu.Game/Screens/Menu/IntroWelcome.cs index 4534107ae8..c1cfccaa69 100644 --- a/osu.Game/Screens/Menu/IntroWelcome.cs +++ b/osu.Game/Screens/Menu/IntroWelcome.cs @@ -101,7 +101,6 @@ namespace osu.Game.Screens.Menu Anchor = Anchor.Centre, Origin = Anchor.Centre, Alpha = 0.5f, - IsIntro = true, AccentColour = Color4.DarkBlue, Size = new Vector2(0.96f) }, diff --git a/osu.Game/Screens/Menu/LogoVisualisation.cs b/osu.Game/Screens/Menu/LogoVisualisation.cs index c72b3a6576..0db7f2a2dc 100644 --- a/osu.Game/Screens/Menu/LogoVisualisation.cs +++ b/osu.Game/Screens/Menu/LogoVisualisation.cs @@ -70,7 +70,6 @@ namespace osu.Game.Screens.Menu private IShader shader; private readonly Texture texture; - public bool IsIntro = false; private Bindable user; private Bindable skin; @@ -89,11 +88,8 @@ namespace osu.Game.Screens.Menu user = api.LocalUser.GetBoundCopy(); skin = skinManager.CurrentSkin.GetBoundCopy(); - if (!IsIntro) - { - user.ValueChanged += _ => updateColour(); - skin.BindValueChanged(_ => updateColour(), true); - } + user.ValueChanged += _ => updateColour(); + skin.BindValueChanged(_ => updateColour(), true); } private void updateAmplitudes() From 0b6ae08c93b16c7c055e99f493d52a91ff922a20 Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 8 Jun 2020 21:31:03 +0200 Subject: [PATCH 1975/2376] Removed unneeded properties --- osu.Game/Screens/Menu/IntroWelcome.cs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Menu/IntroWelcome.cs b/osu.Game/Screens/Menu/IntroWelcome.cs index c1cfccaa69..38405fab6a 100644 --- a/osu.Game/Screens/Menu/IntroWelcome.cs +++ b/osu.Game/Screens/Menu/IntroWelcome.cs @@ -71,10 +71,6 @@ namespace osu.Game.Screens.Menu private class WelcomeIntroSequence : Container { private Sprite welcomeText; - private LogoVisualisation visualizer; - private Container elementContainer; - private Container circleContainer; - private Circle blackCircle; [BackgroundDependencyLoader] private void load(TextureStore textures) @@ -90,12 +86,12 @@ namespace osu.Game.Screens.Menu AutoSizeAxes = Axes.Both, Children = new Drawable[] { - elementContainer = new Container + new Container { AutoSizeAxes = Axes.Both, Children = new Drawable[] { - visualizer = new LogoVisualisation + new LogoVisualisation { RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, @@ -104,12 +100,12 @@ namespace osu.Game.Screens.Menu AccentColour = Color4.DarkBlue, Size = new Vector2(0.96f) }, - circleContainer = new Container + new Container { AutoSizeAxes = Axes.Both, Children = new Drawable[] { - blackCircle = new Circle + new Circle { Anchor = Anchor.Centre, Origin = Anchor.Centre, From a60bb5feac2eae08be730b5def7e9a3df82c3c1d Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 8 Jun 2020 23:45:40 +0200 Subject: [PATCH 1976/2376] Rename baseclass, add xmldoc & change access to internal --- .../Screens/Menu/BasicLogoVisualisation.cs | 229 ----------------- osu.Game/Screens/Menu/LogoVisualisation.cs | 233 ++++++++++++++++-- .../Screens/Menu/MenuLogoVisualisation.cs | 39 +++ osu.Game/Screens/Menu/OsuLogo.cs | 4 +- 4 files changed, 254 insertions(+), 251 deletions(-) delete mode 100644 osu.Game/Screens/Menu/BasicLogoVisualisation.cs create mode 100644 osu.Game/Screens/Menu/MenuLogoVisualisation.cs diff --git a/osu.Game/Screens/Menu/BasicLogoVisualisation.cs b/osu.Game/Screens/Menu/BasicLogoVisualisation.cs deleted file mode 100644 index ab86c38cb4..0000000000 --- a/osu.Game/Screens/Menu/BasicLogoVisualisation.cs +++ /dev/null @@ -1,229 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osuTK; -using osuTK.Graphics; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Batches; -using osu.Framework.Graphics.Colour; -using osu.Framework.Graphics.OpenGL.Vertices; -using osu.Framework.Graphics.Primitives; -using osu.Framework.Graphics.Shaders; -using osu.Framework.Graphics.Textures; -using osu.Game.Beatmaps; -using osu.Game.Graphics; -using System; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Utils; - -namespace osu.Game.Screens.Menu -{ - public class BasicLogoVisualisation : Drawable, IHasAccentColour - { - private readonly IBindable beatmap = new Bindable(); - - /// - /// The number of bars to jump each update iteration. - /// - private const int index_change = 5; - - /// - /// The maximum length of each bar in the visualiser. Will be reduced when kiai is not activated. - /// - private const float bar_length = 600; - - /// - /// The number of bars in one rotation of the visualiser. - /// - private const int bars_per_visualiser = 200; - - /// - /// How many times we should stretch around the circumference (overlapping overselves). - /// - private const float visualiser_rounds = 5; - - /// - /// How much should each bar go down each millisecond (based on a full bar). - /// - private const float decay_per_milisecond = 0.0024f; - - /// - /// Number of milliseconds between each amplitude update. - /// - private const float time_between_updates = 50; - - /// - /// The minimum amplitude to show a bar. - /// - private const float amplitude_dead_zone = 1f / bar_length; - - private int indexOffset; - - public Color4 AccentColour { get; set; } - - private readonly float[] frequencyAmplitudes = new float[256]; - - private IShader shader; - private readonly Texture texture; - - public BasicLogoVisualisation() - { - texture = Texture.WhitePixel; - Blending = BlendingParameters.Additive; - } - - [BackgroundDependencyLoader] - private void load(ShaderManager shaders, IBindable beatmap) - { - this.beatmap.BindTo(beatmap); - shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED); - } - - private void updateAmplitudes() - { - var track = beatmap.Value.TrackLoaded ? beatmap.Value.Track : null; - var effect = beatmap.Value.BeatmapLoaded ? beatmap.Value.Beatmap?.ControlPointInfo.EffectPointAt(track?.CurrentTime ?? Time.Current) : null; - - float[] temporalAmplitudes = track?.CurrentAmplitudes.FrequencyAmplitudes; - - for (int i = 0; i < bars_per_visualiser; i++) - { - if (track?.IsRunning ?? false) - { - float targetAmplitude = (temporalAmplitudes?[(i + indexOffset) % bars_per_visualiser] ?? 0) * (effect?.KiaiMode == true ? 1 : 0.5f); - if (targetAmplitude > frequencyAmplitudes[i]) - frequencyAmplitudes[i] = targetAmplitude; - } - else - { - int index = (i + index_change) % bars_per_visualiser; - if (frequencyAmplitudes[index] > frequencyAmplitudes[i]) - frequencyAmplitudes[i] = frequencyAmplitudes[index]; - } - } - - indexOffset = (indexOffset + index_change) % bars_per_visualiser; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - var delayed = Scheduler.AddDelayed(updateAmplitudes, time_between_updates, true); - delayed.PerformRepeatCatchUpExecutions = false; - } - - protected override void Update() - { - base.Update(); - - float decayFactor = (float)Time.Elapsed * decay_per_milisecond; - - for (int i = 0; i < bars_per_visualiser; i++) - { - //3% of extra bar length to make it a little faster when bar is almost at it's minimum - frequencyAmplitudes[i] -= decayFactor * (frequencyAmplitudes[i] + 0.03f); - if (frequencyAmplitudes[i] < 0) - frequencyAmplitudes[i] = 0; - } - - Invalidate(Invalidation.DrawNode); - } - - protected override DrawNode CreateDrawNode() => new VisualisationDrawNode(this); - - private class VisualisationDrawNode : DrawNode - { - protected new BasicLogoVisualisation Source => (BasicLogoVisualisation)base.Source; - - private IShader shader; - private Texture texture; - - // Assuming the logo is a circle, we don't need a second dimension. - private float size; - - private Color4 colour; - private float[] audioData; - - private readonly QuadBatch vertexBatch = new QuadBatch(100, 10); - - public VisualisationDrawNode(BasicLogoVisualisation source) - : base(source) - { - } - - public override void ApplyState() - { - base.ApplyState(); - - shader = Source.shader; - texture = Source.texture; - size = Source.DrawSize.X; - colour = Source.AccentColour; - audioData = Source.frequencyAmplitudes; - } - - public override void Draw(Action vertexAction) - { - base.Draw(vertexAction); - - shader.Bind(); - - Vector2 inflation = DrawInfo.MatrixInverse.ExtractScale().Xy; - - ColourInfo colourInfo = DrawColourInfo.Colour; - colourInfo.ApplyChild(colour); - - if (audioData != null) - { - for (int j = 0; j < visualiser_rounds; j++) - { - for (int i = 0; i < bars_per_visualiser; i++) - { - if (audioData[i] < amplitude_dead_zone) - continue; - - float rotation = MathUtils.DegreesToRadians(i / (float)bars_per_visualiser * 360 + j * 360 / visualiser_rounds); - float rotationCos = MathF.Cos(rotation); - float rotationSin = MathF.Sin(rotation); - // taking the cos and sin to the 0..1 range - var barPosition = new Vector2(rotationCos / 2 + 0.5f, rotationSin / 2 + 0.5f) * size; - - var barSize = new Vector2(size * MathF.Sqrt(2 * (1 - MathF.Cos(MathUtils.DegreesToRadians(360f / bars_per_visualiser)))) / 2f, bar_length * audioData[i]); - // The distance between the position and the sides of the bar. - var bottomOffset = new Vector2(-rotationSin * barSize.X / 2, rotationCos * barSize.X / 2); - // The distance between the bottom side of the bar and the top side. - var amplitudeOffset = new Vector2(rotationCos * barSize.Y, rotationSin * barSize.Y); - - var rectangle = new Quad( - Vector2Extensions.Transform(barPosition - bottomOffset, DrawInfo.Matrix), - Vector2Extensions.Transform(barPosition - bottomOffset + amplitudeOffset, DrawInfo.Matrix), - Vector2Extensions.Transform(barPosition + bottomOffset, DrawInfo.Matrix), - Vector2Extensions.Transform(barPosition + bottomOffset + amplitudeOffset, DrawInfo.Matrix) - ); - - DrawQuad( - texture, - rectangle, - colourInfo, - null, - vertexBatch.AddAction, - // barSize by itself will make it smooth more in the X axis than in the Y axis, this reverts that. - Vector2.Divide(inflation, barSize.Yx)); - } - } - } - - shader.Unbind(); - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - vertexBatch.Dispose(); - } - } - } -} diff --git a/osu.Game/Screens/Menu/LogoVisualisation.cs b/osu.Game/Screens/Menu/LogoVisualisation.cs index e893ef91bb..6a28740d4e 100644 --- a/osu.Game/Screens/Menu/LogoVisualisation.cs +++ b/osu.Game/Screens/Menu/LogoVisualisation.cs @@ -1,39 +1,232 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osuTK; using osuTK.Graphics; -using osu.Game.Skinning; -using osu.Game.Online.API; -using osu.Game.Users; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Batches; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.OpenGL.Vertices; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.Shaders; +using osu.Framework.Graphics.Textures; +using osu.Game.Beatmaps; +using osu.Game.Graphics; +using System; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Utils; namespace osu.Game.Screens.Menu { - public class LogoVisualisation : BasicLogoVisualisation + /// + /// A visualiser that reacts to music coming from beatmaps. + /// + public class LogoVisualisation : Drawable, IHasAccentColour { - private Bindable user; - private Bindable skin; + private readonly IBindable beatmap = new Bindable(); - [BackgroundDependencyLoader] - private void load(IAPIProvider api, SkinManager skinManager) + /// + /// The number of bars to jump each update iteration. + /// + private const int index_change = 5; + + /// + /// The maximum length of each bar in the visualiser. Will be reduced when kiai is not activated. + /// + private const float bar_length = 600; + + /// + /// The number of bars in one rotation of the visualiser. + /// + private const int bars_per_visualiser = 200; + + /// + /// How many times we should stretch around the circumference (overlapping overselves). + /// + private const float visualiser_rounds = 5; + + /// + /// How much should each bar go down each millisecond (based on a full bar). + /// + private const float decay_per_milisecond = 0.0024f; + + /// + /// Number of milliseconds between each amplitude update. + /// + private const float time_between_updates = 50; + + /// + /// The minimum amplitude to show a bar. + /// + private const float amplitude_dead_zone = 1f / bar_length; + + private int indexOffset; + + public Color4 AccentColour { get; set; } + + private readonly float[] frequencyAmplitudes = new float[256]; + + private IShader shader; + private readonly Texture texture; + + public LogoVisualisation() { - user = api.LocalUser.GetBoundCopy(); - skin = skinManager.CurrentSkin.GetBoundCopy(); - - user.ValueChanged += _ => updateColour(); - skin.BindValueChanged(_ => updateColour(), true); + texture = Texture.WhitePixel; + Blending = BlendingParameters.Additive; } - private void updateColour() + [BackgroundDependencyLoader] + private void load(ShaderManager shaders, IBindable beatmap) { - Color4 defaultColour = Color4.White.Opacity(0.2f); + this.beatmap.BindTo(beatmap); + shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED); + } - if (user.Value?.IsSupporter ?? false) - AccentColour = skin.Value.GetConfig(GlobalSkinColours.MenuGlow)?.Value ?? defaultColour; - else - AccentColour = defaultColour; + private void updateAmplitudes() + { + var track = beatmap.Value.TrackLoaded ? beatmap.Value.Track : null; + var effect = beatmap.Value.BeatmapLoaded ? beatmap.Value.Beatmap?.ControlPointInfo.EffectPointAt(track?.CurrentTime ?? Time.Current) : null; + + float[] temporalAmplitudes = track?.CurrentAmplitudes.FrequencyAmplitudes; + + for (int i = 0; i < bars_per_visualiser; i++) + { + if (track?.IsRunning ?? false) + { + float targetAmplitude = (temporalAmplitudes?[(i + indexOffset) % bars_per_visualiser] ?? 0) * (effect?.KiaiMode == true ? 1 : 0.5f); + if (targetAmplitude > frequencyAmplitudes[i]) + frequencyAmplitudes[i] = targetAmplitude; + } + else + { + int index = (i + index_change) % bars_per_visualiser; + if (frequencyAmplitudes[index] > frequencyAmplitudes[i]) + frequencyAmplitudes[i] = frequencyAmplitudes[index]; + } + } + + indexOffset = (indexOffset + index_change) % bars_per_visualiser; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + var delayed = Scheduler.AddDelayed(updateAmplitudes, time_between_updates, true); + delayed.PerformRepeatCatchUpExecutions = false; + } + + protected override void Update() + { + base.Update(); + + float decayFactor = (float)Time.Elapsed * decay_per_milisecond; + + for (int i = 0; i < bars_per_visualiser; i++) + { + //3% of extra bar length to make it a little faster when bar is almost at it's minimum + frequencyAmplitudes[i] -= decayFactor * (frequencyAmplitudes[i] + 0.03f); + if (frequencyAmplitudes[i] < 0) + frequencyAmplitudes[i] = 0; + } + + Invalidate(Invalidation.DrawNode); + } + + protected override DrawNode CreateDrawNode() => new VisualisationDrawNode(this); + + private class VisualisationDrawNode : DrawNode + { + protected new LogoVisualisation Source => (LogoVisualisation)base.Source; + + private IShader shader; + private Texture texture; + + // Assuming the logo is a circle, we don't need a second dimension. + private float size; + + private Color4 colour; + private float[] audioData; + + private readonly QuadBatch vertexBatch = new QuadBatch(100, 10); + + public VisualisationDrawNode(LogoVisualisation source) + : base(source) + { + } + + public override void ApplyState() + { + base.ApplyState(); + + shader = Source.shader; + texture = Source.texture; + size = Source.DrawSize.X; + colour = Source.AccentColour; + audioData = Source.frequencyAmplitudes; + } + + public override void Draw(Action vertexAction) + { + base.Draw(vertexAction); + + shader.Bind(); + + Vector2 inflation = DrawInfo.MatrixInverse.ExtractScale().Xy; + + ColourInfo colourInfo = DrawColourInfo.Colour; + colourInfo.ApplyChild(colour); + + if (audioData != null) + { + for (int j = 0; j < visualiser_rounds; j++) + { + for (int i = 0; i < bars_per_visualiser; i++) + { + if (audioData[i] < amplitude_dead_zone) + continue; + + float rotation = MathUtils.DegreesToRadians(i / (float)bars_per_visualiser * 360 + j * 360 / visualiser_rounds); + float rotationCos = MathF.Cos(rotation); + float rotationSin = MathF.Sin(rotation); + // taking the cos and sin to the 0..1 range + var barPosition = new Vector2(rotationCos / 2 + 0.5f, rotationSin / 2 + 0.5f) * size; + + var barSize = new Vector2(size * MathF.Sqrt(2 * (1 - MathF.Cos(MathUtils.DegreesToRadians(360f / bars_per_visualiser)))) / 2f, bar_length * audioData[i]); + // The distance between the position and the sides of the bar. + var bottomOffset = new Vector2(-rotationSin * barSize.X / 2, rotationCos * barSize.X / 2); + // The distance between the bottom side of the bar and the top side. + var amplitudeOffset = new Vector2(rotationCos * barSize.Y, rotationSin * barSize.Y); + + var rectangle = new Quad( + Vector2Extensions.Transform(barPosition - bottomOffset, DrawInfo.Matrix), + Vector2Extensions.Transform(barPosition - bottomOffset + amplitudeOffset, DrawInfo.Matrix), + Vector2Extensions.Transform(barPosition + bottomOffset, DrawInfo.Matrix), + Vector2Extensions.Transform(barPosition + bottomOffset + amplitudeOffset, DrawInfo.Matrix) + ); + + DrawQuad( + texture, + rectangle, + colourInfo, + null, + vertexBatch.AddAction, + // barSize by itself will make it smooth more in the X axis than in the Y axis, this reverts that. + Vector2.Divide(inflation, barSize.Yx)); + } + } + } + + shader.Unbind(); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + vertexBatch.Dispose(); + } } } } diff --git a/osu.Game/Screens/Menu/MenuLogoVisualisation.cs b/osu.Game/Screens/Menu/MenuLogoVisualisation.cs new file mode 100644 index 0000000000..5eb3f1efa0 --- /dev/null +++ b/osu.Game/Screens/Menu/MenuLogoVisualisation.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 osuTK.Graphics; +using osu.Game.Skinning; +using osu.Game.Online.API; +using osu.Game.Users; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; + +namespace osu.Game.Screens.Menu +{ + internal class MenuLogoVisualisation : LogoVisualisation + { + private Bindable user; + private Bindable skin; + + [BackgroundDependencyLoader] + private void load(IAPIProvider api, SkinManager skinManager) + { + user = api.LocalUser.GetBoundCopy(); + skin = skinManager.CurrentSkin.GetBoundCopy(); + + user.ValueChanged += _ => updateColour(); + skin.BindValueChanged(_ => updateColour(), true); + } + + private void updateColour() + { + Color4 defaultColour = Color4.White.Opacity(0.2f); + + if (user.Value?.IsSupporter ?? false) + AccentColour = skin.Value.GetConfig(GlobalSkinColours.MenuGlow)?.Value ?? defaultColour; + else + AccentColour = defaultColour; + } + } +} diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index 800520100e..9cadfd7df6 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -38,7 +38,7 @@ namespace osu.Game.Screens.Menu private readonly Container logoBeatContainer; private readonly Container logoAmplitudeContainer; private readonly Container logoHoverContainer; - private readonly LogoVisualisation visualizer; + private readonly MenuLogoVisualisation visualizer; private readonly IntroSequence intro; @@ -139,7 +139,7 @@ namespace osu.Game.Screens.Menu AutoSizeAxes = Axes.Both, Children = new Drawable[] { - visualizer = new LogoVisualisation + visualizer = new MenuLogoVisualisation { RelativeSizeAxes = Axes.Both, Origin = Anchor.Centre, From 44dd7d65bee35d52cce4de57ab79080decb3dce9 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 9 Jun 2020 18:21:37 +0900 Subject: [PATCH 1977/2376] Fix duplicate scores showing --- osu.Game/Online/API/Requests/SubmitRoomScoreRequest.cs | 2 +- osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/osu.Game/Online/API/Requests/SubmitRoomScoreRequest.cs b/osu.Game/Online/API/Requests/SubmitRoomScoreRequest.cs index 50b62cd6ed..8eb2952159 100644 --- a/osu.Game/Online/API/Requests/SubmitRoomScoreRequest.cs +++ b/osu.Game/Online/API/Requests/SubmitRoomScoreRequest.cs @@ -8,7 +8,7 @@ using osu.Game.Scoring; namespace osu.Game.Online.API.Requests { - public class SubmitRoomScoreRequest : APIRequest + public class SubmitRoomScoreRequest : APIRequest { private readonly int scoreId; private readonly int roomId; diff --git a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs index fbe9e3480f..cf0197d26b 100644 --- a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs +++ b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs @@ -97,22 +97,18 @@ namespace osu.Game.Screens.Multi.Play } protected override ScoreInfo CreateScore() - { - submitScore(); - return base.CreateScore(); - } - - private void submitScore() { var score = base.CreateScore(); - score.TotalScore = (int)Math.Round(ScoreProcessor.GetStandardisedScore()); Debug.Assert(token != null); var request = new SubmitRoomScoreRequest(token.Value, roomId.Value ?? 0, playlistItem.ID, score); + request.Success += s => score.OnlineScoreID = s.ID; request.Failure += e => Logger.Error(e, "Failed to submit score"); api.Queue(request); + + return score; } protected override void Dispose(bool isDisposing) From 4fd5ff61eb1b6a01543689c04b2f84daf9160dd0 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 9 Jun 2020 18:53:55 +0900 Subject: [PATCH 1978/2376] Add loading spinner --- .../TestSceneTimeshiftResultsScreen.cs | 67 +++++++++++++++---- .../Multi/Ranking/TimeshiftResultsScreen.cs | 29 +++++++- 2 files changed, 81 insertions(+), 15 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs index 8559e7e2f4..66ebf9abda 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using NUnit.Framework; using osu.Game.Online.API; using osu.Game.Online.API.Requests; @@ -18,19 +19,52 @@ namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneTimeshiftResultsScreen : ScreenTestScene { + private bool roomsReceived; + + [SetUp] + public void Setup() => Schedule(() => + { + roomsReceived = false; + bindHandler(); + }); + [Test] public void TestShowResultsWithScore() { createResults(new TestScoreInfo(new OsuRuleset().RulesetInfo)); + AddWaitStep("wait for display", 5); } [Test] public void TestShowResultsNullScore() { createResults(null); + AddWaitStep("wait for display", 5); + } + + [Test] + public void TestShowResultsNullScoreWithDelay() + { + AddStep("bind delayed handler", () => bindHandler(3000)); + createResults(null); + AddUntilStep("wait for rooms to be received", () => roomsReceived); + AddWaitStep("wait for display", 5); } private void createResults(ScoreInfo score) + { + AddStep("load results", () => + { + LoadScreen(new TimeshiftResultsScreen(score, 1, new PlaylistItem + { + Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, + Ruleset = { Value = new OsuRuleset().RulesetInfo } + })); + }); + + } + + private void bindHandler(double delay = 0) { var roomScores = new List(); @@ -61,26 +95,31 @@ namespace osu.Game.Tests.Visual.Multiplayer }); } - AddStep("bind request handler", () => ((DummyAPIAccess)API).HandleRequest = request => + ((DummyAPIAccess)API).HandleRequest = request => { switch (request) { case GetRoomPlaylistScoresRequest r: - r.TriggerSuccess(new RoomPlaylistScores { Scores = roomScores }); + if (delay == 0) + success(); + else + { + Task.Run(async () => + { + await Task.Delay(TimeSpan.FromMilliseconds(delay)); + Schedule(success); + }); + } + + void success() + { + r.TriggerSuccess(new RoomPlaylistScores { Scores = roomScores }); + roomsReceived = true; + } + break; } - }); - - AddStep("load results", () => - { - LoadScreen(new TimeshiftResultsScreen(score, 1, new PlaylistItem - { - Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo } - })); - }); - - AddWaitStep("wait for display", 10); + }; } } } diff --git a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs index d95cee2ab8..5cafc974f1 100644 --- a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs +++ b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs @@ -4,6 +4,10 @@ using System; using System.Collections.Generic; using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.Multiplayer; @@ -17,6 +21,8 @@ namespace osu.Game.Screens.Multi.Ranking private readonly int roomId; private readonly PlaylistItem playlistItem; + private LoadingSpinner loadingLayer; + public TimeshiftResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem, bool allowRetry = true) : base(score, allowRetry) { @@ -24,10 +30,31 @@ namespace osu.Game.Screens.Multi.Ranking this.playlistItem = playlistItem; } + [BackgroundDependencyLoader] + private void load() + { + AddInternal(loadingLayer = new LoadingLayer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + X = -10, + State = { Value = Score == null ? Visibility.Visible : Visibility.Hidden }, + Padding = new MarginPadding { Bottom = TwoLayerButton.SIZE_EXTENDED.Y } + }); + } + protected override APIRequest FetchScores(Action> scoresCallback) { var req = new GetRoomPlaylistScoresRequest(roomId, playlistItem.ID); - req.Success += r => scoresCallback?.Invoke(r.Scores.Where(s => s.ID != Score?.OnlineScoreID).Select(s => s.CreateScoreInfo(playlistItem))); + + req.Success += r => + { + scoresCallback?.Invoke(r.Scores.Where(s => s.ID != Score?.OnlineScoreID).Select(s => s.CreateScoreInfo(playlistItem))); + loadingLayer.Hide(); + }; + + req.Failure += _ => loadingLayer.Hide(); + return req; } } From 05b1edb9d88135667cf9893a414fa08e4cc4dad6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 9 Jun 2020 19:01:02 +0900 Subject: [PATCH 1979/2376] Fix incorrect beatmap showing --- .../Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs | 1 - .../Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs | 6 ++---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs index 66ebf9abda..9fc7c336cb 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs @@ -61,7 +61,6 @@ namespace osu.Game.Tests.Visual.Multiplayer Ruleset = { Value = new OsuRuleset().RulesetInfo } })); }); - } private void bindHandler(double delay = 0) diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index 81d5d113ae..b06ef8ae83 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -4,11 +4,9 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Localisation; -using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -52,9 +50,9 @@ namespace osu.Game.Screens.Ranking.Expanded } [BackgroundDependencyLoader] - private void load(Bindable working) + private void load() { - var beatmap = working.Value.BeatmapInfo; + var beatmap = score.Beatmap; var metadata = beatmap.Metadata; var creator = metadata.Author?.Username; From ab10732a788c8dbe09d658e19f35c2707c70c8cd Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 9 Jun 2020 22:13:48 +0900 Subject: [PATCH 1980/2376] Remove usages of null-forgiving operator --- osu.Game.Tests/Chat/MessageFormatterTests.cs | 15 ++++++++++----- .../Visual/UserInterface/TestSceneOsuIcon.cs | 8 +++++++- osu.Game/Online/API/APIAccess.cs | 3 ++- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Chat/MessageFormatterTests.cs b/osu.Game.Tests/Chat/MessageFormatterTests.cs index d1a859c84b..600c820ce1 100644 --- a/osu.Game.Tests/Chat/MessageFormatterTests.cs +++ b/osu.Game.Tests/Chat/MessageFormatterTests.cs @@ -428,23 +428,28 @@ namespace osu.Game.Tests.Chat Assert.AreEqual(5, result.Links.Count); Link f = result.Links.Find(l => l.Url == "https://osu.ppy.sh/wiki/wiki links"); - Assert.AreEqual(44, f!.Index); + Assert.That(f, Is.Not.Null); + Assert.AreEqual(44, f.Index); Assert.AreEqual(10, f.Length); f = result.Links.Find(l => l.Url == "http://www.simple-test.com"); - Assert.AreEqual(10, f!.Index); + Assert.That(f, Is.Not.Null); + Assert.AreEqual(10, f.Index); Assert.AreEqual(11, f.Length); f = result.Links.Find(l => l.Url == "http://google.com"); - Assert.AreEqual(97, f!.Index); + Assert.That(f, Is.Not.Null); + Assert.AreEqual(97, f.Index); Assert.AreEqual(4, f.Length); f = result.Links.Find(l => l.Url == "https://osu.ppy.sh"); - Assert.AreEqual(78, f!.Index); + Assert.That(f, Is.Not.Null); + Assert.AreEqual(78, f.Index); Assert.AreEqual(18, f.Length); f = result.Links.Find(l => l.Url == "\uD83D\uDE12"); - Assert.AreEqual(101, f!.Index); + Assert.That(f, Is.Not.Null); + Assert.AreEqual(101, f.Index); Assert.AreEqual(3, f.Length); } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuIcon.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuIcon.cs index 246eb119e8..c5374d50ab 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuIcon.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuIcon.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Diagnostics; using System.Reflection; using NUnit.Framework; using osu.Framework.Extensions.IEnumerableExtensions; @@ -45,7 +46,12 @@ namespace osu.Game.Tests.Visual.UserInterface }); foreach (var p in typeof(OsuIcon).GetProperties(BindingFlags.Public | BindingFlags.Static)) - flow.Add(new Icon($"{nameof(OsuIcon)}.{p.Name}", (IconUsage)p.GetValue(null)!)); + { + var propValue = p.GetValue(null); + Debug.Assert(propValue != null); + + flow.Add(new Icon($"{nameof(OsuIcon)}.{p.Name}", (IconUsage)propValue)); + } AddStep("toggle shadows", () => flow.Children.ForEach(i => i.SpriteIcon.Shadow = !i.SpriteIcon.Shadow)); AddStep("change icons", () => flow.Children.ForEach(i => i.SpriteIcon.Icon = new IconUsage((char)(i.SpriteIcon.Icon.Icon + 1)))); diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index f9e2da9af8..4ea5c192fe 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using Newtonsoft.Json.Linq; using osu.Framework.Bindables; using osu.Framework.Extensions.ExceptionExtensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Game.Configuration; @@ -250,7 +251,7 @@ namespace osu.Game.Online.API { try { - return JObject.Parse(req.GetResponseString()).SelectToken("form_error", true)!.ToObject(); + return JObject.Parse(req.GetResponseString()).SelectToken("form_error", true).AsNonNull().ToObject(); } catch { From 7274213cce3d5a1776bcc715d0f90e73b235a808 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Jun 2020 23:30:42 +0900 Subject: [PATCH 1981/2376] 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 07be3ab0d2..596e5bfa8b 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 8213719c01..1d3bafbfd6 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -24,7 +24,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index fd13455c63..ad7850599b 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From 7dc19220e51947f92b9c1dfe381d96098ffe637e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Jun 2020 23:38:54 +0900 Subject: [PATCH 1982/2376] Apply new resharper formatting fixes --- osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs | 11 ++++++----- .../Screens/Gameplay/Components/TeamScoreDisplay.cs | 5 ++++- osu.Game/Rulesets/Mods/ModEasy.cs | 4 +++- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs index f5b20fd1c5..a69646507a 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs @@ -61,7 +61,9 @@ namespace osu.Game.Rulesets.Osu.Tests private DrawableSlider slider; [SetUpSteps] - public override void SetUpSteps() { } + public override void SetUpSteps() + { + } [TestCase(0)] [TestCase(1)] @@ -132,10 +134,9 @@ namespace osu.Game.Rulesets.Osu.Tests checkPositionChange(16600, sliderRepeat, positionDecreased); } - private void retrieveDrawableSlider(int index) => AddStep($"retrieve {(index + 1).ToOrdinalWords()} slider", () => - { - slider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.ElementAt(index); - }); + private void retrieveDrawableSlider(int index) => + AddStep($"retrieve {(index + 1).ToOrdinalWords()} slider", () => + slider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.ElementAt(index)); private void ensureSnakingIn(double startTime) => checkPositionChange(startTime, sliderEnd, positionIncreased); private void ensureNoSnakingIn(double startTime) => checkPositionChange(startTime, sliderEnd, positionRemainsSame); diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs index 3e60a03f92..da55ba53ea 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs @@ -21,7 +21,10 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components private TeamDisplay teamDisplay; - public bool ShowScore { set => teamDisplay.ShowScore = value; } + public bool ShowScore + { + set => teamDisplay.ShowScore = value; + } public TeamScoreDisplay(TeamColour teamColour) { diff --git a/osu.Game/Rulesets/Mods/ModEasy.cs b/osu.Game/Rulesets/Mods/ModEasy.cs index 7cf9656810..c6f3930029 100644 --- a/osu.Game/Rulesets/Mods/ModEasy.cs +++ b/osu.Game/Rulesets/Mods/ModEasy.cs @@ -35,7 +35,9 @@ namespace osu.Game.Rulesets.Mods private BindableNumber health; - public void ReadFromDifficulty(BeatmapDifficulty difficulty) { } + public void ReadFromDifficulty(BeatmapDifficulty difficulty) + { + } public void ApplyToDifficulty(BeatmapDifficulty difficulty) { From 880a1272288d04cf7eea92b94b39ca7037375e48 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 10 Jun 2020 00:08:48 +0900 Subject: [PATCH 1983/2376] Use async overload --- osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index f145d90356..0151678db3 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -174,7 +174,7 @@ namespace osu.Game.Tests.Beatmaps.IO // arbitrary write to non-hashed file using (var sw = new FileInfo(Directory.GetFiles(extractedFolder, "*.mp3").First()).AppendText()) - sw.WriteLine("text"); + await sw.WriteLineAsync("text"); using (var zip = ZipArchive.Create()) { From 3ae1df07b0c13acefc6a700f07f7a9b74da4a102 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 10 Jun 2020 00:09:29 +0900 Subject: [PATCH 1984/2376] Fix a couple more new formatting issues --- .../Screens/Gameplay/Components/TeamDisplay.cs | 5 ++++- osu.Game/Rulesets/Mods/ModHardRock.cs | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs index 29908e8e7c..b01c93ae03 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs @@ -14,7 +14,10 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components { private readonly TeamScore score; - public bool ShowScore { set => score.FadeTo(value ? 1 : 0, 200); } + public bool ShowScore + { + set => score.FadeTo(value ? 1 : 0, 200); + } public TeamDisplay(TournamentTeam team, TeamColour colour, Bindable currentTeamScore, int pointsToWin) : base(team) diff --git a/osu.Game/Rulesets/Mods/ModHardRock.cs b/osu.Game/Rulesets/Mods/ModHardRock.cs index 58c9a58408..0e589735c1 100644 --- a/osu.Game/Rulesets/Mods/ModHardRock.cs +++ b/osu.Game/Rulesets/Mods/ModHardRock.cs @@ -17,7 +17,9 @@ 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(BeatmapDifficulty difficulty) + { + } public void ApplyToDifficulty(BeatmapDifficulty difficulty) { From e57a2294743e216415aeffa3cfdc12514721dd49 Mon Sep 17 00:00:00 2001 From: Shivam Date: Tue, 9 Jun 2020 20:22:30 +0200 Subject: [PATCH 1985/2376] Move all the graphics related code to TournamentGame --- osu.Game.Tournament/TournamentGame.cs | 97 +++++++++++++++++++++-- osu.Game.Tournament/TournamentGameBase.cs | 94 +--------------------- 2 files changed, 92 insertions(+), 99 deletions(-) diff --git a/osu.Game.Tournament/TournamentGame.cs b/osu.Game.Tournament/TournamentGame.cs index 78bb66d553..8a0190b902 100644 --- a/osu.Game.Tournament/TournamentGame.cs +++ b/osu.Game.Tournament/TournamentGame.cs @@ -1,11 +1,19 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Drawing; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Configuration; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Colour; using osu.Game.Graphics.Cursor; using osu.Game.Tournament.Models; +using osu.Game.Graphics; +using osuTK; using osuTK.Graphics; namespace osu.Game.Tournament @@ -21,17 +29,94 @@ namespace osu.Game.Tournament public static readonly Color4 ELEMENT_FOREGROUND_COLOUR = Color4Extensions.FromHex("#000"); public static readonly Color4 TEXT_COLOUR = Color4Extensions.FromHex("#fff"); + private Drawable heightWarning; + private Bindable windowSize; + + [BackgroundDependencyLoader] + private void load(FrameworkConfigManager frameworkConfig) + { + windowSize = frameworkConfig.GetBindable(FrameworkSetting.WindowedSize); + windowSize.BindValueChanged(size => ScheduleAfterChildren(() => + { + var minWidth = (int)(size.NewValue.Height / 768f * TournamentSceneManager.REQUIRED_WIDTH) - 1; + + heightWarning.Alpha = size.NewValue.Width < minWidth ? 1 : 0; + }), true); + + AddRange(new[] + { + new Container + { + CornerRadius = 10, + Depth = float.MinValue, + Position = new Vector2(5), + Masking = true, + AutoSizeAxes = Axes.Both, + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Children = new Drawable[] + { + new Box + { + Colour = OsuColour.Gray(0.2f), + RelativeSizeAxes = Axes.Both, + }, + new TourneyButton + { + Text = "Save Changes", + Width = 140, + Height = 50, + Padding = new MarginPadding + { + Top = 10, + Left = 10, + }, + Margin = new MarginPadding + { + Right = 10, + Bottom = 10, + }, + Action = SaveChanges, + }, + } + }, + heightWarning = new Container + { + Masking = true, + CornerRadius = 5, + Depth = float.MinValue, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + Colour = Color4.Red, + RelativeSizeAxes = Axes.Both, + }, + new TournamentSpriteText + { + Text = "Please make the window wider", + Font = OsuFont.Torus.With(weight: FontWeight.Bold), + Colour = Color4.White, + Padding = new MarginPadding(20) + } + } + }, + new OsuContextMenuContainer + { + RelativeSizeAxes = Axes.Both, + Child = new TournamentSceneManager() + } + }); + } protected override void LoadComplete() { base.LoadComplete(); - Add(new OsuContextMenuContainer - { - RelativeSizeAxes = Axes.Both, - Child = new TournamentSceneManager() - }); - + MenuCursorContainer.Cursor.AlwaysPresent = true; // required for tooltip display // we don't want to show the menu cursor as it would appear on stream output. MenuCursorContainer.Cursor.Alpha = 0; } diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 85db9e61fb..cc7bb863ed 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -2,28 +2,19 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Drawing; using System.IO; using System.Linq; using Newtonsoft.Json; using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Configuration; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Textures; using osu.Framework.Input; using osu.Framework.IO.Stores; using osu.Framework.Platform; using osu.Game.Beatmaps; -using osu.Game.Graphics; using osu.Game.Online.API.Requests; using osu.Game.Tournament.IPC; using osu.Game.Tournament.Models; using osu.Game.Users; -using osuTK; -using osuTK.Graphics; using osuTK.Input; namespace osu.Game.Tournament @@ -40,19 +31,15 @@ namespace osu.Game.Tournament private TournamentStorage tournamentStorage; private DependencyContainer dependencies; - - private Bindable windowSize; private FileBasedIPC ipc; - private Drawable heightWarning; - protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { return dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); } [BackgroundDependencyLoader] - private void load(Storage storage, FrameworkConfigManager frameworkConfig) + private void load(Storage storage) { Resources.AddStore(new DllResourceStore(typeof(TournamentGameBase).Assembly)); @@ -62,83 +49,12 @@ namespace osu.Game.Tournament this.storage = storage; - windowSize = frameworkConfig.GetBindable(FrameworkSetting.WindowedSize); - windowSize.BindValueChanged(size => ScheduleAfterChildren(() => - { - var minWidth = (int)(size.NewValue.Height / 768f * TournamentSceneManager.REQUIRED_WIDTH) - 1; - - heightWarning.Alpha = size.NewValue.Width < minWidth ? 1 : 0; - }), true); - readBracket(); ladder.CurrentMatch.Value = ladder.Matches.FirstOrDefault(p => p.Current.Value); dependencies.CacheAs(ipc = new FileBasedIPC()); Add(ipc); - - AddRange(new[] - { - new Container - { - CornerRadius = 10, - Depth = float.MinValue, - Position = new Vector2(5), - Masking = true, - AutoSizeAxes = Axes.Both, - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - Children = new Drawable[] - { - new Box - { - Colour = OsuColour.Gray(0.2f), - RelativeSizeAxes = Axes.Both, - }, - new TourneyButton - { - Text = "Save Changes", - Width = 140, - Height = 50, - Padding = new MarginPadding - { - Top = 10, - Left = 10, - }, - Margin = new MarginPadding - { - Right = 10, - Bottom = 10, - }, - Action = SaveChanges, - }, - } - }, - heightWarning = new Container - { - Masking = true, - CornerRadius = 5, - Depth = float.MinValue, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Box - { - Colour = Color4.Red, - RelativeSizeAxes = Axes.Both, - }, - new TournamentSpriteText - { - Text = "Please make the window wider", - Font = OsuFont.Torus.With(weight: FontWeight.Bold), - Colour = Color4.White, - Padding = new MarginPadding(20) - } - } - }, - }); } private void readBracket() @@ -313,14 +229,6 @@ namespace osu.Game.Tournament API.Queue(req); } - protected override void LoadComplete() - { - MenuCursorContainer.Cursor.AlwaysPresent = true; // required for tooltip display - MenuCursorContainer.Cursor.Alpha = 0; - - base.LoadComplete(); - } - protected virtual void SaveChanges() { foreach (var r in ladder.Rounds) From af05ee67cbe67e0577a9cdb8bad11aa01dbfd22a Mon Sep 17 00:00:00 2001 From: Shivam Date: Tue, 9 Jun 2020 20:30:15 +0200 Subject: [PATCH 1986/2376] move base.loadcomplete to the bottom --- osu.Game.Tournament/TournamentGame.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tournament/TournamentGame.cs b/osu.Game.Tournament/TournamentGame.cs index 8a0190b902..3392440902 100644 --- a/osu.Game.Tournament/TournamentGame.cs +++ b/osu.Game.Tournament/TournamentGame.cs @@ -114,11 +114,12 @@ namespace osu.Game.Tournament protected override void LoadComplete() { - base.LoadComplete(); - MenuCursorContainer.Cursor.AlwaysPresent = true; // required for tooltip display + // we don't want to show the menu cursor as it would appear on stream output. MenuCursorContainer.Cursor.Alpha = 0; + + base.LoadComplete(); } } } From c9b4fa92f57b6c6023cbedb9c43aa846b1c7b855 Mon Sep 17 00:00:00 2001 From: Shivam Date: Tue, 9 Jun 2020 20:40:54 +0200 Subject: [PATCH 1987/2376] Hide in-game cursor manually in the testbrowser --- osu.Game.Tournament.Tests/TournamentTestBrowser.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Tournament.Tests/TournamentTestBrowser.cs b/osu.Game.Tournament.Tests/TournamentTestBrowser.cs index f7ad757926..3bc719be7c 100644 --- a/osu.Game.Tournament.Tests/TournamentTestBrowser.cs +++ b/osu.Game.Tournament.Tests/TournamentTestBrowser.cs @@ -19,6 +19,9 @@ namespace osu.Game.Tournament.Tests Depth = 10 }, AddInternal); + MenuCursorContainer.Cursor.AlwaysPresent = true; + MenuCursorContainer.Cursor.Alpha = 0; + // Have to construct this here, rather than in the constructor, because // we depend on some dependencies to be loaded within OsuGameBase.load(). Add(new TestBrowser()); From aacacd75f08f289fa77cc5d9be211145b8378b7d Mon Sep 17 00:00:00 2001 From: Shivam Date: Tue, 9 Jun 2020 21:14:05 +0200 Subject: [PATCH 1988/2376] Remove abstract from the class --- osu.Game.Tournament/TournamentGameBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index cc7bb863ed..bb8c134ecb 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -20,7 +20,7 @@ using osuTK.Input; namespace osu.Game.Tournament { [Cached(typeof(TournamentGameBase))] - public abstract class TournamentGameBase : OsuGameBase + public class TournamentGameBase : OsuGameBase { private const string bracket_filename = "bracket.json"; From 0f39558da2aa09555c0a50e5bb74b32325d07a16 Mon Sep 17 00:00:00 2001 From: Shivam Date: Wed, 10 Jun 2020 08:04:34 +0200 Subject: [PATCH 1989/2376] Apply review comment --- osu.Game.Tournament.Tests/TournamentTestBrowser.cs | 3 --- osu.Game.Tournament/TournamentGame.cs | 10 ---------- osu.Game.Tournament/TournamentGameBase.cs | 9 +++++++++ 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tournament.Tests/TournamentTestBrowser.cs b/osu.Game.Tournament.Tests/TournamentTestBrowser.cs index 3bc719be7c..f7ad757926 100644 --- a/osu.Game.Tournament.Tests/TournamentTestBrowser.cs +++ b/osu.Game.Tournament.Tests/TournamentTestBrowser.cs @@ -19,9 +19,6 @@ namespace osu.Game.Tournament.Tests Depth = 10 }, AddInternal); - MenuCursorContainer.Cursor.AlwaysPresent = true; - MenuCursorContainer.Cursor.Alpha = 0; - // Have to construct this here, rather than in the constructor, because // we depend on some dependencies to be loaded within OsuGameBase.load(). Add(new TestBrowser()); diff --git a/osu.Game.Tournament/TournamentGame.cs b/osu.Game.Tournament/TournamentGame.cs index 3392440902..7b1a174c1e 100644 --- a/osu.Game.Tournament/TournamentGame.cs +++ b/osu.Game.Tournament/TournamentGame.cs @@ -111,15 +111,5 @@ namespace osu.Game.Tournament } }); } - - protected override void LoadComplete() - { - MenuCursorContainer.Cursor.AlwaysPresent = true; // required for tooltip display - - // we don't want to show the menu cursor as it would appear on stream output. - MenuCursorContainer.Cursor.Alpha = 0; - - base.LoadComplete(); - } } } diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index bb8c134ecb..0160065cc4 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -228,6 +228,15 @@ namespace osu.Game.Tournament API.Queue(req); } + protected override void LoadComplete() + { + MenuCursorContainer.Cursor.AlwaysPresent = true; // required for tooltip display + + // we don't want to show the menu cursor as it would appear on stream output. + MenuCursorContainer.Cursor.Alpha = 0; + + base.LoadComplete(); + } protected virtual void SaveChanges() { From a43e1a0ae345722b922cd7b3bb0b533969657103 Mon Sep 17 00:00:00 2001 From: Shivam Date: Wed, 10 Jun 2020 08:41:13 +0200 Subject: [PATCH 1990/2376] Remove whitespace --- osu.Game.Tournament/TournamentGameBase.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 0160065cc4..d17b93bf5d 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -228,6 +228,7 @@ namespace osu.Game.Tournament API.Queue(req); } + protected override void LoadComplete() { MenuCursorContainer.Cursor.AlwaysPresent = true; // required for tooltip display From 4fb71eeb20dcf96a46a54d025eca6689489bf2a5 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 10 Jun 2020 18:23:31 +0300 Subject: [PATCH 1991/2376] Move setting up the ruleset bindable to top-base test scene --- osu.Game/Tests/Visual/EditorTestScene.cs | 1 - osu.Game/Tests/Visual/OsuTestScene.cs | 2 +- osu.Game/Tests/Visual/PlayerTestScene.cs | 14 ++------------ osu.Game/Tests/Visual/SkinnableTestScene.cs | 7 +------ 4 files changed, 4 insertions(+), 20 deletions(-) diff --git a/osu.Game/Tests/Visual/EditorTestScene.cs b/osu.Game/Tests/Visual/EditorTestScene.cs index 4f9a5b53b8..cd08f4712a 100644 --- a/osu.Game/Tests/Visual/EditorTestScene.cs +++ b/osu.Game/Tests/Visual/EditorTestScene.cs @@ -19,7 +19,6 @@ namespace osu.Game.Tests.Visual [BackgroundDependencyLoader] private void load() { - Ruleset.Value = CreateEditorRuleset().RulesetInfo; Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value); } diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index e5d5442074..6d0fc199c4 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -146,7 +146,7 @@ namespace osu.Game.Tests.Visual [BackgroundDependencyLoader] private void load(RulesetStore rulesets) { - Ruleset.Value = rulesets.AvailableRulesets.First(); + Ruleset.Value = CreateRuleset()?.RulesetInfo ?? rulesets.AvailableRulesets.First(); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Tests/Visual/PlayerTestScene.cs b/osu.Game/Tests/Visual/PlayerTestScene.cs index 05b1eea6b3..2c46e7f6d3 100644 --- a/osu.Game/Tests/Visual/PlayerTestScene.cs +++ b/osu.Game/Tests/Visual/PlayerTestScene.cs @@ -24,20 +24,9 @@ namespace osu.Game.Tests.Visual protected OsuConfigManager LocalConfig; - private readonly Ruleset ruleset; - - protected PlayerTestScene() - { - ruleset = CreatePlayerRuleset(); - } - [BackgroundDependencyLoader] private void load() { - // There are test scenes using current value of the ruleset bindable - // on their BDLs (example in TestSceneSliderSnaking's BDL) - Ruleset.Value = ruleset.RulesetInfo; - Dependencies.Cache(LocalConfig = new OsuConfigManager(LocalStorage)); LocalConfig.GetBindable(OsuSetting.DimLevel).Value = 1.0; } @@ -58,7 +47,7 @@ namespace osu.Game.Tests.Visual action?.Invoke(); - AddStep(ruleset.Description, LoadPlayer); + AddStep(CreatePlayerRuleset().Description, LoadPlayer); AddUntilStep("player loaded", () => Player.IsLoaded && Player.Alpha == 1); } @@ -68,6 +57,7 @@ namespace osu.Game.Tests.Visual protected void LoadPlayer() { + var ruleset = Ruleset.Value.CreateInstance(); var beatmap = CreateBeatmap(ruleset.RulesetInfo); Beatmap.Value = CreateWorkingBeatmap(beatmap); diff --git a/osu.Game/Tests/Visual/SkinnableTestScene.cs b/osu.Game/Tests/Visual/SkinnableTestScene.cs index 41147d3768..ea7cdaaac6 100644 --- a/osu.Game/Tests/Visual/SkinnableTestScene.cs +++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs @@ -23,8 +23,6 @@ namespace osu.Game.Tests.Visual { public abstract class SkinnableTestScene : OsuGridTestScene { - private readonly Ruleset ruleset; - private Skin metricsSkin; private Skin defaultSkin; private Skin specialSkin; @@ -33,14 +31,11 @@ namespace osu.Game.Tests.Visual protected SkinnableTestScene() : base(2, 3) { - ruleset = CreateRulesetForSkinProvider(); } [BackgroundDependencyLoader] private void load(AudioManager audio, SkinManager skinManager) { - Ruleset.Value = ruleset.RulesetInfo; - var dllStore = new DllResourceStore(DynamicCompilationOriginal.GetType().Assembly); metricsSkin = new TestLegacySkin(new SkinInfo { Name = "metrics-skin" }, new NamespacedResourceStore(dllStore, "Resources/metrics_skin"), audio, true); @@ -110,7 +105,7 @@ namespace osu.Game.Tests.Visual { new OutlineBox { Alpha = autoSize ? 1 : 0 }, mainProvider.WithChild( - new SkinProvidingContainer(ruleset.CreateLegacySkinProvider(mainProvider, beatmap)) + new SkinProvidingContainer(Ruleset.Value.CreateInstance().CreateLegacySkinProvider(mainProvider, beatmap)) { Child = created, RelativeSizeAxes = !autoSize ? Axes.Both : Axes.None, From b89dcb6a77de715a84faa28fd1b91fc37b167e5b Mon Sep 17 00:00:00 2001 From: Shane Woolcock Date: Thu, 11 Jun 2020 13:02:47 +0930 Subject: [PATCH 1992/2376] Fix cursor not hiding with SDL2 backend --- osu.Desktop/OsuGameDesktop.cs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index 5f74883803..bca30f3f9e 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -122,14 +122,22 @@ namespace osu.Desktop { base.SetHost(host); - if (host.Window is DesktopGameWindow desktopWindow) + switch (host.Window) { - desktopWindow.CursorState |= CursorState.Hidden; + // Legacy osuTK DesktopGameWindow + case DesktopGameWindow desktopGameWindow: + desktopGameWindow.CursorState |= CursorState.Hidden; + desktopGameWindow.SetIconFromStream(Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico")); + desktopGameWindow.Title = Name; + desktopGameWindow.FileDrop += fileDrop; + break; - desktopWindow.SetIconFromStream(Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico")); - desktopWindow.Title = Name; - - desktopWindow.FileDrop += fileDrop; + // SDL2 DesktopWindow + case DesktopWindow desktopWindow: + desktopWindow.CursorState.Value |= CursorState.Hidden; + desktopWindow.Title = Name; + desktopWindow.FileDrop += fileDrop; + break; } } From 702bd2b65d4c4546a57d83b4decaee64304013af Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 11 Jun 2020 13:41:53 +0900 Subject: [PATCH 1993/2376] Fix potential nullref in test --- .../SongSelect/TestScenePlaySongSelect.cs | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index a7e2dbeccb..f7d66ca5cf 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -38,13 +38,9 @@ namespace osu.Game.Tests.Visual.SongSelect public class TestScenePlaySongSelect : ScreenTestScene { private BeatmapManager manager; - private RulesetStore rulesets; - private MusicController music; - private WorkingBeatmap defaultBeatmap; - private TestSongSelect songSelect; [BackgroundDependencyLoader] @@ -308,15 +304,13 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert("random map selected", () => songSelect.CurrentBeatmap != defaultBeatmap); - var sortMode = config.GetBindable(OsuSetting.SongSelectSortingMode); - - AddStep(@"Sort by Artist", delegate { sortMode.Value = SortMode.Artist; }); - AddStep(@"Sort by Title", delegate { sortMode.Value = SortMode.Title; }); - AddStep(@"Sort by Author", delegate { sortMode.Value = SortMode.Author; }); - AddStep(@"Sort by DateAdded", delegate { sortMode.Value = SortMode.DateAdded; }); - AddStep(@"Sort by BPM", delegate { sortMode.Value = SortMode.BPM; }); - AddStep(@"Sort by Length", delegate { sortMode.Value = SortMode.Length; }); - AddStep(@"Sort by Difficulty", delegate { sortMode.Value = SortMode.Difficulty; }); + AddStep(@"Sort by Artist", () => config.Set(OsuSetting.SongSelectSortingMode, SortMode.Artist)); + AddStep(@"Sort by Title", () => config.Set(OsuSetting.SongSelectSortingMode, SortMode.Title)); + AddStep(@"Sort by Author", () => config.Set(OsuSetting.SongSelectSortingMode, SortMode.Author)); + AddStep(@"Sort by DateAdded", () => config.Set(OsuSetting.SongSelectSortingMode, SortMode.DateAdded)); + AddStep(@"Sort by BPM", () => config.Set(OsuSetting.SongSelectSortingMode, SortMode.BPM)); + AddStep(@"Sort by Length", () => config.Set(OsuSetting.SongSelectSortingMode, SortMode.Length)); + AddStep(@"Sort by Difficulty", () => config.Set(OsuSetting.SongSelectSortingMode, SortMode.Difficulty)); } [Test] From 7b012f1def0eb2c0e8e1d0af48ff86184224a802 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 11 Jun 2020 14:55:49 +0900 Subject: [PATCH 1994/2376] Fix test failures --- .../Background/TestSceneUserDimBackgrounds.cs | 12 ++++++-- .../TestSceneExpandedPanelMiddleContent.cs | 30 +++++++++---------- .../Expanded/ExpandedPanelMiddleContent.cs | 2 +- 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs index d601f40afe..19294d12fc 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs @@ -19,6 +19,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets; +using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Scoring; using osu.Game.Screens; @@ -27,6 +28,7 @@ using osu.Game.Screens.Play; using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Screens.Ranking; using osu.Game.Screens.Select; +using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Resources; using osu.Game.Users; using osuTK; @@ -186,9 +188,15 @@ namespace osu.Game.Tests.Visual.Background public void TestTransition() { performFullSetup(); + FadeAccessibleResults results = null; - AddStep("Transition to Results", () => player.Push(results = - new FadeAccessibleResults(new ScoreInfo { User = new User { Username = "osu!" } }))); + + AddStep("Transition to Results", () => player.Push(results = new FadeAccessibleResults(new ScoreInfo + { + User = new User { Username = "osu!" }, + Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo + }))); + AddUntilStep("Wait for results is current", () => results.IsCurrentScreen()); AddUntilStep("Screen is undimmed, original background retained", () => songSelect.IsBackgroundUndimmed() && songSelect.IsBackgroundCurrent() && results.IsBlurCorrect()); diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs index 69511b85c0..7be44a62de 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs @@ -4,7 +4,6 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -33,7 +32,10 @@ namespace osu.Game.Tests.Visual.Ranking { var author = new User { Username = "mapper_name" }; - AddStep("show example score", () => showPanel(createTestBeatmap(author), new TestScoreInfo(new OsuRuleset().RulesetInfo))); + AddStep("show example score", () => showPanel(new TestScoreInfo(new OsuRuleset().RulesetInfo) + { + Beatmap = createTestBeatmap(author) + })); AddAssert("mapper name present", () => this.ChildrenOfType().Any(spriteText => spriteText.Text == "mapper_name")); } @@ -41,38 +43,34 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestMapWithUnknownMapper() { - AddStep("show example score", () => showPanel(createTestBeatmap(null), new TestScoreInfo(new OsuRuleset().RulesetInfo))); + AddStep("show example score", () => showPanel(new TestScoreInfo(new OsuRuleset().RulesetInfo) + { + Beatmap = createTestBeatmap(null) + })); AddAssert("mapped by text not present", () => this.ChildrenOfType().All(spriteText => !containsAny(spriteText.Text, "mapped", "by"))); } - private void showPanel(WorkingBeatmap workingBeatmap, ScoreInfo score) - { - Child = new ExpandedPanelMiddleContentContainer(workingBeatmap, score); - } + private void showPanel(ScoreInfo score) => Child = new ExpandedPanelMiddleContentContainer(score); - private WorkingBeatmap createTestBeatmap(User author) + private BeatmapInfo createTestBeatmap(User author) { - var beatmap = new TestBeatmap(rulesetStore.GetRuleset(0)); + var beatmap = new TestBeatmap(rulesetStore.GetRuleset(0)).BeatmapInfo; + beatmap.Metadata.Author = author; beatmap.Metadata.Title = "Verrrrrrrrrrrrrrrrrrry looooooooooooooooooooooooong beatmap title"; beatmap.Metadata.Artist = "Verrrrrrrrrrrrrrrrrrry looooooooooooooooooooooooong beatmap artist"; - return new TestWorkingBeatmap(beatmap); + return beatmap; } private bool containsAny(string text, params string[] stringsToMatch) => stringsToMatch.Any(text.Contains); private class ExpandedPanelMiddleContentContainer : Container { - [Cached] - private Bindable workingBeatmap { get; set; } - - public ExpandedPanelMiddleContentContainer(WorkingBeatmap beatmap, ScoreInfo score) + public ExpandedPanelMiddleContentContainer(ScoreInfo score) { - workingBeatmap = new Bindable(beatmap); - Anchor = Anchor.Centre; Origin = Anchor.Centre; Size = new Vector2(ScorePanel.EXPANDED_WIDTH, 700); diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index b06ef8ae83..01502c0913 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -53,7 +53,7 @@ namespace osu.Game.Screens.Ranking.Expanded private void load() { var beatmap = score.Beatmap; - var metadata = beatmap.Metadata; + var metadata = beatmap.BeatmapSet?.Metadata ?? beatmap.Metadata; var creator = metadata.Author?.Username; var topStatistics = new List From b7c1cfbe6306b262db5eb1880c74fe105147df78 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 11 Jun 2020 15:07:14 +0900 Subject: [PATCH 1995/2376] Adjust display to avoid overlaps --- osu.Game/Screens/Multi/Components/OverlinedDisplay.cs | 2 +- osu.Game/Screens/Multi/Match/MatchSubScreen.cs | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Multi/Components/OverlinedDisplay.cs b/osu.Game/Screens/Multi/Components/OverlinedDisplay.cs index 71cabd8b50..8d8d4cc404 100644 --- a/osu.Game/Screens/Multi/Components/OverlinedDisplay.cs +++ b/osu.Game/Screens/Multi/Components/OverlinedDisplay.cs @@ -80,7 +80,7 @@ namespace osu.Game.Screens.Multi.Components }, new Drawable[] { - Content = new Container { Margin = new MarginPadding { Top = 5 } } + Content = new Container { Padding = new MarginPadding { Top = 5 } } } } }; diff --git a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs index 9296fe81bd..f837a407a5 100644 --- a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs +++ b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs @@ -130,6 +130,7 @@ namespace osu.Game.Screens.Multi.Match SelectedItem = { BindTarget = SelectedItem } } }, + null, new Drawable[] { new TriangleButton @@ -139,6 +140,12 @@ namespace osu.Game.Screens.Multi.Match Action = showBeatmapResults } } + }, + RowDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.Absolute, 5), + new Dimension(GridSizeMode.AutoSize) } } }, From fca6a6d69f3724091c2f72b1f4ea7034614633d7 Mon Sep 17 00:00:00 2001 From: Shane Woolcock Date: Fri, 12 Jun 2020 09:46:21 +0930 Subject: [PATCH 1996/2376] Implement file drop with DragDrop event --- osu.Desktop/OsuGameDesktop.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index bca30f3f9e..cd31df316a 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -10,7 +10,6 @@ using Microsoft.Win32; using osu.Desktop.Overlays; using osu.Framework.Platform; using osu.Game; -using osuTK.Input; using osu.Desktop.Updater; using osu.Framework; using osu.Framework.Logging; @@ -129,22 +128,20 @@ namespace osu.Desktop desktopGameWindow.CursorState |= CursorState.Hidden; desktopGameWindow.SetIconFromStream(Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico")); desktopGameWindow.Title = Name; - desktopGameWindow.FileDrop += fileDrop; + desktopGameWindow.FileDrop += (_, e) => fileDrop(e.FileNames); break; // SDL2 DesktopWindow case DesktopWindow desktopWindow: desktopWindow.CursorState.Value |= CursorState.Hidden; desktopWindow.Title = Name; - desktopWindow.FileDrop += fileDrop; + desktopWindow.DragDrop += f => fileDrop(new[] { f }); break; } } - private void fileDrop(object sender, FileDropEventArgs e) + private void fileDrop(string[] filePaths) { - var filePaths = e.FileNames; - var firstExtension = Path.GetExtension(filePaths.First()); if (filePaths.Any(f => Path.GetExtension(f) != firstExtension)) return; From a48e36fd31d5e4850a3f132883173a794a457c25 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 12 Jun 2020 12:58:33 +0900 Subject: [PATCH 1997/2376] Fix dotnet publish with runtime specification not working --- osu.Desktop/osu.Desktop.csproj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index c34e1e1221..7a99c70999 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -30,6 +30,10 @@ + + + + From 91b6979c970cce7f6eadc11533b34848add6b8a8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Jun 2020 13:38:20 +0900 Subject: [PATCH 1998/2376] Fix LoadingSpinner not always playing fade in animation --- osu.Game/Graphics/UserInterface/LoadingSpinner.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Graphics/UserInterface/LoadingSpinner.cs b/osu.Game/Graphics/UserInterface/LoadingSpinner.cs index 4f4607c114..8174c4d5fe 100644 --- a/osu.Game/Graphics/UserInterface/LoadingSpinner.cs +++ b/osu.Game/Graphics/UserInterface/LoadingSpinner.cs @@ -17,6 +17,8 @@ namespace osu.Game.Graphics.UserInterface { private readonly SpriteIcon spinner; + protected override bool StartHidden => true; + protected Container MainContents; public const float TRANSITION_DURATION = 500; From 95f57ca88c3f17ddebe7e449d8345e9c672d98fc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Jun 2020 18:05:23 +0900 Subject: [PATCH 1999/2376] Remove duplicate calls to CheckForUpdatesAsync --- osu.Desktop/Updater/SquirrelUpdateManager.cs | 1 - osu.Game/Updater/SimpleUpdateManager.cs | 2 -- 2 files changed, 3 deletions(-) diff --git a/osu.Desktop/Updater/SquirrelUpdateManager.cs b/osu.Desktop/Updater/SquirrelUpdateManager.cs index 3bd10215c2..748969ade5 100644 --- a/osu.Desktop/Updater/SquirrelUpdateManager.cs +++ b/osu.Desktop/Updater/SquirrelUpdateManager.cs @@ -35,7 +35,6 @@ namespace osu.Desktop.Updater notificationOverlay = notification; Splat.Locator.CurrentMutable.Register(() => new SquirrelLogger(), typeof(Splat.ILogger)); - Schedule(() => Task.Run(CheckForUpdateAsync)); } protected override async Task InternalCheckForUpdateAsync() => await checkForUpdateAsync(); diff --git a/osu.Game/Updater/SimpleUpdateManager.cs b/osu.Game/Updater/SimpleUpdateManager.cs index 78d27ab754..b61c88a280 100644 --- a/osu.Game/Updater/SimpleUpdateManager.cs +++ b/osu.Game/Updater/SimpleUpdateManager.cs @@ -28,8 +28,6 @@ namespace osu.Game.Updater private void load(OsuGameBase game) { version = game.Version; - - Schedule(() => Task.Run(CheckForUpdateAsync)); } protected override async Task InternalCheckForUpdateAsync() From 6beb28b685205a886e98235c6c332912b612ad18 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Jun 2020 18:07:39 +0900 Subject: [PATCH 2000/2376] Rename method to be less bad --- osu.Desktop/Updater/SquirrelUpdateManager.cs | 2 +- osu.Game/Updater/SimpleUpdateManager.cs | 2 +- osu.Game/Updater/UpdateManager.cs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Desktop/Updater/SquirrelUpdateManager.cs b/osu.Desktop/Updater/SquirrelUpdateManager.cs index 748969ade5..05c8e835ac 100644 --- a/osu.Desktop/Updater/SquirrelUpdateManager.cs +++ b/osu.Desktop/Updater/SquirrelUpdateManager.cs @@ -37,7 +37,7 @@ namespace osu.Desktop.Updater Splat.Locator.CurrentMutable.Register(() => new SquirrelLogger(), typeof(Splat.ILogger)); } - protected override async Task InternalCheckForUpdateAsync() => await checkForUpdateAsync(); + protected override async Task PerformUpdateCheck() => await checkForUpdateAsync(); private async Task checkForUpdateAsync(bool useDeltaPatching = true, UpdateProgressNotification notification = null) { diff --git a/osu.Game/Updater/SimpleUpdateManager.cs b/osu.Game/Updater/SimpleUpdateManager.cs index b61c88a280..ebb9995c66 100644 --- a/osu.Game/Updater/SimpleUpdateManager.cs +++ b/osu.Game/Updater/SimpleUpdateManager.cs @@ -30,7 +30,7 @@ namespace osu.Game.Updater version = game.Version; } - protected override async Task InternalCheckForUpdateAsync() + protected override async Task PerformUpdateCheck() { try { diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs index abe21f08a4..9037187e8d 100644 --- a/osu.Game/Updater/UpdateManager.cs +++ b/osu.Game/Updater/UpdateManager.cs @@ -49,10 +49,10 @@ namespace osu.Game.Updater if (!CanCheckForUpdate) return; - await InternalCheckForUpdateAsync(); + await PerformUpdateCheck(); } - protected virtual Task InternalCheckForUpdateAsync() + protected virtual Task PerformUpdateCheck() { // Query last version only *once*, so the user can re-check for updates, in case they closed the notification or else. lastVersion ??= config.Get(OsuSetting.Version); From 3dd642a33667d53d5d78a857c1f5548e335f9882 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Jun 2020 18:29:21 +0900 Subject: [PATCH 2001/2376] Ensure only one update check can be running at a time --- osu.Game/Updater/UpdateManager.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs index 9037187e8d..06d6a39066 100644 --- a/osu.Game/Updater/UpdateManager.cs +++ b/osu.Game/Updater/UpdateManager.cs @@ -44,12 +44,22 @@ namespace osu.Game.Updater config.Set(OsuSetting.Version, game.Version); } + private readonly object updateTaskLock = new object(); + + private Task updateCheckTask; + public async Task CheckForUpdateAsync() { if (!CanCheckForUpdate) return; - await PerformUpdateCheck(); + lock (updateTaskLock) + updateCheckTask ??= PerformUpdateCheck(); + + await updateCheckTask; + + lock (updateTaskLock) + updateCheckTask = null; } protected virtual Task PerformUpdateCheck() From 4f809767a5f9cf762244843021a6f33ac99c94af Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Jun 2020 18:36:36 +0900 Subject: [PATCH 2002/2376] Disable button while update check is in progress --- .../Settings/Sections/General/UpdateSettings.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs index 4a2a50885e..869e6c9c51 100644 --- a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs @@ -19,6 +19,8 @@ namespace osu.Game.Overlays.Settings.Sections.General protected override string Header => "Updates"; + private SettingsButton checkForUpdatesButton; + [BackgroundDependencyLoader(true)] private void load(Storage storage, OsuConfigManager config, OsuGame game) { @@ -29,11 +31,14 @@ namespace osu.Game.Overlays.Settings.Sections.General }); // We should only display the button for UpdateManagers that do check for updates - Add(new SettingsButton + Add(checkForUpdatesButton = new SettingsButton { Text = "Check for updates", - Action = () => Schedule(() => Task.Run(updateManager.CheckForUpdateAsync)), - Enabled = { Value = updateManager.CanCheckForUpdate } + Action = () => + { + checkForUpdatesButton.Enabled.Value = false; + Task.Run(updateManager.CheckForUpdateAsync).ContinueWith(t => Schedule(() => checkForUpdatesButton.Enabled.Value = true)); + } }); if (RuntimeInfo.IsDesktop) From 6217fb26daf0a88c9c8bd867f1efb533c0c5c1bb Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 12 Jun 2020 18:50:25 +0900 Subject: [PATCH 2003/2376] Finish up design implementation of timing distribution graph --- .../TestSceneTimingDistributionGraph.cs | 85 ++++++++++++++++++- 1 file changed, 83 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs b/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs index 456ac19383..4129975166 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs @@ -8,6 +8,8 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Osu.Scoring; using osuTK; @@ -21,7 +23,7 @@ namespace osu.Game.Tests.Visual.Ranking { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(300, 100) + Size = new Vector2(400, 130) }); } @@ -58,6 +60,17 @@ namespace osu.Game.Tests.Visual.Ranking public class TimingDistributionGraph : CompositeDrawable { + /// + /// The number of data points shown on the axis below the graph. + /// + private const float axis_points = 5; + + /// + /// An amount to adjust the value of the axis points by, effectively insetting the axis in the graph. + /// Without an inset, the final data point will be placed halfway outside the graph. + /// + private const float axis_value_inset = 0.2f; + private readonly TimingDistribution distribution; public TimingDistributionGraph(TimingDistribution distribution) @@ -74,11 +87,79 @@ namespace osu.Game.Tests.Visual.Ranking for (int i = 0; i < bars.Length; i++) bars[i] = new Bar { Height = (float)distribution.Bins[i] / maxCount }; + Container axisFlow; + InternalChild = new GridContainer { RelativeSizeAxes = Axes.Both, - Content = new[] { bars } + Content = new[] + { + new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] { bars } + } + }, + new Drawable[] + { + axisFlow = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + } + }, + }, + RowDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + } }; + + // We know the total number of bins on each side of the centre ((n - 1) / 2), and the size of each bin. + // So our axis will contain one centre element + 5 points on each side, each with a value depending on the number of bins * bin size. + int sideBins = (distribution.Bins.Length - 1) / 2; + double maxValue = sideBins * distribution.BinSize; + double axisValueStep = maxValue / axis_points * (1 - axis_value_inset); + + axisFlow.Add(new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "0", + Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold) + }); + + for (int i = 1; i <= axis_points; i++) + { + double axisValue = i * axisValueStep; + float position = (float)(axisValue / maxValue); + float alpha = 1f - position * 0.8f; + + axisFlow.Add(new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativePositionAxes = Axes.X, + X = -position / 2, + Alpha = alpha, + Text = axisValue.ToString("-0"), + Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold) + }); + + axisFlow.Add(new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativePositionAxes = Axes.X, + X = position / 2, + Alpha = alpha, + Text = axisValue.ToString("+0"), + Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold) + }); + } } private class Bar : CompositeDrawable From 35f577375c53c9f1a7c2cc159c32d468a2d9ce41 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Jun 2020 19:20:45 +0900 Subject: [PATCH 2004/2376] Restore notification code --- osu.Game/Updater/UpdateManager.cs | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs index 06d6a39066..35f9ad512f 100644 --- a/osu.Game/Updater/UpdateManager.cs +++ b/osu.Game/Updater/UpdateManager.cs @@ -39,6 +39,18 @@ namespace osu.Game.Updater Schedule(() => Task.Run(CheckForUpdateAsync)); + // Query last version only *once*, so the user can re-check for updates, in case they closed the notification or else. + lastVersion ??= config.Get(OsuSetting.Version); + + var version = game.Version; + + if (game.IsDeployedBuild && version != lastVersion) + { + // only show a notification if we've previously saved a version to the config file (ie. not the first run). + if (!string.IsNullOrEmpty(lastVersion)) + Notifications.Post(new UpdateCompleteNotification(version)); + } + // debug / local compilations will reset to a non-release string. // can be useful to check when an install has transitioned between release and otherwise (see OsuConfigManager's migrations). config.Set(OsuSetting.Version, game.Version); @@ -62,23 +74,7 @@ namespace osu.Game.Updater updateCheckTask = null; } - protected virtual Task PerformUpdateCheck() - { - // Query last version only *once*, so the user can re-check for updates, in case they closed the notification or else. - lastVersion ??= config.Get(OsuSetting.Version); - - var version = game.Version; - - if (version != lastVersion) - { - // only show a notification if we've previously saved a version to the config file (ie. not the first run). - if (!string.IsNullOrEmpty(lastVersion)) - Notifications.Post(new UpdateCompleteNotification(version)); - } - - // we aren't doing any async in this method, so we return a completed task instead. - return Task.CompletedTask; - } + protected virtual Task PerformUpdateCheck() => Task.CompletedTask; private class UpdateCompleteNotification : SimpleNotification { From 89cf146d18a804cca959ff18dfceb399bbb31828 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Jun 2020 19:24:50 +0900 Subject: [PATCH 2005/2376] Fix base UpdateManager thinking it can check for updates --- osu.Game/Updater/UpdateManager.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs index 35f9ad512f..d3a05deac5 100644 --- a/osu.Game/Updater/UpdateManager.cs +++ b/osu.Game/Updater/UpdateManager.cs @@ -20,7 +20,9 @@ namespace osu.Game.Updater /// /// Whether this UpdateManager should be or is capable of checking for updates. /// - public bool CanCheckForUpdate => game.IsDeployedBuild; + public bool CanCheckForUpdate => game.IsDeployedBuild && + // only implementations will actually check for updates. + GetType() != typeof(UpdateManager); private string lastVersion; From 446ce2590cf93c4c69072f0e384d803ded88fc16 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Jun 2020 19:25:54 +0900 Subject: [PATCH 2006/2376] Move local back in place --- osu.Game/Updater/UpdateManager.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs index d3a05deac5..51f48264b8 100644 --- a/osu.Game/Updater/UpdateManager.cs +++ b/osu.Game/Updater/UpdateManager.cs @@ -24,8 +24,6 @@ namespace osu.Game.Updater // only implementations will actually check for updates. GetType() != typeof(UpdateManager); - private string lastVersion; - [Resolved] private OsuConfigManager config { get; set; } @@ -42,7 +40,7 @@ namespace osu.Game.Updater Schedule(() => Task.Run(CheckForUpdateAsync)); // Query last version only *once*, so the user can re-check for updates, in case they closed the notification or else. - lastVersion ??= config.Get(OsuSetting.Version); + var lastVersion = config.Get(OsuSetting.Version); var version = game.Version; From f5c3863e6d97f71b65f06de59f2754b03f71f642 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Jun 2020 19:26:46 +0900 Subject: [PATCH 2007/2376] Revert variable usage --- osu.Game/Updater/UpdateManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs index 51f48264b8..bcaaf8e343 100644 --- a/osu.Game/Updater/UpdateManager.cs +++ b/osu.Game/Updater/UpdateManager.cs @@ -53,7 +53,7 @@ namespace osu.Game.Updater // debug / local compilations will reset to a non-release string. // can be useful to check when an install has transitioned between release and otherwise (see OsuConfigManager's migrations). - config.Set(OsuSetting.Version, game.Version); + config.Set(OsuSetting.Version, version); } private readonly object updateTaskLock = new object(); From 7ae421cc8e8a64f73a65e77e92c40c318f23b6a5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Jun 2020 19:32:32 +0900 Subject: [PATCH 2008/2376] Revert more incorrect changes --- osu.Game/Updater/UpdateManager.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs index bcaaf8e343..5da366bde9 100644 --- a/osu.Game/Updater/UpdateManager.cs +++ b/osu.Game/Updater/UpdateManager.cs @@ -39,11 +39,10 @@ namespace osu.Game.Updater Schedule(() => Task.Run(CheckForUpdateAsync)); - // Query last version only *once*, so the user can re-check for updates, in case they closed the notification or else. - var lastVersion = config.Get(OsuSetting.Version); - var version = game.Version; + var lastVersion = config.Get(OsuSetting.Version); + if (game.IsDeployedBuild && version != lastVersion) { // only show a notification if we've previously saved a version to the config file (ie. not the first run). From 9746e24d1efb5e85e22053e867629fc77910c4e7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Jun 2020 19:40:54 +0900 Subject: [PATCH 2009/2376] Rename abstract TestScene --- osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs | 2 +- .../Gameplay/{TestPlayerTestScene.cs => OsuPlayerTestScene.cs} | 2 +- .../Visual/Gameplay/TestSceneCompletionCancellation.cs | 2 +- osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs | 2 +- osu.Game.Tests/Visual/Gameplay/TestScenePause.cs | 2 +- osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) rename osu.Game.Tests/Visual/Gameplay/{TestPlayerTestScene.cs => OsuPlayerTestScene.cs} (87%) diff --git a/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs index 7d3d8b7f16..acefaa006a 100644 --- a/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs @@ -25,7 +25,7 @@ using osu.Game.Users; namespace osu.Game.Tests.Gameplay { [HeadlessTest] - public class TestSceneHitObjectSamples : TestPlayerTestScene + public class TestSceneHitObjectSamples : OsuPlayerTestScene { private readonly SkinInfo userSkinInfo = new SkinInfo(); diff --git a/osu.Game.Tests/Visual/Gameplay/TestPlayerTestScene.cs b/osu.Game.Tests/Visual/Gameplay/OsuPlayerTestScene.cs similarity index 87% rename from osu.Game.Tests/Visual/Gameplay/TestPlayerTestScene.cs rename to osu.Game.Tests/Visual/Gameplay/OsuPlayerTestScene.cs index bbf0136b00..cbf8515567 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestPlayerTestScene.cs +++ b/osu.Game.Tests/Visual/Gameplay/OsuPlayerTestScene.cs @@ -9,7 +9,7 @@ namespace osu.Game.Tests.Visual.Gameplay /// /// A with an arbitrary ruleset value to test with. /// - public abstract class TestPlayerTestScene : PlayerTestScene + public abstract class OsuPlayerTestScene : PlayerTestScene { protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs index f87999ae61..79275d70a7 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs @@ -16,7 +16,7 @@ using osuTK; namespace osu.Game.Tests.Visual.Gameplay { - public class TestSceneCompletionCancellation : TestPlayerTestScene + public class TestSceneCompletionCancellation : OsuPlayerTestScene { private Track track; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs index 744eeed022..2a119f5199 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs @@ -16,7 +16,7 @@ using osuTK; namespace osu.Game.Tests.Visual.Gameplay { - public class TestSceneGameplayRewinding : TestPlayerTestScene + public class TestSceneGameplayRewinding : OsuPlayerTestScene { [Resolved] private AudioManager audioManager { get; set; } diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index 411265d600..387ac42f67 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -16,7 +16,7 @@ using osuTK.Input; namespace osu.Game.Tests.Visual.Gameplay { - public class TestScenePause : TestPlayerTestScene + public class TestScenePause : OsuPlayerTestScene { protected new PausePlayer Player => (PausePlayer)base.Player; diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs index 20911bfa4d..e43e5ba3ce 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs @@ -12,7 +12,7 @@ using osu.Game.Rulesets; namespace osu.Game.Tests.Visual.Gameplay { [HeadlessTest] // we alter unsafe properties on the game host to test inactive window state. - public class TestScenePauseWhenInactive : TestPlayerTestScene + public class TestScenePauseWhenInactive : OsuPlayerTestScene { protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) { From b076cf96b71b07bdd4929f2a92eaa7f303d66a98 Mon Sep 17 00:00:00 2001 From: Power Maker Date: Fri, 12 Jun 2020 13:20:09 +0200 Subject: [PATCH 2010/2376] move cursorRotate.Value check into shouldRotateCursor() method --- osu.Game/Graphics/Cursor/MenuCursor.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/Cursor/MenuCursor.cs b/osu.Game/Graphics/Cursor/MenuCursor.cs index 507d218fb8..b89ad6a356 100644 --- a/osu.Game/Graphics/Cursor/MenuCursor.cs +++ b/osu.Game/Graphics/Cursor/MenuCursor.cs @@ -83,7 +83,7 @@ namespace osu.Game.Graphics.Cursor activeCursor.AdditiveLayer.FadeInFromZero(800, Easing.OutQuint); } - if (shouldRotateCursor(e) && cursorRotate.Value) + if (shouldRotateCursor(e)) { // if cursor is already rotating don't reset its rotate origin if (dragRotationState != DragRotationState.Rotating) @@ -126,7 +126,7 @@ namespace osu.Game.Graphics.Cursor activeCursor.ScaleTo(0.6f, 250, Easing.In); } - private static bool shouldRotateCursor(MouseEvent e) => e.IsPressed(MouseButton.Left) || e.IsPressed(MouseButton.Right); + private bool shouldRotateCursor(MouseEvent e) => cursorRotate.Value && (e.IsPressed(MouseButton.Left) || e.IsPressed(MouseButton.Right)); private static bool anyMainButtonPressed(MouseEvent e) => e.IsPressed(MouseButton.Left) || e.IsPressed(MouseButton.Middle) || e.IsPressed(MouseButton.Right); From 7c3e7b65a820365799af5ebb590276263d28bb2a Mon Sep 17 00:00:00 2001 From: mcendu Date: Fri, 12 Jun 2020 21:22:22 +0800 Subject: [PATCH 2011/2376] add custom file path support for osu\!mania judgement sprite --- .../Skinning/ManiaLegacySkinTransformer.cs | 66 +++++++++++-------- .../LegacyManiaSkinConfigurationLookup.cs | 8 ++- osu.Game/Skinning/LegacySkin.cs | 18 +++++ 3 files changed, 65 insertions(+), 27 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs index e64178083a..9ba544ed59 100644 --- a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs @@ -11,6 +11,7 @@ using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Skinning; +using System.Collections.Generic; namespace osu.Game.Rulesets.Mania.Skinning { @@ -19,6 +20,36 @@ namespace osu.Game.Rulesets.Mania.Skinning private readonly ISkin source; private readonly ManiaBeatmap beatmap; + /// + /// Mapping of to ther corresponding + /// value. + /// + private static readonly IReadOnlyDictionary componentMapping + = new Dictionary + { + { HitResult.Perfect, LegacyManiaSkinConfigurationLookups.Hit300g }, + { HitResult.Great, LegacyManiaSkinConfigurationLookups.Hit300 }, + { HitResult.Good, LegacyManiaSkinConfigurationLookups.Hit200 }, + { HitResult.Ok, LegacyManiaSkinConfigurationLookups.Hit100 }, + { HitResult.Meh, LegacyManiaSkinConfigurationLookups.Hit50 }, + { HitResult.Miss, LegacyManiaSkinConfigurationLookups.Hit0 } + }; + + /// + /// Mapping of to their corresponding + /// default filenames. + /// + private static readonly IReadOnlyDictionary defaultName + = new Dictionary + { + { HitResult.Perfect, "mania-hit300g" }, + { HitResult.Great, "mania-hit300" }, + { HitResult.Good, "mania-hit200" }, + { HitResult.Ok, "mania-hit100" }, + { HitResult.Meh, "mania-hit50" }, + { HitResult.Miss, "mania-hit0" } + }; + private Lazy isLegacySkin; /// @@ -47,15 +78,15 @@ namespace osu.Game.Rulesets.Mania.Skinning public Drawable GetDrawableComponent(ISkinComponent component) { + if (!isLegacySkin.Value || !hasKeyTexture.Value) + return null; + switch (component) { case GameplaySkinComponent resultComponent: - return getResult(resultComponent); + return getResult(resultComponent.Component); case ManiaSkinComponent maniaComponent: - if (!isLegacySkin.Value || !hasKeyTexture.Value) - return null; - switch (maniaComponent.Component) { case ManiaSkinComponents.ColumnBackground: @@ -95,30 +126,13 @@ namespace osu.Game.Rulesets.Mania.Skinning return null; } - private Drawable getResult(GameplaySkinComponent resultComponent) + private Drawable getResult(HitResult result) { - switch (resultComponent.Component) - { - case HitResult.Miss: - return this.GetAnimation("mania-hit0", true, true); + string image = GetConfig( + new ManiaSkinConfigurationLookup(componentMapping[result]) + )?.Value ?? defaultName[result]; - case HitResult.Meh: - return this.GetAnimation("mania-hit50", true, true); - - case HitResult.Ok: - return this.GetAnimation("mania-hit100", true, true); - - case HitResult.Good: - return this.GetAnimation("mania-hit200", true, true); - - case HitResult.Great: - return this.GetAnimation("mania-hit300", true, true); - - case HitResult.Perfect: - return this.GetAnimation("mania-hit300g", true, true); - } - - return null; + return this.GetAnimation(image, true, true); } public Texture GetTexture(string componentName) => source.GetTexture(componentName); diff --git a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs index c76d5c8784..4990ca8e60 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs @@ -43,6 +43,12 @@ namespace osu.Game.Skinning MinimumColumnWidth, LeftStageImage, RightStageImage, - BottomStageImage + BottomStageImage, + Hit300g, + Hit300, + Hit200, + Hit100, + Hit50, + Hit0, } } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 003fa24d5b..390dc871e4 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -257,6 +257,24 @@ namespace osu.Game.Skinning case LegacyManiaSkinConfigurationLookups.RightLineWidth: Debug.Assert(maniaLookup.TargetColumn != null); return SkinUtils.As(new Bindable(existing.ColumnLineWidth[maniaLookup.TargetColumn.Value + 1])); + + case LegacyManiaSkinConfigurationLookups.Hit0: + return SkinUtils.As(getManiaImage(existing, "Hit0")); + + case LegacyManiaSkinConfigurationLookups.Hit50: + return SkinUtils.As(getManiaImage(existing, "Hit50")); + + case LegacyManiaSkinConfigurationLookups.Hit100: + return SkinUtils.As(getManiaImage(existing, "Hit100")); + + case LegacyManiaSkinConfigurationLookups.Hit200: + return SkinUtils.As(getManiaImage(existing, "Hit200")); + + case LegacyManiaSkinConfigurationLookups.Hit300: + return SkinUtils.As(getManiaImage(existing, "Hit300")); + + case LegacyManiaSkinConfigurationLookups.Hit300g: + return SkinUtils.As(getManiaImage(existing, "Hit300g")); } return null; From 8924ff4ba6585e883b8da322d8d3b1662b1fcc76 Mon Sep 17 00:00:00 2001 From: Power Maker Date: Fri, 12 Jun 2020 15:43:19 +0200 Subject: [PATCH 2012/2376] Rename shouldRotateCursor() to shouldKeepRotating() --- osu.Game/Graphics/Cursor/MenuCursor.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Graphics/Cursor/MenuCursor.cs b/osu.Game/Graphics/Cursor/MenuCursor.cs index b89ad6a356..8305f33e25 100644 --- a/osu.Game/Graphics/Cursor/MenuCursor.cs +++ b/osu.Game/Graphics/Cursor/MenuCursor.cs @@ -83,7 +83,7 @@ namespace osu.Game.Graphics.Cursor activeCursor.AdditiveLayer.FadeInFromZero(800, Easing.OutQuint); } - if (shouldRotateCursor(e)) + if (shouldKeepRotating(e)) { // if cursor is already rotating don't reset its rotate origin if (dragRotationState != DragRotationState.Rotating) @@ -104,7 +104,7 @@ namespace osu.Game.Graphics.Cursor activeCursor.ScaleTo(1, 500, Easing.OutElastic); } - if (!shouldRotateCursor(e)) + if (!shouldKeepRotating(e)) { if (dragRotationState == DragRotationState.Rotating) activeCursor.RotateTo(0, 600 * (1 + Math.Abs(activeCursor.Rotation / 720)), Easing.OutElasticHalf); @@ -126,7 +126,7 @@ namespace osu.Game.Graphics.Cursor activeCursor.ScaleTo(0.6f, 250, Easing.In); } - private bool shouldRotateCursor(MouseEvent e) => cursorRotate.Value && (e.IsPressed(MouseButton.Left) || e.IsPressed(MouseButton.Right)); + private bool shouldKeepRotating(MouseEvent e) => cursorRotate.Value && (e.IsPressed(MouseButton.Left) || e.IsPressed(MouseButton.Right)); private static bool anyMainButtonPressed(MouseEvent e) => e.IsPressed(MouseButton.Left) || e.IsPressed(MouseButton.Middle) || e.IsPressed(MouseButton.Right); From c9469dc0ddaf98b6d119e01581e09b7c209720fe Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 12 Jun 2020 22:48:43 +0900 Subject: [PATCH 2013/2376] Add background --- .../TestSceneTimingDistributionGraph.cs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs b/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs index 4129975166..73225ff599 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs @@ -19,12 +19,20 @@ namespace osu.Game.Tests.Visual.Ranking { public TestSceneTimingDistributionGraph() { - Add(new TimingDistributionGraph(createNormalDistribution()) + Children = new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(400, 130) - }); + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#333") + }, + new TimingDistributionGraph(createNormalDistribution()) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(400, 130) + } + }; } private TimingDistribution createNormalDistribution() From ce56c457218879b78b14bd1226c300e27731e194 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 12 Jun 2020 22:48:52 +0900 Subject: [PATCH 2014/2376] Implement the accuracy heatmap --- .../Ranking/TestSceneAccuracyHeatmap.cs | 273 ++++++++++++++++++ 1 file changed, 273 insertions(+) create mode 100644 osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs new file mode 100644 index 0000000000..8386ee5992 --- /dev/null +++ b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs @@ -0,0 +1,273 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Diagnostics; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Framework.Utils; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Tests.Visual.Ranking +{ + public class TestSceneAccuracyHeatmap : OsuManualInputManagerTestScene + { + private readonly Box background; + private readonly Drawable object1; + private readonly Drawable object2; + private readonly Heatmap heatmap; + + public TestSceneAccuracyHeatmap() + { + Children = new[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#333"), + }, + object1 = new BorderCircle + { + Position = new Vector2(256, 192), + Colour = Color4.Yellow, + }, + object2 = new BorderCircle + { + Position = new Vector2(500, 300), + }, + heatmap = new Heatmap + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Scheduler.AddDelayed(() => + { + var randomPos = new Vector2( + RNG.NextSingle(object1.DrawPosition.X - object1.DrawSize.X / 2, object1.DrawPosition.X + object1.DrawSize.X / 2), + RNG.NextSingle(object1.DrawPosition.Y - object1.DrawSize.Y / 2, object1.DrawPosition.Y + object1.DrawSize.Y / 2)); + + // The background is used for ToLocalSpace() since we need to go _inside_ the DrawSizePreservingContainer (Content of TestScene). + heatmap.AddPoint(object2.Position, object1.Position, randomPos, RNG.NextSingle(10, 500)); + InputManager.MoveMouseTo(background.ToScreenSpace(randomPos)); + }, 1, true); + } + + protected override bool OnMouseDown(MouseDownEvent e) + { + heatmap.AddPoint(object2.Position, object1.Position, background.ToLocalSpace(e.ScreenSpaceMouseDownPosition), 50); + return true; + } + + private class Heatmap : CompositeDrawable + { + /// + /// Full size of the heatmap. + /// + private const float size = 100; + + /// + /// Size of the inner circle containing the "hit" points, relative to . + /// All other points outside of the inner circle are "miss" points. + /// + private const float inner_portion = 0.8f; + + private const float rotation = 45; + private const float point_size = 4; + + private Container allPoints; + + public Heatmap() + { + Size = new Vector2(size); + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChildren = new Drawable[] + { + new CircularContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(inner_portion), + Masking = true, + BorderThickness = 2f, + BorderColour = Color4.White, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#202624") + } + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + Children = new Drawable[] + { + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Height = 2, // We're rotating along a diagonal - we don't really care how big this is. + Width = 1f, + Rotation = -rotation, + Alpha = 0.3f, + }, + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Height = 2, // We're rotating along a diagonal - we don't really care how big this is. + Width = 1f, + Rotation = rotation + }, + new Box + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Width = 10, + Height = 2f, + }, + new Box + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Y = -1, + Width = 2f, + Height = 10, + } + } + }, + allPoints = new Container { RelativeSizeAxes = Axes.Both } + }; + + Vector2 centre = new Vector2(size / 2); + int rows = (int)Math.Ceiling(size / point_size); + int cols = (int)Math.Ceiling(size / point_size); + + for (int r = 0; r < rows; r++) + { + for (int c = 0; c < cols; c++) + { + Vector2 pos = new Vector2(c * point_size, r * point_size); + HitType type = HitType.Hit; + + if (Vector2.Distance(pos, centre) > size * inner_portion / 2) + type = HitType.Miss; + + allPoints.Add(new HitPoint(pos, type) + { + Size = new Vector2(point_size), + Colour = type == HitType.Hit ? new Color4(102, 255, 204, 255) : new Color4(255, 102, 102, 255) + }); + } + } + } + + public void AddPoint(Vector2 start, Vector2 end, Vector2 hitPoint, float radius) + { + double angle1 = Math.Atan2(end.Y - hitPoint.Y, hitPoint.X - end.X); // Angle between the end point and the hit point. + double angle2 = Math.Atan2(end.Y - start.Y, start.X - end.X); // Angle between the end point and the start point. + double finalAngle = angle2 - angle1; // Angle between start, end, and hit points. + + float normalisedDistance = Vector2.Distance(hitPoint, end) / radius; + + // Find the most relevant hit point. + double minDist = double.PositiveInfinity; + HitPoint point = null; + + foreach (var p in allPoints) + { + Vector2 localCentre = new Vector2(size / 2); + float localRadius = localCentre.X * inner_portion * normalisedDistance; + double localAngle = finalAngle + 3 * Math.PI / 4; + Vector2 localPoint = localCentre + localRadius * new Vector2((float)Math.Cos(localAngle), (float)Math.Sin(localAngle)); + + float dist = Vector2.Distance(p.DrawPosition + p.DrawSize / 2, localPoint); + + if (dist < minDist) + { + minDist = dist; + point = p; + } + } + + Debug.Assert(point != null); + point.Increment(); + } + } + + private class HitPoint : Circle + { + private readonly HitType type; + + public HitPoint(Vector2 position, HitType type) + { + this.type = type; + + Position = position; + Alpha = 0; + } + + public void Increment() + { + if (Alpha < 1) + Alpha += 0.1f; + else if (type == HitType.Hit) + Colour = ((Color4)Colour).Lighten(0.1f); + } + } + + private enum HitType + { + Hit, + Miss + } + + private class BorderCircle : CircularContainer + { + public BorderCircle() + { + Origin = Anchor.Centre; + Size = new Vector2(100); + + Masking = true; + BorderThickness = 2; + BorderColour = Color4.White; + + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true + }, + new Circle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(4), + } + }; + } + } + } +} From 81c392b841999e9f237a7b3f2e24d467957c876f Mon Sep 17 00:00:00 2001 From: Shivam Date: Fri, 12 Jun 2020 15:57:23 +0200 Subject: [PATCH 2015/2376] Change hash to be lowercase and change sample directories --- osu.Game/Screens/Menu/IntroCircles.cs | 2 +- osu.Game/Screens/Menu/IntroScreen.cs | 2 +- osu.Game/Screens/Menu/IntroTriangles.cs | 2 +- osu.Game/Screens/Menu/IntroWelcome.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Menu/IntroCircles.cs b/osu.Game/Screens/Menu/IntroCircles.cs index 113d496855..d4cd073b7a 100644 --- a/osu.Game/Screens/Menu/IntroCircles.cs +++ b/osu.Game/Screens/Menu/IntroCircles.cs @@ -24,7 +24,7 @@ namespace osu.Game.Screens.Menu private void load(AudioManager audio) { if (MenuVoice.Value) - welcome = audio.Samples.Get(@"Intro/lazer/welcome"); + welcome = audio.Samples.Get(@"Intro/welcome"); } protected override void LogoArriving(OsuLogo logo, bool resuming) diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index abc7a3c7ee..2f9d43bed6 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -72,7 +72,7 @@ namespace osu.Game.Screens.Menu MenuVoice = config.GetBindable(OsuSetting.MenuVoice); MenuMusic = config.GetBindable(OsuSetting.MenuMusic); - Seeya = audio.Samples.Get(@"Intro/lazer/seeya"); + Seeya = audio.Samples.Get(@"Intro/seeya"); BeatmapSetInfo setInfo = null; diff --git a/osu.Game/Screens/Menu/IntroTriangles.cs b/osu.Game/Screens/Menu/IntroTriangles.cs index fb84ccffd0..9be74a0fd9 100644 --- a/osu.Game/Screens/Menu/IntroTriangles.cs +++ b/osu.Game/Screens/Menu/IntroTriangles.cs @@ -47,7 +47,7 @@ namespace osu.Game.Screens.Menu private void load() { if (MenuVoice.Value && !UsingThemedIntro) - welcome = audio.Samples.Get(@"Intro/lazer/welcome"); + welcome = audio.Samples.Get(@"Intro/welcome"); } protected override void LogoArriving(OsuLogo logo, bool resuming) diff --git a/osu.Game/Screens/Menu/IntroWelcome.cs b/osu.Game/Screens/Menu/IntroWelcome.cs index 38405fab6a..dec3af5ac9 100644 --- a/osu.Game/Screens/Menu/IntroWelcome.cs +++ b/osu.Game/Screens/Menu/IntroWelcome.cs @@ -17,7 +17,7 @@ namespace osu.Game.Screens.Menu { public class IntroWelcome : IntroScreen { - protected override string BeatmapHash => "64E00D7022195959BFA3109D09C2E2276C8F12F486B91FCF6175583E973B48F2"; + protected override string BeatmapHash => "64e00d7022195959bfa3109d09c2e2276c8f12f486b91fcf6175583e973b48f2"; protected override string BeatmapFile => "welcome.osz"; private const double delay_step_two = 2142; private SampleChannel welcome; From 6000e0f86a48e89d6e64f14cb4f9b5046f04c486 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 12 Jun 2020 23:01:22 +0900 Subject: [PATCH 2016/2376] Increase size to match timing distribution graph --- osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs index 8386ee5992..b605ddcc35 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs @@ -75,7 +75,7 @@ namespace osu.Game.Tests.Visual.Ranking /// /// Full size of the heatmap. /// - private const float size = 100; + private const float size = 130; /// /// Size of the inner circle containing the "hit" points, relative to . From 8a9d01119753923a0230e0c5bea40f93b4586cdf Mon Sep 17 00:00:00 2001 From: mcendu Date: Fri, 12 Jun 2020 22:09:58 +0800 Subject: [PATCH 2017/2376] add test files --- .../Resources/special-skin/mania/hit0@2x.png | Bin 0 -> 56621 bytes .../Resources/special-skin/mania/hit100@2x.png | Bin 0 -> 66535 bytes .../Resources/special-skin/mania/hit200@2x.png | Bin 0 -> 82190 bytes .../Resources/special-skin/mania/hit300@2x.png | Bin 0 -> 92906 bytes .../special-skin/mania/hit300g-0@2x.png | Bin 0 -> 56026 bytes .../special-skin/mania/hit300g-1@2x.png | Bin 0 -> 57038 bytes .../Resources/special-skin/mania/hit50@2x.png | Bin 0 -> 57215 bytes .../special-skin/mania/stage-bottom@2x.png | Bin 0 -> 1965 bytes .../Resources/special-skin/skin.ini | 11 +++++++++-- 9 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit0@2x.png create mode 100644 osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit100@2x.png create mode 100644 osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit200@2x.png create mode 100644 osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit300@2x.png create mode 100644 osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit300g-0@2x.png create mode 100644 osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit300g-1@2x.png create mode 100644 osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit50@2x.png create mode 100644 osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/stage-bottom@2x.png diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit0@2x.png b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit0@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..2e7b9bc34f3ea66e5d4e687adb312f36d7c5a4bd GIT binary patch literal 56621 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

    ~<2&bFepI2$u~| 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 FV1mC)D$D=? literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit200@2x.png b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit200@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..24ad926375e8c88df664bdd9555ddddc1a748be9 GIT binary patch literal 82190 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 IM6N<$f=61fdH?_b literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit300@2x.png b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit300@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..098561f980ddc8a17308c29a62a249fd2812e370 GIT binary patch literal 92906 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

    E=Rmmw{ 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­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 literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit300g-1@2x.png b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/hit300g-1@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..f17b2b1e737a4d9b1c88247a738dadec3d9dfd1a GIT binary patch literal 57038 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 Ks#k-08_-AT>ABZ45&7U3%MLFgSfv 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

    }+-2g^TWTo$yAMT?d2TCHI=II`n0mRKNHw#M7` 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 literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/stage-bottom@2x.png b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/stage-bottom@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..ca590eaf08e65dc1ae40bce9f9f74e69c3a0f8a4 GIT binary patch literal 1965 zcmV;e2U7TnP)3L?K*_rKGQL~x> z08mZsB?AB;00{&D5QGE*00=??0RRLcfdBx4kU#(cK}a9~fFL9g06-8D2ml}m2?PKT zgaiTr2ton@00ep2fXKN$&0jO!YxaDt*~2%@9=&BYwcLXAPMQr~G21k5w&6kJxjb8A z_D|IAZ~bOB28!G08;c&-tz|}co88}PcJ#Q}%BR*6wH~u$-xc>$EBeCeshNF!$ZXkE z{4@Pc`rGCr-6ySR6WyVQ-FPAY_E=?Chs?hHsuAqU`YZ}YpPY$;++^|Vo<)i3z{N(e z%4_H7h_!?Q{lQCS*LIoxvfXU+xV41Z-S{|4x@)q3->SZFltBJiXSV8T{Ikhd%-($* zp6p+HV)pyGz&!{aR!1JHReDt%hW5g z1K;zRuk~Pe(Cp;lMj{afL)Gwe%S|Ve0rQ)YM(Ha}HGe(sZ;8)r6ciOiZ}ulkWz);! zV4G#u@vkXPAZ6QC-xPgLGxO0G&FJ%#jMQEgnrBKU55PwW3zcakeCk+SS65)``!)Nu zs-X{Fj-OxMlN~U98rf^a?Aq5z_{3rBc&+M-vwN)+|NrxT#S0Gfo_*)y=MO6;A1nFH7I=uowik4k0!)k7a$v5vLSeW3((Z%h21DwFB;V?Aq$W35s$!_q+l zK`=jQWYyQ}lkPL4SOv73A6O*{ewqUi1+`6gtYa-kU+8n(nTy`@@i{9+$LiMDsEw{LB_v=Mv^2@D zpOsnbgQbVdcrlBv6w-umnnt|}B>sf?4^fHa%hTEItb7)KKOGP-B|T4|1QxogdNKKZ zQT5W_y!R=IG;6D#1dhe?A3AfqFr43q>B;kZTMWoW0>LZ_Hk(VlE^iIPd|K9_31l(p z3`BkA7K7^ln_eg?S(E2VP(3U)Mh;3?2=@6%W92jDjrOCU?>0azJDHxIJRjX*W-2aC<`i!vK1W|>Q0q$ktUI|pP`0cSxKJ0lB-ksec|RJs+vn*xXE-!7fSfYz~nhXkU$VjH#_pBS30K`7rs$(9fwB6 z{(Gu!2G~y0>pnvIV&z2qJftsl4DPH^$-}K?&zn34njwK8SUSioOO;p@m}!+!y2V_aS|uvyf{;@iFMEQPPv={(K2ggam?M=`zf8#+-+LJJy{Q_p(w+ zrP*rtPBPcN@V7zC8pJa@9nXoW_&wPpoYO-EPqieNo>|zCSrBU*!f@fI|YoEM>|n zPSIwHzK3s{ZT~a5YF%eAdh8g>e$U-)X1jk5--xkYwL@Ph;U763PoC?HgiCi(6(uZG zq9CJSb1EuXz5b$H@^Ccz-uNWB6-t)7Tg?V;H%98+mOLE4FN&^unyfTq&~sl z1tmf(yq*;`W3Npt3XQlm0(WyM3ZzOb-I_Ft&==@HH4+E_AViH4762d!2?PKTgaiTr z2ton@00bd{004rJKmY(iScwh*)v^2`>Vy9Q*Vy#8=R%=B00000NkvXXu0mjflsBGC literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/skin.ini b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/skin.ini index 56564776b3..eaa50d1a61 100644 --- a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/skin.ini +++ b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/skin.ini @@ -1,6 +1,13 @@ [General] -Version: 2.4 +Version: 2.5 [Mania] Keys: 4 -ColumnLineWidth: 3,1,3,1,1 \ No newline at end of file +ColumnLineWidth: 3,1,3,1,1 +Hit0: mania/hit0 +Hit50: mania/hit50 +Hit100: mania/hit100 +Hit200: mania/hit200 +Hit300: mania/hit300 +Hit300g: mania/hit300g +BottomStageImage: mania/stage-bottom \ No newline at end of file From b663c940aea1da0d66b8cea0c86637edaacf81d3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 12 Jun 2020 23:46:46 +0900 Subject: [PATCH 2018/2376] Rename enum --- .../Visual/Ranking/TestSceneAccuracyHeatmap.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs index b605ddcc35..9f82287640 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs @@ -167,15 +167,15 @@ namespace osu.Game.Tests.Visual.Ranking for (int c = 0; c < cols; c++) { Vector2 pos = new Vector2(c * point_size, r * point_size); - HitType type = HitType.Hit; + HitPointType pointType = HitPointType.Hit; if (Vector2.Distance(pos, centre) > size * inner_portion / 2) - type = HitType.Miss; + pointType = HitPointType.Miss; - allPoints.Add(new HitPoint(pos, type) + allPoints.Add(new HitPoint(pos, pointType) { Size = new Vector2(point_size), - Colour = type == HitType.Hit ? new Color4(102, 255, 204, 255) : new Color4(255, 102, 102, 255) + Colour = pointType == HitPointType.Hit ? new Color4(102, 255, 204, 255) : new Color4(255, 102, 102, 255) }); } } @@ -216,11 +216,11 @@ namespace osu.Game.Tests.Visual.Ranking private class HitPoint : Circle { - private readonly HitType type; + private readonly HitPointType pointType; - public HitPoint(Vector2 position, HitType type) + public HitPoint(Vector2 position, HitPointType pointType) { - this.type = type; + this.pointType = pointType; Position = position; Alpha = 0; @@ -230,12 +230,12 @@ namespace osu.Game.Tests.Visual.Ranking { if (Alpha < 1) Alpha += 0.1f; - else if (type == HitType.Hit) + else if (pointType == HitPointType.Hit) Colour = ((Color4)Colour).Lighten(0.1f); } } - private enum HitType + private enum HitPointType { Hit, Miss From 586e3d405c8d7863137b542b914b044ffbe4fde2 Mon Sep 17 00:00:00 2001 From: mcendu Date: Fri, 12 Jun 2020 22:48:18 +0800 Subject: [PATCH 2019/2376] add proper decoding support? --- osu.Game/Skinning/LegacyManiaSkinDecoder.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs index a988bd589f..cbd6aa17dc 100644 --- a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs +++ b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs @@ -111,11 +111,11 @@ namespace osu.Game.Skinning HandleColours(currentConfig, line); break; + // Custom sprite paths case string _ when pair.Key.StartsWith("NoteImage"): - currentConfig.ImageLookups[pair.Key] = pair.Value; - break; - case string _ when pair.Key.StartsWith("KeyImage"): + case string _ when pair.Key.StartsWith("Hit"): + case "BottomStageImage": currentConfig.ImageLookups[pair.Key] = pair.Value; break; } From 7def6a91812670529b671b5e89abf243d8a8c30b Mon Sep 17 00:00:00 2001 From: mcendu Date: Fri, 12 Jun 2020 23:06:19 +0800 Subject: [PATCH 2020/2376] fix tests incorrectly showing judgements not present in mania --- .../Skinning/TestSceneDrawableJudgement.cs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs index 497b80950a..540bf82e1f 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs @@ -6,6 +6,7 @@ using System.Linq; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mania.Scoring; using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; @@ -16,14 +17,17 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning { public TestSceneDrawableJudgement() { + var HitWindows = new ManiaHitWindows(); + foreach (HitResult result in Enum.GetValues(typeof(HitResult)).OfType().Skip(1)) { - AddStep("Show " + result.GetDescription(), () => SetContents(() => - new DrawableManiaJudgement(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - })); + if (HitWindows.IsHitResultAllowed(result)) + AddStep("Show " + result.GetDescription(), () => SetContents(() => + new DrawableManiaJudgement(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + })); } } } From c6e087b9947aa46a8bed3abe3928ebd1d2a0ba04 Mon Sep 17 00:00:00 2001 From: mcendu Date: Fri, 12 Jun 2020 23:11:50 +0800 Subject: [PATCH 2021/2376] remove incorrectly added key --- osu.Game/Skinning/LegacyManiaSkinDecoder.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs index cbd6aa17dc..0806676fde 100644 --- a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs +++ b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs @@ -115,7 +115,6 @@ namespace osu.Game.Skinning case string _ when pair.Key.StartsWith("NoteImage"): case string _ when pair.Key.StartsWith("KeyImage"): case string _ when pair.Key.StartsWith("Hit"): - case "BottomStageImage": currentConfig.ImageLookups[pair.Key] = pair.Value; break; } From da46288ef09489d5d98a163f5ada7e292aed091b Mon Sep 17 00:00:00 2001 From: mcendu Date: Fri, 12 Jun 2020 23:20:04 +0800 Subject: [PATCH 2022/2376] remove stage bottom image --- .../special-skin/mania/stage-bottom@2x.png | Bin 1965 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/stage-bottom@2x.png diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/stage-bottom@2x.png b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/stage-bottom@2x.png deleted file mode 100644 index ca590eaf08e65dc1ae40bce9f9f74e69c3a0f8a4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1965 zcmV;e2U7TnP)3L?K*_rKGQL~x> z08mZsB?AB;00{&D5QGE*00=??0RRLcfdBx4kU#(cK}a9~fFL9g06-8D2ml}m2?PKT zgaiTr2ton@00ep2fXKN$&0jO!YxaDt*~2%@9=&BYwcLXAPMQr~G21k5w&6kJxjb8A z_D|IAZ~bOB28!G08;c&-tz|}co88}PcJ#Q}%BR*6wH~u$-xc>$EBeCeshNF!$ZXkE z{4@Pc`rGCr-6ySR6WyVQ-FPAY_E=?Chs?hHsuAqU`YZ}YpPY$;++^|Vo<)i3z{N(e z%4_H7h_!?Q{lQCS*LIoxvfXU+xV41Z-S{|4x@)q3->SZFltBJiXSV8T{Ikhd%-($* zp6p+HV)pyGz&!{aR!1JHReDt%hW5g z1K;zRuk~Pe(Cp;lMj{afL)Gwe%S|Ve0rQ)YM(Ha}HGe(sZ;8)r6ciOiZ}ulkWz);! zV4G#u@vkXPAZ6QC-xPgLGxO0G&FJ%#jMQEgnrBKU55PwW3zcakeCk+SS65)``!)Nu zs-X{Fj-OxMlN~U98rf^a?Aq5z_{3rBc&+M-vwN)+|NrxT#S0Gfo_*)y=MO6;A1nFH7I=uowik4k0!)k7a$v5vLSeW3((Z%h21DwFB;V?Aq$W35s$!_q+l zK`=jQWYyQ}lkPL4SOv73A6O*{ewqUi1+`6gtYa-kU+8n(nTy`@@i{9+$LiMDsEw{LB_v=Mv^2@D zpOsnbgQbVdcrlBv6w-umnnt|}B>sf?4^fHa%hTEItb7)KKOGP-B|T4|1QxogdNKKZ zQT5W_y!R=IG;6D#1dhe?A3AfqFr43q>B;kZTMWoW0>LZ_Hk(VlE^iIPd|K9_31l(p z3`BkA7K7^ln_eg?S(E2VP(3U)Mh;3?2=@6%W92jDjrOCU?>0azJDHxIJRjX*W-2aC<`i!vK1W|>Q0q$ktUI|pP`0cSxKJ0lB-ksec|RJs+vn*xXE-!7fSfYz~nhXkU$VjH#_pBS30K`7rs$(9fwB6 z{(Gu!2G~y0>pnvIV&z2qJftsl4DPH^$-}K?&zn34njwK8SUSioOO;p@m}!+!y2V_aS|uvyf{;@iFMEQPPv={(K2ggam?M=`zf8#+-+LJJy{Q_p(w+ zrP*rtPBPcN@V7zC8pJa@9nXoW_&wPpoYO-EPqieNo>|zCSrBU*!f@fI|YoEM>|n zPSIwHzK3s{ZT~a5YF%eAdh8g>e$U-)X1jk5--xkYwL@Ph;U763PoC?HgiCi(6(uZG zq9CJSb1EuXz5b$H@^Ccz-uNWB6-t)7Tg?V;H%98+mOLE4FN&^unyfTq&~sl z1tmf(yq*;`W3Npt3XQlm0(WyM3ZzOb-I_Ft&==@HH4+E_AViH4762d!2?PKTgaiTr z2ton@00bd{004rJKmY(iScwh*)v^2`>Vy9Q*Vy#8=R%=B00000NkvXXu0mjflsBGC From a42bfcb5aba1910cc975fed0d420864917d9069f Mon Sep 17 00:00:00 2001 From: mcendu Date: Fri, 12 Jun 2020 23:23:57 +0800 Subject: [PATCH 2023/2376] remove reference to stage bottom from skin ini --- osu.Game.Rulesets.Mania.Tests/Resources/special-skin/skin.ini | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/skin.ini b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/skin.ini index eaa50d1a61..941abac1da 100644 --- a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/skin.ini +++ b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/skin.ini @@ -9,5 +9,4 @@ Hit50: mania/hit50 Hit100: mania/hit100 Hit200: mania/hit200 Hit300: mania/hit300 -Hit300g: mania/hit300g -BottomStageImage: mania/stage-bottom \ No newline at end of file +Hit300g: mania/hit300g \ No newline at end of file From aa476835e7b657b414093fb3dcf6b19c3d935d9d Mon Sep 17 00:00:00 2001 From: mcendu Date: Sat, 13 Jun 2020 11:31:34 +0800 Subject: [PATCH 2024/2376] tidy up code --- .../Skinning/TestSceneDrawableJudgement.cs | 6 ++- .../Skinning/ManiaLegacySkinTransformer.cs | 40 +++++++++---------- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs index 540bf82e1f..a4d4ec50f8 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs @@ -17,17 +17,19 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning { public TestSceneDrawableJudgement() { - var HitWindows = new ManiaHitWindows(); + var hitWindows = new ManiaHitWindows(); foreach (HitResult result in Enum.GetValues(typeof(HitResult)).OfType().Skip(1)) { - if (HitWindows.IsHitResultAllowed(result)) + if (hitWindows.IsHitResultAllowed(result)) + { AddStep("Show " + result.GetDescription(), () => SetContents(() => new DrawableManiaJudgement(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null) { Anchor = Anchor.Centre, Origin = Anchor.Centre, })); + } } } } diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs index 9ba544ed59..3304330233 100644 --- a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs @@ -24,31 +24,31 @@ namespace osu.Game.Rulesets.Mania.Skinning /// Mapping of to ther corresponding /// value. ///

    - private static readonly IReadOnlyDictionary componentMapping + private static readonly IReadOnlyDictionary hitresult_mapping = new Dictionary - { - { HitResult.Perfect, LegacyManiaSkinConfigurationLookups.Hit300g }, - { HitResult.Great, LegacyManiaSkinConfigurationLookups.Hit300 }, - { HitResult.Good, LegacyManiaSkinConfigurationLookups.Hit200 }, - { HitResult.Ok, LegacyManiaSkinConfigurationLookups.Hit100 }, - { HitResult.Meh, LegacyManiaSkinConfigurationLookups.Hit50 }, - { HitResult.Miss, LegacyManiaSkinConfigurationLookups.Hit0 } - }; + { + { HitResult.Perfect, LegacyManiaSkinConfigurationLookups.Hit300g }, + { HitResult.Great, LegacyManiaSkinConfigurationLookups.Hit300 }, + { HitResult.Good, LegacyManiaSkinConfigurationLookups.Hit200 }, + { HitResult.Ok, LegacyManiaSkinConfigurationLookups.Hit100 }, + { HitResult.Meh, LegacyManiaSkinConfigurationLookups.Hit50 }, + { HitResult.Miss, LegacyManiaSkinConfigurationLookups.Hit0 } + }; /// /// Mapping of to their corresponding /// default filenames. /// - private static readonly IReadOnlyDictionary defaultName + private static readonly IReadOnlyDictionary default_hitresult_skin_filenames = new Dictionary - { - { HitResult.Perfect, "mania-hit300g" }, - { HitResult.Great, "mania-hit300" }, - { HitResult.Good, "mania-hit200" }, - { HitResult.Ok, "mania-hit100" }, - { HitResult.Meh, "mania-hit50" }, - { HitResult.Miss, "mania-hit0" } - }; + { + { HitResult.Perfect, "mania-hit300g" }, + { HitResult.Great, "mania-hit300" }, + { HitResult.Good, "mania-hit200" }, + { HitResult.Ok, "mania-hit100" }, + { HitResult.Meh, "mania-hit50" }, + { HitResult.Miss, "mania-hit0" } + }; private Lazy isLegacySkin; @@ -129,8 +129,8 @@ namespace osu.Game.Rulesets.Mania.Skinning private Drawable getResult(HitResult result) { string image = GetConfig( - new ManiaSkinConfigurationLookup(componentMapping[result]) - )?.Value ?? defaultName[result]; + new ManiaSkinConfigurationLookup(hitresult_mapping[result]) + )?.Value ?? default_hitresult_skin_filenames[result]; return this.GetAnimation(image, true, true); } From 7212ab3a1afe56a5ae0654811d8ce535cf1e24c6 Mon Sep 17 00:00:00 2001 From: clayton Date: Fri, 12 Jun 2020 23:48:30 -0700 Subject: [PATCH 2025/2376] Add new beatmap genres and languages --- osu.Game/Overlays/BeatmapListing/SearchGenre.cs | 6 +++++- osu.Game/Overlays/BeatmapListing/SearchLanguage.cs | 12 +++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListing/SearchGenre.cs b/osu.Game/Overlays/BeatmapListing/SearchGenre.cs index b12bba6249..de437fac3e 100644 --- a/osu.Game/Overlays/BeatmapListing/SearchGenre.cs +++ b/osu.Game/Overlays/BeatmapListing/SearchGenre.cs @@ -20,6 +20,10 @@ namespace osu.Game.Overlays.BeatmapListing [Description("Hip Hop")] HipHop = 9, - Electronic = 10 + Electronic = 10, + Metal = 11, + Classical = 12, + Folk = 13, + Jazz = 14 } } diff --git a/osu.Game/Overlays/BeatmapListing/SearchLanguage.cs b/osu.Game/Overlays/BeatmapListing/SearchLanguage.cs index dac7e4f1a2..ef7576344a 100644 --- a/osu.Game/Overlays/BeatmapListing/SearchLanguage.cs +++ b/osu.Game/Overlays/BeatmapListing/SearchLanguage.cs @@ -11,7 +11,7 @@ namespace osu.Game.Overlays.BeatmapListing [Order(0)] Any, - [Order(11)] + [Order(13)] Other, [Order(1)] @@ -23,7 +23,7 @@ namespace osu.Game.Overlays.BeatmapListing [Order(2)] Chinese, - [Order(10)] + [Order(12)] Instrumental, [Order(7)] @@ -42,6 +42,12 @@ namespace osu.Game.Overlays.BeatmapListing Spanish, [Order(5)] - Italian + Italian, + + [Order(10)] + Russian, + + [Order(11)] + Polish } } From 9d98adee1e0b0b806f4d678e3310a5fb3cc3edc3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 13 Jun 2020 16:10:41 +0900 Subject: [PATCH 2026/2376] Update fastlane to fix upload failure for iOS releases --- Gemfile.lock | 72 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 45 insertions(+), 27 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index e3954c2681..bf971d2c22 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -5,6 +5,22 @@ GEM addressable (2.7.0) public_suffix (>= 2.0.2, < 5.0) atomos (0.1.3) + aws-eventstream (1.1.0) + aws-partitions (1.329.0) + aws-sdk-core (3.99.2) + aws-eventstream (~> 1, >= 1.0.2) + aws-partitions (~> 1, >= 1.239.0) + aws-sigv4 (~> 1.1) + jmespath (~> 1.0) + aws-sdk-kms (1.34.1) + aws-sdk-core (~> 3, >= 3.99.0) + aws-sigv4 (~> 1.1) + aws-sdk-s3 (1.68.1) + aws-sdk-core (~> 3, >= 3.99.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.1) + aws-sigv4 (1.1.4) + aws-eventstream (~> 1.0, >= 1.0.2) babosa (1.0.3) claide (1.0.3) colored (1.2) @@ -13,23 +29,24 @@ GEM highline (~> 1.7.2) declarative (0.0.10) declarative-option (0.1.0) - digest-crc (0.4.1) + digest-crc (0.5.1) domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) dotenv (2.7.5) emoji_regex (1.0.1) - excon (0.71.1) - faraday (0.17.3) + excon (0.74.0) + faraday (1.0.1) multipart-post (>= 1.2, < 3) faraday-cookie_jar (0.0.6) faraday (>= 0.7.4) http-cookie (~> 1.0.0) - faraday_middleware (0.13.1) - faraday (>= 0.7.4, < 1.0) + faraday_middleware (1.0.0) + faraday (~> 1.0) fastimage (2.1.7) - fastlane (2.140.0) + fastlane (2.149.1) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.3, < 3.0.0) + aws-sdk-s3 (~> 1.0) babosa (>= 1.0.2, < 2.0.0) bundler (>= 1.12.0, < 3.0.0) colored @@ -37,12 +54,12 @@ GEM dotenv (>= 2.1.1, < 3.0.0) emoji_regex (>= 0.1, < 2.0) excon (>= 0.71.0, < 1.0.0) - faraday (~> 0.17) + faraday (>= 0.17, < 2.0) faraday-cookie_jar (~> 0.0.6) - faraday_middleware (~> 0.13.1) + faraday_middleware (>= 0.13.1, < 2.0) fastimage (>= 2.1.0, < 3.0.0) gh_inspector (>= 1.1.2, < 2.0.0) - google-api-client (>= 0.29.2, < 0.37.0) + google-api-client (>= 0.37.0, < 0.39.0) google-cloud-storage (>= 1.15.0, < 2.0.0) highline (>= 1.7.2, < 2.0.0) json (< 3.0.0) @@ -69,7 +86,7 @@ GEM souyuz (= 0.9.1) fastlane-plugin-xamarin (0.6.3) gh_inspector (1.1.3) - google-api-client (0.36.4) + google-api-client (0.38.0) addressable (~> 2.5, >= 2.5.1) googleauth (~> 0.9) httpclient (>= 2.8.1, < 3.0) @@ -80,27 +97,28 @@ GEM google-cloud-core (1.5.0) google-cloud-env (~> 1.0) google-cloud-errors (~> 1.0) - google-cloud-env (1.3.0) - faraday (~> 0.11) - google-cloud-errors (1.0.0) - google-cloud-storage (1.25.1) + google-cloud-env (1.3.2) + faraday (>= 0.17.3, < 2.0) + google-cloud-errors (1.0.1) + google-cloud-storage (1.26.2) addressable (~> 2.5) digest-crc (~> 0.4) google-api-client (~> 0.33) google-cloud-core (~> 1.2) googleauth (~> 0.9) mini_mime (~> 1.0) - googleauth (0.10.0) - faraday (~> 0.12) + googleauth (0.12.0) + faraday (>= 0.17.3, < 2.0) jwt (>= 1.4, < 3.0) memoist (~> 0.16) multi_json (~> 1.11) os (>= 0.9, < 2.0) - signet (~> 0.12) + signet (~> 0.14) highline (1.7.10) http-cookie (1.0.3) domain_name (~> 0.5) httpclient (2.8.3) + jmespath (1.4.0) json (2.3.0) jwt (2.1.0) memoist (0.16.2) @@ -114,7 +132,7 @@ GEM naturally (2.2.0) nokogiri (1.10.7) mini_portile2 (~> 2.4.0) - os (1.0.1) + os (1.1.0) plist (3.5.0) public_suffix (2.0.5) representable (3.0.4) @@ -125,12 +143,12 @@ GEM rouge (2.0.7) rubyzip (1.3.0) security (0.1.3) - signet (0.12.0) + signet (0.14.0) addressable (~> 2.3) - faraday (~> 0.9) + faraday (>= 0.17.3, < 2.0) jwt (>= 1.5, < 3.0) multi_json (~> 1.10) - simctl (1.6.7) + simctl (1.6.8) CFPropertyList naturally slack-notifier (2.3.2) @@ -141,17 +159,17 @@ GEM terminal-notifier (2.0.0) terminal-table (1.8.0) unicode-display_width (~> 1.1, >= 1.1.1) - tty-cursor (0.7.0) - tty-screen (0.7.0) - tty-spinner (0.9.2) + tty-cursor (0.7.1) + tty-screen (0.8.0) + tty-spinner (0.9.3) tty-cursor (~> 0.7) uber (0.1.0) unf (0.1.4) unf_ext - unf_ext (0.0.7.6) - unicode-display_width (1.6.1) + unf_ext (0.0.7.7) + unicode-display_width (1.7.0) word_wrap (1.0.0) - xcodeproj (1.14.0) + xcodeproj (1.16.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) From 7bc70e644a56a76b9d2be063262e6de378853bc3 Mon Sep 17 00:00:00 2001 From: clayton Date: Sat, 13 Jun 2020 00:20:34 -0700 Subject: [PATCH 2027/2376] Add Unspecified language --- osu.Game/Overlays/BeatmapListing/SearchLanguage.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/BeatmapListing/SearchLanguage.cs b/osu.Game/Overlays/BeatmapListing/SearchLanguage.cs index ef7576344a..43f16059e9 100644 --- a/osu.Game/Overlays/BeatmapListing/SearchLanguage.cs +++ b/osu.Game/Overlays/BeatmapListing/SearchLanguage.cs @@ -48,6 +48,9 @@ namespace osu.Game.Overlays.BeatmapListing Russian, [Order(11)] - Polish + Polish, + + [Order(14)] + Unspecified } } From c490dba7b3e0e22202ab861e0132a5779d818f0f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 13 Jun 2020 18:18:46 +0900 Subject: [PATCH 2028/2376] Fix crash on local score display --- osu.Game/Scoring/ScoreStore.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Scoring/ScoreStore.cs b/osu.Game/Scoring/ScoreStore.cs index 9627481f4d..f5c5cd5dad 100644 --- a/osu.Game/Scoring/ScoreStore.cs +++ b/osu.Game/Scoring/ScoreStore.cs @@ -18,6 +18,8 @@ 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.Ruleset); } } From 9230c148c7f68ea6908568d72052ca7e165678e2 Mon Sep 17 00:00:00 2001 From: Power Maker Date: Sat, 13 Jun 2020 12:18:50 +0200 Subject: [PATCH 2029/2376] Add cursor rotation on middle mouse button --- osu.Game/Graphics/Cursor/MenuCursor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Cursor/MenuCursor.cs b/osu.Game/Graphics/Cursor/MenuCursor.cs index 8305f33e25..ff28dddd40 100644 --- a/osu.Game/Graphics/Cursor/MenuCursor.cs +++ b/osu.Game/Graphics/Cursor/MenuCursor.cs @@ -126,7 +126,7 @@ namespace osu.Game.Graphics.Cursor activeCursor.ScaleTo(0.6f, 250, Easing.In); } - private bool shouldKeepRotating(MouseEvent e) => cursorRotate.Value && (e.IsPressed(MouseButton.Left) || e.IsPressed(MouseButton.Right)); + private bool shouldKeepRotating(MouseEvent e) => cursorRotate.Value && (anyMainButtonPressed(e)); private static bool anyMainButtonPressed(MouseEvent e) => e.IsPressed(MouseButton.Left) || e.IsPressed(MouseButton.Middle) || e.IsPressed(MouseButton.Right); From 619c541cf559f546c5718673c4acf279f98ce320 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 13 Jun 2020 12:41:00 +0200 Subject: [PATCH 2030/2376] Rewrite test to use dummy API --- .../Online/TestSceneCommentsContainer.cs | 117 +++++++++++++----- 1 file changed, 89 insertions(+), 28 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs index 42e6b9087c..26ad0b0d3f 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs @@ -1,52 +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.Collections.Generic; +using System.Linq; using NUnit.Framework; -using osu.Game.Online.API.Requests; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics; -using osu.Game.Overlays.Comments; using osu.Game.Overlays; using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Game.Users; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays.Comments; namespace osu.Game.Tests.Visual.Online { [TestFixture] public class TestSceneCommentsContainer : OsuTestScene { - protected override bool UseOnlineAPI => true; - [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); - public TestSceneCommentsContainer() - { - BasicScrollContainer scroll; - TestCommentsContainer comments; + private DummyAPIAccess dummyAPI => (DummyAPIAccess)API; - Add(scroll = new BasicScrollContainer + private CommentsContainer commentsContainer; + + [SetUp] + public void SetUp() => Schedule(() => + Child = new BasicScrollContainer { RelativeSizeAxes = Axes.Both, - Child = comments = new TestCommentsContainer() + Child = commentsContainer = new CommentsContainer() }); - AddStep("Big Black comments", () => comments.ShowComments(CommentableType.Beatmapset, 41823)); - AddStep("Airman comments", () => comments.ShowComments(CommentableType.Beatmapset, 24313)); - AddStep("Lazer build comments", () => comments.ShowComments(CommentableType.Build, 4772)); - AddStep("News comments", () => comments.ShowComments(CommentableType.NewsPost, 715)); - AddStep("Trigger user change", comments.User.TriggerChange); - AddStep("Idle state", () => - { - scroll.Clear(); - scroll.Add(comments = new TestCommentsContainer()); - }); - } - - private class TestCommentsContainer : CommentsContainer + [Test] + public void TestIdleState() { - public new Bindable User => base.User; + AddUntilStep("loading spinner shown", + () => commentsContainer.ChildrenOfType().Single().IsLoading); } + + [Test] + public void TestSingleCommentsPage() + { + setUpCommentsResponse(exampleComments); + AddStep("show comments", () => commentsContainer.ShowComments(CommentableType.Beatmapset, 123)); + AddUntilStep("show more button hidden", + () => commentsContainer.ChildrenOfType().Single().Alpha == 0); + } + + [Test] + public void TestMultipleCommentPages() + { + var comments = exampleComments; + comments.HasMore = true; + comments.TopLevelCount = 10; + + setUpCommentsResponse(comments); + AddStep("show comments", () => commentsContainer.ShowComments(CommentableType.Beatmapset, 123)); + AddUntilStep("show more button visible", + () => commentsContainer.ChildrenOfType().Single().Alpha == 1); + } + + private void setUpCommentsResponse(CommentBundle commentBundle) + => AddStep("set up response", () => + { + dummyAPI.HandleRequest = request => + { + if (!(request is GetCommentsRequest getCommentsRequest)) + return; + + getCommentsRequest.TriggerSuccess(commentBundle); + }; + }); + + private CommentBundle exampleComments => new CommentBundle + { + Comments = new List + { + new Comment + { + Id = 1, + Message = "This is a comment", + LegacyName = "FirstUser", + CreatedAt = DateTimeOffset.Now, + VotesCount = 19, + RepliesCount = 1 + }, + new Comment + { + Id = 5, + ParentId = 1, + Message = "This is a child comment", + LegacyName = "SecondUser", + CreatedAt = DateTimeOffset.Now, + VotesCount = 4, + }, + new Comment + { + Id = 10, + Message = "This is another comment", + LegacyName = "ThirdUser", + CreatedAt = DateTimeOffset.Now, + VotesCount = 0 + }, + }, + IncludedComments = new List(), + }; } } From 5655e090d1e1102aea9c82b461ffec7402538c65 Mon Sep 17 00:00:00 2001 From: mcendu Date: Sat, 13 Jun 2020 18:45:06 +0800 Subject: [PATCH 2031/2376] revert movement of is mania skin check statements --- .../Skinning/ManiaLegacySkinTransformer.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs index 3304330233..f386712222 100644 --- a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs @@ -78,8 +78,6 @@ namespace osu.Game.Rulesets.Mania.Skinning public Drawable GetDrawableComponent(ISkinComponent component) { - if (!isLegacySkin.Value || !hasKeyTexture.Value) - return null; switch (component) { @@ -87,6 +85,9 @@ namespace osu.Game.Rulesets.Mania.Skinning return getResult(resultComponent.Component); case ManiaSkinComponent maniaComponent: + if (!isLegacySkin.Value || !hasKeyTexture.Value) + return null; + switch (maniaComponent.Component) { case ManiaSkinComponents.ColumnBackground: From 4eeb22ca18e4598ab693c7beb0fd5309188f8ee8 Mon Sep 17 00:00:00 2001 From: mcendu Date: Sat, 13 Jun 2020 18:47:40 +0800 Subject: [PATCH 2032/2376] rename a few variables and fix typo --- .../Skinning/ManiaLegacySkinTransformer.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs index f386712222..07d0ce66e2 100644 --- a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Mania.Skinning private readonly ManiaBeatmap beatmap; /// - /// Mapping of to ther corresponding + /// Mapping of to their corresponding /// value. /// private static readonly IReadOnlyDictionary hitresult_mapping @@ -129,11 +129,11 @@ namespace osu.Game.Rulesets.Mania.Skinning private Drawable getResult(HitResult result) { - string image = GetConfig( + string filename = GetConfig( new ManiaSkinConfigurationLookup(hitresult_mapping[result]) )?.Value ?? default_hitresult_skin_filenames[result]; - return this.GetAnimation(image, true, true); + return this.GetAnimation(filename, true, true); } public Texture GetTexture(string componentName) => source.GetTexture(componentName); From e8046654c8da91496a72c341e8f2f68bd4be66da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 13 Jun 2020 12:48:16 +0200 Subject: [PATCH 2033/2376] Add failing test case --- .../Visual/Online/TestSceneCommentsContainer.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs index 26ad0b0d3f..08130e60db 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs @@ -64,6 +64,21 @@ namespace osu.Game.Tests.Visual.Online () => commentsContainer.ChildrenOfType().Single().Alpha == 1); } + [Test] + public void TestMultipleLoads() + { + var comments = exampleComments; + int topLevelCommentCount = exampleComments.Comments.Count(comment => comment.IsTopLevel); + + AddStep("hide container", () => commentsContainer.Hide()); + setUpCommentsResponse(comments); + AddRepeatStep("show comments multiple times", + () => commentsContainer.ShowComments(CommentableType.Beatmapset, 456), 2); + AddStep("show container", () => commentsContainer.Show()); + AddUntilStep("comment count is correct", + () => commentsContainer.ChildrenOfType().Count() == topLevelCommentCount); + } + private void setUpCommentsResponse(CommentBundle commentBundle) => AddStep("set up response", () => { From aab606b237953d1d9844fd36245fe8b7e42fca08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 13 Jun 2020 13:00:05 +0200 Subject: [PATCH 2034/2376] Cancel scheduled asynchronous load of comments --- osu.Game/Overlays/Comments/CommentsContainer.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Comments/CommentsContainer.cs b/osu.Game/Overlays/Comments/CommentsContainer.cs index e7bfeaf968..f71808ba89 100644 --- a/osu.Game/Overlays/Comments/CommentsContainer.cs +++ b/osu.Game/Overlays/Comments/CommentsContainer.cs @@ -12,6 +12,7 @@ using osu.Game.Online.API.Requests.Responses; using System.Threading; using System.Linq; using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Threading; using osu.Game.Users; namespace osu.Game.Overlays.Comments @@ -30,6 +31,7 @@ namespace osu.Game.Overlays.Comments private IAPIProvider api { get; set; } private GetCommentsRequest request; + private ScheduledDelegate scheduledCommentsLoad; private CancellationTokenSource loadCancellation; private int currentPage; @@ -152,8 +154,9 @@ namespace osu.Game.Overlays.Comments request?.Cancel(); loadCancellation?.Cancel(); + scheduledCommentsLoad?.Cancel(); request = new GetCommentsRequest(id.Value, type.Value, Sort.Value, currentPage++, 0); - request.Success += res => Schedule(() => onSuccess(res)); + request.Success += res => scheduledCommentsLoad = Schedule(() => onSuccess(res)); api.PerformAsync(request); } From 8402d4a5f3c2e2cac8e9b5c4948a961f0e5d1b50 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 13 Jun 2020 21:18:56 +0900 Subject: [PATCH 2035/2376] Remove newline --- osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs index 07d0ce66e2..74a983fac8 100644 --- a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs @@ -78,7 +78,6 @@ namespace osu.Game.Rulesets.Mania.Skinning public Drawable GetDrawableComponent(ISkinComponent component) { - switch (component) { case GameplaySkinComponent resultComponent: From b9e247da8f3d4eeccf3448bda6d0c4554666cd55 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 13 Jun 2020 21:19:06 +0900 Subject: [PATCH 2036/2376] Simplify lookup code --- osu.Game/Skinning/LegacySkin.cs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 390dc871e4..0b2b723440 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -259,22 +259,12 @@ namespace osu.Game.Skinning return SkinUtils.As(new Bindable(existing.ColumnLineWidth[maniaLookup.TargetColumn.Value + 1])); case LegacyManiaSkinConfigurationLookups.Hit0: - return SkinUtils.As(getManiaImage(existing, "Hit0")); - case LegacyManiaSkinConfigurationLookups.Hit50: - return SkinUtils.As(getManiaImage(existing, "Hit50")); - case LegacyManiaSkinConfigurationLookups.Hit100: - return SkinUtils.As(getManiaImage(existing, "Hit100")); - case LegacyManiaSkinConfigurationLookups.Hit200: - return SkinUtils.As(getManiaImage(existing, "Hit200")); - case LegacyManiaSkinConfigurationLookups.Hit300: - return SkinUtils.As(getManiaImage(existing, "Hit300")); - case LegacyManiaSkinConfigurationLookups.Hit300g: - return SkinUtils.As(getManiaImage(existing, "Hit300g")); + return SkinUtils.As(getManiaImage(existing, maniaLookup.Lookup.ToString())); } return null; From 1cd96b80021c39e82e091808a89ac15c2426fc36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 13 Jun 2020 15:05:52 +0200 Subject: [PATCH 2037/2376] Rework StableInfo into a DI'd data structure --- osu.Game.Tournament/IPC/FileBasedIPC.cs | 48 ++++--------------- osu.Game.Tournament/Models/StableInfo.cs | 43 ++++++++++++++++- osu.Game.Tournament/Screens/SetupScreen.cs | 14 ++---- .../Screens/StablePathSelectScreen.cs | 20 ++++---- osu.Game.Tournament/TournamentGameBase.cs | 2 + 5 files changed, 67 insertions(+), 60 deletions(-) diff --git a/osu.Game.Tournament/IPC/FileBasedIPC.cs b/osu.Game.Tournament/IPC/FileBasedIPC.cs index 16f2b0b1fd..a9b39c7ba2 100644 --- a/osu.Game.Tournament/IPC/FileBasedIPC.cs +++ b/osu.Game.Tournament/IPC/FileBasedIPC.cs @@ -5,7 +5,6 @@ using System; using System.IO; using System.Linq; using System.Collections.Generic; -using Newtonsoft.Json; using Microsoft.Win32; using osu.Framework.Allocation; using osu.Framework.Logging; @@ -34,14 +33,13 @@ namespace osu.Game.Tournament.IPC [Resolved] private LadderInfo ladder { get; set; } + [Resolved] + private StableInfo stableInfo { get; set; } + private int lastBeatmapId; private ScheduledDelegate scheduled; private GetBeatmapRequest beatmapLookupRequest; - public StableInfo StableInfo { get; private set; } - - public const string STABLE_CONFIG = "tournament/stable.json"; - public Storage IPCStorage { get; private set; } [Resolved] @@ -165,8 +163,8 @@ namespace osu.Game.Tournament.IPC private string findStablePath() { - if (!string.IsNullOrEmpty(readStableConfig())) - return StableInfo.StablePath.Value; + if (!string.IsNullOrEmpty(stableInfo.StablePath)) + return stableInfo.StablePath; string stableInstallPath = string.Empty; @@ -204,43 +202,13 @@ namespace osu.Game.Tournament.IPC if (!ipcFileExistsInDirectory(path)) return false; - StableInfo.StablePath.Value = path; - - using (var stream = tournamentStorage.GetStream(STABLE_CONFIG, FileAccess.Write, FileMode.Create)) - using (var sw = new StreamWriter(stream)) - { - sw.Write(JsonConvert.SerializeObject(StableInfo, - new JsonSerializerSettings - { - Formatting = Formatting.Indented, - NullValueHandling = NullValueHandling.Ignore, - DefaultValueHandling = DefaultValueHandling.Ignore, - })); - } - + stableInfo.StablePath = path; LocateStableStorage(); + stableInfo.SaveChanges(); + return true; } - private string readStableConfig() - { - if (StableInfo == null) - StableInfo = new StableInfo(); - - if (tournamentStorage.Exists(FileBasedIPC.STABLE_CONFIG)) - { - using (Stream stream = tournamentStorage.GetStream(FileBasedIPC.STABLE_CONFIG, FileAccess.Read, FileMode.Open)) - using (var sr = new StreamReader(stream)) - { - StableInfo = JsonConvert.DeserializeObject(sr.ReadToEnd()); - } - - return StableInfo.StablePath.Value; - } - - return null; - } - private string findFromEnvVar() { try diff --git a/osu.Game.Tournament/Models/StableInfo.cs b/osu.Game.Tournament/Models/StableInfo.cs index 4818842151..1faf6beaff 100644 --- a/osu.Game.Tournament/Models/StableInfo.cs +++ b/osu.Game.Tournament/Models/StableInfo.cs @@ -2,7 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osu.Framework.Bindables; +using System.IO; +using Newtonsoft.Json; +using osu.Framework.Platform; namespace osu.Game.Tournament.Models { @@ -12,6 +14,43 @@ namespace osu.Game.Tournament.Models [Serializable] public class StableInfo { - public Bindable StablePath = new Bindable(string.Empty); + public string StablePath { get; set; } + + public event Action OnStableInfoSaved; + + private const string config_path = "tournament/stable.json"; + + private readonly Storage storage; + + public StableInfo(Storage storage) + { + this.storage = storage; + + if (!storage.Exists(config_path)) + return; + + using (Stream stream = storage.GetStream(config_path, FileAccess.Read, FileMode.Open)) + using (var sr = new StreamReader(stream)) + { + JsonConvert.PopulateObject(sr.ReadToEnd(), this); + } + } + + public void SaveChanges() + { + using (var stream = storage.GetStream(config_path, FileAccess.Write, FileMode.Create)) + using (var sw = new StreamWriter(stream)) + { + sw.Write(JsonConvert.SerializeObject(this, + new JsonSerializerSettings + { + Formatting = Formatting.Indented, + NullValueHandling = NullValueHandling.Ignore, + DefaultValueHandling = DefaultValueHandling.Ignore, + })); + } + + OnStableInfoSaved?.Invoke(); + } } } diff --git a/osu.Game.Tournament/Screens/SetupScreen.cs b/osu.Game.Tournament/Screens/SetupScreen.cs index 503a2487da..98bc292901 100644 --- a/osu.Game.Tournament/Screens/SetupScreen.cs +++ b/osu.Game.Tournament/Screens/SetupScreen.cs @@ -31,6 +31,9 @@ namespace osu.Game.Tournament.Screens [Resolved] private MatchIPCInfo ipc { get; set; } + [Resolved] + private StableInfo stableInfo { get; set; } + [Resolved] private IAPIProvider api { get; set; } @@ -57,6 +60,7 @@ namespace osu.Game.Tournament.Screens }; api.LocalUser.BindValueChanged(_ => Schedule(reload)); + stableInfo.OnStableInfoSaved += () => Schedule(reload); reload(); } @@ -66,21 +70,13 @@ namespace osu.Game.Tournament.Screens private void reload() { var fileBasedIpc = ipc as FileBasedIPC; - StableInfo stableInfo = fileBasedIpc?.StableInfo; fillFlow.Children = new Drawable[] { new ActionableInfo { Label = "Current IPC source", ButtonText = "Change source", - Action = () => - { - stableInfo?.StablePath.BindValueChanged(_ => - { - Schedule(reload); - }); - sceneManager?.SetScreen(new StablePathSelectScreen()); - }, + Action = () => sceneManager?.SetScreen(new StablePathSelectScreen()), Value = fileBasedIpc?.IPCStorage?.GetFullPath(string.Empty) ?? "Not found", Failing = fileBasedIpc?.IPCStorage == null, Description = "The osu!stable installation which is currently being used as a data source. If a source is not found, make sure you have created an empty ipc.txt in your stable cutting-edge installation." diff --git a/osu.Game.Tournament/Screens/StablePathSelectScreen.cs b/osu.Game.Tournament/Screens/StablePathSelectScreen.cs index ad0c06e4f9..816f0ed4b8 100644 --- a/osu.Game.Tournament/Screens/StablePathSelectScreen.cs +++ b/osu.Game.Tournament/Screens/StablePathSelectScreen.cs @@ -15,34 +15,36 @@ using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Overlays; using osu.Game.Tournament.IPC; using osu.Game.Tournament.Components; +using osu.Game.Tournament.Models; using osuTK; namespace osu.Game.Tournament.Screens { public class StablePathSelectScreen : TournamentScreen { - private DirectorySelector directorySelector; - [Resolved] - private MatchIPCInfo ipc { get; set; } - - private DialogOverlay overlay; + private GameHost host { get; set; } [Resolved(canBeNull: true)] private TournamentSceneManager sceneManager { get; set; } [Resolved] - private GameHost host { get; set; } + private MatchIPCInfo ipc { get; set; } + + [Resolved] + private StableInfo stableInfo { get; set; } + + private DirectorySelector directorySelector; + private DialogOverlay overlay; [BackgroundDependencyLoader(true)] private void load(Storage storage, OsuColour colours) { - var fileBasedIpc = ipc as FileBasedIPC; var initialPath = new DirectoryInfo(storage.GetFullPath(string.Empty)).Parent?.FullName; - if (!string.IsNullOrEmpty(fileBasedIpc?.StableInfo.StablePath.Value)) + if (!string.IsNullOrEmpty(stableInfo.StablePath)) { - initialPath = new DirectoryInfo(host.GetStorage(fileBasedIpc.StableInfo.StablePath.Value).GetFullPath(string.Empty)).Parent?.FullName; + initialPath = new DirectoryInfo(host.GetStorage(stableInfo.StablePath).GetFullPath(string.Empty)).Parent?.FullName; } AddRangeInternal(new Drawable[] diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 718c8ee644..5fc1d03f6d 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -53,6 +53,8 @@ namespace osu.Game.Tournament ladder.CurrentMatch.Value = ladder.Matches.FirstOrDefault(p => p.Current.Value); + dependencies.CacheAs(new StableInfo(storage)); + dependencies.CacheAs(ipc = new FileBasedIPC()); Add(ipc); } From 586d5791e029d2de9a779fdbf9d9c12af3cfab28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 13 Jun 2020 15:07:21 +0200 Subject: [PATCH 2038/2376] Remove unused argument --- .../Screens/TestSceneStablePathSelectScreen.cs | 3 +-- osu.Game.Tournament/Screens/StablePathSelectScreen.cs | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneStablePathSelectScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneStablePathSelectScreen.cs index ce0626dd0f..6e63b2d799 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneStablePathSelectScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneStablePathSelectScreen.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Game.Tournament.Screens; -using osu.Framework.Platform; namespace osu.Game.Tournament.Tests.Screens { @@ -15,7 +14,7 @@ namespace osu.Game.Tournament.Tests.Screens private class StablePathSelectTestScreen : StablePathSelectScreen { - protected override void ChangePath(Storage storage) + protected override void ChangePath() { Expire(); } diff --git a/osu.Game.Tournament/Screens/StablePathSelectScreen.cs b/osu.Game.Tournament/Screens/StablePathSelectScreen.cs index 816f0ed4b8..a830cbe4b9 100644 --- a/osu.Game.Tournament/Screens/StablePathSelectScreen.cs +++ b/osu.Game.Tournament/Screens/StablePathSelectScreen.cs @@ -108,7 +108,7 @@ namespace osu.Game.Tournament.Screens Origin = Anchor.Centre, Width = 300, Text = "Select stable path", - Action = () => ChangePath(storage) + Action = ChangePath }, new TriangleButton { @@ -135,7 +135,7 @@ namespace osu.Game.Tournament.Screens }); } - protected virtual void ChangePath(Storage storage) + protected virtual void ChangePath() { var target = directorySelector.CurrentDirectory.Value.FullName; var fileBasedIpc = ipc as FileBasedIPC; From 992aa0041e3933dcf8ded8b9778cb76bc5c02ff1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 13 Jun 2020 15:27:46 +0200 Subject: [PATCH 2039/2376] Allow auto-detect to work after choosing manually --- osu.Game.Tournament/IPC/FileBasedIPC.cs | 64 +++++++++++-------- .../Screens/StablePathSelectScreen.cs | 3 +- 2 files changed, 38 insertions(+), 29 deletions(-) diff --git a/osu.Game.Tournament/IPC/FileBasedIPC.cs b/osu.Game.Tournament/IPC/FileBasedIPC.cs index a9b39c7ba2..01466231a6 100644 --- a/osu.Game.Tournament/IPC/FileBasedIPC.cs +++ b/osu.Game.Tournament/IPC/FileBasedIPC.cs @@ -5,6 +5,7 @@ using System; using System.IO; using System.Linq; using System.Collections.Generic; +using JetBrains.Annotations; using Microsoft.Win32; using osu.Framework.Allocation; using osu.Framework.Logging; @@ -21,6 +22,8 @@ namespace osu.Game.Tournament.IPC { public class FileBasedIPC : MatchIPCInfo { + public Storage IPCStorage { get; private set; } + [Resolved] protected IAPIProvider API { get; private set; } @@ -36,22 +39,22 @@ namespace osu.Game.Tournament.IPC [Resolved] private StableInfo stableInfo { get; set; } + [Resolved] + private Storage tournamentStorage { get; set; } + private int lastBeatmapId; private ScheduledDelegate scheduled; private GetBeatmapRequest beatmapLookupRequest; - public Storage IPCStorage { get; private set; } - - [Resolved] - private Storage tournamentStorage { get; set; } - [BackgroundDependencyLoader] private void load() { - LocateStableStorage(); + var stablePath = stableInfo.StablePath ?? findStablePath(); + initialiseIPCStorage(stablePath); } - public Storage LocateStableStorage() + [CanBeNull] + private Storage initialiseIPCStorage(string path) { scheduled?.Cancel(); @@ -59,8 +62,6 @@ namespace osu.Game.Tournament.IPC try { - var path = findStablePath(); - if (string.IsNullOrEmpty(path)) return null; @@ -159,13 +160,37 @@ namespace osu.Game.Tournament.IPC return IPCStorage; } + public bool SetIPCLocation(string path) + { + if (!ipcFileExistsInDirectory(path)) + return false; + + var newStorage = initialiseIPCStorage(stableInfo.StablePath = path); + if (newStorage == null) + return false; + + stableInfo.SaveChanges(); + return true; + } + + public bool AutoDetectIPCLocation() + { + var autoDetectedPath = findStablePath(); + if (string.IsNullOrEmpty(autoDetectedPath)) + return false; + + var newStorage = initialiseIPCStorage(stableInfo.StablePath = autoDetectedPath); + if (newStorage == null) + return false; + + stableInfo.SaveChanges(); + return true; + } + private static bool ipcFileExistsInDirectory(string p) => File.Exists(Path.Combine(p, "ipc.txt")); private string findStablePath() { - if (!string.IsNullOrEmpty(stableInfo.StablePath)) - return stableInfo.StablePath; - string stableInstallPath = string.Empty; try @@ -183,10 +208,7 @@ namespace osu.Game.Tournament.IPC stableInstallPath = r.Invoke(); if (stableInstallPath != null) - { - SetIPCLocation(stableInstallPath); return stableInstallPath; - } } return null; @@ -197,18 +219,6 @@ namespace osu.Game.Tournament.IPC } } - public bool SetIPCLocation(string path) - { - if (!ipcFileExistsInDirectory(path)) - return false; - - stableInfo.StablePath = path; - LocateStableStorage(); - stableInfo.SaveChanges(); - - return true; - } - private string findFromEnvVar() { try diff --git a/osu.Game.Tournament/Screens/StablePathSelectScreen.cs b/osu.Game.Tournament/Screens/StablePathSelectScreen.cs index a830cbe4b9..2a54dffc7c 100644 --- a/osu.Game.Tournament/Screens/StablePathSelectScreen.cs +++ b/osu.Game.Tournament/Screens/StablePathSelectScreen.cs @@ -156,9 +156,8 @@ namespace osu.Game.Tournament.Screens protected virtual void AutoDetect() { var fileBasedIpc = ipc as FileBasedIPC; - fileBasedIpc?.LocateStableStorage(); - if (fileBasedIpc?.IPCStorage == null) + if (!fileBasedIpc?.AutoDetectIPCLocation() ?? true) { overlay = new DialogOverlay(); overlay.Push(new IPCErrorDialog("Failed to auto detect", "An osu! stable cutting-edge installation could not be auto detected.\nPlease try and manually point to the directory.")); From 34cd9f7a699d62dd339c9a18bb95fefeddab0de1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 13 Jun 2020 15:32:30 +0200 Subject: [PATCH 2040/2376] Streamline autodetect & manual set path --- osu.Game.Tournament/IPC/FileBasedIPC.cs | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tournament/IPC/FileBasedIPC.cs b/osu.Game.Tournament/IPC/FileBasedIPC.cs index 01466231a6..de9df3ca35 100644 --- a/osu.Game.Tournament/IPC/FileBasedIPC.cs +++ b/osu.Game.Tournament/IPC/FileBasedIPC.cs @@ -162,7 +162,7 @@ namespace osu.Game.Tournament.IPC public bool SetIPCLocation(string path) { - if (!ipcFileExistsInDirectory(path)) + if (path == null || !ipcFileExistsInDirectory(path)) return false; var newStorage = initialiseIPCStorage(stableInfo.StablePath = path); @@ -173,22 +173,11 @@ namespace osu.Game.Tournament.IPC return true; } - public bool AutoDetectIPCLocation() - { - var autoDetectedPath = findStablePath(); - if (string.IsNullOrEmpty(autoDetectedPath)) - return false; - - var newStorage = initialiseIPCStorage(stableInfo.StablePath = autoDetectedPath); - if (newStorage == null) - return false; - - stableInfo.SaveChanges(); - return true; - } + public bool AutoDetectIPCLocation() => SetIPCLocation(findStablePath()); private static bool ipcFileExistsInDirectory(string p) => File.Exists(Path.Combine(p, "ipc.txt")); + [CanBeNull] private string findStablePath() { string stableInstallPath = string.Empty; From e0518fd451ac9aebd3b4506ada28eb202e55cf25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 13 Jun 2020 15:38:29 +0200 Subject: [PATCH 2041/2376] Fix silent failure --- osu.Game.Tournament/Screens/StablePathSelectScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/StablePathSelectScreen.cs b/osu.Game.Tournament/Screens/StablePathSelectScreen.cs index 2a54dffc7c..0b9900c0d4 100644 --- a/osu.Game.Tournament/Screens/StablePathSelectScreen.cs +++ b/osu.Game.Tournament/Screens/StablePathSelectScreen.cs @@ -141,7 +141,7 @@ namespace osu.Game.Tournament.Screens var fileBasedIpc = ipc as FileBasedIPC; Logger.Log($"Changing Stable CE location to {target}"); - if (!fileBasedIpc?.SetIPCLocation(target) ?? false) + if (!fileBasedIpc?.SetIPCLocation(target) ?? true) { overlay = new DialogOverlay(); overlay.Push(new IPCErrorDialog("This is an invalid IPC Directory", "Select a directory that contains an osu! stable cutting edge installation and make sure it has an empty ipc.txt file in it.")); From 5dd47bf393b5116b9077ef4aeea3942ca5a0d766 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 13 Jun 2020 16:01:00 +0200 Subject: [PATCH 2042/2376] Remove unnecessary members --- osu.Game.Tournament/IPC/FileBasedIPC.cs | 3 --- osu.Game.Tournament/Screens/StablePathSelectScreen.cs | 9 --------- 2 files changed, 12 deletions(-) diff --git a/osu.Game.Tournament/IPC/FileBasedIPC.cs b/osu.Game.Tournament/IPC/FileBasedIPC.cs index de9df3ca35..d52a2b6445 100644 --- a/osu.Game.Tournament/IPC/FileBasedIPC.cs +++ b/osu.Game.Tournament/IPC/FileBasedIPC.cs @@ -39,9 +39,6 @@ namespace osu.Game.Tournament.IPC [Resolved] private StableInfo stableInfo { get; set; } - [Resolved] - private Storage tournamentStorage { get; set; } - private int lastBeatmapId; private ScheduledDelegate scheduled; private GetBeatmapRequest beatmapLookupRequest; diff --git a/osu.Game.Tournament/Screens/StablePathSelectScreen.cs b/osu.Game.Tournament/Screens/StablePathSelectScreen.cs index 0b9900c0d4..958c3ef822 100644 --- a/osu.Game.Tournament/Screens/StablePathSelectScreen.cs +++ b/osu.Game.Tournament/Screens/StablePathSelectScreen.cs @@ -15,7 +15,6 @@ using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Overlays; using osu.Game.Tournament.IPC; using osu.Game.Tournament.Components; -using osu.Game.Tournament.Models; using osuTK; namespace osu.Game.Tournament.Screens @@ -31,9 +30,6 @@ namespace osu.Game.Tournament.Screens [Resolved] private MatchIPCInfo ipc { get; set; } - [Resolved] - private StableInfo stableInfo { get; set; } - private DirectorySelector directorySelector; private DialogOverlay overlay; @@ -42,11 +38,6 @@ namespace osu.Game.Tournament.Screens { var initialPath = new DirectoryInfo(storage.GetFullPath(string.Empty)).Parent?.FullName; - if (!string.IsNullOrEmpty(stableInfo.StablePath)) - { - initialPath = new DirectoryInfo(host.GetStorage(stableInfo.StablePath).GetFullPath(string.Empty)).Parent?.FullName; - } - AddRangeInternal(new Drawable[] { new Container From 201bfda3382931077cc8acc26082ce740004f123 Mon Sep 17 00:00:00 2001 From: Ronnie Moir <7267697+H2n9@users.noreply.github.com> Date: Sat, 13 Jun 2020 15:16:27 +0100 Subject: [PATCH 2043/2376] Give ModTimeRamp an adjust pitch setting. Implement in ModWindDown and ModWindUp --- osu.Game/Rulesets/Mods/ModTimeRamp.cs | 16 +++++++++++++++- osu.Game/Rulesets/Mods/ModWindDown.cs | 7 +++++++ osu.Game/Rulesets/Mods/ModWindUp.cs | 7 +++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index c1f3e357a1..a38aa2bac6 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -26,6 +26,9 @@ namespace osu.Game.Rulesets.Mods [SettingSource("Final rate", "The final speed to ramp to")] public abstract BindableNumber FinalRate { get; } + [SettingSource("Adjust Pitch", "Should pitch be adjusted with speed")] + public abstract BindableBool AdjustPitch { get; } + public override string SettingDescription => $"{InitialRate.Value:N2}x to {FinalRate.Value:N2}x"; private double finalRateTime; @@ -44,12 +47,15 @@ namespace osu.Game.Rulesets.Mods { // for preview purpose at song select. eventually we'll want to be able to update every frame. FinalRate.BindValueChanged(val => applyAdjustment(1), true); + + AdjustPitch.BindValueChanged(updatePitchAdjustment, false); } public void ApplyToTrack(Track track) { this.track = track; - track.AddAdjustment(AdjustableProperty.Frequency, SpeedChange); + + track.AddAdjustment(AdjustPitch.Value ? AdjustableProperty.Frequency : AdjustableProperty.Tempo, SpeedChange); FinalRate.TriggerChange(); } @@ -75,5 +81,13 @@ namespace osu.Game.Rulesets.Mods /// The amount of adjustment to apply (from 0..1). private void applyAdjustment(double amount) => SpeedChange.Value = InitialRate.Value + (FinalRate.Value - InitialRate.Value) * Math.Clamp(amount, 0, 1); + + private void updatePitchAdjustment(ValueChangedEvent value) + { + // remove existing old adjustment + track.RemoveAdjustment(value.OldValue ? AdjustableProperty.Frequency : AdjustableProperty.Tempo, SpeedChange); + + track.AddAdjustment(value.NewValue ? AdjustableProperty.Frequency : AdjustableProperty.Tempo, SpeedChange); + } } } diff --git a/osu.Game/Rulesets/Mods/ModWindDown.cs b/osu.Game/Rulesets/Mods/ModWindDown.cs index 5e634ac434..e46b4eff2e 100644 --- a/osu.Game/Rulesets/Mods/ModWindDown.cs +++ b/osu.Game/Rulesets/Mods/ModWindDown.cs @@ -37,6 +37,13 @@ namespace osu.Game.Rulesets.Mods Precision = 0.01, }; + [SettingSource("Adjust Pitch", "Should pitch be adjusted with speed")] + public override BindableBool AdjustPitch { get; } = new BindableBool + { + Default = true, + Value = true + }; + public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModWindUp)).ToArray(); } } diff --git a/osu.Game/Rulesets/Mods/ModWindUp.cs b/osu.Game/Rulesets/Mods/ModWindUp.cs index 74c6fc22d3..02203a474d 100644 --- a/osu.Game/Rulesets/Mods/ModWindUp.cs +++ b/osu.Game/Rulesets/Mods/ModWindUp.cs @@ -37,6 +37,13 @@ namespace osu.Game.Rulesets.Mods Precision = 0.01, }; + [SettingSource("Adjust Pitch", "Should pitch be adjusted with speed")] + public override BindableBool AdjustPitch { get; } = new BindableBool + { + Default = true, + Value = true + }; + public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModWindDown)).ToArray(); } } From 2cadab8d29a3e5f30fbc9676f1a23d3fdd6df682 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 13 Jun 2020 16:20:59 +0200 Subject: [PATCH 2044/2376] Add xmldoc --- osu.Game.Tournament/IPC/FileBasedIPC.cs | 10 ++++++++++ osu.Game.Tournament/Models/StableInfo.cs | 6 ++++++ 2 files changed, 16 insertions(+) diff --git a/osu.Game.Tournament/IPC/FileBasedIPC.cs b/osu.Game.Tournament/IPC/FileBasedIPC.cs index d52a2b6445..681839ebc4 100644 --- a/osu.Game.Tournament/IPC/FileBasedIPC.cs +++ b/osu.Game.Tournament/IPC/FileBasedIPC.cs @@ -157,6 +157,11 @@ namespace osu.Game.Tournament.IPC return IPCStorage; } + /// + /// Manually sets the path to the directory used for inter-process communication with a cutting-edge install. + /// + /// Path to the IPC directory + /// Whether the supplied path was a valid IPC directory. public bool SetIPCLocation(string path) { if (path == null || !ipcFileExistsInDirectory(path)) @@ -170,6 +175,11 @@ namespace osu.Game.Tournament.IPC return true; } + /// + /// Tries to automatically detect the path to the directory used for inter-process communication + /// with a cutting-edge install. + /// + /// Whether an IPC directory was successfully auto-detected. public bool AutoDetectIPCLocation() => SetIPCLocation(findStablePath()); private static bool ipcFileExistsInDirectory(string p) => File.Exists(Path.Combine(p, "ipc.txt")); diff --git a/osu.Game.Tournament/Models/StableInfo.cs b/osu.Game.Tournament/Models/StableInfo.cs index 1faf6beaff..0b0050a245 100644 --- a/osu.Game.Tournament/Models/StableInfo.cs +++ b/osu.Game.Tournament/Models/StableInfo.cs @@ -14,8 +14,14 @@ namespace osu.Game.Tournament.Models [Serializable] public class StableInfo { + /// + /// Path to the IPC directory used by the stable (cutting-edge) install. + /// public string StablePath { get; set; } + /// + /// Fired whenever stable info is successfully saved to file. + /// public event Action OnStableInfoSaved; private const string config_path = "tournament/stable.json"; From 308ec6a491a051e029e9fd3d2ee30fa03cd080bf Mon Sep 17 00:00:00 2001 From: mcendu Date: Sat, 13 Jun 2020 23:05:57 +0800 Subject: [PATCH 2045/2376] add extension method for mania skin config retrieval --- .../Skinning/ManiaSkinConfigExtensions.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 osu.Game.Rulesets.Mania/Skinning/ManiaSkinConfigExtensions.cs diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaSkinConfigExtensions.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaSkinConfigExtensions.cs new file mode 100644 index 0000000000..2e17a6bef1 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Skinning/ManiaSkinConfigExtensions.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.Bindables; +using osu.Game.Skinning; + +namespace osu.Game.Rulesets.Mania.Skinning +{ + public static class ManiaSkinConfigExtensions + { + /// + /// Retrieve a per-column-count skin configuration. + /// + /// The skin from which configuration is retrieved. + /// The value to retrieve. + /// If not null, denotes the index of the column to which the entry applies. + public static IBindable GetManiaSkinConfig(this ISkin skin, LegacyManiaSkinConfigurationLookups lookup, int? index = null) + => skin.GetConfig( + new ManiaSkinConfigurationLookup(lookup, index)); + } +} From bd7b7b50176c37cd799a560a060adf5a9f74daab Mon Sep 17 00:00:00 2001 From: mcendu Date: Sat, 13 Jun 2020 23:06:25 +0800 Subject: [PATCH 2046/2376] make all former LegacyManiaElement subclasses use extension method Remove LegacyManiaElement --- .../Skinning/LegacyHitTarget.cs | 8 +++--- .../Skinning/LegacyManiaColumnElement.cs | 6 ++--- .../Skinning/LegacyManiaElement.cs | 25 ------------------- .../Skinning/LegacyStageBackground.cs | 7 +++--- .../Skinning/LegacyStageForeground.cs | 5 ++-- 5 files changed, 14 insertions(+), 37 deletions(-) delete mode 100644 osu.Game.Rulesets.Mania/Skinning/LegacyManiaElement.cs diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs index 40752d3f4b..d055ef3480 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs @@ -14,7 +14,7 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Mania.Skinning { - public class LegacyHitTarget : LegacyManiaElement + public class LegacyHitTarget : CompositeDrawable { private readonly IBindable direction = new Bindable(); @@ -28,13 +28,13 @@ namespace osu.Game.Rulesets.Mania.Skinning [BackgroundDependencyLoader] private void load(ISkinSource skin, IScrollingInfo scrollingInfo) { - string targetImage = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.HitTargetImage)?.Value + string targetImage = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.HitTargetImage)?.Value ?? "mania-stage-hint"; - bool showJudgementLine = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ShowJudgementLine)?.Value + bool showJudgementLine = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ShowJudgementLine)?.Value ?? true; - Color4 lineColour = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.JudgementLineColour)?.Value + Color4 lineColour = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.JudgementLineColour)?.Value ?? Color4.White; InternalChild = directionContainer = new Container diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs index 05b731ec5d..0c46a00bed 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Mania.Skinning /// /// A which is placed somewhere within a . /// - public class LegacyManiaColumnElement : LegacyManiaElement + public class LegacyManiaColumnElement : CompositeDrawable { [Resolved] protected Column Column { get; private set; } @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Mania.Skinning } } - protected override IBindable GetManiaSkinConfig(ISkin skin, LegacyManiaSkinConfigurationLookups lookup, int? index = null) - => base.GetManiaSkinConfig(skin, lookup, index ?? Column.Index); + protected IBindable GetManiaSkinConfig(ISkin skin, LegacyManiaSkinConfigurationLookups lookup, int? index = null) + => skin.GetManiaSkinConfig(lookup, index ?? Column.Index); } } diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyManiaElement.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyManiaElement.cs deleted file mode 100644 index 11fdd663a1..0000000000 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyManiaElement.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Bindables; -using osu.Framework.Graphics.Containers; -using osu.Game.Skinning; - -namespace osu.Game.Rulesets.Mania.Skinning -{ - /// - /// A mania legacy skin element. - /// - public class LegacyManiaElement : CompositeDrawable - { - /// - /// Retrieve a per-column-count skin configuration. - /// - /// The skin from which configuration is retrieved. - /// The value to retrieve. - /// If not null, denotes the index of the column to which the entry applies. - protected virtual IBindable GetManiaSkinConfig(ISkin skin, LegacyManiaSkinConfigurationLookups lookup, int? index = null) - => skin.GetConfig( - new ManiaSkinConfigurationLookup(lookup, index)); - } -} diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs index f177284399..7f5de601ca 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs @@ -3,13 +3,14 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Game.Skinning; using osuTK; namespace osu.Game.Rulesets.Mania.Skinning { - public class LegacyStageBackground : LegacyManiaElement + public class LegacyStageBackground : CompositeDrawable { private Drawable leftSprite; private Drawable rightSprite; @@ -22,10 +23,10 @@ namespace osu.Game.Rulesets.Mania.Skinning [BackgroundDependencyLoader] private void load(ISkinSource skin) { - string leftImage = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.LeftStageImage)?.Value + string leftImage = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.LeftStageImage)?.Value ?? "mania-stage-left"; - string rightImage = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.RightStageImage)?.Value + string rightImage = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.RightStageImage)?.Value ?? "mania-stage-right"; InternalChildren = new[] diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyStageForeground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyStageForeground.cs index 9719005d54..4609fcc849 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyStageForeground.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyStageForeground.cs @@ -4,13 +4,14 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; using osuTK; namespace osu.Game.Rulesets.Mania.Skinning { - public class LegacyStageForeground : LegacyManiaElement + public class LegacyStageForeground : CompositeDrawable { private readonly IBindable direction = new Bindable(); @@ -24,7 +25,7 @@ namespace osu.Game.Rulesets.Mania.Skinning [BackgroundDependencyLoader] private void load(ISkinSource skin, IScrollingInfo scrollingInfo) { - string bottomImage = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.BottomStageImage)?.Value + string bottomImage = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.BottomStageImage)?.Value ?? "mania-stage-bottom"; sprite = skin.GetAnimation(bottomImage, true, true)?.With(d => From ffae73a966c8afcc131c9de8b082e44950211487 Mon Sep 17 00:00:00 2001 From: mcendu Date: Sat, 13 Jun 2020 23:07:04 +0800 Subject: [PATCH 2047/2376] let retrievals outside mania skin components use extension https://github.com/ppy/osu/pull/9264#discussion_r439730321 --- .../Skinning/ManiaLegacySkinTransformer.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs index 74a983fac8..19a107eb0d 100644 --- a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs @@ -71,8 +71,7 @@ namespace osu.Game.Rulesets.Mania.Skinning { isLegacySkin = new Lazy(() => source.GetConfig(LegacySkinConfiguration.LegacySetting.Version) != null); hasKeyTexture = new Lazy(() => source.GetAnimation( - GetConfig( - new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.KeyImage, 0))?.Value + this.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.KeyImage)?.Value ?? "mania-key1", true, true) != null); } @@ -128,9 +127,8 @@ namespace osu.Game.Rulesets.Mania.Skinning private Drawable getResult(HitResult result) { - string filename = GetConfig( - new ManiaSkinConfigurationLookup(hitresult_mapping[result]) - )?.Value ?? default_hitresult_skin_filenames[result]; + string filename = this.GetManiaSkinConfig(hitresult_mapping[result])?.Value + ?? default_hitresult_skin_filenames[result]; return this.GetAnimation(filename, true, true); } From 9a0a1ba0df10b87baa552e9d5869892d4cbfe3d9 Mon Sep 17 00:00:00 2001 From: mcendu Date: Sat, 13 Jun 2020 23:12:15 +0800 Subject: [PATCH 2048/2376] correct logic of hasKeyTexture determination --- osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs index 19a107eb0d..7a2fa711e3 100644 --- a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs @@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Mania.Skinning { isLegacySkin = new Lazy(() => source.GetConfig(LegacySkinConfiguration.LegacySetting.Version) != null); hasKeyTexture = new Lazy(() => source.GetAnimation( - this.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.KeyImage)?.Value + this.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.KeyImage, 0)?.Value ?? "mania-key1", true, true) != null); } From eb92c3390d6b4299a2c23f0ade181ae8e4b575b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 13 Jun 2020 17:17:58 +0200 Subject: [PATCH 2049/2376] Check for nulls when looking for ipc.txt --- osu.Game.Tournament/IPC/FileBasedIPC.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tournament/IPC/FileBasedIPC.cs b/osu.Game.Tournament/IPC/FileBasedIPC.cs index 681839ebc4..a17491bf2d 100644 --- a/osu.Game.Tournament/IPC/FileBasedIPC.cs +++ b/osu.Game.Tournament/IPC/FileBasedIPC.cs @@ -182,7 +182,7 @@ namespace osu.Game.Tournament.IPC /// Whether an IPC directory was successfully auto-detected. public bool AutoDetectIPCLocation() => SetIPCLocation(findStablePath()); - private static bool ipcFileExistsInDirectory(string p) => File.Exists(Path.Combine(p, "ipc.txt")); + private static bool ipcFileExistsInDirectory(string p) => p != null && File.Exists(Path.Combine(p, "ipc.txt")); [CanBeNull] private string findStablePath() From 77eb428184473b09e0cb370adaf11e45e0b9ed9e Mon Sep 17 00:00:00 2001 From: Ronnie Moir <7267697+H2n9@users.noreply.github.com> Date: Sat, 13 Jun 2020 16:30:21 +0100 Subject: [PATCH 2050/2376] Use consistent setting casing --- osu.Game/Rulesets/Mods/ModWindDown.cs | 2 +- osu.Game/Rulesets/Mods/ModWindUp.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModWindDown.cs b/osu.Game/Rulesets/Mods/ModWindDown.cs index e46b4eff2e..679b50057b 100644 --- a/osu.Game/Rulesets/Mods/ModWindDown.cs +++ b/osu.Game/Rulesets/Mods/ModWindDown.cs @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Mods Precision = 0.01, }; - [SettingSource("Adjust Pitch", "Should pitch be adjusted with speed")] + [SettingSource("Adjust pitch", "Should pitch be adjusted with speed")] public override BindableBool AdjustPitch { get; } = new BindableBool { Default = true, diff --git a/osu.Game/Rulesets/Mods/ModWindUp.cs b/osu.Game/Rulesets/Mods/ModWindUp.cs index 02203a474d..b733bf423e 100644 --- a/osu.Game/Rulesets/Mods/ModWindUp.cs +++ b/osu.Game/Rulesets/Mods/ModWindUp.cs @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Mods Precision = 0.01, }; - [SettingSource("Adjust Pitch", "Should pitch be adjusted with speed")] + [SettingSource("Adjust pitch", "Should pitch be adjusted with speed")] public override BindableBool AdjustPitch { get; } = new BindableBool { Default = true, From dc5bb12fa8e9f6f47c14c57b8242b24f24aa37c3 Mon Sep 17 00:00:00 2001 From: Ronnie Moir <7267697+H2n9@users.noreply.github.com> Date: Sat, 13 Jun 2020 16:32:43 +0100 Subject: [PATCH 2051/2376] Use local helper for selecting adjusted property --- osu.Game/Rulesets/Mods/ModTimeRamp.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index a38aa2bac6..9f30f340fd 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Mods [SettingSource("Final rate", "The final speed to ramp to")] public abstract BindableNumber FinalRate { get; } - [SettingSource("Adjust Pitch", "Should pitch be adjusted with speed")] + [SettingSource("Adjust pitch", "Should pitch be adjusted with speed")] public abstract BindableBool AdjustPitch { get; } public override string SettingDescription => $"{InitialRate.Value:N2}x to {FinalRate.Value:N2}x"; @@ -55,9 +55,8 @@ namespace osu.Game.Rulesets.Mods { this.track = track; - track.AddAdjustment(AdjustPitch.Value ? AdjustableProperty.Frequency : AdjustableProperty.Tempo, SpeedChange); - FinalRate.TriggerChange(); + AdjustPitch.TriggerChange(); } public virtual void ApplyToBeatmap(IBeatmap beatmap) @@ -85,9 +84,12 @@ namespace osu.Game.Rulesets.Mods private void updatePitchAdjustment(ValueChangedEvent value) { // remove existing old adjustment - track.RemoveAdjustment(value.OldValue ? AdjustableProperty.Frequency : AdjustableProperty.Tempo, SpeedChange); + track.RemoveAdjustment(adjustmentForPitchSetting(value.OldValue), SpeedChange); - track.AddAdjustment(value.NewValue ? AdjustableProperty.Frequency : AdjustableProperty.Tempo, SpeedChange); + track.AddAdjustment(adjustmentForPitchSetting(value.NewValue), SpeedChange); } + + private AdjustableProperty adjustmentForPitchSetting(bool value) + => value ? AdjustableProperty.Frequency : AdjustableProperty.Tempo; } } From 4bfc16b4ce73ad2a5ee48282ee5636896e48ae95 Mon Sep 17 00:00:00 2001 From: Shivam Date: Sat, 13 Jun 2020 17:48:15 +0200 Subject: [PATCH 2052/2376] Implement changes from review Moves seeya back to the introscreen and uses a virtual string to change whenever it's needed and removed remainingTime() --- osu.Game/Screens/Menu/IntroScreen.cs | 8 +++++--- osu.Game/Screens/Menu/IntroWelcome.cs | 18 +++++++----------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index 2f9d43bed6..88d18d0073 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -49,7 +49,9 @@ namespace osu.Game.Screens.Menu private const int exit_delay = 3000; - protected SampleChannel Seeya { get; set; } + private SampleChannel seeya; + + protected virtual string SeeyaSampleName => "Intro/seeya"; private LeasedBindable beatmap; @@ -72,7 +74,7 @@ namespace osu.Game.Screens.Menu MenuVoice = config.GetBindable(OsuSetting.MenuVoice); MenuMusic = config.GetBindable(OsuSetting.MenuMusic); - Seeya = audio.Samples.Get(@"Intro/seeya"); + seeya = audio.Samples.Get(SeeyaSampleName); BeatmapSetInfo setInfo = null; @@ -124,7 +126,7 @@ namespace osu.Game.Screens.Menu double fadeOutTime = exit_delay; // we also handle the exit transition. if (MenuVoice.Value) - Seeya.Play(); + seeya.Play(); else fadeOutTime = 500; diff --git a/osu.Game/Screens/Menu/IntroWelcome.cs b/osu.Game/Screens/Menu/IntroWelcome.cs index dec3af5ac9..a431752369 100644 --- a/osu.Game/Screens/Menu/IntroWelcome.cs +++ b/osu.Game/Screens/Menu/IntroWelcome.cs @@ -22,17 +22,15 @@ namespace osu.Game.Screens.Menu private const double delay_step_two = 2142; private SampleChannel welcome; private SampleChannel pianoReverb; + protected override string SeeyaSampleName => "Intro/Welcome/seeya"; [BackgroundDependencyLoader] private void load(AudioManager audio) { - Seeya = audio.Samples.Get(@"Intro/welcome/seeya"); - if (MenuVoice.Value) - { - welcome = audio.Samples.Get(@"Intro/welcome/welcome"); - pianoReverb = audio.Samples.Get(@"Intro/welcome/welcome_piano"); - } + welcome = audio.Samples.Get(@"Intro/Welcome/welcome"); + + pianoReverb = audio.Samples.Get(@"Intro/Welcome/welcome_piano"); } protected override void LogoArriving(OsuLogo logo, bool resuming) @@ -126,7 +124,7 @@ namespace osu.Game.Screens.Menu Width = 750, Height = 78, Alpha = 0, - Texture = textures.Get(@"Welcome/welcome_text@2x") + Texture = textures.Get(@"Welcome/welcome_text") }, }; } @@ -135,13 +133,11 @@ namespace osu.Game.Screens.Menu { base.LoadComplete(); - double remainingTime() => delay_step_two - TransformDelay; - using (BeginDelayedSequence(0, true)) { welcomeText.ResizeHeightTo(welcomeText.Height * 2, 500, Easing.In); - welcomeText.FadeIn(remainingTime()); - welcomeText.ScaleTo(welcomeText.Scale + new Vector2(0.1f), remainingTime(), Easing.Out).OnComplete(_ => Expire()); + welcomeText.FadeIn(delay_step_two); + welcomeText.ScaleTo(welcomeText.Scale + new Vector2(0.1f), delay_step_two, Easing.Out).OnComplete(_ => Expire()); } } } From 51bbd91373a54b7cfbd24151d97345e4d193cfb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 13 Jun 2020 19:28:21 +0200 Subject: [PATCH 2053/2376] Bring back initial directory behaviour --- osu.Game.Tournament/Screens/StablePathSelectScreen.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/StablePathSelectScreen.cs b/osu.Game.Tournament/Screens/StablePathSelectScreen.cs index 958c3ef822..b4d56f60c7 100644 --- a/osu.Game.Tournament/Screens/StablePathSelectScreen.cs +++ b/osu.Game.Tournament/Screens/StablePathSelectScreen.cs @@ -36,7 +36,8 @@ namespace osu.Game.Tournament.Screens [BackgroundDependencyLoader(true)] private void load(Storage storage, OsuColour colours) { - var initialPath = new DirectoryInfo(storage.GetFullPath(string.Empty)).Parent?.FullName; + var initialStorage = (ipc as FileBasedIPC)?.IPCStorage ?? storage; + var initialPath = new DirectoryInfo(initialStorage.GetFullPath(string.Empty)).Parent?.FullName; AddRangeInternal(new Drawable[] { From 7b95c55afb17429a8eb1b253c62767c11f3792c9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 14 Jun 2020 11:33:59 +0900 Subject: [PATCH 2054/2376] Fix HardwareCorrectionOffsetClock breaking ElapsedTime readings --- osu.Game/Screens/Play/GameplayClockContainer.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 2f85d6ad1e..fe1d22e987 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -251,8 +251,9 @@ namespace osu.Game.Screens.Play private class HardwareCorrectionOffsetClock : FramedOffsetClock { - // we always want to apply the same real-time offset, so it should be adjusted by the playback rate to achieve this. - public override double CurrentTime => SourceTime + Offset * Rate; + // 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 * (1 - Rate); public HardwareCorrectionOffsetClock(IClock source, bool processSource = true) : base(source, processSource) From 1164a1048330211410a9f050176bf868c56a023d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 14 Jun 2020 11:34:07 +0900 Subject: [PATCH 2055/2376] Add test coverage --- .../TestSceneGameplayClockContainer.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 osu.Game.Tests/Gameplay/TestSceneGameplayClockContainer.cs diff --git a/osu.Game.Tests/Gameplay/TestSceneGameplayClockContainer.cs b/osu.Game.Tests/Gameplay/TestSceneGameplayClockContainer.cs new file mode 100644 index 0000000000..a97566ba7b --- /dev/null +++ b/osu.Game.Tests/Gameplay/TestSceneGameplayClockContainer.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; +using NUnit.Framework; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; +using osu.Game.Screens.Play; +using osu.Game.Tests.Visual; + +namespace osu.Game.Tests.Gameplay +{ + public class TestSceneGameplayClockContainer : OsuTestScene + { + [Test] + public void TestStartThenElapsedTime() + { + GameplayClockContainer gcc = null; + + AddStep("create container", () => Add(gcc = new GameplayClockContainer(CreateWorkingBeatmap(new OsuRuleset().RulesetInfo), Array.Empty(), 0))); + AddStep("start track", () => gcc.Start()); + AddUntilStep("elapsed greater than zero", () => gcc.GameplayClock.ElapsedFrameTime > 0); + } + } +} From abe07b742ebb16190fe0a4601f1fbbc8094748c7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 14 Jun 2020 13:20:58 +0900 Subject: [PATCH 2056/2376] Fix drag scroll in editor timeline no longer working correctly --- osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs | 2 -- .../Edit/Compose/Components/ComposeBlueprintContainer.cs | 3 +++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index d07cffff0c..cc417bbb10 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -44,8 +44,6 @@ namespace osu.Game.Screens.Edit.Compose.Components private readonly BindableList selectedHitObjects = new BindableList(); - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; - [Resolved(canBeNull: true)] private IPositionSnapProvider snapProvider { get; set; } diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 0b5d8262fd..e1f311f1b8 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -11,6 +11,7 @@ using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; +using osuTK; namespace osu.Game.Screens.Edit.Compose.Components { @@ -26,6 +27,8 @@ namespace osu.Game.Screens.Edit.Compose.Components private readonly Container placementBlueprintContainer; + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; + private InputManager inputManager; private readonly IEnumerable drawableHitObjects; From 0d53d0ffc8f361576a60ebda3ae1e5dece2c6144 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Jun 2020 00:46:20 +0900 Subject: [PATCH 2057/2376] Fix back-to-front math --- osu.Game/Screens/Play/GameplayClockContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index fe1d22e987..0653373c91 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -253,7 +253,7 @@ 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 * (1 - Rate); + public override double CurrentTime => base.CurrentTime + Offset * (Rate - 1); public HardwareCorrectionOffsetClock(IClock source, bool processSource = true) : base(source, processSource) From 9907b4763bb76963eee9d652640559f07e5fce1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 14 Jun 2020 18:39:41 +0200 Subject: [PATCH 2058/2376] Remove redundant default argument value --- osu.Game/Rulesets/Mods/ModTimeRamp.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index 9f30f340fd..09c50ce115 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Mods // for preview purpose at song select. eventually we'll want to be able to update every frame. FinalRate.BindValueChanged(val => applyAdjustment(1), true); - AdjustPitch.BindValueChanged(updatePitchAdjustment, false); + AdjustPitch.BindValueChanged(updatePitchAdjustment); } public void ApplyToTrack(Track track) From 5f0a345eebd0410364a79b69aa0465490b48712a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 14 Jun 2020 18:48:49 +0200 Subject: [PATCH 2059/2376] Unify method naming --- osu.Game/Rulesets/Mods/ModTimeRamp.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index 09c50ce115..edca3edf46 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -46,9 +46,8 @@ namespace osu.Game.Rulesets.Mods protected ModTimeRamp() { // for preview purpose at song select. eventually we'll want to be able to update every frame. - FinalRate.BindValueChanged(val => applyAdjustment(1), true); - - AdjustPitch.BindValueChanged(updatePitchAdjustment); + FinalRate.BindValueChanged(val => applyRateAdjustment(1), true); + AdjustPitch.BindValueChanged(applyPitchAdjustment); } public void ApplyToTrack(Track track) @@ -71,17 +70,17 @@ namespace osu.Game.Rulesets.Mods public virtual void Update(Playfield playfield) { - applyAdjustment((track.CurrentTime - beginRampTime) / finalRateTime); + applyRateAdjustment((track.CurrentTime - beginRampTime) / finalRateTime); } /// /// Adjust the rate along the specified ramp /// /// The amount of adjustment to apply (from 0..1). - private void applyAdjustment(double amount) => + private void applyRateAdjustment(double amount) => SpeedChange.Value = InitialRate.Value + (FinalRate.Value - InitialRate.Value) * Math.Clamp(amount, 0, 1); - private void updatePitchAdjustment(ValueChangedEvent value) + private void applyPitchAdjustment(ValueChangedEvent value) { // remove existing old adjustment track.RemoveAdjustment(adjustmentForPitchSetting(value.OldValue), SpeedChange); From e6ddd0380e2ee3b12aacd5a2fdf009ede4605672 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 14 Jun 2020 18:50:07 +0200 Subject: [PATCH 2060/2376] Rename bool arguments for readability --- osu.Game/Rulesets/Mods/ModTimeRamp.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index edca3edf46..df059eef7d 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -80,15 +80,15 @@ namespace osu.Game.Rulesets.Mods private void applyRateAdjustment(double amount) => SpeedChange.Value = InitialRate.Value + (FinalRate.Value - InitialRate.Value) * Math.Clamp(amount, 0, 1); - private void applyPitchAdjustment(ValueChangedEvent value) + private void applyPitchAdjustment(ValueChangedEvent adjustPitchSetting) { // remove existing old adjustment - track.RemoveAdjustment(adjustmentForPitchSetting(value.OldValue), SpeedChange); + track.RemoveAdjustment(adjustmentForPitchSetting(adjustPitchSetting.OldValue), SpeedChange); - track.AddAdjustment(adjustmentForPitchSetting(value.NewValue), SpeedChange); + track.AddAdjustment(adjustmentForPitchSetting(adjustPitchSetting.NewValue), SpeedChange); } - private AdjustableProperty adjustmentForPitchSetting(bool value) - => value ? AdjustableProperty.Frequency : AdjustableProperty.Tempo; + private AdjustableProperty adjustmentForPitchSetting(bool adjustPitchSettingValue) + => adjustPitchSettingValue ? AdjustableProperty.Frequency : AdjustableProperty.Tempo; } } From b8fa1a2c41445264c0835d0600ddf378bd42948a Mon Sep 17 00:00:00 2001 From: Joehu Date: Sun, 14 Jun 2020 11:22:38 -0700 Subject: [PATCH 2061/2376] Add shortcut to go home --- .../Input/Bindings/GlobalActionContainer.cs | 5 +++++ .../Overlays/Toolbar/ToolbarHomeButton.cs | 19 ++++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 71771abede..618798a6d8 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -39,6 +39,8 @@ namespace osu.Game.Input.Bindings new KeyBinding(InputKey.Escape, GlobalAction.Back), new KeyBinding(InputKey.ExtraMouseButton1, GlobalAction.Back), + new KeyBinding(new[] { InputKey.Alt, InputKey.Home }, GlobalAction.Home), + new KeyBinding(InputKey.Up, GlobalAction.SelectPrevious), new KeyBinding(InputKey.Down, GlobalAction.SelectNext), @@ -152,5 +154,8 @@ namespace osu.Game.Input.Bindings [Description("Next Selection")] SelectNext, + + [Description("Home")] + Home, } } diff --git a/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs b/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs index 6f5e703a66..e642f0c453 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs @@ -2,10 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Bindings; +using osu.Game.Input.Bindings; namespace osu.Game.Overlays.Toolbar { - public class ToolbarHomeButton : ToolbarButton + public class ToolbarHomeButton : ToolbarButton, IKeyBindingHandler { public ToolbarHomeButton() { @@ -13,5 +15,20 @@ namespace osu.Game.Overlays.Toolbar TooltipMain = "Home"; TooltipSub = "Return to the main menu"; } + + public bool OnPressed(GlobalAction action) + { + if (action == GlobalAction.Home) + { + Click(); + return true; + } + + return false; + } + + public void OnReleased(GlobalAction action) + { + } } } From 1f7679e829bb39c4bbc88a6599faa527e93a805d Mon Sep 17 00:00:00 2001 From: Joehu Date: Sun, 14 Jun 2020 11:24:23 -0700 Subject: [PATCH 2062/2376] Fix home button not flashing when pressing shortcut --- osu.Game/Overlays/Toolbar/ToolbarButton.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index 3d66d3c28e..e0ea88fcf3 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -78,9 +78,8 @@ namespace osu.Game.Overlays.Toolbar HoverBackground = new Box { RelativeSizeAxes = Axes.Both, - Colour = OsuColour.Gray(80).Opacity(180), + Colour = OsuColour.Gray(80).Opacity(0), Blending = BlendingParameters.Additive, - Alpha = 0, }, Flow = new FillFlowContainer { @@ -146,14 +145,14 @@ namespace osu.Game.Overlays.Toolbar protected override bool OnHover(HoverEvent e) { - HoverBackground.FadeIn(200); + HoverBackground.FadeColour(OsuColour.Gray(80).Opacity(180), 200); tooltipContainer.FadeIn(100); return base.OnHover(e); } protected override void OnHoverLost(HoverLostEvent e) { - HoverBackground.FadeOut(200); + HoverBackground.FadeColour(OsuColour.Gray(80).Opacity(0), 200); tooltipContainer.FadeOut(100); } } From 978636b90c5f1f1ff943cbe4027543c595e52201 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Jun 2020 09:38:33 +0900 Subject: [PATCH 2063/2376] Fix storyboard sample playback failing when expected to play at 0ms --- osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs index f3f8308964..8292b02068 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs @@ -51,7 +51,7 @@ namespace osu.Game.Storyboards.Drawables LifetimeStart = sampleInfo.StartTime; LifetimeEnd = double.MaxValue; } - else if (Time.Current - Time.Elapsed < sampleInfo.StartTime) + else if (Time.Current - Time.Elapsed <= sampleInfo.StartTime) { // We've passed the start time of the sample. We only play the sample if we're within an allowable range // from the sample's start, to reduce layering if we've been fast-forwarded far into the future From fdf7c56ba28db7165d1651dcdfb2e69520866e35 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Jun 2020 11:18:12 +0900 Subject: [PATCH 2064/2376] Add test coverage --- .../Gameplay/TestSceneStoryboardSamples.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs index 84506739ab..2c85c4809b 100644 --- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.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.IO; using System.Threading.Tasks; @@ -10,7 +11,12 @@ using osu.Framework.Audio.Sample; using osu.Framework.IO.Stores; using osu.Framework.Testing; using osu.Game.Audio; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; +using osu.Game.Screens.Play; using osu.Game.Skinning; +using osu.Game.Storyboards; +using osu.Game.Storyboards.Drawables; using osu.Game.Tests.Resources; using osu.Game.Tests.Visual; @@ -43,6 +49,27 @@ namespace osu.Game.Tests.Gameplay AddAssert("sample is non-null", () => channel != null); } + [Test] + public void TestSamplePlaybackAtZero() + { + GameplayClockContainer gameplayContainer = null; + DrawableStoryboardSample sample = null; + + AddStep("create container", () => + { + Add(gameplayContainer = new GameplayClockContainer(CreateWorkingBeatmap(new OsuRuleset().RulesetInfo), Array.Empty(), 0)); + + gameplayContainer.Add(sample = new DrawableStoryboardSample(new StoryboardSampleInfo(string.Empty, 0, 1)) + { + Clock = gameplayContainer.GameplayClock + }); + }); + + AddStep("start time", () => gameplayContainer.Start()); + + AddUntilStep("sample playback succeeded", () => sample.LifetimeEnd < double.MaxValue); + } + private class TestSkin : LegacySkin { public TestSkin(string resourceName, AudioManager audioManager) From b41567c66c48f2fc679cb91518e70e7ee7799b88 Mon Sep 17 00:00:00 2001 From: Joehu Date: Sun, 14 Jun 2020 22:02:21 -0700 Subject: [PATCH 2065/2376] Split hover and flash to separate boxes --- osu.Game/Overlays/Toolbar/ToolbarButton.cs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index e0ea88fcf3..cbcb4060a3 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -62,6 +62,7 @@ namespace osu.Game.Overlays.Toolbar protected ConstrainedIconContainer IconContainer; protected SpriteText DrawableText; protected Box HoverBackground; + private readonly Box FlashBackground; private readonly FillFlowContainer tooltipContainer; private readonly SpriteText tooltip1; private readonly SpriteText tooltip2; @@ -78,7 +79,14 @@ namespace osu.Game.Overlays.Toolbar HoverBackground = new Box { RelativeSizeAxes = Axes.Both, - Colour = OsuColour.Gray(80).Opacity(0), + Colour = OsuColour.Gray(80).Opacity(180), + Blending = BlendingParameters.Additive, + Alpha = 0, + }, + FlashBackground = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Transparent, Blending = BlendingParameters.Additive, }, Flow = new FillFlowContainer @@ -138,21 +146,21 @@ namespace osu.Game.Overlays.Toolbar protected override bool OnClick(ClickEvent e) { - HoverBackground.FlashColour(Color4.White.Opacity(100), 500, Easing.OutQuint); + FlashBackground.FlashColour(Color4.White.Opacity(100), 500, Easing.OutQuint); tooltipContainer.FadeOut(100); return base.OnClick(e); } protected override bool OnHover(HoverEvent e) { - HoverBackground.FadeColour(OsuColour.Gray(80).Opacity(180), 200); + HoverBackground.FadeIn(200); tooltipContainer.FadeIn(100); return base.OnHover(e); } protected override void OnHoverLost(HoverLostEvent e) { - HoverBackground.FadeColour(OsuColour.Gray(80).Opacity(0), 200); + HoverBackground.FadeOut(200); tooltipContainer.FadeOut(100); } } From 941fdf5e76a40d6d3400ab3b75a4869414c55aa6 Mon Sep 17 00:00:00 2001 From: Joehu Date: Sun, 14 Jun 2020 22:16:17 -0700 Subject: [PATCH 2066/2376] Fix flash background naming --- osu.Game/Overlays/Toolbar/ToolbarButton.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index cbcb4060a3..e752516baf 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -62,7 +62,7 @@ namespace osu.Game.Overlays.Toolbar protected ConstrainedIconContainer IconContainer; protected SpriteText DrawableText; protected Box HoverBackground; - private readonly Box FlashBackground; + private readonly Box flashBackground; private readonly FillFlowContainer tooltipContainer; private readonly SpriteText tooltip1; private readonly SpriteText tooltip2; @@ -83,7 +83,7 @@ namespace osu.Game.Overlays.Toolbar Blending = BlendingParameters.Additive, Alpha = 0, }, - FlashBackground = new Box + flashBackground = new Box { RelativeSizeAxes = Axes.Both, Colour = Color4.Transparent, @@ -146,7 +146,7 @@ namespace osu.Game.Overlays.Toolbar protected override bool OnClick(ClickEvent e) { - FlashBackground.FlashColour(Color4.White.Opacity(100), 500, Easing.OutQuint); + flashBackground.FlashColour(Color4.White.Opacity(100), 500, Easing.OutQuint); tooltipContainer.FadeOut(100); return base.OnClick(e); } From 1770b70b8164442d21f79ea4b0e9116a91552403 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Jun 2020 16:14:35 +0900 Subject: [PATCH 2067/2376] Change implementation to ensure flashBackground is not present by default --- osu.Game/Overlays/Toolbar/ToolbarButton.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index e752516baf..86a3f5d8aa 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -86,7 +86,8 @@ namespace osu.Game.Overlays.Toolbar flashBackground = new Box { RelativeSizeAxes = Axes.Both, - Colour = Color4.Transparent, + Alpha = 0, + Colour = Color4.White.Opacity(100), Blending = BlendingParameters.Additive, }, Flow = new FillFlowContainer @@ -146,7 +147,7 @@ namespace osu.Game.Overlays.Toolbar protected override bool OnClick(ClickEvent e) { - flashBackground.FlashColour(Color4.White.Opacity(100), 500, Easing.OutQuint); + flashBackground.FadeOutFromOne(800, Easing.OutQuint); tooltipContainer.FadeOut(100); return base.OnClick(e); } From 60381d581718dad2d92dfef6b0da3ff8026fa9fe Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 17 Apr 2020 03:38:12 +0300 Subject: [PATCH 2068/2376] Remove IRulesetTestScene and use OsuTestScene.CreateRuleset() instead --- .../Rulesets/Testing/IRulesetTestScene.cs | 20 ------------------- osu.Game/Tests/Visual/OsuTestScene.cs | 6 +++--- 2 files changed, 3 insertions(+), 23 deletions(-) delete mode 100644 osu.Game/Rulesets/Testing/IRulesetTestScene.cs diff --git a/osu.Game/Rulesets/Testing/IRulesetTestScene.cs b/osu.Game/Rulesets/Testing/IRulesetTestScene.cs deleted file mode 100644 index e8b8a79eb5..0000000000 --- a/osu.Game/Rulesets/Testing/IRulesetTestScene.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -namespace osu.Game.Rulesets.Testing -{ - /// - /// An interface that can be assigned to test scenes to indicate - /// that the test scene is testing ruleset-specific components. - /// This is to cache required ruleset dependencies for the components. - /// - public interface IRulesetTestScene - { - /// - /// Retrieves the ruleset that is going - /// to be tested by this test scene. - /// - /// The . - Ruleset CreateRuleset(); - } -} diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index eb1905cbe1..82ba989306 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -20,7 +20,6 @@ using osu.Game.Database; using osu.Game.Online.API; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Testing; using osu.Game.Rulesets.UI; using osu.Game.Screens; using osu.Game.Storyboards; @@ -70,8 +69,9 @@ namespace osu.Game.Tests.Visual { var baseDependencies = base.CreateChildDependencies(parent); - if (this is IRulesetTestScene rts) - baseDependencies = rulesetDependencies = new DrawableRulesetDependencies(rts.CreateRuleset(), baseDependencies); + var providedRuleset = CreateRuleset(); + if (providedRuleset != null) + baseDependencies = rulesetDependencies = new DrawableRulesetDependencies(providedRuleset, baseDependencies); Dependencies = new OsuScreenDependencies(false, baseDependencies); From f14774e795ab68f593008346a43c1c70401681e7 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 15 Jun 2020 11:47:31 +0300 Subject: [PATCH 2069/2376] Rename to TestSceneRulesetDependencies, mark as headless and avoid throwing Avoid throw not implemented exceptions from TestRuleset since it's now set as the global ruleset bindable automatically by base, throwing to innocent components is probably not needed. --- ...ene.cs => TestSceneRulesetDependencies.cs} | 41 +++++++++++++------ 1 file changed, 28 insertions(+), 13 deletions(-) rename osu.Game.Tests/Testing/{TestSceneRulesetTestScene.cs => TestSceneRulesetDependencies.cs} (73%) diff --git a/osu.Game.Tests/Testing/TestSceneRulesetTestScene.cs b/osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs similarity index 73% rename from osu.Game.Tests/Testing/TestSceneRulesetTestScene.cs rename to osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs index 6d8502e651..80f1b02794 100644 --- a/osu.Game.Tests/Testing/TestSceneRulesetTestScene.cs +++ b/osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs @@ -9,21 +9,28 @@ using osu.Framework.Audio.Track; using osu.Framework.Configuration.Tracking; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; +using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Rulesets; using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Testing; using osu.Game.Rulesets.UI; using osu.Game.Tests.Resources; using osu.Game.Tests.Visual; namespace osu.Game.Tests.Testing { - public class TestSceneRulesetTestScene : OsuTestScene, IRulesetTestScene + /// + /// A test scene ensuring the dependencies for the + /// provided ruleset below are cached at the base implementation. + /// + [HeadlessTest] + public class TestSceneRulesetDependencies : OsuTestScene { + protected override Ruleset CreateRuleset() => new TestRuleset(); + [Test] public void TestRetrieveTexture() { @@ -45,8 +52,6 @@ namespace osu.Game.Tests.Testing Dependencies.Get() != null); } - public Ruleset CreateRuleset() => new TestRuleset(); - private class TestRuleset : Ruleset { public override string Description => string.Empty; @@ -62,19 +67,29 @@ namespace osu.Game.Tests.Testing public override IResourceStore CreateResourceStore() => new NamespacedResourceStore(TestResources.GetStore(), @"Resources"); public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new TestRulesetConfigManager(); - public override IEnumerable GetModsFor(ModType type) => throw new NotImplementedException(); - public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => throw new NotImplementedException(); - public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => throw new NotImplementedException(); - public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => throw new NotImplementedException(); + public override IEnumerable GetModsFor(ModType type) => Array.Empty(); + public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => null; + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => null; + public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => null; } private class TestRulesetConfigManager : IRulesetConfigManager { - public void Load() => throw new NotImplementedException(); - public bool Save() => throw new NotImplementedException(); - public TrackedSettings CreateTrackedSettings() => throw new NotImplementedException(); - public void LoadInto(TrackedSettings settings) => throw new NotImplementedException(); - public void Dispose() => throw new NotImplementedException(); + public void Load() + { + } + + public bool Save() => true; + + public TrackedSettings CreateTrackedSettings() => new TrackedSettings(); + + public void LoadInto(TrackedSettings settings) + { + } + + public void Dispose() + { + } } } } From f4b57933c347b99eb4f6238f6dbb9729fae51176 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2020 08:54:34 +0000 Subject: [PATCH 2070/2376] Bump Microsoft.Build.Traversal from 2.0.48 to 2.0.50 Bumps [Microsoft.Build.Traversal](https://github.com/Microsoft/MSBuildSdks) from 2.0.48 to 2.0.50. - [Release notes](https://github.com/Microsoft/MSBuildSdks/releases) - [Changelog](https://github.com/microsoft/MSBuildSdks/blob/master/RELEASE.md) - [Commits](https://github.com/Microsoft/MSBuildSdks/compare/Microsoft.Build.Traversal.2.0.48...Microsoft.Build.Traversal.2.0.50) Signed-off-by: dependabot-preview[bot] --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index bdb90eb0e9..9aa5b6192b 100644 --- a/global.json +++ b/global.json @@ -5,6 +5,6 @@ "version": "3.1.100" }, "msbuild-sdks": { - "Microsoft.Build.Traversal": "2.0.48" + "Microsoft.Build.Traversal": "2.0.50" } } \ No newline at end of file From ad5bd1f0c00ae10c0c857759e38b1f588d2f4b45 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Jun 2020 18:45:50 +0900 Subject: [PATCH 2071/2376] Update in line with other/unspecified switch See https://github.com/ppy/osu-web/commit/289f0f0a209f1f840270db07794a7bfd52439db1. --- osu.Game/Overlays/BeatmapListing/SearchLanguage.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListing/SearchLanguage.cs b/osu.Game/Overlays/BeatmapListing/SearchLanguage.cs index 43f16059e9..eee5d8f7e1 100644 --- a/osu.Game/Overlays/BeatmapListing/SearchLanguage.cs +++ b/osu.Game/Overlays/BeatmapListing/SearchLanguage.cs @@ -11,8 +11,8 @@ namespace osu.Game.Overlays.BeatmapListing [Order(0)] Any, - [Order(13)] - Other, + [Order(14)] + Unspecified, [Order(1)] English, @@ -50,7 +50,7 @@ namespace osu.Game.Overlays.BeatmapListing [Order(11)] Polish, - [Order(14)] - Unspecified + [Order(13)] + Other } } From d57b58a7dd6a8429cd684cf8d6e2fbd0f2f643ad Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 15 Jun 2020 18:42:16 +0900 Subject: [PATCH 2072/2376] Add temporary fix for tournament song bar disappearance --- osu.Game.Tournament/Components/SongBar.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tournament/Components/SongBar.cs b/osu.Game.Tournament/Components/SongBar.cs index e86fd890c1..fc7fcef892 100644 --- a/osu.Game.Tournament/Components/SongBar.cs +++ b/osu.Game.Tournament/Components/SongBar.cs @@ -77,6 +77,8 @@ namespace osu.Game.Tournament.Components flow = new FillFlowContainer { RelativeSizeAxes = Axes.X, + // Todo: This is a hack for https://github.com/ppy/osu-framework/issues/3617 since this container is at the very edge of the screen and potentially initially masked away. + Height = 1, AutoSizeAxes = Axes.Y, LayoutDuration = 500, LayoutEasing = Easing.OutQuint, From c3c5a99a2288819f7c95bbf64069cd21838d54c8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Jun 2020 20:23:35 +0900 Subject: [PATCH 2073/2376] Load imported scores to results screen rather than gameplay --- osu.Game/OsuGame.cs | 23 ++++++++++++++++--- .../Screens/Ranking/ReplayDownloadButton.cs | 2 +- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 7ecd7851d7..7c5e4b8d94 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -35,7 +35,6 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Input; using osu.Game.Overlays.Notifications; -using osu.Game.Screens.Play; using osu.Game.Input.Bindings; using osu.Game.Online.Chat; using osu.Game.Skinning; @@ -43,6 +42,8 @@ using osuTK.Graphics; using osu.Game.Overlays.Volume; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; +using osu.Game.Screens.Play; +using osu.Game.Screens.Ranking; using osu.Game.Screens.Select; using osu.Game.Updater; using osu.Game.Utils; @@ -360,7 +361,7 @@ namespace osu.Game /// Present a score's replay immediately. /// The user should have already requested this interactively. ///
    - public void PresentScore(ScoreInfo score) + public void PresentScore(ScoreInfo score, ScorePresentType presentType = ScorePresentType.Results) { // The given ScoreInfo may have missing properties if it was retrieved from online data. Re-retrieve it from the database // to ensure all the required data for presenting a replay are present. @@ -392,9 +393,19 @@ namespace osu.Game PerformFromScreen(screen => { + Ruleset.Value = databasedScore.ScoreInfo.Ruleset; Beatmap.Value = BeatmapManager.GetWorkingBeatmap(databasedBeatmap); - screen.Push(new ReplayPlayerLoader(databasedScore)); + switch (presentType) + { + case ScorePresentType.Gameplay: + screen.Push(new ReplayPlayerLoader(databasedScore)); + break; + + case ScorePresentType.Results: + screen.Push(new SoloResultsScreen(databasedScore.ScoreInfo)); + break; + } }, validScreens: new[] { typeof(PlaySongSelect) }); } @@ -1000,4 +1011,10 @@ namespace osu.Game Exit(); } } + + public enum ScorePresentType + { + Results, + Gameplay + } } diff --git a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs index 9d4e3af230..d0142e57fe 100644 --- a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs +++ b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs @@ -56,7 +56,7 @@ namespace osu.Game.Screens.Ranking switch (State.Value) { case DownloadState.LocallyAvailable: - game?.PresentScore(Model.Value); + game?.PresentScore(Model.Value, ScorePresentType.Gameplay); break; case DownloadState.NotDownloaded: From 90d69c121625ddc69e88b5a232e7c4f6afa51076 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Jun 2020 20:31:47 +0900 Subject: [PATCH 2074/2376] Allow legacy score to be constructed even if replay file is missing --- osu.Game/Scoring/LegacyDatabasedScore.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Scoring/LegacyDatabasedScore.cs b/osu.Game/Scoring/LegacyDatabasedScore.cs index bd673eaa29..8908775472 100644 --- a/osu.Game/Scoring/LegacyDatabasedScore.cs +++ b/osu.Game/Scoring/LegacyDatabasedScore.cs @@ -16,7 +16,10 @@ namespace osu.Game.Scoring { ScoreInfo = score; - var replayFilename = score.Files.First(f => f.Filename.EndsWith(".osr", StringComparison.InvariantCultureIgnoreCase)).FileInfo.StoragePath; + var replayFilename = score.Files.FirstOrDefault(f => f.Filename.EndsWith(".osr", StringComparison.InvariantCultureIgnoreCase))?.FileInfo.StoragePath; + + if (replayFilename == null) + return; using (var stream = store.GetStream(replayFilename)) Replay = new DatabasedLegacyScoreDecoder(rulesets, beatmaps).Parse(stream).Replay; From 17a70bf6ee241a0e26b064de1bc51cb04b0140a7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Jun 2020 20:32:27 +0900 Subject: [PATCH 2075/2376] Add test coverage --- .../Visual/Navigation/OsuGameTestScene.cs | 3 + .../Navigation/TestScenePresentScore.cs | 155 ++++++++++++++++++ osu.Game/Screens/Play/ReplayPlayerLoader.cs | 8 +- 3 files changed, 162 insertions(+), 4 deletions(-) create mode 100644 osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs diff --git a/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs b/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs index 31afce86ae..c4acf4f7da 100644 --- a/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs +++ b/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs @@ -17,6 +17,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Overlays; using osu.Game.Rulesets; +using osu.Game.Scoring; using osu.Game.Screens; using osu.Game.Screens.Menu; using osuTK.Graphics; @@ -100,6 +101,8 @@ namespace osu.Game.Tests.Visual.Navigation public new BeatmapManager BeatmapManager => base.BeatmapManager; + public new ScoreManager ScoreManager => base.ScoreManager; + public new SettingsPanel Settings => base.Settings; public new MusicController MusicController => base.MusicController; diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs new file mode 100644 index 0000000000..b2e18849c9 --- /dev/null +++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs @@ -0,0 +1,155 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Screens; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mania; +using osu.Game.Rulesets.Osu; +using osu.Game.Scoring; +using osu.Game.Screens.Menu; +using osu.Game.Screens.Play; +using osu.Game.Screens.Ranking; + +namespace osu.Game.Tests.Visual.Navigation +{ + public class TestScenePresentScore : OsuGameTestScene + { + private BeatmapSetInfo beatmap; + + [SetUpSteps] + public new void SetUpSteps() + { + AddStep("import beatmap", () => + { + var difficulty = new BeatmapDifficulty(); + var metadata = new BeatmapMetadata + { + Artist = "SomeArtist", + AuthorString = "SomeAuthor", + Title = "import" + }; + + beatmap = Game.BeatmapManager.Import(new BeatmapSetInfo + { + Hash = Guid.NewGuid().ToString(), + OnlineBeatmapSetID = 1, + Metadata = metadata, + Beatmaps = new List + { + new BeatmapInfo + { + OnlineBeatmapID = 1 * 1024, + Metadata = metadata, + BaseDifficulty = difficulty, + Ruleset = new OsuRuleset().RulesetInfo + }, + new BeatmapInfo + { + OnlineBeatmapID = 1 * 2048, + Metadata = metadata, + BaseDifficulty = difficulty, + Ruleset = new OsuRuleset().RulesetInfo + }, + } + }).Result; + }); + } + + [Test] + public void TestFromMainMenu([Values] ScorePresentType type) + { + var firstImport = importScore(1); + var secondimport = importScore(3); + + presentAndConfirm(firstImport, type); + returnToMenu(); + presentAndConfirm(secondimport, type); + returnToMenu(); + returnToMenu(); + } + + [Test] + public void TestFromMainMenuDifferentRuleset([Values] ScorePresentType type) + { + var firstImport = importScore(1); + var secondimport = importScore(3, new ManiaRuleset().RulesetInfo); + + presentAndConfirm(firstImport, type); + returnToMenu(); + presentAndConfirm(secondimport, type); + returnToMenu(); + returnToMenu(); + } + + [Test] + public void TestFromSongSelect([Values] ScorePresentType type) + { + var firstImport = importScore(1); + presentAndConfirm(firstImport, type); + + var secondimport = importScore(3); + presentAndConfirm(secondimport, type); + } + + [Test] + public void TestFromSongSelectDifferentRuleset([Values] ScorePresentType type) + { + var firstImport = importScore(1); + presentAndConfirm(firstImport, type); + + var secondimport = importScore(3, new ManiaRuleset().RulesetInfo); + presentAndConfirm(secondimport, type); + } + + private void returnToMenu() + { + AddStep("return to menu", () => Game.ScreenStack.CurrentScreen.Exit()); + AddUntilStep("wait for menu", () => Game.ScreenStack.CurrentScreen is MainMenu); + } + + private Func importScore(int i, RulesetInfo ruleset = null) + { + ScoreInfo imported = null; + AddStep($"import score {i}", () => + { + imported = Game.ScoreManager.Import(new ScoreInfo + { + Hash = Guid.NewGuid().ToString(), + OnlineScoreID = i, + Beatmap = beatmap.Beatmaps.First(), + Ruleset = ruleset ?? new OsuRuleset().RulesetInfo + }).Result; + }); + + AddAssert($"import {i} succeeded", () => imported != null); + + return () => imported; + } + + private void presentAndConfirm(Func getImport, ScorePresentType type) + { + AddStep("present score", () => Game.PresentScore(getImport(), type)); + + switch (type) + { + case ScorePresentType.Results: + AddUntilStep("wait for results", () => Game.ScreenStack.CurrentScreen is ResultsScreen); + AddUntilStep("correct score displayed", () => ((ResultsScreen)Game.ScreenStack.CurrentScreen).Score.ID == getImport().ID); + AddAssert("correct ruleset selected", () => Game.Ruleset.Value.ID == getImport().Ruleset.ID); + break; + + case ScorePresentType.Gameplay: + AddUntilStep("wait for player loader", () => Game.ScreenStack.CurrentScreen is ReplayPlayerLoader); + AddUntilStep("correct score displayed", () => ((ReplayPlayerLoader)Game.ScreenStack.CurrentScreen).Score.ID == getImport().ID); + AddAssert("correct ruleset selected", () => Game.Ruleset.Value.ID == getImport().Ruleset.ID); + break; + } + } + } +} diff --git a/osu.Game/Screens/Play/ReplayPlayerLoader.cs b/osu.Game/Screens/Play/ReplayPlayerLoader.cs index 4572570437..9eff4cb8fc 100644 --- a/osu.Game/Screens/Play/ReplayPlayerLoader.cs +++ b/osu.Game/Screens/Play/ReplayPlayerLoader.cs @@ -9,7 +9,7 @@ namespace osu.Game.Screens.Play { public class ReplayPlayerLoader : PlayerLoader { - private readonly ScoreInfo scoreInfo; + public readonly ScoreInfo Score; public ReplayPlayerLoader(Score score) : base(() => new ReplayPlayer(score)) @@ -17,14 +17,14 @@ namespace osu.Game.Screens.Play if (score.Replay == null) throw new ArgumentException($"{nameof(score)} must have a non-null {nameof(score.Replay)}.", nameof(score)); - scoreInfo = score.ScoreInfo; + Score = score.ScoreInfo; } public override void OnEntering(IScreen last) { // these will be reverted thanks to PlayerLoader's lease. - Mods.Value = scoreInfo.Mods; - Ruleset.Value = scoreInfo.Ruleset; + Mods.Value = Score.Mods; + Ruleset.Value = Score.Ruleset; base.OnEntering(last); } From 1ce374ae2f640c7adcfb16146769e4ad7d030ba3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Jun 2020 20:34:46 +0900 Subject: [PATCH 2076/2376] 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 596e5bfa8b..6387356686 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@
    - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 1d3bafbfd6..8c098b79c6 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -24,7 +24,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index ad7850599b..373ad09597 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From f9db37a1de9c425661df474f2d56fbf2d9ddfa27 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 15 Jun 2020 21:48:59 +0900 Subject: [PATCH 2077/2376] Split out types --- .../Scoring/OsuScoreProcessor.cs | 18 -- .../Scoring/TimingDistribution.cs | 17 ++ osu.Game.Rulesets.Osu/Statistics/Heatmap.cs | 186 ++++++++++++++++++ .../Statistics/TimingDistributionGraph.cs | 139 +++++++++++++ .../Ranking/TestSceneAccuracyHeatmap.cs | 175 +--------------- .../TestSceneTimingDistributionGraph.cs | 130 +----------- 6 files changed, 344 insertions(+), 321 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Scoring/TimingDistribution.cs create mode 100644 osu.Game.Rulesets.Osu/Statistics/Heatmap.cs create mode 100644 osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs index 83339bd061..a9d48df52d 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs @@ -7,7 +7,6 @@ using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; -using osu.Game.Scoring; namespace osu.Game.Rulesets.Osu.Scoring { @@ -62,11 +61,6 @@ namespace osu.Game.Rulesets.Osu.Scoring } } - public override void PopulateScore(ScoreInfo score) - { - base.PopulateScore(score); - } - protected override void Reset(bool storeResults) { base.Reset(storeResults); @@ -78,16 +72,4 @@ namespace osu.Game.Rulesets.Osu.Scoring public override HitWindows CreateHitWindows() => new OsuHitWindows(); } - - public class TimingDistribution - { - public readonly int[] Bins; - public readonly double BinSize; - - public TimingDistribution(int binCount, double binSize) - { - Bins = new int[binCount]; - BinSize = binSize; - } - } } diff --git a/osu.Game.Rulesets.Osu/Scoring/TimingDistribution.cs b/osu.Game.Rulesets.Osu/Scoring/TimingDistribution.cs new file mode 100644 index 0000000000..46f259f3d8 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Scoring/TimingDistribution.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. + +namespace osu.Game.Rulesets.Osu.Scoring +{ + public class TimingDistribution + { + public readonly int[] Bins; + public readonly double BinSize; + + public TimingDistribution(int binCount, double binSize) + { + Bins = new int[binCount]; + BinSize = binSize; + } + } +} diff --git a/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs b/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs new file mode 100644 index 0000000000..8105d12991 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs @@ -0,0 +1,186 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Diagnostics; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Osu.Statistics +{ + public class Heatmap : CompositeDrawable + { + /// + /// Full size of the heatmap. + /// + private const float size = 130; + + /// + /// Size of the inner circle containing the "hit" points, relative to . + /// All other points outside of the inner circle are "miss" points. + /// + private const float inner_portion = 0.8f; + + private const float rotation = 45; + private const float point_size = 4; + + private Container allPoints; + + public Heatmap() + { + Size = new Vector2(size); + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChildren = new Drawable[] + { + new CircularContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(inner_portion), + Masking = true, + BorderThickness = 2f, + BorderColour = Color4.White, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#202624") + } + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + Children = new Drawable[] + { + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Height = 2, // We're rotating along a diagonal - we don't really care how big this is. + Width = 1f, + Rotation = -rotation, + Alpha = 0.3f, + }, + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Height = 2, // We're rotating along a diagonal - we don't really care how big this is. + Width = 1f, + Rotation = rotation + }, + new Box + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Width = 10, + Height = 2f, + }, + new Box + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Y = -1, + Width = 2f, + Height = 10, + } + } + }, + allPoints = new Container { RelativeSizeAxes = Axes.Both } + }; + + Vector2 centre = new Vector2(size / 2); + int rows = (int)Math.Ceiling(size / point_size); + int cols = (int)Math.Ceiling(size / point_size); + + for (int r = 0; r < rows; r++) + { + for (int c = 0; c < cols; c++) + { + Vector2 pos = new Vector2(c * point_size, r * point_size); + HitPointType pointType = HitPointType.Hit; + + if (Vector2.Distance(pos, centre) > size * inner_portion / 2) + pointType = HitPointType.Miss; + + allPoints.Add(new HitPoint(pos, pointType) + { + Size = new Vector2(point_size), + Colour = pointType == HitPointType.Hit ? new Color4(102, 255, 204, 255) : new Color4(255, 102, 102, 255) + }); + } + } + } + + public void AddPoint(Vector2 start, Vector2 end, Vector2 hitPoint, float radius) + { + double angle1 = Math.Atan2(end.Y - hitPoint.Y, hitPoint.X - end.X); // Angle between the end point and the hit point. + double angle2 = Math.Atan2(end.Y - start.Y, start.X - end.X); // Angle between the end point and the start point. + double finalAngle = angle2 - angle1; // Angle between start, end, and hit points. + + float normalisedDistance = Vector2.Distance(hitPoint, end) / radius; + + // Find the most relevant hit point. + double minDist = double.PositiveInfinity; + HitPoint point = null; + + foreach (var p in allPoints) + { + Vector2 localCentre = new Vector2(size / 2); + float localRadius = localCentre.X * inner_portion * normalisedDistance; + double localAngle = finalAngle + 3 * Math.PI / 4; + Vector2 localPoint = localCentre + localRadius * new Vector2((float)Math.Cos(localAngle), (float)Math.Sin(localAngle)); + + float dist = Vector2.Distance(p.DrawPosition + p.DrawSize / 2, localPoint); + + if (dist < minDist) + { + minDist = dist; + point = p; + } + } + + Debug.Assert(point != null); + point.Increment(); + } + + private class HitPoint : Circle + { + private readonly HitPointType pointType; + + public HitPoint(Vector2 position, HitPointType pointType) + { + this.pointType = pointType; + + Position = position; + Alpha = 0; + } + + public void Increment() + { + if (Alpha < 1) + Alpha += 0.1f; + else if (pointType == HitPointType.Hit) + Colour = ((Color4)Colour).Lighten(0.1f); + } + } + + private enum HitPointType + { + Hit, + Miss + } + } +} diff --git a/osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs b/osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs new file mode 100644 index 0000000000..a47d726988 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs @@ -0,0 +1,139 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets.Osu.Scoring; + +namespace osu.Game.Rulesets.Osu.Statistics +{ + public class TimingDistributionGraph : CompositeDrawable + { + /// + /// The number of data points shown on the axis below the graph. + /// + private const float axis_points = 5; + + /// + /// An amount to adjust the value of the axis points by, effectively insetting the axis in the graph. + /// Without an inset, the final data point will be placed halfway outside the graph. + /// + private const float axis_value_inset = 0.2f; + + private readonly TimingDistribution distribution; + + public TimingDistributionGraph(TimingDistribution distribution) + { + this.distribution = distribution; + } + + [BackgroundDependencyLoader] + private void load() + { + int maxCount = distribution.Bins.Max(); + + var bars = new Drawable[distribution.Bins.Length]; + for (int i = 0; i < bars.Length; i++) + bars[i] = new Bar { Height = (float)distribution.Bins[i] / maxCount }; + + Container axisFlow; + + InternalChild = new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] { bars } + } + }, + new Drawable[] + { + axisFlow = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + } + }, + }, + RowDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + } + }; + + // We know the total number of bins on each side of the centre ((n - 1) / 2), and the size of each bin. + // So our axis will contain one centre element + 5 points on each side, each with a value depending on the number of bins * bin size. + int sideBins = (distribution.Bins.Length - 1) / 2; + double maxValue = sideBins * distribution.BinSize; + double axisValueStep = maxValue / axis_points * (1 - axis_value_inset); + + axisFlow.Add(new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "0", + Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold) + }); + + for (int i = 1; i <= axis_points; i++) + { + double axisValue = i * axisValueStep; + float position = (float)(axisValue / maxValue); + float alpha = 1f - position * 0.8f; + + axisFlow.Add(new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativePositionAxes = Axes.X, + X = -position / 2, + Alpha = alpha, + Text = axisValue.ToString("-0"), + Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold) + }); + + axisFlow.Add(new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativePositionAxes = Axes.X, + X = position / 2, + Alpha = alpha, + Text = axisValue.ToString("+0"), + Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold) + }); + } + } + + private class Bar : CompositeDrawable + { + public Bar() + { + Anchor = Anchor.BottomCentre; + Origin = Anchor.BottomCentre; + + RelativeSizeAxes = Axes.Both; + + Padding = new MarginPadding { Horizontal = 1 }; + + InternalChild = new Circle + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#66FFCC") + }; + } + } + } +} diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs index 9f82287640..9e5fda0ae6 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs @@ -1,15 +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 osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Framework.Utils; +using osu.Game.Rulesets.Osu.Statistics; using osuTK; using osuTK.Graphics; @@ -70,177 +68,6 @@ namespace osu.Game.Tests.Visual.Ranking return true; } - private class Heatmap : CompositeDrawable - { - /// - /// Full size of the heatmap. - /// - private const float size = 130; - - /// - /// Size of the inner circle containing the "hit" points, relative to . - /// All other points outside of the inner circle are "miss" points. - /// - private const float inner_portion = 0.8f; - - private const float rotation = 45; - private const float point_size = 4; - - private Container allPoints; - - public Heatmap() - { - Size = new Vector2(size); - } - - [BackgroundDependencyLoader] - private void load() - { - InternalChildren = new Drawable[] - { - new CircularContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Size = new Vector2(inner_portion), - Masking = true, - BorderThickness = 2f, - BorderColour = Color4.White, - Child = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex("#202624") - } - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Masking = true, - Children = new Drawable[] - { - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Y, - Height = 2, // We're rotating along a diagonal - we don't really care how big this is. - Width = 1f, - Rotation = -rotation, - Alpha = 0.3f, - }, - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Y, - Height = 2, // We're rotating along a diagonal - we don't really care how big this is. - Width = 1f, - Rotation = rotation - }, - new Box - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Width = 10, - Height = 2f, - }, - new Box - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Y = -1, - Width = 2f, - Height = 10, - } - } - }, - allPoints = new Container { RelativeSizeAxes = Axes.Both } - }; - - Vector2 centre = new Vector2(size / 2); - int rows = (int)Math.Ceiling(size / point_size); - int cols = (int)Math.Ceiling(size / point_size); - - for (int r = 0; r < rows; r++) - { - for (int c = 0; c < cols; c++) - { - Vector2 pos = new Vector2(c * point_size, r * point_size); - HitPointType pointType = HitPointType.Hit; - - if (Vector2.Distance(pos, centre) > size * inner_portion / 2) - pointType = HitPointType.Miss; - - allPoints.Add(new HitPoint(pos, pointType) - { - Size = new Vector2(point_size), - Colour = pointType == HitPointType.Hit ? new Color4(102, 255, 204, 255) : new Color4(255, 102, 102, 255) - }); - } - } - } - - public void AddPoint(Vector2 start, Vector2 end, Vector2 hitPoint, float radius) - { - double angle1 = Math.Atan2(end.Y - hitPoint.Y, hitPoint.X - end.X); // Angle between the end point and the hit point. - double angle2 = Math.Atan2(end.Y - start.Y, start.X - end.X); // Angle between the end point and the start point. - double finalAngle = angle2 - angle1; // Angle between start, end, and hit points. - - float normalisedDistance = Vector2.Distance(hitPoint, end) / radius; - - // Find the most relevant hit point. - double minDist = double.PositiveInfinity; - HitPoint point = null; - - foreach (var p in allPoints) - { - Vector2 localCentre = new Vector2(size / 2); - float localRadius = localCentre.X * inner_portion * normalisedDistance; - double localAngle = finalAngle + 3 * Math.PI / 4; - Vector2 localPoint = localCentre + localRadius * new Vector2((float)Math.Cos(localAngle), (float)Math.Sin(localAngle)); - - float dist = Vector2.Distance(p.DrawPosition + p.DrawSize / 2, localPoint); - - if (dist < minDist) - { - minDist = dist; - point = p; - } - } - - Debug.Assert(point != null); - point.Increment(); - } - } - - private class HitPoint : Circle - { - private readonly HitPointType pointType; - - public HitPoint(Vector2 position, HitPointType pointType) - { - this.pointType = pointType; - - Position = position; - Alpha = 0; - } - - public void Increment() - { - if (Alpha < 1) - Alpha += 0.1f; - else if (pointType == HitPointType.Hit) - Colour = ((Color4)Colour).Lighten(0.1f); - } - } - - private enum HitPointType - { - Hit, - Miss - } - private class BorderCircle : CircularContainer { public BorderCircle() diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs b/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs index 73225ff599..7530fc42b8 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs @@ -2,15 +2,11 @@ // 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.Extensions.Color4Extensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Osu.Scoring; +using osu.Game.Rulesets.Osu.Statistics; using osuTK; namespace osu.Game.Tests.Visual.Ranking @@ -65,128 +61,4 @@ namespace osu.Game.Tests.Visual.Ranking return distribution; } } - - public class TimingDistributionGraph : CompositeDrawable - { - /// - /// The number of data points shown on the axis below the graph. - /// - private const float axis_points = 5; - - /// - /// An amount to adjust the value of the axis points by, effectively insetting the axis in the graph. - /// Without an inset, the final data point will be placed halfway outside the graph. - /// - private const float axis_value_inset = 0.2f; - - private readonly TimingDistribution distribution; - - public TimingDistributionGraph(TimingDistribution distribution) - { - this.distribution = distribution; - } - - [BackgroundDependencyLoader] - private void load() - { - int maxCount = distribution.Bins.Max(); - - var bars = new Drawable[distribution.Bins.Length]; - for (int i = 0; i < bars.Length; i++) - bars[i] = new Bar { Height = (float)distribution.Bins[i] / maxCount }; - - Container axisFlow; - - InternalChild = new GridContainer - { - RelativeSizeAxes = Axes.Both, - Content = new[] - { - new Drawable[] - { - new GridContainer - { - RelativeSizeAxes = Axes.Both, - Content = new[] { bars } - } - }, - new Drawable[] - { - axisFlow = new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y - } - }, - }, - RowDimensions = new[] - { - new Dimension(), - new Dimension(GridSizeMode.AutoSize), - } - }; - - // We know the total number of bins on each side of the centre ((n - 1) / 2), and the size of each bin. - // So our axis will contain one centre element + 5 points on each side, each with a value depending on the number of bins * bin size. - int sideBins = (distribution.Bins.Length - 1) / 2; - double maxValue = sideBins * distribution.BinSize; - double axisValueStep = maxValue / axis_points * (1 - axis_value_inset); - - axisFlow.Add(new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Text = "0", - Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold) - }); - - for (int i = 1; i <= axis_points; i++) - { - double axisValue = i * axisValueStep; - float position = (float)(axisValue / maxValue); - float alpha = 1f - position * 0.8f; - - axisFlow.Add(new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativePositionAxes = Axes.X, - X = -position / 2, - Alpha = alpha, - Text = axisValue.ToString("-0"), - Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold) - }); - - axisFlow.Add(new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativePositionAxes = Axes.X, - X = position / 2, - Alpha = alpha, - Text = axisValue.ToString("+0"), - Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold) - }); - } - } - - private class Bar : CompositeDrawable - { - public Bar() - { - Anchor = Anchor.BottomCentre; - Origin = Anchor.BottomCentre; - - RelativeSizeAxes = Axes.Both; - - Padding = new MarginPadding { Horizontal = 1 }; - - InternalChild = new Circle - { - RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex("#66FFCC") - }; - } - } - } } From d2155c3da3c01e54636a5ce4876618f68acb855f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Jun 2020 22:19:02 +0900 Subject: [PATCH 2078/2376] Fix thread safety --- osu.Game/Updater/UpdateManager.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs index 5da366bde9..61775a26b7 100644 --- a/osu.Game/Updater/UpdateManager.cs +++ b/osu.Game/Updater/UpdateManager.cs @@ -64,10 +64,12 @@ namespace osu.Game.Updater if (!CanCheckForUpdate) return; - lock (updateTaskLock) - updateCheckTask ??= PerformUpdateCheck(); + Task waitTask; - await updateCheckTask; + lock (updateTaskLock) + waitTask = (updateCheckTask ??= PerformUpdateCheck()); + + await waitTask; lock (updateTaskLock) updateCheckTask = null; From 53b7057ee05a3551c07ac7e0d2a00f15f13b4c29 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Jun 2020 22:19:11 +0900 Subject: [PATCH 2079/2376] Don't show update button when updates are not feasible --- .../Sections/General/UpdateSettings.cs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs index 869e6c9c51..9fca820cac 100644 --- a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs @@ -30,16 +30,18 @@ namespace osu.Game.Overlays.Settings.Sections.General Bindable = config.GetBindable(OsuSetting.ReleaseStream), }); - // We should only display the button for UpdateManagers that do check for updates - Add(checkForUpdatesButton = new SettingsButton + if (updateManager.CanCheckForUpdate) { - Text = "Check for updates", - Action = () => + Add(checkForUpdatesButton = new SettingsButton { - checkForUpdatesButton.Enabled.Value = false; - Task.Run(updateManager.CheckForUpdateAsync).ContinueWith(t => Schedule(() => checkForUpdatesButton.Enabled.Value = true)); - } - }); + Text = "Check for updates", + Action = () => + { + checkForUpdatesButton.Enabled.Value = false; + Task.Run(updateManager.CheckForUpdateAsync).ContinueWith(t => Schedule(() => checkForUpdatesButton.Enabled.Value = true)); + } + }); + } if (RuntimeInfo.IsDesktop) { From 97067976f75a416dd8adc290aed8841ab51740ad Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Jun 2020 22:23:06 +0900 Subject: [PATCH 2080/2376] Add null check --- osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs index 9fca820cac..9c7d0b0be4 100644 --- a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs @@ -30,7 +30,7 @@ namespace osu.Game.Overlays.Settings.Sections.General Bindable = config.GetBindable(OsuSetting.ReleaseStream), }); - if (updateManager.CanCheckForUpdate) + if (updateManager?.CanCheckForUpdate == true) { Add(checkForUpdatesButton = new SettingsButton { From 900da8849866bc13ddc2e71c4065d24c4ed4a74c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 15 Jun 2020 22:44:55 +0900 Subject: [PATCH 2081/2376] Populate hit offsets from score processor --- .../Judgements/OsuHitCircleJudgementResult.cs | 23 +++++++ .../Objects/Drawables/DrawableHitCircle.cs | 28 ++++++++- osu.Game.Rulesets.Osu/Scoring/HitOffset.cs | 23 +++++++ .../Scoring/OsuScoreProcessor.cs | 61 ++++++++++++++++++- osu.Game/Scoring/ScoreInfo.cs | 2 + 5 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Judgements/OsuHitCircleJudgementResult.cs create mode 100644 osu.Game.Rulesets.Osu/Scoring/HitOffset.cs diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuHitCircleJudgementResult.cs b/osu.Game.Rulesets.Osu/Judgements/OsuHitCircleJudgementResult.cs new file mode 100644 index 0000000000..103d02958d --- /dev/null +++ b/osu.Game.Rulesets.Osu/Judgements/OsuHitCircleJudgementResult.cs @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Objects; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Judgements +{ + public class OsuHitCircleJudgementResult : OsuJudgementResult + { + public HitCircle HitCircle => (HitCircle)HitObject; + + public Vector2? HitPosition; + public float? Radius; + + public OsuHitCircleJudgementResult(HitObject hitObject, Judgement judgement) + : base(hitObject, judgement) + { + } + } +} diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index d73ad888f4..2f86400b25 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -7,8 +7,11 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Input; using osu.Framework.Input.Bindings; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; using osu.Game.Rulesets.Scoring; using osuTK; @@ -32,6 +35,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected virtual OsuSkinComponents CirclePieceComponent => OsuSkinComponents.HitCircle; + private InputManager inputManager; + public DrawableHitCircle(HitCircle h) : base(h) { @@ -86,6 +91,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables AccentColour.BindValueChanged(accent => ApproachCircle.Colour = accent.NewValue, true); } + protected override void LoadComplete() + { + base.LoadComplete(); + + inputManager = GetContainingInputManager(); + } + public override double LifetimeStart { get => base.LifetimeStart; @@ -126,7 +138,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables return; } - ApplyResult(r => r.Type = result); + ApplyResult(r => + { + var circleResult = (OsuHitCircleJudgementResult)r; + + if (result != HitResult.Miss) + { + var localMousePosition = ToLocalSpace(inputManager.CurrentState.Mouse.Position); + circleResult.HitPosition = HitObject.StackedPosition + (localMousePosition - DrawSize / 2); + circleResult.Radius = (float)HitObject.Radius; + } + + circleResult.Type = result; + }); } protected override void UpdateInitialTransforms() @@ -172,6 +196,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public Drawable ProxiedLayer => ApproachCircle; + protected override JudgementResult CreateResult(Judgement judgement) => new OsuHitCircleJudgementResult(HitObject, judgement); + public class HitReceptor : CompositeDrawable, IKeyBindingHandler { // IsHovered is used diff --git a/osu.Game.Rulesets.Osu/Scoring/HitOffset.cs b/osu.Game.Rulesets.Osu/Scoring/HitOffset.cs new file mode 100644 index 0000000000..e6a5a01b48 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Scoring/HitOffset.cs @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osuTK; + +namespace osu.Game.Rulesets.Osu.Scoring +{ + public class HitOffset + { + public readonly Vector2 Position1; + public readonly Vector2 Position2; + public readonly Vector2 HitPosition; + public readonly float Radius; + + public HitOffset(Vector2 position1, Vector2 position2, Vector2 hitPosition, float radius) + { + Position1 = position1; + Position2 = position2; + HitPosition = hitPosition; + Radius = radius; + } + } +} diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs index a9d48df52d..97be372e37 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs @@ -2,11 +2,15 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using System.Diagnostics; using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Judgements; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; namespace osu.Game.Rulesets.Osu.Scoring { @@ -28,6 +32,7 @@ namespace osu.Game.Rulesets.Osu.Scoring private const int timing_distribution_centre_bin_index = timing_distribution_bins; private TimingDistribution timingDistribution; + private readonly List hitOffsets = new List(); public override void ApplyBeatmap(IBeatmap beatmap) { @@ -39,6 +44,8 @@ namespace osu.Game.Rulesets.Osu.Scoring base.ApplyBeatmap(beatmap); } + private OsuHitCircleJudgementResult lastCircleResult; + protected override void OnResultApplied(JudgementResult result) { base.OnResultApplied(result); @@ -47,6 +54,8 @@ namespace osu.Game.Rulesets.Osu.Scoring { int binOffset = (int)(result.TimeOffset / timingDistribution.BinSize); timingDistribution.Bins[timing_distribution_centre_bin_index + binOffset]++; + + addHitOffset(result); } } @@ -58,17 +67,67 @@ namespace osu.Game.Rulesets.Osu.Scoring { int binOffset = (int)(result.TimeOffset / timingDistribution.BinSize); timingDistribution.Bins[timing_distribution_centre_bin_index + binOffset]--; + + removeHitOffset(result); } } + private void addHitOffset(JudgementResult result) + { + if (!(result is OsuHitCircleJudgementResult circleResult)) + return; + + if (lastCircleResult == null) + { + lastCircleResult = circleResult; + return; + } + + if (circleResult.HitPosition != null) + { + Debug.Assert(circleResult.Radius != null); + hitOffsets.Add(new HitOffset(lastCircleResult.HitCircle.StackedEndPosition, circleResult.HitCircle.StackedEndPosition, circleResult.HitPosition.Value, circleResult.Radius.Value)); + } + + lastCircleResult = circleResult; + } + + private void removeHitOffset(JudgementResult result) + { + if (!(result is OsuHitCircleJudgementResult circleResult)) + return; + + if (hitOffsets.Count > 0 && circleResult.HitPosition != null) + hitOffsets.RemoveAt(hitOffsets.Count - 1); + } + protected override void Reset(bool storeResults) { base.Reset(storeResults); timingDistribution.Bins.AsSpan().Clear(); + hitOffsets.Clear(); } - protected override JudgementResult CreateResult(HitObject hitObject, Judgement judgement) => new OsuJudgementResult(hitObject, judgement); + public override void PopulateScore(ScoreInfo score) + { + base.PopulateScore(score); + + score.ExtraStatistics["timing_distribution"] = timingDistribution; + score.ExtraStatistics["hit_offsets"] = hitOffsets; + } + + protected override JudgementResult CreateResult(HitObject hitObject, Judgement judgement) + { + switch (hitObject) + { + case HitCircle _: + return new OsuHitCircleJudgementResult(hitObject, judgement); + + default: + return new OsuJudgementResult(hitObject, judgement); + } + } public override HitWindows CreateHitWindows() => new OsuHitWindows(); } diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 7b37c267bc..38b37afc55 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -166,6 +166,8 @@ namespace osu.Game.Scoring } } + public Dictionary ExtraStatistics = new Dictionary(); + [JsonIgnore] public List Files { get; set; } From 89b54be67395cfdaa6ef6b37862e899545e54c72 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 15 Jun 2020 22:45:18 +0900 Subject: [PATCH 2082/2376] Add initial implementation of the statistics panel --- osu.Game.Rulesets.Osu/OsuRuleset.cs | 14 ++++ osu.Game.Rulesets.Osu/Statistics/Heatmap.cs | 10 ++- .../Ranking/TestSceneAccuracyHeatmap.cs | 4 +- .../Ranking/TestScreenStatisticsPanel.cs | 24 +++++++ osu.Game/Rulesets/Ruleset.cs | 3 + .../Ranking/Statistics/StatisticContainer.cs | 70 +++++++++++++++++++ .../Ranking/Statistics/StatisticsPanel.cs | 56 +++++++++++++++ 7 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 osu.Game.Tests/Visual/Ranking/TestScreenStatisticsPanel.cs create mode 100644 osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs create mode 100644 osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 689a7b35ea..a76413480d 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -29,6 +29,8 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Skinning; using System; +using osu.Game.Rulesets.Osu.Statistics; +using osu.Game.Screens.Ranking.Statistics; namespace osu.Game.Rulesets.Osu { @@ -186,5 +188,17 @@ namespace osu.Game.Rulesets.Osu public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new OsuReplayFrame(); public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new OsuRulesetConfigManager(settings, RulesetInfo); + + public override IEnumerable CreateStatistics(ScoreInfo score) => new[] + { + new StatisticContainer("Timing Distribution") + { + Child = new TimingDistributionGraph((TimingDistribution)score.ExtraStatistics.GetValueOrDefault("timing_distribution")) + }, + new StatisticContainer("Accuracy Heatmap") + { + Child = new Heatmap((List)score.ExtraStatistics.GetValueOrDefault("hit_offsets")) + }, + }; } } diff --git a/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs b/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs index 8105d12991..89d861a6d1 100644 --- a/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs @@ -2,12 +2,14 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Game.Rulesets.Osu.Scoring; using osuTK; using osuTK.Graphics; @@ -29,10 +31,13 @@ namespace osu.Game.Rulesets.Osu.Statistics private const float rotation = 45; private const float point_size = 4; + private readonly IReadOnlyList offsets; private Container allPoints; - public Heatmap() + public Heatmap(IReadOnlyList offsets) { + this.offsets = offsets; + Size = new Vector2(size); } @@ -122,6 +127,9 @@ namespace osu.Game.Rulesets.Osu.Statistics }); } } + + foreach (var o in offsets) + AddPoint(o.Position1, o.Position2, o.HitPosition, o.Radius); } public void AddPoint(Vector2 start, Vector2 end, Vector2 hitPoint, float radius) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs index 9e5fda0ae6..a1b2dccea3 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs @@ -1,12 +1,14 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Framework.Utils; +using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Osu.Statistics; using osuTK; using osuTK.Graphics; @@ -38,7 +40,7 @@ namespace osu.Game.Tests.Visual.Ranking { Position = new Vector2(500, 300), }, - heatmap = new Heatmap + heatmap = new Heatmap(new List()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/Ranking/TestScreenStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestScreenStatisticsPanel.cs new file mode 100644 index 0000000000..e61cf9568d --- /dev/null +++ b/osu.Game.Tests/Visual/Ranking/TestScreenStatisticsPanel.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 NUnit.Framework; +using osu.Game.Rulesets.Osu; +using osu.Game.Scoring; +using osu.Game.Screens.Ranking.Statistics; + +namespace osu.Game.Tests.Visual.Ranking +{ + public class TestScreenStatisticsPanel : OsuTestScene + { + [Test] + public void TestScore() + { + loadPanel(new TestScoreInfo(new OsuRuleset().RulesetInfo)); + } + + private void loadPanel(ScoreInfo score) => AddStep("load panel", () => + { + Child = new StatisticsPanel(score); + }); + } +} diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 4f28607733..5c349ca557 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -23,6 +23,7 @@ using osu.Game.Scoring; using osu.Game.Skinning; using osu.Game.Users; using JetBrains.Annotations; +using osu.Game.Screens.Ranking.Statistics; namespace osu.Game.Rulesets { @@ -208,5 +209,7 @@ namespace osu.Game.Rulesets ///
    /// An empty frame for the current ruleset, or null if unsupported. public virtual IConvertibleReplayFrame CreateConvertibleReplayFrame() => null; + + public virtual IEnumerable CreateStatistics(ScoreInfo score) => Enumerable.Empty(); } } diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs new file mode 100644 index 0000000000..8d10529496 --- /dev/null +++ b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.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 osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osuTK; + +namespace osu.Game.Screens.Ranking.Statistics +{ + public class StatisticContainer : Container + { + protected override Container Content => content; + + private readonly Container content; + + public StatisticContainer(string name) + { + InternalChild = new GridContainer + { + RelativeSizeAxes = Axes.X, + Content = new[] + { + new Drawable[] + { + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5, 0), + Children = new Drawable[] + { + new Circle + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Height = 9, + Width = 4, + Colour = Color4Extensions.FromHex("#00FFAA") + }, + new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Text = name, + Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold), + } + } + } + }, + new Drawable[] + { + content = new Container + { + RelativeSizeAxes = Axes.Both + } + }, + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + } + }; + } + } +} diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs new file mode 100644 index 0000000000..8ab85527db --- /dev/null +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -0,0 +1,56 @@ +// 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.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Scoring; +using osuTK; + +namespace osu.Game.Screens.Ranking.Statistics +{ + public class StatisticsPanel : CompositeDrawable + { + public StatisticsPanel(ScoreInfo score) + { + // Todo: Not correct. + RelativeSizeAxes = Axes.Both; + + Container statistics; + + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#333") + }, + new ScorePanel(score) // Todo: Temporary + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + State = PanelState.Expanded, + X = 30 + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding + { + Left = ScorePanel.EXPANDED_WIDTH + 30 + 50, + Right = 50 + }, + Child = statistics = new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Spacing = new Vector2(30, 15), + } + } + }; + + foreach (var s in score.Ruleset.CreateInstance().CreateStatistics(score)) + statistics.Add(s); + } + } +} From c79d8a425178de9326d3bdb19d96e93bf002ca44 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Jun 2020 00:12:32 +0900 Subject: [PATCH 2083/2376] Update ChannelTabControl in line with TabControl changes --- osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs b/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs index cb6abb7cc6..19c6f437b6 100644 --- a/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs +++ b/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs @@ -78,19 +78,10 @@ namespace osu.Game.Overlays.Chat.Tabs /// The channel that is going to be removed. public void RemoveChannel(Channel channel) { - if (Current.Value == channel) - { - var allChannels = TabContainer.AllTabItems.Select(tab => tab.Value).ToList(); - var isNextTabSelector = allChannels[allChannels.IndexOf(channel) + 1] == selectorTab.Value; - - // selectorTab is not switchable, so we have to explicitly select it if it's the only tab left - if (isNextTabSelector && allChannels.Count == 2) - SelectTab(selectorTab); - else - SwitchTab(isNextTabSelector ? -1 : 1); - } - RemoveItem(channel); + + if (SelectedTab == null) + SelectTab(selectorTab); } protected override void SelectTab(TabItem tab) From a65c1a9abdb358f69065e72b1ebe00ab2e1ee95a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 16 Jun 2020 16:08:41 +0900 Subject: [PATCH 2084/2376] Fix test name --- ...TestScreenStatisticsPanel.cs => TestSceneStatisticsPanel.cs} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename osu.Game.Tests/Visual/Ranking/{TestScreenStatisticsPanel.cs => TestSceneStatisticsPanel.cs} (91%) diff --git a/osu.Game.Tests/Visual/Ranking/TestScreenStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs similarity index 91% rename from osu.Game.Tests/Visual/Ranking/TestScreenStatisticsPanel.cs rename to osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index e61cf9568d..22ee6077cb 100644 --- a/osu.Game.Tests/Visual/Ranking/TestScreenStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -8,7 +8,7 @@ using osu.Game.Screens.Ranking.Statistics; namespace osu.Game.Tests.Visual.Ranking { - public class TestScreenStatisticsPanel : OsuTestScene + public class TestSceneStatisticsPanel : OsuTestScene { [Test] public void TestScore() From 9ea7c3dc90474e56bdf07c30988f1cd5007ef919 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 16 Jun 2020 16:31:02 +0900 Subject: [PATCH 2085/2376] Make heatmap support dynamic sizing --- osu.Game.Rulesets.Osu/Statistics/Heatmap.cs | 155 +++++++++++------- .../Ranking/TestSceneAccuracyHeatmap.cs | 16 +- 2 files changed, 108 insertions(+), 63 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs b/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs index 89d861a6d1..7e140e6fd2 100644 --- a/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs @@ -9,6 +9,7 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Layout; using osu.Game.Rulesets.Osu.Scoring; using osuTK; using osuTK.Graphics; @@ -18,12 +19,7 @@ namespace osu.Game.Rulesets.Osu.Statistics public class Heatmap : CompositeDrawable { /// - /// Full size of the heatmap. - /// - private const float size = 130; - - /// - /// Size of the inner circle containing the "hit" points, relative to . + /// Size of the inner circle containing the "hit" points, relative to the size of this . /// All other points outside of the inner circle are "miss" points. /// private const float inner_portion = 0.8f; @@ -34,77 +30,106 @@ namespace osu.Game.Rulesets.Osu.Statistics private readonly IReadOnlyList offsets; private Container allPoints; + private readonly LayoutValue sizeLayout = new LayoutValue(Invalidation.DrawSize); + public Heatmap(IReadOnlyList offsets) { this.offsets = offsets; - Size = new Vector2(size); + AddLayout(sizeLayout); } [BackgroundDependencyLoader] private void load() { - InternalChildren = new Drawable[] + InternalChild = new Container { - new CircularContainer + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fit, + Children = new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Size = new Vector2(inner_portion), - Masking = true, - BorderThickness = 2f, - BorderColour = Color4.White, - Child = new Box + new CircularContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(inner_portion), + Masking = true, + BorderThickness = 2f, + BorderColour = Color4.White, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#202624") + } + }, + new Container { RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex("#202624") - } - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Masking = true, - Children = new Drawable[] - { - new Box + Masking = true, + Children = new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Y, - Height = 2, // We're rotating along a diagonal - we don't really care how big this is. - Width = 1f, - Rotation = -rotation, - Alpha = 0.3f, - }, - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Y, - Height = 2, // We're rotating along a diagonal - we don't really care how big this is. - Width = 1f, - Rotation = rotation - }, - new Box - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Width = 10, - Height = 2f, - }, - new Box - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Y = -1, - Width = 2f, - Height = 10, + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Height = 2, // We're rotating along a diagonal - we don't really care how big this is. + Width = 1f, + Rotation = -rotation, + Alpha = 0.3f, + }, + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Height = 2, // We're rotating along a diagonal - we don't really care how big this is. + Width = 1f, + Rotation = rotation + }, + new Box + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Width = 10, + Height = 2f, + }, + new Box + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Y = -1, + Width = 2f, + Height = 10, + } } + }, + allPoints = new Container + { + RelativeSizeAxes = Axes.Both } - }, - allPoints = new Container { RelativeSizeAxes = Axes.Both } + } }; + } + + protected override void Update() + { + base.Update(); + validateHitPoints(); + } + + private void validateHitPoints() + { + if (sizeLayout.IsValid) + return; + + allPoints.Clear(); + + // Since the content is fit, both dimensions should have the same size. + float size = allPoints.DrawSize.X; Vector2 centre = new Vector2(size / 2); int rows = (int)Math.Ceiling(size / point_size); @@ -130,16 +155,24 @@ namespace osu.Game.Rulesets.Osu.Statistics foreach (var o in offsets) AddPoint(o.Position1, o.Position2, o.HitPosition, o.Radius); + + sizeLayout.Validate(); } - public void AddPoint(Vector2 start, Vector2 end, Vector2 hitPoint, float radius) + protected void AddPoint(Vector2 start, Vector2 end, Vector2 hitPoint, float radius) { + if (allPoints.Count == 0) + return; + double angle1 = Math.Atan2(end.Y - hitPoint.Y, hitPoint.X - end.X); // Angle between the end point and the hit point. double angle2 = Math.Atan2(end.Y - start.Y, start.X - end.X); // Angle between the end point and the start point. double finalAngle = angle2 - angle1; // Angle between start, end, and hit points. float normalisedDistance = Vector2.Distance(hitPoint, end) / radius; + // Since the content is fit, both dimensions should have the same size. + float size = allPoints.DrawSize.X; + // Find the most relevant hit point. double minDist = double.PositiveInfinity; HitPoint point = null; diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs index a1b2dccea3..53c8e56f53 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs @@ -20,7 +20,7 @@ namespace osu.Game.Tests.Visual.Ranking private readonly Box background; private readonly Drawable object1; private readonly Drawable object2; - private readonly Heatmap heatmap; + private readonly TestHeatmap heatmap; public TestSceneAccuracyHeatmap() { @@ -40,10 +40,11 @@ namespace osu.Game.Tests.Visual.Ranking { Position = new Vector2(500, 300), }, - heatmap = new Heatmap(new List()) + heatmap = new TestHeatmap(new List()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, + Size = new Vector2(130) } }; } @@ -70,6 +71,17 @@ namespace osu.Game.Tests.Visual.Ranking return true; } + private class TestHeatmap : Heatmap + { + public TestHeatmap(IReadOnlyList offsets) + : base(offsets) + { + } + + public new void AddPoint(Vector2 start, Vector2 end, Vector2 hitPoint, float radius) + => base.AddPoint(start, end, hitPoint, radius); + } + private class BorderCircle : CircularContainer { public BorderCircle() From c3d4ffed00655912d207ba0606bbeec4f7106cb1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Jun 2020 16:46:33 +0900 Subject: [PATCH 2086/2376] 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 6387356686..b95b794004 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 8c098b79c6..6ec57e5100 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -24,7 +24,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 373ad09597..0bfff24805 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From 3dbe164b2c4da72b25c26d25156bc5a8de3ce28b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 16 Jun 2020 17:20:38 +0900 Subject: [PATCH 2087/2376] Add some very basic safety checks around non-existent data --- osu.Game.Rulesets.Osu/Statistics/Heatmap.cs | 7 +++++-- .../Statistics/TimingDistributionGraph.cs | 3 +++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs b/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs index 7e140e6fd2..51508a5e8b 100644 --- a/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs @@ -153,8 +153,11 @@ namespace osu.Game.Rulesets.Osu.Statistics } } - foreach (var o in offsets) - AddPoint(o.Position1, o.Position2, o.HitPosition, o.Radius); + if (offsets?.Count > 0) + { + foreach (var o in offsets) + AddPoint(o.Position1, o.Position2, o.HitPosition, o.Radius); + } sizeLayout.Validate(); } diff --git a/osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs b/osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs index a47d726988..0ba94b7101 100644 --- a/osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs +++ b/osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs @@ -36,6 +36,9 @@ namespace osu.Game.Rulesets.Osu.Statistics [BackgroundDependencyLoader] private void load() { + if (distribution?.Bins == null || distribution.Bins.Length == 0) + return; + int maxCount = distribution.Bins.Max(); var bars = new Drawable[distribution.Bins.Length]; From 076eac2362480a80749a0b42d1caaf7295f25ae3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 16 Jun 2020 17:46:34 +0900 Subject: [PATCH 2088/2376] Inset entire graph rather than just the axis --- .../Statistics/TimingDistributionGraph.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs b/osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs index 0ba94b7101..1f9f38bf3b 100644 --- a/osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs +++ b/osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs @@ -20,12 +20,6 @@ namespace osu.Game.Rulesets.Osu.Statistics ///
    private const float axis_points = 5; - /// - /// An amount to adjust the value of the axis points by, effectively insetting the axis in the graph. - /// Without an inset, the final data point will be placed halfway outside the graph. - /// - private const float axis_value_inset = 0.2f; - private readonly TimingDistribution distribution; public TimingDistributionGraph(TimingDistribution distribution) @@ -50,6 +44,7 @@ namespace osu.Game.Rulesets.Osu.Statistics InternalChild = new GridContainer { RelativeSizeAxes = Axes.Both, + Width = 0.8f, Content = new[] { new Drawable[] @@ -80,7 +75,7 @@ namespace osu.Game.Rulesets.Osu.Statistics // So our axis will contain one centre element + 5 points on each side, each with a value depending on the number of bins * bin size. int sideBins = (distribution.Bins.Length - 1) / 2; double maxValue = sideBins * distribution.BinSize; - double axisValueStep = maxValue / axis_points * (1 - axis_value_inset); + double axisValueStep = maxValue / axis_points; axisFlow.Add(new OsuSpriteText { From 9442fc00ac408c314ca078bbfa5d3c426adf6aa2 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 16 Jun 2020 17:48:59 +0900 Subject: [PATCH 2089/2376] Temporary hack to make replay player populate scores --- osu.Game/Screens/Play/ReplayPlayer.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index b443603128..d7580ea271 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -27,11 +27,14 @@ namespace osu.Game.Screens.Play protected override void GotoRanking() { - this.Push(CreateResults(DrawableRuleset.ReplayScore.ScoreInfo)); + this.Push(CreateResults(CreateScore())); } protected override ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, false); - protected override ScoreInfo CreateScore() => score.ScoreInfo; + // protected override ScoreInfo CreateScore() + // { + // return score.ScoreInfo; + // } } } From a2ddb4edb456ed7b0c78ff4af1f67e4e3a312289 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 16 Jun 2020 17:49:28 +0900 Subject: [PATCH 2090/2376] Change interface for creating statistic rows --- osu.Game.Rulesets.Osu/OsuRuleset.cs | 37 +++++++++++++++---- osu.Game/Rulesets/Ruleset.cs | 2 +- .../Ranking/Statistics/StatisticContainer.cs | 2 +- .../Ranking/Statistics/StatisticRow.cs | 15 ++++++++ .../Ranking/Statistics/StatisticsPanel.cs | 16 ++++++-- 5 files changed, 59 insertions(+), 13 deletions(-) create mode 100644 osu.Game/Screens/Ranking/Statistics/StatisticRow.cs diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index a76413480d..67a9bda1a9 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -29,6 +29,7 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Skinning; using System; +using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Osu.Statistics; using osu.Game.Screens.Ranking.Statistics; @@ -189,16 +190,36 @@ namespace osu.Game.Rulesets.Osu public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new OsuRulesetConfigManager(settings, RulesetInfo); - public override IEnumerable CreateStatistics(ScoreInfo score) => new[] + public override StatisticRow[] CreateStatistics(ScoreInfo score) => new[] { - new StatisticContainer("Timing Distribution") + new StatisticRow { - Child = new TimingDistributionGraph((TimingDistribution)score.ExtraStatistics.GetValueOrDefault("timing_distribution")) - }, - new StatisticContainer("Accuracy Heatmap") - { - Child = new Heatmap((List)score.ExtraStatistics.GetValueOrDefault("hit_offsets")) - }, + Content = new Drawable[] + { + new StatisticContainer("Timing Distribution") + { + RelativeSizeAxes = Axes.X, + Height = 130, + Child = new TimingDistributionGraph((TimingDistribution)score.ExtraStatistics.GetValueOrDefault("timing_distribution")) + { + RelativeSizeAxes = Axes.Both + } + }, + new StatisticContainer("Accuracy Heatmap") + { + RelativeSizeAxes = Axes.Both, + Child = new Heatmap((List)score.ExtraStatistics.GetValueOrDefault("hit_offsets")) + { + RelativeSizeAxes = Axes.Both + } + }, + }, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.Absolute, 130), + } + } }; } } diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 5c349ca557..f05685b6e9 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -210,6 +210,6 @@ namespace osu.Game.Rulesets /// An empty frame for the current ruleset, or null if unsupported. public virtual IConvertibleReplayFrame CreateConvertibleReplayFrame() => null; - public virtual IEnumerable CreateStatistics(ScoreInfo score) => Enumerable.Empty(); + public virtual StatisticRow[] CreateStatistics(ScoreInfo score) => Array.Empty(); } } diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs index 8d10529496..d7b42c1c2f 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs @@ -21,7 +21,7 @@ namespace osu.Game.Screens.Ranking.Statistics { InternalChild = new GridContainer { - RelativeSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Both, Content = new[] { new Drawable[] diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticRow.cs b/osu.Game/Screens/Ranking/Statistics/StatisticRow.cs new file mode 100644 index 0000000000..5d39ef57b2 --- /dev/null +++ b/osu.Game/Screens/Ranking/Statistics/StatisticRow.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 System; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; + +namespace osu.Game.Screens.Ranking.Statistics +{ + public class StatisticRow + { + public Drawable[] Content = Array.Empty(); + public Dimension[] ColumnDimensions = Array.Empty(); + } +} diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 8ab85527db..6c5fa1837a 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -17,7 +17,7 @@ namespace osu.Game.Screens.Ranking.Statistics // Todo: Not correct. RelativeSizeAxes = Axes.Both; - Container statistics; + FillFlowContainer statisticRows; InternalChildren = new Drawable[] { @@ -41,16 +41,26 @@ namespace osu.Game.Screens.Ranking.Statistics Left = ScorePanel.EXPANDED_WIDTH + 30 + 50, Right = 50 }, - Child = statistics = new FillFlowContainer + Child = statisticRows = new FillFlowContainer { RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, Spacing = new Vector2(30, 15), } } }; foreach (var s in score.Ruleset.CreateInstance().CreateStatistics(score)) - statistics.Add(s); + { + statisticRows.Add(new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Content = new[] { s.Content }, + ColumnDimensions = s.ColumnDimensions, + RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) } + }); + } } } } From 808e216059e00b52cf7c02a62ea1ca94bd5aaccd Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 16 Jun 2020 17:49:37 +0900 Subject: [PATCH 2091/2376] Improve test scene --- .../Visual/Ranking/TestSceneStatisticsPanel.cs | 13 ++++++++++++- .../Ranking/TestSceneTimingDistributionGraph.cs | 4 ++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index 22ee6077cb..c02be9ab5d 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.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.Collections.Generic; using NUnit.Framework; using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Ranking.Statistics; @@ -13,7 +15,16 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestScore() { - loadPanel(new TestScoreInfo(new OsuRuleset().RulesetInfo)); + var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) + { + ExtraStatistics = + { + ["timing_distribution"] = TestSceneTimingDistributionGraph.CreateNormalDistribution(), + ["hit_offsets"] = new List() + } + }; + + loadPanel(score); } private void loadPanel(ScoreInfo score) => AddStep("load panel", () => diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs b/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs index 7530fc42b8..2249655093 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs @@ -22,7 +22,7 @@ namespace osu.Game.Tests.Visual.Ranking RelativeSizeAxes = Axes.Both, Colour = Color4Extensions.FromHex("#333") }, - new TimingDistributionGraph(createNormalDistribution()) + new TimingDistributionGraph(CreateNormalDistribution()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -31,7 +31,7 @@ namespace osu.Game.Tests.Visual.Ranking }; } - private TimingDistribution createNormalDistribution() + public static TimingDistribution CreateNormalDistribution() { var distribution = new TimingDistribution(51, 5); From e7687a09271d12f5cadf93b00b7ff798733d9340 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 16 Jun 2020 17:49:43 +0900 Subject: [PATCH 2092/2376] Temporary placement inside results screen --- osu.Game/Screens/Ranking/ResultsScreen.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index fbb9b95478..4d589b4527 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -16,6 +16,7 @@ using osu.Game.Online.API; using osu.Game.Scoring; using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Play; +using osu.Game.Screens.Ranking.Statistics; using osuTK; namespace osu.Game.Screens.Ranking @@ -134,6 +135,11 @@ namespace osu.Game.Screens.Ranking }, }); } + + AddInternal(new StatisticsPanel(Score) + { + RelativeSizeAxes = Axes.Both + }); } protected override void LoadComplete() From 3f1b9edabe20478578e05f85476d8acaf06eb62d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Jun 2020 20:16:04 +0900 Subject: [PATCH 2093/2376] Fix regression in android build parsing behaviour --- osu.Android/OsuGameAndroid.cs | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index 19ed7ffcf5..5f936ffce4 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.cs @@ -3,6 +3,7 @@ using System; using Android.App; +using Android.OS; using osu.Game; using osu.Game.Updater; @@ -18,9 +19,32 @@ namespace osu.Android try { - // todo: needs checking before play store redeploy. - string versionName = packageInfo.VersionName; - // undo play store version garbling + // We store the osu! build number in the "VersionCode" field to better support google play releases. + // If we were to use the main build number, it would require a new submission each time (similar to TestFlight). + // In order to do this, we should split it up and pad the numbers to still ensure sequential increase over time. + // + // We also need to be aware that older SDK versions store this as a 32bit int. + // + // Basic conversion format (as done in Fastfile): 2020.606.0 -> 202006060 + + // https://stackoverflow.com/questions/52977079/android-sdk-28-versioncode-in-packageinfo-has-been-deprecated + string versionName = string.Empty; + + if (Build.VERSION.SdkInt >= BuildVersionCodes.P) + { + versionName = packageInfo.LongVersionCode.ToString(); + versionName = versionName.Substring(versionName.Length - 9); + } + else + { + +#pragma warning disable CS0618 // Type or member is obsolete + // this is required else older SDKs will report missing method exception. + versionName = packageInfo.VersionCode.ToString(); +#pragma warning restore CS0618 // Type or member is obsolete + } + + // undo play store version garbling (as mentioned above). return new Version(int.Parse(versionName.Substring(0, 4)), int.Parse(versionName.Substring(4, 4)), int.Parse(versionName.Substring(8, 1))); } catch From 115ea244beb9953c4e189e1cfa426c0f705e1c79 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Jun 2020 20:20:46 +0900 Subject: [PATCH 2094/2376] Add note about substring usage --- osu.Android/OsuGameAndroid.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index 5f936ffce4..136b85699a 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.cs @@ -33,6 +33,7 @@ namespace osu.Android if (Build.VERSION.SdkInt >= BuildVersionCodes.P) { versionName = packageInfo.LongVersionCode.ToString(); + // ensure we only read the trailing portion of long (the part we are interested in). versionName = versionName.Substring(versionName.Length - 9); } else From c5358cbb6b22d3167efe2a21868d23fc91b523b0 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 16 Jun 2020 21:01:10 +0900 Subject: [PATCH 2095/2376] Remove blank line --- osu.Android/OsuGameAndroid.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index 136b85699a..7542a2b997 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.cs @@ -38,7 +38,6 @@ namespace osu.Android } else { - #pragma warning disable CS0618 // Type or member is obsolete // this is required else older SDKs will report missing method exception. versionName = packageInfo.VersionCode.ToString(); @@ -58,4 +57,4 @@ namespace osu.Android protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager(); } -} \ No newline at end of file +} From 693a760a193e8521d7db350318bd9a1e93d4f9db Mon Sep 17 00:00:00 2001 From: Shivam Date: Tue, 16 Jun 2020 15:44:59 +0200 Subject: [PATCH 2096/2376] Use RelativeSizeAxes for width --- osu.Game/Screens/Menu/IntroWelcome.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Menu/IntroWelcome.cs b/osu.Game/Screens/Menu/IntroWelcome.cs index a431752369..711c7b64e4 100644 --- a/osu.Game/Screens/Menu/IntroWelcome.cs +++ b/osu.Game/Screens/Menu/IntroWelcome.cs @@ -120,9 +120,9 @@ namespace osu.Game.Screens.Menu { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Scale = new Vector2(0.3f), - Width = 750, - Height = 78, + RelativeSizeAxes = Axes.X, + Scale = new Vector2(0.1f), + Height = 156, Alpha = 0, Texture = textures.Get(@"Welcome/welcome_text") }, From 1cf16038a708f1c586b6405b58bb3d70082cf2c5 Mon Sep 17 00:00:00 2001 From: Ronnie Moir <7267697+H2n9@users.noreply.github.com> Date: Tue, 16 Jun 2020 14:54:05 +0100 Subject: [PATCH 2097/2376] Create IApplicableToSample --- osu.Game/Rulesets/Mods/IApplicableToSample.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 osu.Game/Rulesets/Mods/IApplicableToSample.cs diff --git a/osu.Game/Rulesets/Mods/IApplicableToSample.cs b/osu.Game/Rulesets/Mods/IApplicableToSample.cs new file mode 100644 index 0000000000..559d127cfc --- /dev/null +++ b/osu.Game/Rulesets/Mods/IApplicableToSample.cs @@ -0,0 +1,15 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Audio.Sample; + +namespace osu.Game.Rulesets.Mods +{ + /// + /// An interface for mods that make adjustments to a sample. + /// + public interface IApplicableToSample : IApplicableMod + { + void ApplyToSample(SampleChannel sample); + } +} From 9f4f3ce2cc055867cbfe58f723c334f7b08b1448 Mon Sep 17 00:00:00 2001 From: Ronnie Moir <7267697+H2n9@users.noreply.github.com> Date: Tue, 16 Jun 2020 14:54:50 +0100 Subject: [PATCH 2098/2376] Handle IApplicableToSample mods --- .../Storyboards/Drawables/DrawableStoryboardSample.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs index 8292b02068..2b9c66d2e6 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs @@ -1,11 +1,14 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mods; namespace osu.Game.Storyboards.Drawables { @@ -28,12 +31,17 @@ namespace osu.Game.Storyboards.Drawables } [BackgroundDependencyLoader] - private void load(IBindable beatmap) + private void load(IBindable beatmap, IBindable> mods) { channel = beatmap.Value.Skin.GetSample(sampleInfo); if (channel != null) + { channel.Volume.Value = sampleInfo.Volume / 100.0; + + foreach (var mod in mods.Value.OfType()) + mod.ApplyToSample(channel); + } } protected override void Update() From 4138f6119f24ce50991a64c89cd91e4c5fa28a23 Mon Sep 17 00:00:00 2001 From: Ronnie Moir <7267697+H2n9@users.noreply.github.com> Date: Tue, 16 Jun 2020 14:58:23 +0100 Subject: [PATCH 2099/2376] Update rate adjust mods to also use IApplicableToSample --- osu.Game/Rulesets/Mods/ModRateAdjust.cs | 8 +++++++- osu.Game/Rulesets/Mods/ModTimeRamp.cs | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModRateAdjust.cs b/osu.Game/Rulesets/Mods/ModRateAdjust.cs index cb2ff149f1..ecd625c3b4 100644 --- a/osu.Game/Rulesets/Mods/ModRateAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModRateAdjust.cs @@ -2,12 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Audio.Track; using osu.Framework.Bindables; namespace osu.Game.Rulesets.Mods { - public abstract class ModRateAdjust : Mod, IApplicableToTrack + public abstract class ModRateAdjust : Mod, IApplicableToTrack, IApplicableToSample { public abstract BindableNumber SpeedChange { get; } @@ -16,6 +17,11 @@ namespace osu.Game.Rulesets.Mods track.AddAdjustment(AdjustableProperty.Tempo, SpeedChange); } + public virtual void ApplyToSample(SampleChannel sample) + { + sample.AddAdjustment(AdjustableProperty.Frequency, SpeedChange); + } + public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value:N2}x"; } } diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index df059eef7d..352e3ae915 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -10,10 +10,11 @@ using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.Objects; +using osu.Framework.Audio.Sample; namespace osu.Game.Rulesets.Mods { - public abstract class ModTimeRamp : Mod, IUpdatableByPlayfield, IApplicableToBeatmap, IApplicableToTrack + public abstract class ModTimeRamp : Mod, IUpdatableByPlayfield, IApplicableToBeatmap, IApplicableToTrack, IApplicableToSample { /// /// The point in the beatmap at which the final ramping rate should be reached. @@ -58,6 +59,11 @@ namespace osu.Game.Rulesets.Mods AdjustPitch.TriggerChange(); } + public void ApplyToSample(SampleChannel sample) + { + sample.AddAdjustment(AdjustableProperty.Frequency, SpeedChange); + } + public virtual void ApplyToBeatmap(IBeatmap beatmap) { HitObject lastObject = beatmap.HitObjects.LastOrDefault(); From 5e74985edafa034306c4559d32e99aa495697d80 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 17 Jun 2020 19:28:40 +0900 Subject: [PATCH 2100/2376] Add scrolling capability to results screen --- osu.Game/Screens/Ranking/ResultsScreen.cs | 61 +++++++++++++++-------- 1 file changed, 41 insertions(+), 20 deletions(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 4d589b4527..11ed9fb5b7 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -24,6 +24,7 @@ namespace osu.Game.Screens.Ranking public abstract class ResultsScreen : OsuScreen { protected const float BACKGROUND_BLUR = 20; + private static readonly float screen_height = 768 - TwoLayerButton.SIZE_EXTENDED.Y; public override bool DisallowExternalBeatmapRulesetChanges => true; @@ -68,10 +69,24 @@ namespace osu.Game.Screens.Ranking { new ResultsScrollContainer { - Child = panels = new ScorePanelList + Child = new FillFlowContainer { - RelativeSizeAxes = Axes.Both, - SelectedScore = { BindTarget = SelectedScore } + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + panels = new ScorePanelList + { + RelativeSizeAxes = Axes.X, + Height = screen_height, + SelectedScore = { BindTarget = SelectedScore } + }, + new StatisticsPanel(Score) + { + RelativeSizeAxes = Axes.X, + Height = screen_height, + } + } } } }, @@ -135,11 +150,6 @@ namespace osu.Game.Screens.Ranking }, }); } - - AddInternal(new StatisticsPanel(Score) - { - RelativeSizeAxes = Axes.Both - }); } protected override void LoadComplete() @@ -180,27 +190,38 @@ namespace osu.Game.Screens.Ranking return base.OnExiting(next); } + [Cached] private class ResultsScrollContainer : OsuScrollContainer { - private readonly Container content; - - protected override Container Content => content; - public ResultsScrollContainer() { - base.Content.Add(content = new Container - { - RelativeSizeAxes = Axes.X - }); - RelativeSizeAxes = Axes.Both; ScrollbarVisible = false; } - protected override void Update() + protected override void OnUserScroll(float value, bool animated = true, double? distanceDecay = default) { - base.Update(); - content.Height = Math.Max(768 - TwoLayerButton.SIZE_EXTENDED.Y, DrawHeight); + if (!animated) + { + // If the user is scrolling via mouse drag, follow the mouse 1:1. + base.OnUserScroll(value, false, distanceDecay); + } + else + { + float direction = Math.Sign(value - Target); + float target = Target + direction * screen_height; + + if (target <= -screen_height / 2 || target >= ScrollableExtent + screen_height / 2) + { + // If the user is already at either extent and scrolling in the clamped direction, we want to follow the default scroll exactly so that the bounces aren't too harsh. + base.OnUserScroll(value, true, distanceDecay); + } + else + { + // Otherwise, scroll one screen in the target direction. + base.OnUserScroll(target, true, distanceDecay); + } + } } } } From c3e268616fdd1b44cc146f0a07b7e05cd788866f Mon Sep 17 00:00:00 2001 From: Ronnie Moir <7267697+H2n9@users.noreply.github.com> Date: Wed, 17 Jun 2020 11:43:32 +0100 Subject: [PATCH 2101/2376] Implement grouping interface IApplicableToAudio --- osu.Game/Rulesets/Mods/IApplicableToAudio.cs | 10 ++++++++++ osu.Game/Rulesets/Mods/ModRateAdjust.cs | 2 +- osu.Game/Rulesets/Mods/ModTimeRamp.cs | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 osu.Game/Rulesets/Mods/IApplicableToAudio.cs diff --git a/osu.Game/Rulesets/Mods/IApplicableToAudio.cs b/osu.Game/Rulesets/Mods/IApplicableToAudio.cs new file mode 100644 index 0000000000..40e13764c6 --- /dev/null +++ b/osu.Game/Rulesets/Mods/IApplicableToAudio.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace osu.Game.Rulesets.Mods +{ + public interface IApplicableToAudio : IApplicableToTrack, IApplicableToSample + { + } +} diff --git a/osu.Game/Rulesets/Mods/ModRateAdjust.cs b/osu.Game/Rulesets/Mods/ModRateAdjust.cs index ecd625c3b4..874384686f 100644 --- a/osu.Game/Rulesets/Mods/ModRateAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModRateAdjust.cs @@ -8,7 +8,7 @@ using osu.Framework.Bindables; namespace osu.Game.Rulesets.Mods { - public abstract class ModRateAdjust : Mod, IApplicableToTrack, IApplicableToSample + public abstract class ModRateAdjust : Mod, IApplicableToAudio { public abstract BindableNumber SpeedChange { get; } diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index 352e3ae915..cbd07efa97 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -14,7 +14,7 @@ using osu.Framework.Audio.Sample; namespace osu.Game.Rulesets.Mods { - public abstract class ModTimeRamp : Mod, IUpdatableByPlayfield, IApplicableToBeatmap, IApplicableToTrack, IApplicableToSample + public abstract class ModTimeRamp : Mod, IUpdatableByPlayfield, IApplicableToBeatmap, IApplicableToAudio { /// /// The point in the beatmap at which the final ramping rate should be reached. From 725b2e540bdfd86d836c6345f6e6ea1daead0865 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 17 Jun 2020 22:29:00 +0900 Subject: [PATCH 2102/2376] wip --- .../Visual/Ranking/TestSceneScorePanel.cs | 38 ++++++++ osu.Game/Screens/Ranking/ResultsScreen.cs | 89 +++++++++++++------ osu.Game/Screens/Ranking/ScorePanel.cs | 60 +++++++++++++ osu.Game/Screens/Ranking/ScorePanelList.cs | 59 +++++++++--- .../Ranking/Statistics/StatisticsPanel.cs | 7 -- 5 files changed, 205 insertions(+), 48 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs index 250fdc5ebd..1c5087ee94 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs @@ -3,6 +3,7 @@ using NUnit.Framework; using osu.Framework.Graphics; +using osu.Framework.Utils; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; @@ -101,6 +102,39 @@ namespace osu.Game.Tests.Visual.Ranking AddWaitStep("wait for transition", 10); } + [Test] + public void TestSceneTrackingScorePanel() + { + var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.925, Rank = ScoreRank.A }; + + addPanelStep(score, PanelState.Contracted); + + AddStep("enable tracking", () => + { + panel.Anchor = Anchor.CentreLeft; + panel.Origin = Anchor.CentreLeft; + panel.Tracking = true; + + Add(panel.CreateTrackingComponent().With(d => + { + d.Anchor = Anchor.Centre; + d.Origin = Anchor.Centre; + })); + }); + + assertTracking(true); + + AddStep("expand panel", () => panel.State = PanelState.Expanded); + AddWaitStep("wait for transition", 2); + assertTracking(true); + + AddStep("stop tracking", () => panel.Tracking = false); + assertTracking(false); + + AddStep("start tracking", () => panel.Tracking = true); + assertTracking(true); + } + private void addPanelStep(ScoreInfo score, PanelState state = PanelState.Expanded) => AddStep("add panel", () => { Child = panel = new ScorePanel(score) @@ -110,5 +144,9 @@ namespace osu.Game.Tests.Visual.Ranking State = state }; }); + + private void assertTracking(bool tracking) => AddAssert($"{(tracking ? "is" : "is not")} tracking", () => + Precision.AlmostEquals(panel.ScreenSpaceDrawQuad.TopLeft, panel.CreateTrackingComponent().ScreenSpaceDrawQuad.TopLeft) == tracking + && Precision.AlmostEquals(panel.ScreenSpaceDrawQuad.BottomRight, panel.CreateTrackingComponent().ScreenSpaceDrawQuad.BottomRight) == tracking); } } diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 11ed9fb5b7..4ef012f6f2 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -10,6 +11,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Screens; +using osu.Framework.Utils; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; @@ -44,6 +46,9 @@ namespace osu.Game.Screens.Ranking [Resolved] private IAPIProvider api { get; set; } + private Container scorePanelContainer; + private ResultsScrollContainer scrollContainer; + private Container expandedPanelProxyContainer; private Drawable bottomPanel; private ScorePanelList panels; @@ -58,6 +63,13 @@ namespace osu.Game.Screens.Ranking [BackgroundDependencyLoader] private void load() { + scorePanelContainer = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + }; + FillFlowContainer buttons; InternalChild = new GridContainer @@ -67,26 +79,35 @@ namespace osu.Game.Screens.Ranking { new Drawable[] { - new ResultsScrollContainer + new Container { - Child = new FillFlowContainer + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new Drawable[] + scorePanelContainer, + scrollContainer = new ResultsScrollContainer { - panels = new ScorePanelList + Child = new FillFlowContainer { RelativeSizeAxes = Axes.X, - Height = screen_height, - SelectedScore = { BindTarget = SelectedScore } - }, - new StatisticsPanel(Score) - { - RelativeSizeAxes = Axes.X, - Height = screen_height, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + panels = new ScorePanelList(scorePanelContainer) + { + RelativeSizeAxes = Axes.X, + Height = screen_height, + SelectedScore = { BindTarget = SelectedScore } + }, + new StatisticsPanel(Score) + { + RelativeSizeAxes = Axes.X, + Height = screen_height, + } + } } - } + }, + expandedPanelProxyContainer = new Container { RelativeSizeAxes = Axes.Both } } } }, @@ -173,6 +194,21 @@ namespace osu.Game.Screens.Ranking /// An responsible for the fetch operation. This will be queued and performed automatically. protected virtual APIRequest FetchScores(Action> scoresCallback) => null; + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + ScorePanel expandedPanel = scorePanelContainer.Single(p => p.State == PanelState.Expanded); + expandedPanel.Tracking = false; + expandedPanel.Anchor = Anchor.Centre; + expandedPanel.Origin = Anchor.Centre; + + scorePanelContainer.X = (float)Interpolation.Lerp(0, -DrawWidth / 2 + ScorePanel.EXPANDED_WIDTH / 2f, Math.Clamp(scrollContainer.Current / (screen_height * 0.8f), 0, 1)); + + if (expandedPanelProxyContainer.Count == 0) + expandedPanelProxyContainer.Add(expandedPanel.CreateProxy()); + } + public override void OnEntering(IScreen last) { base.OnEntering(last); @@ -205,22 +241,21 @@ namespace osu.Game.Screens.Ranking { // If the user is scrolling via mouse drag, follow the mouse 1:1. base.OnUserScroll(value, false, distanceDecay); + return; + } + + float direction = Math.Sign(value - Target); + float target = Target + direction * screen_height; + + if (target <= -screen_height / 2 || target >= ScrollableExtent + screen_height / 2) + { + // If the user is already at either extent and scrolling in the clamped direction, we want to follow the default scroll exactly so that the bounces aren't too harsh. + base.OnUserScroll(value, true, distanceDecay); } else { - float direction = Math.Sign(value - Target); - float target = Target + direction * screen_height; - - if (target <= -screen_height / 2 || target >= ScrollableExtent + screen_height / 2) - { - // If the user is already at either extent and scrolling in the clamped direction, we want to follow the default scroll exactly so that the bounces aren't too harsh. - base.OnUserScroll(value, true, distanceDecay); - } - else - { - // Otherwise, scroll one screen in the target direction. - base.OnUserScroll(target, true, distanceDecay); - } + // Otherwise, scroll one screen in the target direction. + base.OnUserScroll(target, true, distanceDecay); } } } diff --git a/osu.Game/Screens/Ranking/ScorePanel.cs b/osu.Game/Screens/Ranking/ScorePanel.cs index 65fb901c89..7ca96a9a58 100644 --- a/osu.Game/Screens/Ranking/ScorePanel.cs +++ b/osu.Game/Screens/Ranking/ScorePanel.cs @@ -182,6 +182,40 @@ namespace osu.Game.Screens.Ranking } } + private bool tracking; + private Vector2 lastNonTrackingPosition; + + /// + /// Whether this should track the position of the tracking component created via . + /// + public bool Tracking + { + get => tracking; + set + { + if (tracking == value) + return; + + tracking = value; + + if (tracking) + lastNonTrackingPosition = Position; + else + Position = lastNonTrackingPosition; + } + } + + protected override void Update() + { + base.Update(); + + if (Tracking && trackingComponent != null) + { + Vector2 topLeftPos = Parent.ToLocalSpace(trackingComponent.ScreenSpaceDrawQuad.TopLeft); + Position = topLeftPos - AnchorPosition + OriginPosition; + } + } + private void updateState() { topLayerContent?.FadeOut(content_fade_duration).Expire(); @@ -248,5 +282,31 @@ namespace osu.Game.Screens.Ranking => base.ReceivePositionalInputAt(screenSpacePos) || topLayerContainer.ReceivePositionalInputAt(screenSpacePos) || middleLayerContainer.ReceivePositionalInputAt(screenSpacePos); + + private TrackingComponent trackingComponent; + + public TrackingComponent CreateTrackingComponent() => trackingComponent ??= new TrackingComponent(this); + + public class TrackingComponent : Drawable + { + public readonly ScorePanel Panel; + + public TrackingComponent(ScorePanel panel) + { + Panel = panel; + } + + protected override void Update() + { + base.Update(); + Size = Panel.DrawSize; + } + + // In ScorePanelList, score panels are added _before_ the flow, but this means that input will be blocked by the scroll container. + // So by forwarding input events, we remove the need to consider the order in which input is handled. + protected override bool OnClick(ClickEvent e) => Panel.TriggerEvent(e); + + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Panel.ReceivePositionalInputAt(screenSpacePos); + } } } diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index 1142297274..d49085bc96 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -27,11 +28,20 @@ namespace osu.Game.Screens.Ranking public readonly Bindable SelectedScore = new Bindable(); + private readonly Container panels; private readonly Flow flow; private readonly Scroll scroll; private ScorePanel expandedPanel; - public ScorePanelList() + /// + /// Creates a new . + /// + /// The target container in which s should reside. + /// s are set to track by default, but this allows + /// This should be placed _before_ the in the hierarchy. + /// + /// + public ScorePanelList(Container panelTarget = null) { RelativeSizeAxes = Axes.Both; @@ -47,6 +57,18 @@ namespace osu.Game.Screens.Ranking AutoSizeAxes = Axes.Both, } }; + + if (panelTarget == null) + { + // To prevent 1-frame sizing issues, the panel container is added _before_ the scroll + flow containers + AddInternal(panels = new Container + { + RelativeSizeAxes = Axes.Both, + Depth = 1 + }); + } + else + panels = panelTarget; } protected override void LoadComplete() @@ -62,10 +84,9 @@ namespace osu.Game.Screens.Ranking /// The to add. public void AddScore(ScoreInfo score) { - flow.Add(new ScorePanel(score) + var panel = new ScorePanel(score) { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, + Tracking = true }.With(p => { p.StateChanged += s => @@ -73,6 +94,13 @@ namespace osu.Game.Screens.Ranking if (s == PanelState.Expanded) SelectedScore.Value = p.Score; }; + }); + + panels.Add(panel); + flow.Add(panel.CreateTrackingComponent().With(d => + { + d.Anchor = Anchor.Centre; + d.Origin = Anchor.Centre; })); if (SelectedScore.Value == score) @@ -99,14 +127,15 @@ namespace osu.Game.Screens.Ranking private void selectedScoreChanged(ValueChangedEvent score) { // Contract the old panel. - foreach (var p in flow.Where(p => p.Score == score.OldValue)) + foreach (var t in flow.Where(t => t.Panel.Score == score.OldValue)) { - p.State = PanelState.Contracted; - p.Margin = new MarginPadding(); + t.Panel.State = PanelState.Contracted; + t.Margin = new MarginPadding(); } // Find the panel corresponding to the new score. - expandedPanel = flow.SingleOrDefault(p => p.Score == score.NewValue); + var expandedTrackingComponent = flow.SingleOrDefault(t => t.Panel.Score == score.NewValue); + expandedPanel = expandedTrackingComponent?.Panel; // handle horizontal scroll only when not hovering the expanded panel. scroll.HandleScroll = () => expandedPanel?.IsHovered != true; @@ -114,9 +143,11 @@ namespace osu.Game.Screens.Ranking if (expandedPanel == null) return; + Debug.Assert(expandedTrackingComponent != null); + // Expand the new panel. + expandedTrackingComponent.Margin = new MarginPadding { Horizontal = expanded_panel_spacing }; expandedPanel.State = PanelState.Expanded; - expandedPanel.Margin = new MarginPadding { Horizontal = expanded_panel_spacing }; // Scroll to the new panel. This is done manually since we need: // 1) To scroll after the scroll container's visible range is updated. @@ -145,15 +176,15 @@ namespace osu.Game.Screens.Ranking flow.Padding = new MarginPadding { Horizontal = offset }; } - private class Flow : FillFlowContainer + private class Flow : FillFlowContainer { public override IEnumerable FlowingChildren => applySorting(AliveInternalChildren); - public int GetPanelIndex(ScoreInfo score) => applySorting(Children).TakeWhile(s => s.Score != score).Count(); + public int GetPanelIndex(ScoreInfo score) => applySorting(Children).TakeWhile(s => s.Panel.Score != score).Count(); - private IEnumerable applySorting(IEnumerable drawables) => drawables.OfType() - .OrderByDescending(s => s.Score.TotalScore) - .ThenBy(s => s.Score.OnlineScoreID); + private IEnumerable applySorting(IEnumerable drawables) => drawables.OfType() + .OrderByDescending(s => s.Panel.Score.TotalScore) + .ThenBy(s => s.Panel.Score.OnlineScoreID); } private class Scroll : OsuScrollContainer diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 6c5fa1837a..bae6d0ffbb 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -26,13 +26,6 @@ namespace osu.Game.Screens.Ranking.Statistics RelativeSizeAxes = Axes.Both, Colour = Color4Extensions.FromHex("#333") }, - new ScorePanel(score) // Todo: Temporary - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - State = PanelState.Expanded, - X = 30 - }, new Container { RelativeSizeAxes = Axes.Both, From bed5e857df7d2af44ae5f4bbfa304f04be741da1 Mon Sep 17 00:00:00 2001 From: Ronnie Moir <7267697+H2n9@users.noreply.github.com> Date: Wed, 17 Jun 2020 14:49:55 +0100 Subject: [PATCH 2103/2376] Add missing license header and remove unused usings --- osu.Game/Rulesets/Mods/IApplicableToAudio.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Mods/IApplicableToAudio.cs b/osu.Game/Rulesets/Mods/IApplicableToAudio.cs index 40e13764c6..901da7af55 100644 --- a/osu.Game/Rulesets/Mods/IApplicableToAudio.cs +++ b/osu.Game/Rulesets/Mods/IApplicableToAudio.cs @@ -1,6 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Text; +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. namespace osu.Game.Rulesets.Mods { From 69d85ca3aeab18758ad644e1592d99a83fe35506 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 18 Jun 2020 13:20:16 +0900 Subject: [PATCH 2104/2376] Add more cards to results screen test --- .../Visual/Ranking/TestSceneResultsScreen.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 125aa0a1e7..ea33aa62e3 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -8,6 +10,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Game.Beatmaps; +using osu.Game.Online.API; using osu.Game.Rulesets.Osu; using osu.Game.Scoring; using osu.Game.Screens; @@ -113,6 +116,22 @@ namespace osu.Game.Tests.Visual.Ranking RetryOverlay = InternalChildren.OfType().SingleOrDefault(); } + + protected override APIRequest FetchScores(Action> scoresCallback) + { + var scores = new List(); + + for (int i = 0; i < 20; i++) + { + var score = new TestScoreInfo(new OsuRuleset().RulesetInfo); + score.TotalScore += 10 - i; + scores.Add(score); + } + + scoresCallback?.Invoke(scores); + + return null; + } } private class UnrankedSoloResultsScreen : SoloResultsScreen From c31a05977d7b62a81e146de50ab374c8e2cca0d1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 18 Jun 2020 16:50:45 +0900 Subject: [PATCH 2105/2376] Re-implement statistics as a click-in panel --- osu.Game/Screens/Ranking/ResultsScreen.cs | 109 +++++++----------- osu.Game/Screens/Ranking/ScorePanel.cs | 74 ++++++------ osu.Game/Screens/Ranking/ScorePanelList.cs | 67 ++++++----- .../Ranking/Statistics/StatisticsPanel.cs | 34 +++--- 4 files changed, 135 insertions(+), 149 deletions(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 4ef012f6f2..927628a811 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -11,7 +10,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Screens; -using osu.Framework.Utils; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; @@ -46,11 +44,9 @@ namespace osu.Game.Screens.Ranking [Resolved] private IAPIProvider api { get; set; } - private Container scorePanelContainer; - private ResultsScrollContainer scrollContainer; - private Container expandedPanelProxyContainer; + private StatisticsPanel statisticsPanel; private Drawable bottomPanel; - private ScorePanelList panels; + private ScorePanelList scorePanelList; protected ResultsScreen(ScoreInfo score, bool allowRetry = true) { @@ -63,13 +59,6 @@ namespace osu.Game.Screens.Ranking [BackgroundDependencyLoader] private void load() { - scorePanelContainer = new Container - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - }; - FillFlowContainer buttons; InternalChild = new GridContainer @@ -84,30 +73,26 @@ namespace osu.Game.Screens.Ranking RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - scorePanelContainer, - scrollContainer = new ResultsScrollContainer + new OsuScrollContainer { - Child = new FillFlowContainer + RelativeSizeAxes = Axes.Both, + ScrollbarVisible = false, + Child = new Container { RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, + Height = screen_height, Children = new Drawable[] { - panels = new ScorePanelList(scorePanelContainer) + scorePanelList = new ScorePanelList { - RelativeSizeAxes = Axes.X, - Height = screen_height, - SelectedScore = { BindTarget = SelectedScore } + RelativeSizeAxes = Axes.Both, + SelectedScore = { BindTarget = SelectedScore }, + PostExpandAction = onExpandedPanelClicked }, - new StatisticsPanel(Score) - { - RelativeSizeAxes = Axes.X, - Height = screen_height, - } + statisticsPanel = new StatisticsPanel(Score) { RelativeSizeAxes = Axes.Both } } } }, - expandedPanelProxyContainer = new Container { RelativeSizeAxes = Axes.Both } } } }, @@ -155,7 +140,7 @@ namespace osu.Game.Screens.Ranking }; if (Score != null) - panels.AddScore(Score); + scorePanelList.AddScore(Score); if (player != null && allowRetry) { @@ -180,7 +165,7 @@ namespace osu.Game.Screens.Ranking var req = FetchScores(scores => Schedule(() => { foreach (var s in scores) - panels.AddScore(s); + scorePanelList.AddScore(s); })); if (req != null) @@ -194,21 +179,6 @@ namespace osu.Game.Screens.Ranking /// An responsible for the fetch operation. This will be queued and performed automatically. protected virtual APIRequest FetchScores(Action> scoresCallback) => null; - protected override void UpdateAfterChildren() - { - base.UpdateAfterChildren(); - - ScorePanel expandedPanel = scorePanelContainer.Single(p => p.State == PanelState.Expanded); - expandedPanel.Tracking = false; - expandedPanel.Anchor = Anchor.Centre; - expandedPanel.Origin = Anchor.Centre; - - scorePanelContainer.X = (float)Interpolation.Lerp(0, -DrawWidth / 2 + ScorePanel.EXPANDED_WIDTH / 2f, Math.Clamp(scrollContainer.Current / (screen_height * 0.8f), 0, 1)); - - if (expandedPanelProxyContainer.Count == 0) - expandedPanelProxyContainer.Add(expandedPanel.CreateProxy()); - } - public override void OnEntering(IScreen last) { base.OnEntering(last); @@ -226,36 +196,39 @@ namespace osu.Game.Screens.Ranking return base.OnExiting(next); } - [Cached] - private class ResultsScrollContainer : OsuScrollContainer + private void onExpandedPanelClicked() { - public ResultsScrollContainer() + statisticsPanel.ToggleVisibility(); + + if (statisticsPanel.State.Value == Visibility.Hidden) { - RelativeSizeAxes = Axes.Both; - ScrollbarVisible = false; + foreach (var panel in scorePanelList.Panels) + { + if (panel.State == PanelState.Contracted) + panel.FadeIn(150); + else + { + panel.MoveTo(panel.GetTrackingPosition(), 150, Easing.OutQuint).OnComplete(p => + { + scorePanelList.HandleScroll = true; + p.Tracking = true; + }); + } + } } - - protected override void OnUserScroll(float value, bool animated = true, double? distanceDecay = default) + else { - if (!animated) + foreach (var panel in scorePanelList.Panels) { - // If the user is scrolling via mouse drag, follow the mouse 1:1. - base.OnUserScroll(value, false, distanceDecay); - return; - } + if (panel.State == PanelState.Contracted) + panel.FadeOut(150, Easing.OutQuint); + else + { + scorePanelList.HandleScroll = false; - float direction = Math.Sign(value - Target); - float target = Target + direction * screen_height; - - if (target <= -screen_height / 2 || target >= ScrollableExtent + screen_height / 2) - { - // If the user is already at either extent and scrolling in the clamped direction, we want to follow the default scroll exactly so that the bounces aren't too harsh. - base.OnUserScroll(value, true, distanceDecay); - } - else - { - // Otherwise, scroll one screen in the target direction. - base.OnUserScroll(target, true, distanceDecay); + panel.Tracking = false; + panel.MoveTo(new Vector2(scorePanelList.CurrentScrollPosition, panel.GetTrackingPosition().Y), 150, Easing.OutQuint); + } } } } diff --git a/osu.Game/Screens/Ranking/ScorePanel.cs b/osu.Game/Screens/Ranking/ScorePanel.cs index 7ca96a9a58..31b2796c13 100644 --- a/osu.Game/Screens/Ranking/ScorePanel.cs +++ b/osu.Game/Screens/Ranking/ScorePanel.cs @@ -76,6 +76,18 @@ namespace osu.Game.Screens.Ranking private static readonly Color4 contracted_middle_layer_colour = Color4Extensions.FromHex("#353535"); public event Action StateChanged; + public Action PostExpandAction; + + /// + /// Whether this should track the position of the tracking component created via . + /// + public bool Tracking; + + /// + /// Whether this can enter into an state. + /// + public bool CanExpand = true; + public readonly ScoreInfo Score; private Container content; @@ -182,38 +194,18 @@ namespace osu.Game.Screens.Ranking } } - private bool tracking; - private Vector2 lastNonTrackingPosition; - - /// - /// Whether this should track the position of the tracking component created via . - /// - public bool Tracking - { - get => tracking; - set - { - if (tracking == value) - return; - - tracking = value; - - if (tracking) - lastNonTrackingPosition = Position; - else - Position = lastNonTrackingPosition; - } - } - protected override void Update() { base.Update(); if (Tracking && trackingComponent != null) - { - Vector2 topLeftPos = Parent.ToLocalSpace(trackingComponent.ScreenSpaceDrawQuad.TopLeft); - Position = topLeftPos - AnchorPosition + OriginPosition; - } + Position = GetTrackingPosition(); + } + + public Vector2 GetTrackingPosition() + { + Vector2 topLeftPos = Parent.ToLocalSpace(trackingComponent.ScreenSpaceDrawQuad.TopLeft); + return topLeftPos - AnchorPosition + OriginPosition; } private void updateState() @@ -270,10 +262,28 @@ namespace osu.Game.Screens.Ranking } } + public override Vector2 Size + { + get => base.Size; + set + { + base.Size = value; + + if (trackingComponent != null) + trackingComponent.Size = value; + } + } + protected override bool OnClick(ClickEvent e) { if (State == PanelState.Contracted) - State = PanelState.Expanded; + { + if (CanExpand) + State = PanelState.Expanded; + return true; + } + + PostExpandAction?.Invoke(); return true; } @@ -296,17 +306,13 @@ namespace osu.Game.Screens.Ranking Panel = panel; } - protected override void Update() - { - base.Update(); - Size = Panel.DrawSize; - } - // In ScorePanelList, score panels are added _before_ the flow, but this means that input will be blocked by the scroll container. // So by forwarding input events, we remove the need to consider the order in which input is handled. protected override bool OnClick(ClickEvent e) => Panel.TriggerEvent(e); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Panel.ReceivePositionalInputAt(screenSpacePos); + + public override bool IsPresent => Panel.IsPresent; } } } diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index d49085bc96..e332f462bb 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -26,9 +26,15 @@ namespace osu.Game.Screens.Ranking /// private const float expanded_panel_spacing = 15; + public Action PostExpandAction; + public readonly Bindable SelectedScore = new Bindable(); + public float CurrentScrollPosition => scroll.Current; + + public IReadOnlyList Panels => panels; private readonly Container panels; + private readonly Flow flow; private readonly Scroll scroll; private ScorePanel expandedPanel; @@ -36,39 +42,27 @@ namespace osu.Game.Screens.Ranking /// /// Creates a new . /// - /// The target container in which s should reside. - /// s are set to track by default, but this allows - /// This should be placed _before_ the in the hierarchy. - /// - /// - public ScorePanelList(Container panelTarget = null) + public ScorePanelList() { RelativeSizeAxes = Axes.Both; InternalChild = scroll = new Scroll { RelativeSizeAxes = Axes.Both, - Child = flow = new Flow + HandleScroll = () => HandleScroll && expandedPanel?.IsHovered != true, // handle horizontal scroll only when not hovering the expanded panel. + Children = new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(panel_spacing, 0), - AutoSizeAxes = Axes.Both, + panels = new Container { RelativeSizeAxes = Axes.Both }, + flow = new Flow + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(panel_spacing, 0), + AutoSizeAxes = Axes.Both, + }, } }; - - if (panelTarget == null) - { - // To prevent 1-frame sizing issues, the panel container is added _before_ the scroll + flow containers - AddInternal(panels = new Container - { - RelativeSizeAxes = Axes.Both, - Depth = 1 - }); - } - else - panels = panelTarget; } protected override void LoadComplete() @@ -78,6 +72,25 @@ namespace osu.Game.Screens.Ranking SelectedScore.BindValueChanged(selectedScoreChanged, true); } + private bool handleScroll = true; + + public bool HandleScroll + { + get => handleScroll; + set + { + handleScroll = value; + + foreach (var p in panels) + p.CanExpand = value; + + scroll.ScrollbarVisible = value; + + if (!value) + scroll.ScrollTo(CurrentScrollPosition, false); + } + } + /// /// Adds a to this list. /// @@ -86,7 +99,8 @@ namespace osu.Game.Screens.Ranking { var panel = new ScorePanel(score) { - Tracking = true + Tracking = true, + PostExpandAction = () => PostExpandAction?.Invoke() }.With(p => { p.StateChanged += s => @@ -137,9 +151,6 @@ namespace osu.Game.Screens.Ranking var expandedTrackingComponent = flow.SingleOrDefault(t => t.Panel.Score == score.NewValue); expandedPanel = expandedTrackingComponent?.Panel; - // handle horizontal scroll only when not hovering the expanded panel. - scroll.HandleScroll = () => expandedPanel?.IsHovered != true; - if (expandedPanel == null) return; diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index bae6d0ffbb..cc9007f527 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -1,17 +1,17 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; using osu.Game.Scoring; using osuTK; namespace osu.Game.Screens.Ranking.Statistics { - public class StatisticsPanel : CompositeDrawable + public class StatisticsPanel : VisibilityContainer { + protected override bool StartHidden => true; + public StatisticsPanel(ScoreInfo score) { // Todo: Not correct. @@ -19,27 +19,19 @@ namespace osu.Game.Screens.Ranking.Statistics FillFlowContainer statisticRows; - InternalChildren = new Drawable[] + InternalChild = new Container { - new Box + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { - RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex("#333") + Left = ScorePanel.EXPANDED_WIDTH + 30 + 50, + Right = 50 }, - new Container + Child = statisticRows = new FillFlowContainer { RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding - { - Left = ScorePanel.EXPANDED_WIDTH + 30 + 50, - Right = 50 - }, - Child = statisticRows = new FillFlowContainer - { - RelativeSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Spacing = new Vector2(30, 15), - } + Direction = FillDirection.Vertical, + Spacing = new Vector2(30, 15), } }; @@ -55,5 +47,9 @@ namespace osu.Game.Screens.Ranking.Statistics }); } } + + protected override void PopIn() => this.FadeIn(); + + protected override void PopOut() => this.FadeOut(); } } From 6c8a24260bd4edd511b9284db59fe70e12f97347 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 18 Jun 2020 17:06:05 +0900 Subject: [PATCH 2106/2376] Add padding --- osu.Game/Screens/Ranking/ResultsScreen.cs | 2 +- osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 927628a811..4a7cb6679a 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -227,7 +227,7 @@ namespace osu.Game.Screens.Ranking scorePanelList.HandleScroll = false; panel.Tracking = false; - panel.MoveTo(new Vector2(scorePanelList.CurrentScrollPosition, panel.GetTrackingPosition().Y), 150, Easing.OutQuint); + panel.MoveTo(new Vector2(scorePanelList.CurrentScrollPosition + StatisticsPanel.SIDE_PADDING, panel.GetTrackingPosition().Y), 150, Easing.OutQuint); } } } diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index cc9007f527..733c855426 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -10,6 +10,8 @@ namespace osu.Game.Screens.Ranking.Statistics { public class StatisticsPanel : VisibilityContainer { + public const float SIDE_PADDING = 30; + protected override bool StartHidden => true; public StatisticsPanel(ScoreInfo score) @@ -24,8 +26,10 @@ namespace osu.Game.Screens.Ranking.Statistics RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { - Left = ScorePanel.EXPANDED_WIDTH + 30 + 50, - Right = 50 + Left = ScorePanel.EXPANDED_WIDTH + SIDE_PADDING * 3, + Right = SIDE_PADDING, + Top = SIDE_PADDING, + Bottom = 50 // Approximate padding to the bottom of the score panel. }, Child = statisticRows = new FillFlowContainer { From 20db5b33abc952390f58a9110266f55f5377fc51 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 18 Jun 2020 22:11:03 +0900 Subject: [PATCH 2107/2376] Rework score processor to provide more generic events --- osu.Game.Rulesets.Osu/OsuRuleset.cs | 5 +- .../Scoring/OsuScoreProcessor.cs | 128 +++++++----------- osu.Game.Rulesets.Osu/Statistics/Heatmap.cs | 24 +++- .../Statistics/TimingDistributionGraph.cs | 45 ++++-- .../Ranking/TestSceneAccuracyHeatmap.cs | 9 +- .../Ranking/TestSceneStatisticsPanel.cs | 9 +- .../TestSceneTimingDistributionGraph.cs | 34 ++--- osu.Game/Scoring/ScoreInfo.cs | 4 +- 8 files changed, 126 insertions(+), 132 deletions(-) diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 67a9bda1a9..c7003deed2 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -29,6 +29,7 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Skinning; using System; +using System.Linq; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Osu.Statistics; using osu.Game.Screens.Ranking.Statistics; @@ -200,7 +201,7 @@ namespace osu.Game.Rulesets.Osu { RelativeSizeAxes = Axes.X, Height = 130, - Child = new TimingDistributionGraph((TimingDistribution)score.ExtraStatistics.GetValueOrDefault("timing_distribution")) + Child = new TimingDistributionGraph(score.HitEvents.Cast().ToList()) { RelativeSizeAxes = Axes.Both } @@ -208,7 +209,7 @@ namespace osu.Game.Rulesets.Osu new StatisticContainer("Accuracy Heatmap") { RelativeSizeAxes = Axes.Both, - Child = new Heatmap((List)score.ExtraStatistics.GetValueOrDefault("hit_offsets")) + Child = new Heatmap(score.Beatmap, score.HitEvents.Cast().ToList()) { RelativeSizeAxes = Axes.Both } diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs index 97be372e37..9694367210 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs @@ -1,120 +1,52 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; -using System.Diagnostics; -using osu.Game.Beatmaps; +using System.Linq; +using JetBrains.Annotations; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; +using osuTK; namespace osu.Game.Rulesets.Osu.Scoring { public class OsuScoreProcessor : ScoreProcessor { - /// - /// The number of bins on each side of the timing distribution. - /// - private const int timing_distribution_bins = 25; - - /// - /// The total number of bins in the timing distribution, including bins on both sides and the centre bin at 0. - /// - private const int total_timing_distribution_bins = timing_distribution_bins * 2 + 1; - - /// - /// The centre bin, with a timing distribution very close to/at 0. - /// - private const int timing_distribution_centre_bin_index = timing_distribution_bins; - - private TimingDistribution timingDistribution; - private readonly List hitOffsets = new List(); - - public override void ApplyBeatmap(IBeatmap beatmap) - { - var hitWindows = CreateHitWindows(); - hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty); - - timingDistribution = new TimingDistribution(total_timing_distribution_bins, hitWindows.WindowFor(hitWindows.LowestSuccessfulHitResult()) / timing_distribution_bins); - - base.ApplyBeatmap(beatmap); - } - - private OsuHitCircleJudgementResult lastCircleResult; + private readonly List hitEvents = new List(); + private HitObject lastHitObject; protected override void OnResultApplied(JudgementResult result) { base.OnResultApplied(result); - if (result.IsHit) - { - int binOffset = (int)(result.TimeOffset / timingDistribution.BinSize); - timingDistribution.Bins[timing_distribution_centre_bin_index + binOffset]++; - - addHitOffset(result); - } + hitEvents.Add(new HitEvent(result.TimeOffset, result.Type, result.HitObject, lastHitObject, (result as OsuHitCircleJudgementResult)?.HitPosition)); + lastHitObject = result.HitObject; } protected override void OnResultReverted(JudgementResult result) { base.OnResultReverted(result); - if (result.IsHit) - { - int binOffset = (int)(result.TimeOffset / timingDistribution.BinSize); - timingDistribution.Bins[timing_distribution_centre_bin_index + binOffset]--; - - removeHitOffset(result); - } - } - - private void addHitOffset(JudgementResult result) - { - if (!(result is OsuHitCircleJudgementResult circleResult)) - return; - - if (lastCircleResult == null) - { - lastCircleResult = circleResult; - return; - } - - if (circleResult.HitPosition != null) - { - Debug.Assert(circleResult.Radius != null); - hitOffsets.Add(new HitOffset(lastCircleResult.HitCircle.StackedEndPosition, circleResult.HitCircle.StackedEndPosition, circleResult.HitPosition.Value, circleResult.Radius.Value)); - } - - lastCircleResult = circleResult; - } - - private void removeHitOffset(JudgementResult result) - { - if (!(result is OsuHitCircleJudgementResult circleResult)) - return; - - if (hitOffsets.Count > 0 && circleResult.HitPosition != null) - hitOffsets.RemoveAt(hitOffsets.Count - 1); + hitEvents.RemoveAt(hitEvents.Count - 1); } protected override void Reset(bool storeResults) { base.Reset(storeResults); - timingDistribution.Bins.AsSpan().Clear(); - hitOffsets.Clear(); + hitEvents.Clear(); + lastHitObject = null; } public override void PopulateScore(ScoreInfo score) { base.PopulateScore(score); - score.ExtraStatistics["timing_distribution"] = timingDistribution; - score.ExtraStatistics["hit_offsets"] = hitOffsets; + score.HitEvents.AddRange(hitEvents.Select(e => e).Cast()); } protected override JudgementResult CreateResult(HitObject hitObject, Judgement judgement) @@ -131,4 +63,42 @@ namespace osu.Game.Rulesets.Osu.Scoring public override HitWindows CreateHitWindows() => new OsuHitWindows(); } + + public readonly struct HitEvent + { + /// + /// The time offset from the end of at which the event occurred. + /// + public readonly double TimeOffset; + + /// + /// The hit result. + /// + public readonly HitResult Result; + + /// + /// The on which the result occurred. + /// + public readonly HitObject HitObject; + + /// + /// The occurring prior to . + /// + [CanBeNull] + public readonly HitObject LastHitObject; + + /// + /// The player's cursor position, if available, at the time of the event. + /// + public readonly Vector2? CursorPosition; + + public HitEvent(double timeOffset, HitResult result, HitObject hitObject, [CanBeNull] HitObject lastHitObject, Vector2? cursorPosition) + { + TimeOffset = timeOffset; + Result = result; + HitObject = hitObject; + LastHitObject = lastHitObject; + CursorPosition = cursorPosition; + } + } } diff --git a/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs b/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs index 51508a5e8b..95cfc5b768 100644 --- a/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs @@ -10,6 +10,8 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Layout; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Scoring; using osuTK; using osuTK.Graphics; @@ -27,14 +29,16 @@ namespace osu.Game.Rulesets.Osu.Statistics private const float rotation = 45; private const float point_size = 4; - private readonly IReadOnlyList offsets; private Container allPoints; + private readonly BeatmapInfo beatmap; + private readonly IReadOnlyList hitEvents; private readonly LayoutValue sizeLayout = new LayoutValue(Invalidation.DrawSize); - public Heatmap(IReadOnlyList offsets) + public Heatmap(BeatmapInfo beatmap, IReadOnlyList hitEvents) { - this.offsets = offsets; + this.beatmap = beatmap; + this.hitEvents = hitEvents; AddLayout(sizeLayout); } @@ -153,10 +157,18 @@ namespace osu.Game.Rulesets.Osu.Statistics } } - if (offsets?.Count > 0) + if (hitEvents.Count > 0) { - foreach (var o in offsets) - AddPoint(o.Position1, o.Position2, o.HitPosition, o.Radius); + // Todo: This should probably not be done like this. + float radius = OsuHitObject.OBJECT_RADIUS * (1.0f - 0.7f * (beatmap.BaseDifficulty.CircleSize - 5) / 5) / 2; + + foreach (var e in hitEvents) + { + if (e.LastHitObject == null || e.CursorPosition == null) + continue; + + AddPoint(((OsuHitObject)e.LastHitObject).StackedEndPosition, ((OsuHitObject)e.HitObject).StackedEndPosition, e.CursorPosition.Value, radius); + } } sizeLayout.Validate(); diff --git a/osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs b/osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs index 1f9f38bf3b..b319cc5aa9 100644 --- a/osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs +++ b/osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; @@ -15,29 +17,52 @@ namespace osu.Game.Rulesets.Osu.Statistics { public class TimingDistributionGraph : CompositeDrawable { + /// + /// The number of bins on each side of the timing distribution. + /// + private const int timing_distribution_bins = 25; + + /// + /// The total number of bins in the timing distribution, including bins on both sides and the centre bin at 0. + /// + private const int total_timing_distribution_bins = timing_distribution_bins * 2 + 1; + + /// + /// The centre bin, with a timing distribution very close to/at 0. + /// + private const int timing_distribution_centre_bin_index = timing_distribution_bins; + /// /// The number of data points shown on the axis below the graph. /// private const float axis_points = 5; - private readonly TimingDistribution distribution; + private readonly List hitEvents; - public TimingDistributionGraph(TimingDistribution distribution) + public TimingDistributionGraph(List hitEvents) { - this.distribution = distribution; + this.hitEvents = hitEvents; } [BackgroundDependencyLoader] private void load() { - if (distribution?.Bins == null || distribution.Bins.Length == 0) + if (hitEvents.Count == 0) return; - int maxCount = distribution.Bins.Max(); + int[] bins = new int[total_timing_distribution_bins]; + double binSize = hitEvents.Max(e => Math.Abs(e.TimeOffset)) / timing_distribution_bins; - var bars = new Drawable[distribution.Bins.Length]; + foreach (var e in hitEvents) + { + int binOffset = (int)(e.TimeOffset / binSize); + bins[timing_distribution_centre_bin_index + binOffset]++; + } + + int maxCount = bins.Max(); + var bars = new Drawable[total_timing_distribution_bins]; for (int i = 0; i < bars.Length; i++) - bars[i] = new Bar { Height = (float)distribution.Bins[i] / maxCount }; + bars[i] = new Bar { Height = (float)bins[i] / maxCount }; Container axisFlow; @@ -71,10 +96,8 @@ namespace osu.Game.Rulesets.Osu.Statistics } }; - // We know the total number of bins on each side of the centre ((n - 1) / 2), and the size of each bin. - // So our axis will contain one centre element + 5 points on each side, each with a value depending on the number of bins * bin size. - int sideBins = (distribution.Bins.Length - 1) / 2; - double maxValue = sideBins * distribution.BinSize; + // Our axis will contain one centre element + 5 points on each side, each with a value depending on the number of bins * bin size. + double maxValue = timing_distribution_bins * binSize; double axisValueStep = maxValue / axis_points; axisFlow.Add(new OsuSpriteText diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs index 53c8e56f53..ba6a0e42c2 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs @@ -8,8 +8,11 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Osu.Statistics; +using osu.Game.Tests.Beatmaps; using osuTK; using osuTK.Graphics; @@ -40,7 +43,7 @@ namespace osu.Game.Tests.Visual.Ranking { Position = new Vector2(500, 300), }, - heatmap = new TestHeatmap(new List()) + heatmap = new TestHeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, new List()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -73,8 +76,8 @@ namespace osu.Game.Tests.Visual.Ranking private class TestHeatmap : Heatmap { - public TestHeatmap(IReadOnlyList offsets) - : base(offsets) + public TestHeatmap(BeatmapInfo beatmap, List events) + : base(beatmap, events) { } diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index c02be9ab5d..faabdf2cb6 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -1,10 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; +using System.Linq; using NUnit.Framework; using osu.Game.Rulesets.Osu; -using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Ranking.Statistics; @@ -17,11 +16,7 @@ namespace osu.Game.Tests.Visual.Ranking { var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { - ExtraStatistics = - { - ["timing_distribution"] = TestSceneTimingDistributionGraph.CreateNormalDistribution(), - ["hit_offsets"] = new List() - } + HitEvents = TestSceneTimingDistributionGraph.CreateDistributedHitEvents().Cast().ToList(), }; loadPanel(score); diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs b/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs index 2249655093..178d6d95b5 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs @@ -1,12 +1,15 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Osu.Statistics; +using osu.Game.Rulesets.Scoring; using osuTK; namespace osu.Game.Tests.Visual.Ranking @@ -22,7 +25,7 @@ namespace osu.Game.Tests.Visual.Ranking RelativeSizeAxes = Axes.Both, Colour = Color4Extensions.FromHex("#333") }, - new TimingDistributionGraph(CreateNormalDistribution()) + new TimingDistributionGraph(CreateDistributedHitEvents()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -31,34 +34,19 @@ namespace osu.Game.Tests.Visual.Ranking }; } - public static TimingDistribution CreateNormalDistribution() + public static List CreateDistributedHitEvents() { - var distribution = new TimingDistribution(51, 5); + var hitEvents = new List(); - // We create an approximately-normal distribution of 51 elements by using the 13th binomial row (14 initial elements) and subdividing the inner values twice. - var row = new List { 1 }; - for (int i = 0; i < 13; i++) - row.Add(row[i] * (13 - i) / (i + 1)); - - // Each subdivision yields 2n-1 total elements, so first subdivision will contain 27 elements, and the second will contain 53 elements. - for (int div = 0; div < 2; div++) + for (int i = 0; i < 50; i++) { - var newRow = new List { 1 }; + int count = (int)(Math.Pow(25 - Math.Abs(i - 25), 2)); - for (int i = 0; i < row.Count - 1; i++) - { - newRow.Add((row[i] + row[i + 1]) / 2); - newRow.Add(row[i + 1]); - } - - row = newRow; + for (int j = 0; j < count; j++) + hitEvents.Add(new HitEvent(i - 25, HitResult.Perfect, new HitCircle(), new HitCircle(), null)); } - // After the subdivisions take place, we're left with 53 values which we use the inner 51 of. - for (int i = 1; i < row.Count - 1; i++) - distribution.Bins[i - 1] = row[i]; - - return distribution; + return hitEvents; } } } diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 38b37afc55..6fc5892b3c 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -166,7 +166,9 @@ namespace osu.Game.Scoring } } - public Dictionary ExtraStatistics = new Dictionary(); + [NotMapped] + [JsonIgnore] + public List HitEvents = new List(); [JsonIgnore] public List Files { get; set; } From ecdfcb1955f4929bc11fe1e3d1e8e1ddadfbd119 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 18 Jun 2020 22:21:30 +0900 Subject: [PATCH 2108/2376] Display placeholder if no statistics available --- .../Ranking/TestSceneStatisticsPanel.cs | 17 +++++- osu.Game/Screens/Ranking/ResultsScreen.cs | 6 +- .../Ranking/Statistics/StatisticsPanel.cs | 61 +++++++++++++------ 3 files changed, 63 insertions(+), 21 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index faabdf2cb6..cc3415a530 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -3,6 +3,8 @@ using System.Linq; using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Osu; using osu.Game.Scoring; using osu.Game.Screens.Ranking.Statistics; @@ -12,7 +14,7 @@ namespace osu.Game.Tests.Visual.Ranking public class TestSceneStatisticsPanel : OsuTestScene { [Test] - public void TestScore() + public void TestScoreWithStatistics() { var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { @@ -22,9 +24,20 @@ namespace osu.Game.Tests.Visual.Ranking loadPanel(score); } + [Test] + public void TestScoreWithoutStatistics() + { + loadPanel(new TestScoreInfo(new OsuRuleset().RulesetInfo)); + } + private void loadPanel(ScoreInfo score) => AddStep("load panel", () => { - Child = new StatisticsPanel(score); + Child = new StatisticsPanel + { + RelativeSizeAxes = Axes.Both, + State = { Value = Visibility.Visible }, + Score = { Value = score } + }; }); } } diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 4a7cb6679a..c02a120a73 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -89,7 +89,11 @@ namespace osu.Game.Screens.Ranking SelectedScore = { BindTarget = SelectedScore }, PostExpandAction = onExpandedPanelClicked }, - statisticsPanel = new StatisticsPanel(Score) { RelativeSizeAxes = Axes.Both } + statisticsPanel = new StatisticsPanel + { + RelativeSizeAxes = Axes.Both, + Score = { BindTarget = SelectedScore } + } } } }, diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 733c855426..28a8bc460e 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -1,8 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Online.Placeholders; using osu.Game.Scoring; using osuTK; @@ -12,15 +15,14 @@ namespace osu.Game.Screens.Ranking.Statistics { public const float SIDE_PADDING = 30; + public readonly Bindable Score = new Bindable(); + protected override bool StartHidden => true; - public StatisticsPanel(ScoreInfo score) + private readonly Container content; + + public StatisticsPanel() { - // Todo: Not correct. - RelativeSizeAxes = Axes.Both; - - FillFlowContainer statisticRows; - InternalChild = new Container { RelativeSizeAxes = Axes.Both, @@ -31,24 +33,47 @@ namespace osu.Game.Screens.Ranking.Statistics Top = SIDE_PADDING, Bottom = 50 // Approximate padding to the bottom of the score panel. }, - Child = statisticRows = new FillFlowContainer + Child = content = new Container { RelativeSizeAxes = Axes.Both }, + }; + } + + [BackgroundDependencyLoader] + private void load() + { + Score.BindValueChanged(populateStatistics, true); + } + + private void populateStatistics(ValueChangedEvent score) + { + foreach (var child in content) + child.FadeOut(150).Expire(); + + var newScore = score.NewValue; + + if (newScore.HitEvents == null || newScore.HitEvents.Count == 0) + content.Add(new MessagePlaceholder("Score has no statistics :(")); + else + { + var rows = new FillFlowContainer { RelativeSizeAxes = Axes.Both, Direction = FillDirection.Vertical, Spacing = new Vector2(30, 15), - } - }; + }; - foreach (var s in score.Ruleset.CreateInstance().CreateStatistics(score)) - { - statisticRows.Add(new GridContainer + foreach (var row in newScore.Ruleset.CreateInstance().CreateStatistics(newScore)) { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Content = new[] { s.Content }, - ColumnDimensions = s.ColumnDimensions, - RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) } - }); + rows.Add(new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Content = new[] { row.Content }, + ColumnDimensions = row.ColumnDimensions, + RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) } + }); + } + + content.Add(rows); } } From 53f507f51af7adc2e48200281fc8ff1598489142 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 18 Jun 2020 22:27:10 +0900 Subject: [PATCH 2109/2376] Fade background --- osu.Game/Screens/Ranking/ResultsScreen.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index c02a120a73..5073adcc50 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -219,6 +219,8 @@ namespace osu.Game.Screens.Ranking }); } } + + Background.FadeTo(0.5f, 150); } else { @@ -234,6 +236,8 @@ namespace osu.Game.Screens.Ranking panel.MoveTo(new Vector2(scorePanelList.CurrentScrollPosition + StatisticsPanel.SIDE_PADDING, panel.GetTrackingPosition().Y), 150, Easing.OutQuint); } } + + Background.FadeTo(0.1f, 150); } } } From 85a0f78600e97866c1726eefeed64113fef5d76c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 18 Jun 2020 22:27:27 +0900 Subject: [PATCH 2110/2376] Hide statistics panel on first exit --- osu.Game/Screens/Ranking/ResultsScreen.cs | 24 ++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 5073adcc50..de1939352f 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -87,7 +87,7 @@ namespace osu.Game.Screens.Ranking { RelativeSizeAxes = Axes.Both, SelectedScore = { BindTarget = SelectedScore }, - PostExpandAction = onExpandedPanelClicked + PostExpandAction = () => statisticsPanel.ToggleVisibility() }, statisticsPanel = new StatisticsPanel { @@ -174,6 +174,8 @@ namespace osu.Game.Screens.Ranking if (req != null) api.Queue(req); + + statisticsPanel.State.BindValueChanged(onStatisticsStateChanged, true); } /// @@ -195,17 +197,23 @@ namespace osu.Game.Screens.Ranking public override bool OnExiting(IScreen next) { + if (statisticsPanel.State.Value == Visibility.Visible) + { + statisticsPanel.Hide(); + return true; + } + Background.FadeTo(1, 250); return base.OnExiting(next); } - private void onExpandedPanelClicked() + private void onStatisticsStateChanged(ValueChangedEvent state) { - statisticsPanel.ToggleVisibility(); - - if (statisticsPanel.State.Value == Visibility.Hidden) + if (state.NewValue == Visibility.Hidden) { + Background.FadeTo(0.5f, 150); + foreach (var panel in scorePanelList.Panels) { if (panel.State == PanelState.Contracted) @@ -219,11 +227,11 @@ namespace osu.Game.Screens.Ranking }); } } - - Background.FadeTo(0.5f, 150); } else { + Background.FadeTo(0.1f, 150); + foreach (var panel in scorePanelList.Panels) { if (panel.State == PanelState.Contracted) @@ -236,8 +244,6 @@ namespace osu.Game.Screens.Ranking panel.MoveTo(new Vector2(scorePanelList.CurrentScrollPosition + StatisticsPanel.SIDE_PADDING, panel.GetTrackingPosition().Y), 150, Easing.OutQuint); } } - - Background.FadeTo(0.1f, 150); } } } From add1265d5354b8ead7644b3a6389fc287fc3d7b5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Jun 2020 23:35:03 +0900 Subject: [PATCH 2111/2376] Block screen suspend while gameplay is active --- osu.Game/Screens/Play/Player.cs | 7 ++++ .../Screens/Play/ScreenSuspensionHandler.cs | 42 +++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 osu.Game/Screens/Play/ScreenSuspensionHandler.cs diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 83991ad027..d3b88e56ae 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -125,6 +125,8 @@ namespace osu.Game.Screens.Play private GameplayBeatmap gameplayBeatmap; + private ScreenSuspensionHandler screenSuspension; + private DependencyContainer dependencies; protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) @@ -179,6 +181,7 @@ namespace osu.Game.Screens.Play InternalChild = GameplayClockContainer = new GameplayClockContainer(Beatmap.Value, Mods.Value, DrawableRuleset.GameplayStartTime); AddInternal(gameplayBeatmap = new GameplayBeatmap(playableBeatmap)); + AddInternal(screenSuspension = new ScreenSuspensionHandler(GameplayClockContainer)); dependencies.CacheAs(gameplayBeatmap); @@ -628,12 +631,16 @@ namespace osu.Game.Screens.Play public override void OnSuspending(IScreen next) { + screenSuspension?.Expire(); + fadeOut(); base.OnSuspending(next); } public override bool OnExiting(IScreen next) { + screenSuspension?.Expire(); + if (completionProgressDelegate != null && !completionProgressDelegate.Cancelled && !completionProgressDelegate.Completed) { // proceed to result screen if beatmap already finished playing diff --git a/osu.Game/Screens/Play/ScreenSuspensionHandler.cs b/osu.Game/Screens/Play/ScreenSuspensionHandler.cs new file mode 100644 index 0000000000..948276f03f --- /dev/null +++ b/osu.Game/Screens/Play/ScreenSuspensionHandler.cs @@ -0,0 +1,42 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Platform; + +namespace osu.Game.Screens.Play +{ + internal class ScreenSuspensionHandler : Component + { + private readonly GameplayClockContainer gameplayClockContainer; + private Bindable isPaused; + + [Resolved] + private GameHost host { get; set; } + + public ScreenSuspensionHandler(GameplayClockContainer gameplayClockContainer) + { + this.gameplayClockContainer = gameplayClockContainer; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + isPaused = gameplayClockContainer.IsPaused.GetBoundCopy(); + isPaused.BindValueChanged(paused => host.AllowScreenSuspension.Value = paused.NewValue, true); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + isPaused?.UnbindAll(); + + if (host != null) + host.AllowScreenSuspension.Value = true; + } + } +} From 7da56ec7fd27ebddc7253b6151b50f93ad289dd4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Jun 2020 23:52:35 +0900 Subject: [PATCH 2112/2376] Add null check and xmldoc --- osu.Game/Screens/Play/ScreenSuspensionHandler.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/ScreenSuspensionHandler.cs b/osu.Game/Screens/Play/ScreenSuspensionHandler.cs index 948276f03f..59ad74d81a 100644 --- a/osu.Game/Screens/Play/ScreenSuspensionHandler.cs +++ b/osu.Game/Screens/Play/ScreenSuspensionHandler.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.Bindables; using osu.Framework.Graphics; @@ -8,7 +10,10 @@ using osu.Framework.Platform; namespace osu.Game.Screens.Play { - internal class ScreenSuspensionHandler : Component + /// + /// Ensures screen is not suspended / dimmed while gameplay is active. + /// + public class ScreenSuspensionHandler : Component { private readonly GameplayClockContainer gameplayClockContainer; private Bindable isPaused; @@ -16,9 +21,9 @@ namespace osu.Game.Screens.Play [Resolved] private GameHost host { get; set; } - public ScreenSuspensionHandler(GameplayClockContainer gameplayClockContainer) + public ScreenSuspensionHandler([NotNull] GameplayClockContainer gameplayClockContainer) { - this.gameplayClockContainer = gameplayClockContainer; + this.gameplayClockContainer = gameplayClockContainer ?? throw new ArgumentNullException(nameof(gameplayClockContainer)); } protected override void LoadComplete() From 290ae373469bd43d66c6c965edb7e0c016ff797a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Jun 2020 23:54:20 +0900 Subject: [PATCH 2113/2376] Add assertion of only usage game-wide --- osu.Game/Screens/Play/ScreenSuspensionHandler.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Screens/Play/ScreenSuspensionHandler.cs b/osu.Game/Screens/Play/ScreenSuspensionHandler.cs index 59ad74d81a..8585a5c309 100644 --- a/osu.Game/Screens/Play/ScreenSuspensionHandler.cs +++ b/osu.Game/Screens/Play/ScreenSuspensionHandler.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Diagnostics; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -30,6 +31,10 @@ namespace osu.Game.Screens.Play { base.LoadComplete(); + // This is the only usage game-wide of suspension changes. + // Assert to ensure we don't accidentally forget this in the future. + Debug.Assert(host.AllowScreenSuspension.Value); + isPaused = gameplayClockContainer.IsPaused.GetBoundCopy(); isPaused.BindValueChanged(paused => host.AllowScreenSuspension.Value = paused.NewValue, true); } From f04f2d21755103041272a66484730a5ae8687cfc Mon Sep 17 00:00:00 2001 From: Ronnie Moir <7267697+H2n9@users.noreply.github.com> Date: Thu, 18 Jun 2020 21:46:32 +0100 Subject: [PATCH 2114/2376] Add test scene --- .../Gameplay/TestSceneStoryboardSamples.cs | 57 +++++++++++++++++++ .../Drawables/DrawableStoryboardSample.cs | 17 +++--- 2 files changed, 66 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs index 552d163b2f..60911d6792 100644 --- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs @@ -10,9 +10,12 @@ using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.IO.Stores; using osu.Framework.Testing; +using osu.Framework.Timing; using osu.Game.Audio; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.Play; using osu.Game.Skinning; using osu.Game.Storyboards; @@ -70,6 +73,37 @@ namespace osu.Game.Tests.Gameplay AddUntilStep("sample playback succeeded", () => sample.LifetimeEnd < double.MaxValue); } + [Test] + public void TestSamplePlaybackWithRateMods() + { + GameplayClockContainer gameplayContainer = null; + TestDrawableStoryboardSample sample = null; + + OsuModDoubleTime doubleTimeMod = null; + + AddStep("create container", () => + { + var beatmap = Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); + + Add(gameplayContainer = new GameplayClockContainer(beatmap, new[] { doubleTimeMod = new OsuModDoubleTime() }, 0)); + + SelectedMods.Value = new[] { doubleTimeMod }; + Beatmap.Value = new TestCustomSkinWorkingBeatmap(beatmap.Beatmap, gameplayContainer.GameplayClock, Audio); + }); + + AddStep("create storyboard sample", () => + { + gameplayContainer.Add(sample = new TestDrawableStoryboardSample(new StoryboardSampleInfo("test-sample", 1, 1)) + { + Clock = gameplayContainer.GameplayClock + }); + }); + + AddStep("start", () => gameplayContainer.Start()); + + AddAssert("sample playback rate matches mod rates", () => sample.TestChannel.AggregateFrequency.Value == doubleTimeMod.SpeedChange.Value); + } + private class TestSkin : LegacySkin { public TestSkin(string resourceName, AudioManager audioManager) @@ -99,5 +133,28 @@ namespace osu.Game.Tests.Gameplay { } } + + private class TestCustomSkinWorkingBeatmap : ClockBackedTestWorkingBeatmap + { + private readonly AudioManager audio; + + public TestCustomSkinWorkingBeatmap(IBeatmap beatmap, IFrameBasedClock referenceClock, AudioManager audio) + : base(beatmap, null, referenceClock, audio) + { + this.audio = audio; + } + + protected override ISkin GetSkin() => new TestSkin("test-sample", audio); + } + + private class TestDrawableStoryboardSample : DrawableStoryboardSample + { + public TestDrawableStoryboardSample(StoryboardSampleInfo sampleInfo) + : base(sampleInfo) + { + } + + public SampleChannel TestChannel => Channel; + } } } diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs index 2b9c66d2e6..04df46410e 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs @@ -20,7 +20,8 @@ namespace osu.Game.Storyboards.Drawables private const double allowable_late_start = 100; private readonly StoryboardSampleInfo sampleInfo; - private SampleChannel channel; + + protected SampleChannel Channel; public override bool RemoveWhenNotAlive => false; @@ -33,14 +34,14 @@ namespace osu.Game.Storyboards.Drawables [BackgroundDependencyLoader] private void load(IBindable beatmap, IBindable> mods) { - channel = beatmap.Value.Skin.GetSample(sampleInfo); + Channel = beatmap.Value.Skin.GetSample(sampleInfo); - if (channel != null) + if (Channel != null) { - channel.Volume.Value = sampleInfo.Volume / 100.0; + Channel.Volume.Value = sampleInfo.Volume / 100.0; foreach (var mod in mods.Value.OfType()) - mod.ApplyToSample(channel); + mod.ApplyToSample(Channel); } } @@ -52,7 +53,7 @@ namespace osu.Game.Storyboards.Drawables if (Time.Current < sampleInfo.StartTime) { // We've rewound before the start time of the sample - channel?.Stop(); + Channel?.Stop(); // In the case that the user fast-forwards to a point far beyond the start time of the sample, // we want to be able to fall into the if-conditional below (therefore we must not have a life time end) @@ -64,7 +65,7 @@ namespace osu.Game.Storyboards.Drawables // We've passed the start time of the sample. We only play the sample if we're within an allowable range // from the sample's start, to reduce layering if we've been fast-forwarded far into the future if (Time.Current - sampleInfo.StartTime < allowable_late_start) - channel?.Play(); + Channel?.Play(); // In the case that the user rewinds to a point far behind the start time of the sample, // we want to be able to fall into the if-conditional above (therefore we must not have a life time start) @@ -75,7 +76,7 @@ namespace osu.Game.Storyboards.Drawables protected override void Dispose(bool isDisposing) { - channel?.Stop(); + Channel?.Stop(); base.Dispose(isDisposing); } } From 5530e2a1dbaa413a4383348ca7b2cf042d3c1c60 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 15:35:39 +0900 Subject: [PATCH 2115/2376] Add test for delayed score fetch --- .../Visual/Ranking/TestSceneResultsScreen.cs | 63 ++++++++++++++++++- 1 file changed, 60 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index ea33aa62e3..9d3c22d87c 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -4,11 +4,13 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Screens; +using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Rulesets.Osu; @@ -16,6 +18,7 @@ using osu.Game.Scoring; using osu.Game.Screens; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; +using osuTK.Input; namespace osu.Game.Tests.Visual.Ranking { @@ -44,7 +47,7 @@ namespace osu.Game.Tests.Visual.Ranking private UnrankedSoloResultsScreen createUnrankedSoloResultsScreen() => new UnrankedSoloResultsScreen(new TestScoreInfo(new OsuRuleset().RulesetInfo)); [Test] - public void ResultsWithoutPlayer() + public void TestResultsWithoutPlayer() { TestResultsScreen screen = null; OsuScreenStack stack; @@ -63,7 +66,7 @@ namespace osu.Game.Tests.Visual.Ranking } [Test] - public void ResultsWithPlayer() + public void TestResultsWithPlayer() { TestResultsScreen screen = null; @@ -73,7 +76,7 @@ namespace osu.Game.Tests.Visual.Ranking } [Test] - public void ResultsForUnranked() + public void TestResultsForUnranked() { UnrankedSoloResultsScreen screen = null; @@ -82,6 +85,24 @@ namespace osu.Game.Tests.Visual.Ranking AddAssert("retry overlay present", () => screen.RetryOverlay != null); } + [Test] + public void TestFetchScoresAfterShowingStatistics() + { + DelayedFetchResultsScreen screen = null; + + AddStep("load results", () => Child = new TestResultsContainer(screen = new DelayedFetchResultsScreen(new TestScoreInfo(new OsuRuleset().RulesetInfo), 3000))); + AddUntilStep("wait for loaded", () => screen.IsLoaded); + AddStep("click expanded panel", () => + { + var expandedPanel = this.ChildrenOfType().Single(p => p.State == PanelState.Expanded); + InputManager.MoveMouseTo(expandedPanel); + InputManager.Click(MouseButton.Left); + }); + + AddUntilStep("wait for fetch", () => screen.FetchCompleted); + AddAssert("expanded panel still on screen", () => this.ChildrenOfType().Single(p => p.State == PanelState.Expanded).ScreenSpaceDrawQuad.TopLeft.X > 0); + } + private class TestResultsContainer : Container { [Cached(typeof(Player))] @@ -134,6 +155,42 @@ namespace osu.Game.Tests.Visual.Ranking } } + private class DelayedFetchResultsScreen : TestResultsScreen + { + public bool FetchCompleted { get; private set; } + + private readonly double delay; + + public DelayedFetchResultsScreen(ScoreInfo score, double delay) + : base(score) + { + this.delay = delay; + } + + protected override APIRequest FetchScores(Action> scoresCallback) + { + Task.Run(async () => + { + await Task.Delay(TimeSpan.FromMilliseconds(delay)); + + var scores = new List(); + + for (int i = 0; i < 20; i++) + { + var score = new TestScoreInfo(new OsuRuleset().RulesetInfo); + score.TotalScore += 10 - i; + scores.Add(score); + } + + scoresCallback?.Invoke(scores); + + Schedule(() => FetchCompleted = true); + }); + + return null; + } + } + private class UnrankedSoloResultsScreen : SoloResultsScreen { public HotkeyRetryOverlay RetryOverlay; From ec16b0fc5a3888c65b198da0640b563accba5803 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 17:28:35 +0900 Subject: [PATCH 2116/2376] Rework score panel tracking to fix visual edge cases --- .../Visual/Ranking/TestSceneScorePanel.cs | 38 -------- osu.Game/Screens/Ranking/ResultsScreen.cs | 97 +++++++++++++------ osu.Game/Screens/Ranking/ScorePanel.cs | 45 ++------- osu.Game/Screens/Ranking/ScorePanelList.cs | 79 +++++++++------ .../Ranking/ScorePanelTrackingContainer.cs | 35 +++++++ 5 files changed, 155 insertions(+), 139 deletions(-) create mode 100644 osu.Game/Screens/Ranking/ScorePanelTrackingContainer.cs diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs index 1c5087ee94..250fdc5ebd 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs @@ -3,7 +3,6 @@ using NUnit.Framework; using osu.Framework.Graphics; -using osu.Framework.Utils; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; @@ -102,39 +101,6 @@ namespace osu.Game.Tests.Visual.Ranking AddWaitStep("wait for transition", 10); } - [Test] - public void TestSceneTrackingScorePanel() - { - var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.925, Rank = ScoreRank.A }; - - addPanelStep(score, PanelState.Contracted); - - AddStep("enable tracking", () => - { - panel.Anchor = Anchor.CentreLeft; - panel.Origin = Anchor.CentreLeft; - panel.Tracking = true; - - Add(panel.CreateTrackingComponent().With(d => - { - d.Anchor = Anchor.Centre; - d.Origin = Anchor.Centre; - })); - }); - - assertTracking(true); - - AddStep("expand panel", () => panel.State = PanelState.Expanded); - AddWaitStep("wait for transition", 2); - assertTracking(true); - - AddStep("stop tracking", () => panel.Tracking = false); - assertTracking(false); - - AddStep("start tracking", () => panel.Tracking = true); - assertTracking(true); - } - private void addPanelStep(ScoreInfo score, PanelState state = PanelState.Expanded) => AddStep("add panel", () => { Child = panel = new ScorePanel(score) @@ -144,9 +110,5 @@ namespace osu.Game.Tests.Visual.Ranking State = state }; }); - - private void assertTracking(bool tracking) => AddAssert($"{(tracking ? "is" : "is not")} tracking", () => - Precision.AlmostEquals(panel.ScreenSpaceDrawQuad.TopLeft, panel.CreateTrackingComponent().ScreenSpaceDrawQuad.TopLeft) == tracking - && Precision.AlmostEquals(panel.ScreenSpaceDrawQuad.BottomRight, panel.CreateTrackingComponent().ScreenSpaceDrawQuad.BottomRight) == tracking); } } diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index de1939352f..133efd6e7b 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -47,6 +48,7 @@ namespace osu.Game.Screens.Ranking private StatisticsPanel statisticsPanel; private Drawable bottomPanel; private ScorePanelList scorePanelList; + private Container detachedPanelContainer; protected ResultsScreen(ScoreInfo score, bool allowRetry = true) { @@ -89,11 +91,15 @@ namespace osu.Game.Screens.Ranking SelectedScore = { BindTarget = SelectedScore }, PostExpandAction = () => statisticsPanel.ToggleVisibility() }, + detachedPanelContainer = new Container + { + RelativeSizeAxes = Axes.Both + }, statisticsPanel = new StatisticsPanel { RelativeSizeAxes = Axes.Both, Score = { BindTarget = SelectedScore } - } + }, } } }, @@ -169,7 +175,7 @@ namespace osu.Game.Screens.Ranking var req = FetchScores(scores => Schedule(() => { foreach (var s in scores) - scorePanelList.AddScore(s); + addScore(s); })); if (req != null) @@ -208,42 +214,71 @@ namespace osu.Game.Screens.Ranking return base.OnExiting(next); } + private void addScore(ScoreInfo score) + { + var panel = scorePanelList.AddScore(score); + + if (detachedPanel != null) + panel.Alpha = 0; + } + + private ScorePanel detachedPanel; + private void onStatisticsStateChanged(ValueChangedEvent state) { - if (state.NewValue == Visibility.Hidden) + if (state.NewValue == Visibility.Visible) { - Background.FadeTo(0.5f, 150); + // Detach the panel in its original location, and move into the desired location in the local container. + var expandedPanel = scorePanelList.GetPanelForScore(SelectedScore.Value); + var screenSpacePos = expandedPanel.ScreenSpaceDrawQuad.TopLeft; - foreach (var panel in scorePanelList.Panels) - { - if (panel.State == PanelState.Contracted) - panel.FadeIn(150); - else - { - panel.MoveTo(panel.GetTrackingPosition(), 150, Easing.OutQuint).OnComplete(p => - { - scorePanelList.HandleScroll = true; - p.Tracking = true; - }); - } - } - } - else - { + // Detach and move into the local container. + scorePanelList.Detach(expandedPanel); + detachedPanelContainer.Add(expandedPanel); + + // Move into its original location in the local container. + var origLocation = detachedPanelContainer.ToLocalSpace(screenSpacePos); + expandedPanel.MoveTo(origLocation); + expandedPanel.MoveToX(origLocation.X); + + // Move into the final location. + expandedPanel.MoveToX(StatisticsPanel.SIDE_PADDING, 150, Easing.OutQuint); + + // Hide contracted panels. + foreach (var contracted in scorePanelList.GetScorePanels().Where(p => p.State == PanelState.Contracted)) + contracted.FadeOut(150, Easing.OutQuint); + scorePanelList.HandleInput = false; + + // Dim background. Background.FadeTo(0.1f, 150); - foreach (var panel in scorePanelList.Panels) - { - if (panel.State == PanelState.Contracted) - panel.FadeOut(150, Easing.OutQuint); - else - { - scorePanelList.HandleScroll = false; + detachedPanel = expandedPanel; + } + else if (detachedPanel != null) + { + var screenSpacePos = detachedPanel.ScreenSpaceDrawQuad.TopLeft; - panel.Tracking = false; - panel.MoveTo(new Vector2(scorePanelList.CurrentScrollPosition + StatisticsPanel.SIDE_PADDING, panel.GetTrackingPosition().Y), 150, Easing.OutQuint); - } - } + // Remove from the local container and re-attach. + detachedPanelContainer.Remove(detachedPanel); + scorePanelList.Attach(detachedPanel); + + // Move into its original location in the attached container. + var origLocation = detachedPanel.Parent.ToLocalSpace(screenSpacePos); + detachedPanel.MoveTo(origLocation); + detachedPanel.MoveToX(origLocation.X); + + // Move into the final location. + detachedPanel.MoveToX(0, 150, Easing.OutQuint); + + // Show contracted panels. + foreach (var contracted in scorePanelList.GetScorePanels().Where(p => p.State == PanelState.Contracted)) + contracted.FadeIn(150, Easing.OutQuint); + scorePanelList.HandleInput = true; + + // Un-dim background. + Background.FadeTo(0.5f, 150); + + detachedPanel = null; } } } diff --git a/osu.Game/Screens/Ranking/ScorePanel.cs b/osu.Game/Screens/Ranking/ScorePanel.cs index 31b2796c13..257279bdc9 100644 --- a/osu.Game/Screens/Ranking/ScorePanel.cs +++ b/osu.Game/Screens/Ranking/ScorePanel.cs @@ -78,11 +78,6 @@ namespace osu.Game.Screens.Ranking public event Action StateChanged; public Action PostExpandAction; - /// - /// Whether this should track the position of the tracking component created via . - /// - public bool Tracking; - /// /// Whether this can enter into an state. /// @@ -194,20 +189,6 @@ namespace osu.Game.Screens.Ranking } } - protected override void Update() - { - base.Update(); - - if (Tracking && trackingComponent != null) - Position = GetTrackingPosition(); - } - - public Vector2 GetTrackingPosition() - { - Vector2 topLeftPos = Parent.ToLocalSpace(trackingComponent.ScreenSpaceDrawQuad.TopLeft); - return topLeftPos - AnchorPosition + OriginPosition; - } - private void updateState() { topLayerContent?.FadeOut(content_fade_duration).Expire(); @@ -269,8 +250,8 @@ namespace osu.Game.Screens.Ranking { base.Size = value; - if (trackingComponent != null) - trackingComponent.Size = value; + if (trackingContainer != null) + trackingContainer.Size = value; } } @@ -293,26 +274,14 @@ namespace osu.Game.Screens.Ranking || topLayerContainer.ReceivePositionalInputAt(screenSpacePos) || middleLayerContainer.ReceivePositionalInputAt(screenSpacePos); - private TrackingComponent trackingComponent; + private ScorePanelTrackingContainer trackingContainer; - public TrackingComponent CreateTrackingComponent() => trackingComponent ??= new TrackingComponent(this); - - public class TrackingComponent : Drawable + public ScorePanelTrackingContainer CreateTrackingContainer() { - public readonly ScorePanel Panel; + if (trackingContainer != null) + throw new InvalidOperationException("A score panel container has already been created."); - public TrackingComponent(ScorePanel panel) - { - Panel = panel; - } - - // In ScorePanelList, score panels are added _before_ the flow, but this means that input will be blocked by the scroll container. - // So by forwarding input events, we remove the need to consider the order in which input is handled. - protected override bool OnClick(ClickEvent e) => Panel.TriggerEvent(e); - - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Panel.ReceivePositionalInputAt(screenSpacePos); - - public override bool IsPresent => Panel.IsPresent; + return trackingContainer = new ScorePanelTrackingContainer(this); } } } diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index e332f462bb..32903860ec 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -32,9 +32,6 @@ namespace osu.Game.Screens.Ranking public float CurrentScrollPosition => scroll.Current; - public IReadOnlyList Panels => panels; - private readonly Container panels; - private readonly Flow flow; private readonly Scroll scroll; private ScorePanel expandedPanel; @@ -49,10 +46,9 @@ namespace osu.Game.Screens.Ranking InternalChild = scroll = new Scroll { RelativeSizeAxes = Axes.Both, - HandleScroll = () => HandleScroll && expandedPanel?.IsHovered != true, // handle horizontal scroll only when not hovering the expanded panel. + HandleScroll = () => expandedPanel?.IsHovered != true, // handle horizontal scroll only when not hovering the expanded panel. Children = new Drawable[] { - panels = new Container { RelativeSizeAxes = Axes.Both }, flow = new Flow { Anchor = Anchor.Centre, @@ -72,34 +68,14 @@ namespace osu.Game.Screens.Ranking SelectedScore.BindValueChanged(selectedScoreChanged, true); } - private bool handleScroll = true; - - public bool HandleScroll - { - get => handleScroll; - set - { - handleScroll = value; - - foreach (var p in panels) - p.CanExpand = value; - - scroll.ScrollbarVisible = value; - - if (!value) - scroll.ScrollTo(CurrentScrollPosition, false); - } - } - /// /// Adds a to this list. /// /// The to add. - public void AddScore(ScoreInfo score) + public ScorePanel AddScore(ScoreInfo score) { var panel = new ScorePanel(score) { - Tracking = true, PostExpandAction = () => PostExpandAction?.Invoke() }.With(p => { @@ -110,8 +86,7 @@ namespace osu.Game.Screens.Ranking }; }); - panels.Add(panel); - flow.Add(panel.CreateTrackingComponent().With(d => + flow.Add(panel.CreateTrackingContainer().With(d => { d.Anchor = Anchor.Centre; d.Origin = Anchor.Centre; @@ -132,6 +107,8 @@ namespace osu.Game.Screens.Ranking scroll.InstantScrollTarget = (scroll.InstantScrollTarget ?? scroll.Target) + ScorePanel.CONTRACTED_WIDTH + panel_spacing; } } + + return panel; } /// @@ -187,15 +164,53 @@ namespace osu.Game.Screens.Ranking flow.Padding = new MarginPadding { Horizontal = offset }; } - private class Flow : FillFlowContainer + private bool handleInput = true; + + public bool HandleInput + { + get => handleInput; + set + { + handleInput = value; + scroll.ScrollbarVisible = value; + } + } + + public override bool PropagatePositionalInputSubTree => HandleInput && base.PropagatePositionalInputSubTree; + + public override bool PropagateNonPositionalInputSubTree => HandleInput && base.PropagateNonPositionalInputSubTree; + + public IEnumerable GetScorePanels() => flow.Select(t => t.Panel); + + public ScorePanel GetPanelForScore(ScoreInfo score) => flow.Single(t => t.Panel.Score == score).Panel; + + public void Detach(ScorePanel panel) + { + var container = flow.FirstOrDefault(t => t.Panel == panel); + if (container == null) + throw new InvalidOperationException("Panel is not contained by the score panel list."); + + container.Detach(); + } + + public void Attach(ScorePanel panel) + { + var container = flow.FirstOrDefault(t => t.Panel == panel); + if (container == null) + throw new InvalidOperationException("Panel is not contained by the score panel list."); + + container.Attach(); + } + + private class Flow : FillFlowContainer { public override IEnumerable FlowingChildren => applySorting(AliveInternalChildren); public int GetPanelIndex(ScoreInfo score) => applySorting(Children).TakeWhile(s => s.Panel.Score != score).Count(); - private IEnumerable applySorting(IEnumerable drawables) => drawables.OfType() - .OrderByDescending(s => s.Panel.Score.TotalScore) - .ThenBy(s => s.Panel.Score.OnlineScoreID); + private IEnumerable applySorting(IEnumerable drawables) => drawables.OfType() + .OrderByDescending(s => s.Panel.Score.TotalScore) + .ThenBy(s => s.Panel.Score.OnlineScoreID); } private class Scroll : OsuScrollContainer diff --git a/osu.Game/Screens/Ranking/ScorePanelTrackingContainer.cs b/osu.Game/Screens/Ranking/ScorePanelTrackingContainer.cs new file mode 100644 index 0000000000..f6f26d0f8a --- /dev/null +++ b/osu.Game/Screens/Ranking/ScorePanelTrackingContainer.cs @@ -0,0 +1,35 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Graphics.Containers; + +namespace osu.Game.Screens.Ranking +{ + public class ScorePanelTrackingContainer : CompositeDrawable + { + public readonly ScorePanel Panel; + + public ScorePanelTrackingContainer(ScorePanel panel) + { + Panel = panel; + Attach(); + } + + public void Detach() + { + if (InternalChildren.Count == 0) + throw new InvalidOperationException("Score panel container is not attached."); + + RemoveInternal(Panel); + } + + public void Attach() + { + if (InternalChildren.Count > 0) + throw new InvalidOperationException("Score panel container is already attached."); + + AddInternal(Panel); + } + } +} From 55196efe6e4ae03efe09e7dbc2b79d7d90f8e4c5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 18:02:54 +0900 Subject: [PATCH 2117/2376] Fix panel depth ordering --- osu.Game/Screens/Ranking/ScorePanelList.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index 32903860ec..8f9064c2d1 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -211,6 +211,22 @@ namespace osu.Game.Screens.Ranking private IEnumerable applySorting(IEnumerable drawables) => drawables.OfType() .OrderByDescending(s => s.Panel.Score.TotalScore) .ThenBy(s => s.Panel.Score.OnlineScoreID); + + protected override int Compare(Drawable x, Drawable y) + { + var tX = (ScorePanelTrackingContainer)x; + var tY = (ScorePanelTrackingContainer)y; + + int result = tY.Panel.Score.TotalScore.CompareTo(tX.Panel.Score.TotalScore); + + if (result != 0) + return result; + + if (tX.Panel.Score.OnlineScoreID == null || tY.Panel.Score.OnlineScoreID == null) + return base.Compare(x, y); + + return tX.Panel.Score.OnlineScoreID.Value.CompareTo(tY.Panel.Score.OnlineScoreID.Value); + } } private class Scroll : OsuScrollContainer From c9ad3192b02ae4a9a2cc4d6b19adb9b84d54d4ef Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 18:02:57 +0900 Subject: [PATCH 2118/2376] Add more tests --- .../Visual/Ranking/TestSceneResultsScreen.cs | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 9d3c22d87c..ac364b5233 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Framework.Testing; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Rulesets.Osu; @@ -18,6 +19,7 @@ using osu.Game.Scoring; using osu.Game.Screens; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; +using osu.Game.Screens.Ranking.Statistics; using osuTK.Input; namespace osu.Game.Tests.Visual.Ranking @@ -85,6 +87,73 @@ namespace osu.Game.Tests.Visual.Ranking AddAssert("retry overlay present", () => screen.RetryOverlay != null); } + [Test] + public void TestShowHideStatistics() + { + TestResultsScreen screen = null; + + AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen())); + AddUntilStep("wait for loaded", () => screen.IsLoaded); + + AddStep("click expanded panel", () => + { + var expandedPanel = this.ChildrenOfType().Single(p => p.State == PanelState.Expanded); + InputManager.MoveMouseTo(expandedPanel); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("statistics shown", () => this.ChildrenOfType().Single().State.Value == Visibility.Visible); + + AddUntilStep("expanded panel at the left of the screen", () => + { + var expandedPanel = this.ChildrenOfType().Single(p => p.State == PanelState.Expanded); + return expandedPanel.ScreenSpaceDrawQuad.TopLeft.X - screen.ScreenSpaceDrawQuad.TopLeft.X < 150; + }); + + AddStep("click expanded panel", () => + { + var expandedPanel = this.ChildrenOfType().Single(p => p.State == PanelState.Expanded); + InputManager.MoveMouseTo(expandedPanel); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("statistics hidden", () => this.ChildrenOfType().Single().State.Value == Visibility.Hidden); + + AddUntilStep("expanded panel in centre of screen", () => + { + var expandedPanel = this.ChildrenOfType().Single(p => p.State == PanelState.Expanded); + return Precision.AlmostEquals(expandedPanel.ScreenSpaceDrawQuad.Centre.X, screen.ScreenSpaceDrawQuad.Centre.X, 1); + }); + } + + [Test] + public void TestShowStatisticsAndClickOtherPanel() + { + TestResultsScreen screen = null; + + AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen())); + AddUntilStep("wait for loaded", () => screen.IsLoaded); + + ScorePanel expandedPanel = null; + ScorePanel contractedPanel = null; + + AddStep("click expanded panel then contracted panel", () => + { + expandedPanel = this.ChildrenOfType().Single(p => p.State == PanelState.Expanded); + InputManager.MoveMouseTo(expandedPanel); + InputManager.Click(MouseButton.Left); + + contractedPanel = this.ChildrenOfType().First(p => p.State == PanelState.Contracted && p.ScreenSpaceDrawQuad.TopLeft.X > screen.ScreenSpaceDrawQuad.TopLeft.X); + InputManager.MoveMouseTo(contractedPanel); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("statistics shown", () => this.ChildrenOfType().Single().State.Value == Visibility.Visible); + + AddAssert("contracted panel still contracted", () => contractedPanel.State == PanelState.Contracted); + AddAssert("expanded panel still expanded", () => expandedPanel.State == PanelState.Expanded); + } + [Test] public void TestFetchScoresAfterShowingStatistics() { From cae3a5f447e166b57bfed30a52e4a51964e4e2a6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 19:08:36 +0900 Subject: [PATCH 2119/2376] Rework heatmap for more consistent performance --- osu.Game.Rulesets.Osu/Statistics/Heatmap.cs | 94 +++++++++---------- .../Ranking/TestSceneAccuracyHeatmap.cs | 55 +++++++---- 2 files changed, 78 insertions(+), 71 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs b/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs index 95cfc5b768..8ebc8e9001 100644 --- a/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs @@ -26,10 +26,15 @@ namespace osu.Game.Rulesets.Osu.Statistics /// private const float inner_portion = 0.8f; - private const float rotation = 45; - private const float point_size = 4; + /// + /// Number of rows/columns of points. + /// 4px per point @ 128x128 size (the contents of the are always square). 1024 total points. + /// + private const int points_per_dimension = 32; - private Container allPoints; + private const float rotation = 45; + + private GridContainer pointGrid; private readonly BeatmapInfo beatmap; private readonly IReadOnlyList hitEvents; @@ -111,52 +116,39 @@ namespace osu.Game.Rulesets.Osu.Statistics } } }, - allPoints = new Container + pointGrid = new GridContainer { RelativeSizeAxes = Axes.Both } } }; - } - protected override void Update() - { - base.Update(); - validateHitPoints(); - } + Vector2 centre = new Vector2(points_per_dimension) / 2; + float innerRadius = centre.X * inner_portion; - private void validateHitPoints() - { - if (sizeLayout.IsValid) - return; + Drawable[][] points = new Drawable[points_per_dimension][]; - allPoints.Clear(); - - // Since the content is fit, both dimensions should have the same size. - float size = allPoints.DrawSize.X; - - Vector2 centre = new Vector2(size / 2); - int rows = (int)Math.Ceiling(size / point_size); - int cols = (int)Math.Ceiling(size / point_size); - - for (int r = 0; r < rows; r++) + for (int r = 0; r < points_per_dimension; r++) { - for (int c = 0; c < cols; c++) + points[r] = new Drawable[points_per_dimension]; + + for (int c = 0; c < points_per_dimension; c++) { - Vector2 pos = new Vector2(c * point_size, r * point_size); - HitPointType pointType = HitPointType.Hit; + HitPointType pointType = Vector2.Distance(new Vector2(c, r), centre) <= innerRadius + ? HitPointType.Hit + : HitPointType.Miss; - if (Vector2.Distance(pos, centre) > size * inner_portion / 2) - pointType = HitPointType.Miss; - - allPoints.Add(new HitPoint(pos, pointType) + var point = new HitPoint(pointType) { - Size = new Vector2(point_size), Colour = pointType == HitPointType.Hit ? new Color4(102, 255, 204, 255) : new Color4(255, 102, 102, 255) - }); + }; + + points[r][c] = point; } } + pointGrid.Content = points; + if (hitEvents.Count > 0) { // Todo: This should probably not be done like this. @@ -170,41 +162,39 @@ namespace osu.Game.Rulesets.Osu.Statistics AddPoint(((OsuHitObject)e.LastHitObject).StackedEndPosition, ((OsuHitObject)e.HitObject).StackedEndPosition, e.CursorPosition.Value, radius); } } - - sizeLayout.Validate(); } protected void AddPoint(Vector2 start, Vector2 end, Vector2 hitPoint, float radius) { - if (allPoints.Count == 0) + if (pointGrid.Content.Length == 0) return; double angle1 = Math.Atan2(end.Y - hitPoint.Y, hitPoint.X - end.X); // Angle between the end point and the hit point. double angle2 = Math.Atan2(end.Y - start.Y, start.X - end.X); // Angle between the end point and the start point. double finalAngle = angle2 - angle1; // Angle between start, end, and hit points. - float normalisedDistance = Vector2.Distance(hitPoint, end) / radius; - // Since the content is fit, both dimensions should have the same size. - float size = allPoints.DrawSize.X; + // Convert the above into the local search space. + Vector2 localCentre = new Vector2(points_per_dimension) / 2; + float localRadius = localCentre.X * inner_portion * normalisedDistance; // The radius inside the inner portion which of the heatmap which the closest point lies. + double localAngle = finalAngle + 3 * Math.PI / 4; // The angle inside the heatmap on which the closest point lies. + Vector2 localPoint = localCentre + localRadius * new Vector2((float)Math.Cos(localAngle), (float)Math.Sin(localAngle)); // Find the most relevant hit point. double minDist = double.PositiveInfinity; HitPoint point = null; - foreach (var p in allPoints) + for (int r = 0; r < points_per_dimension; r++) { - Vector2 localCentre = new Vector2(size / 2); - float localRadius = localCentre.X * inner_portion * normalisedDistance; - double localAngle = finalAngle + 3 * Math.PI / 4; - Vector2 localPoint = localCentre + localRadius * new Vector2((float)Math.Cos(localAngle), (float)Math.Sin(localAngle)); - - float dist = Vector2.Distance(p.DrawPosition + p.DrawSize / 2, localPoint); - - if (dist < minDist) + for (int c = 0; c < points_per_dimension; c++) { - minDist = dist; - point = p; + float dist = Vector2.Distance(new Vector2(c, r), localPoint); + + if (dist < minDist) + { + minDist = dist; + point = (HitPoint)pointGrid.Content[r][c]; + } } } @@ -216,11 +206,11 @@ namespace osu.Game.Rulesets.Osu.Statistics { private readonly HitPointType pointType; - public HitPoint(Vector2 position, HitPointType pointType) + public HitPoint(HitPointType pointType) { this.pointType = pointType; - Position = position; + RelativeSizeAxes = Axes.Both; Alpha = 0; } diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs index ba6a0e42c2..52cc41fbd8 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs @@ -2,11 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using NUnit.Framework; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; +using osu.Framework.Threading; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Rulesets.Osu; @@ -20,13 +22,18 @@ namespace osu.Game.Tests.Visual.Ranking { public class TestSceneAccuracyHeatmap : OsuManualInputManagerTestScene { - private readonly Box background; - private readonly Drawable object1; - private readonly Drawable object2; - private readonly TestHeatmap heatmap; + private Box background; + private Drawable object1; + private Drawable object2; + private TestHeatmap heatmap; + private ScheduledDelegate automaticAdditionDelegate; - public TestSceneAccuracyHeatmap() + [SetUp] + public void Setup() => Schedule(() => { + automaticAdditionDelegate?.Cancel(); + automaticAdditionDelegate = null; + Children = new[] { background = new Box @@ -41,7 +48,7 @@ namespace osu.Game.Tests.Visual.Ranking }, object2 = new BorderCircle { - Position = new Vector2(500, 300), + Position = new Vector2(100, 300), }, heatmap = new TestHeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, new List()) { @@ -50,22 +57,32 @@ namespace osu.Game.Tests.Visual.Ranking Size = new Vector2(130) } }; + }); + + [Test] + public void TestManyHitPointsAutomatic() + { + AddStep("add scheduled delegate", () => + { + automaticAdditionDelegate = Scheduler.AddDelayed(() => + { + var randomPos = new Vector2( + RNG.NextSingle(object1.DrawPosition.X - object1.DrawSize.X / 2, object1.DrawPosition.X + object1.DrawSize.X / 2), + RNG.NextSingle(object1.DrawPosition.Y - object1.DrawSize.Y / 2, object1.DrawPosition.Y + object1.DrawSize.Y / 2)); + + // The background is used for ToLocalSpace() since we need to go _inside_ the DrawSizePreservingContainer (Content of TestScene). + heatmap.AddPoint(object2.Position, object1.Position, randomPos, RNG.NextSingle(10, 500)); + InputManager.MoveMouseTo(background.ToScreenSpace(randomPos)); + }, 1, true); + }); + + AddWaitStep("wait for some hit points", 10); } - protected override void LoadComplete() + [Test] + public void TestManualPlacement() { - base.LoadComplete(); - - Scheduler.AddDelayed(() => - { - var randomPos = new Vector2( - RNG.NextSingle(object1.DrawPosition.X - object1.DrawSize.X / 2, object1.DrawPosition.X + object1.DrawSize.X / 2), - RNG.NextSingle(object1.DrawPosition.Y - object1.DrawSize.Y / 2, object1.DrawPosition.Y + object1.DrawSize.Y / 2)); - - // The background is used for ToLocalSpace() since we need to go _inside_ the DrawSizePreservingContainer (Content of TestScene). - heatmap.AddPoint(object2.Position, object1.Position, randomPos, RNG.NextSingle(10, 500)); - InputManager.MoveMouseTo(background.ToScreenSpace(randomPos)); - }, 1, true); + AddStep("return user input", () => InputManager.UseParentInput = true); } protected override bool OnMouseDown(MouseDownEvent e) From d3e4e6325884a2ac753d3c0c2c2601accb7a4d2f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 19:12:48 +0900 Subject: [PATCH 2120/2376] Remove unnecessary class --- .../Scoring/TimingDistribution.cs | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 osu.Game.Rulesets.Osu/Scoring/TimingDistribution.cs diff --git a/osu.Game.Rulesets.Osu/Scoring/TimingDistribution.cs b/osu.Game.Rulesets.Osu/Scoring/TimingDistribution.cs deleted file mode 100644 index 46f259f3d8..0000000000 --- a/osu.Game.Rulesets.Osu/Scoring/TimingDistribution.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -namespace osu.Game.Rulesets.Osu.Scoring -{ - public class TimingDistribution - { - public readonly int[] Bins; - public readonly double BinSize; - - public TimingDistribution(int binCount, double binSize) - { - Bins = new int[binCount]; - BinSize = binSize; - } - } -} From a3ff25177ad782e562732315a74be1557ca19ffc Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 19:12:55 +0900 Subject: [PATCH 2121/2376] Asyncify statistics load --- .../Ranking/Statistics/StatisticsPanel.cs | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 28a8bc460e..acaf91246d 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -1,10 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.UserInterface; using osu.Game.Online.Placeholders; using osu.Game.Scoring; using osuTK; @@ -20,6 +22,7 @@ namespace osu.Game.Screens.Ranking.Statistics protected override bool StartHidden => true; private readonly Container content; + private readonly LoadingSpinner spinner; public StatisticsPanel() { @@ -33,7 +36,11 @@ namespace osu.Game.Screens.Ranking.Statistics Top = SIDE_PADDING, Bottom = 50 // Approximate padding to the bottom of the score panel. }, - Child = content = new Container { RelativeSizeAxes = Axes.Both }, + Children = new Drawable[] + { + content = new Container { RelativeSizeAxes = Axes.Both }, + spinner = new LoadingSpinner() + } }; } @@ -43,8 +50,12 @@ namespace osu.Game.Screens.Ranking.Statistics Score.BindValueChanged(populateStatistics, true); } + private CancellationTokenSource loadCancellation; + private void populateStatistics(ValueChangedEvent score) { + loadCancellation?.Cancel(); + foreach (var child in content) child.FadeOut(150).Expire(); @@ -54,6 +65,8 @@ namespace osu.Game.Screens.Ranking.Statistics content.Add(new MessagePlaceholder("Score has no statistics :(")); else { + spinner.Show(); + var rows = new FillFlowContainer { RelativeSizeAxes = Axes.Both, @@ -73,7 +86,14 @@ namespace osu.Game.Screens.Ranking.Statistics }); } - content.Add(rows); + LoadComponentAsync(rows, d => + { + if (Score.Value != newScore) + return; + + spinner.Hide(); + content.Add(d); + }, (loadCancellation = new CancellationTokenSource()).Token); } } From 8c9506197d30b1635bb51541959978221d7f0d94 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 19:41:36 +0900 Subject: [PATCH 2122/2376] Increase the number of bins in the timing distribution --- osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs b/osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs index b319cc5aa9..30d25f581f 100644 --- a/osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs +++ b/osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Statistics /// /// The number of bins on each side of the timing distribution. /// - private const int timing_distribution_bins = 25; + private const int timing_distribution_bins = 50; /// /// The total number of bins in the timing distribution, including bins on both sides and the centre bin at 0. From ef56225d9adfda9bd45038746cd03edfb244a7b0 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 19:43:46 +0900 Subject: [PATCH 2123/2376] Rename CursorPosition -< PositionOffset --- osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs | 8 ++++---- osu.Game.Rulesets.Osu/Statistics/Heatmap.cs | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs index 9694367210..0a9ce83912 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs @@ -88,17 +88,17 @@ namespace osu.Game.Rulesets.Osu.Scoring public readonly HitObject LastHitObject; /// - /// The player's cursor position, if available, at the time of the event. + /// The player's position offset, if available, at the time of the event. /// - public readonly Vector2? CursorPosition; + public readonly Vector2? PositionOffset; - public HitEvent(double timeOffset, HitResult result, HitObject hitObject, [CanBeNull] HitObject lastHitObject, Vector2? cursorPosition) + public HitEvent(double timeOffset, HitResult result, HitObject hitObject, [CanBeNull] HitObject lastHitObject, Vector2? positionOffset) { TimeOffset = timeOffset; Result = result; HitObject = hitObject; LastHitObject = lastHitObject; - CursorPosition = cursorPosition; + PositionOffset = positionOffset; } } } diff --git a/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs b/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs index 8ebc8e9001..b648dd5e47 100644 --- a/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs @@ -156,10 +156,10 @@ namespace osu.Game.Rulesets.Osu.Statistics foreach (var e in hitEvents) { - if (e.LastHitObject == null || e.CursorPosition == null) + if (e.LastHitObject == null || e.PositionOffset == null) continue; - AddPoint(((OsuHitObject)e.LastHitObject).StackedEndPosition, ((OsuHitObject)e.HitObject).StackedEndPosition, e.CursorPosition.Value, radius); + AddPoint(((OsuHitObject)e.LastHitObject).StackedEndPosition, ((OsuHitObject)e.HitObject).StackedEndPosition, e.PositionOffset.Value, radius); } } } From eab00ec9d9644f32e72268c819fad8cf5801e17c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 19:58:35 +0900 Subject: [PATCH 2124/2376] Move hit events to the ScoreProcessor --- .../Judgements/OsuHitCircleJudgementResult.cs | 9 ++- .../Objects/Drawables/DrawableHitCircle.cs | 4 +- osu.Game.Rulesets.Osu/OsuRuleset.cs | 5 +- .../Scoring/OsuScoreProcessor.cs | 76 ------------------- osu.Game.Rulesets.Osu/Statistics/Heatmap.cs | 36 ++++----- .../Statistics/TimingDistributionGraph.cs | 15 ++-- .../Ranking/TestSceneAccuracyHeatmap.cs | 10 +-- .../Ranking/TestSceneStatisticsPanel.cs | 3 +- .../TestSceneTimingDistributionGraph.cs | 4 +- osu.Game/Rulesets/Scoring/HitEvent.cs | 48 ++++++++++++ osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 18 +++++ osu.Game/Scoring/ScoreInfo.cs | 2 +- 12 files changed, 106 insertions(+), 124 deletions(-) create mode 100644 osu.Game/Rulesets/Scoring/HitEvent.cs diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuHitCircleJudgementResult.cs b/osu.Game.Rulesets.Osu/Judgements/OsuHitCircleJudgementResult.cs index 103d02958d..9b33e746b3 100644 --- a/osu.Game.Rulesets.Osu/Judgements/OsuHitCircleJudgementResult.cs +++ b/osu.Game.Rulesets.Osu/Judgements/OsuHitCircleJudgementResult.cs @@ -10,10 +10,15 @@ namespace osu.Game.Rulesets.Osu.Judgements { public class OsuHitCircleJudgementResult : OsuJudgementResult { + /// + /// The . + /// public HitCircle HitCircle => (HitCircle)HitObject; - public Vector2? HitPosition; - public float? Radius; + /// + /// The position of the player's cursor when was hit. + /// + public Vector2? CursorPositionAtHit; public OsuHitCircleJudgementResult(HitObject hitObject, Judgement judgement) : base(hitObject, judgement) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index 2f86400b25..854fc4c91c 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -142,11 +142,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { var circleResult = (OsuHitCircleJudgementResult)r; + // Todo: This should also consider misses, but they're a little more interesting to handle, since we don't necessarily know the position at the time of a miss. if (result != HitResult.Miss) { var localMousePosition = ToLocalSpace(inputManager.CurrentState.Mouse.Position); - circleResult.HitPosition = HitObject.StackedPosition + (localMousePosition - DrawSize / 2); - circleResult.Radius = (float)HitObject.Radius; + circleResult.CursorPositionAtHit = HitObject.StackedPosition + (localMousePosition - DrawSize / 2); } circleResult.Type = result; diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index c7003deed2..45980cb3d5 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -29,7 +29,6 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Skinning; using System; -using System.Linq; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Osu.Statistics; using osu.Game.Screens.Ranking.Statistics; @@ -201,7 +200,7 @@ namespace osu.Game.Rulesets.Osu { RelativeSizeAxes = Axes.X, Height = 130, - Child = new TimingDistributionGraph(score.HitEvents.Cast().ToList()) + Child = new TimingDistributionGraph(score) { RelativeSizeAxes = Axes.Both } @@ -209,7 +208,7 @@ namespace osu.Game.Rulesets.Osu new StatisticContainer("Accuracy Heatmap") { RelativeSizeAxes = Axes.Both, - Child = new Heatmap(score.Beatmap, score.HitEvents.Cast().ToList()) + Child = new Heatmap(score) { RelativeSizeAxes = Axes.Both } diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs index 0a9ce83912..231a24cac5 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs @@ -1,54 +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.Collections.Generic; -using System.Linq; -using JetBrains.Annotations; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; -using osu.Game.Scoring; -using osuTK; namespace osu.Game.Rulesets.Osu.Scoring { public class OsuScoreProcessor : ScoreProcessor { - private readonly List hitEvents = new List(); - private HitObject lastHitObject; - - protected override void OnResultApplied(JudgementResult result) - { - base.OnResultApplied(result); - - hitEvents.Add(new HitEvent(result.TimeOffset, result.Type, result.HitObject, lastHitObject, (result as OsuHitCircleJudgementResult)?.HitPosition)); - lastHitObject = result.HitObject; - } - - protected override void OnResultReverted(JudgementResult result) - { - base.OnResultReverted(result); - - hitEvents.RemoveAt(hitEvents.Count - 1); - } - - protected override void Reset(bool storeResults) - { - base.Reset(storeResults); - - hitEvents.Clear(); - lastHitObject = null; - } - - public override void PopulateScore(ScoreInfo score) - { - base.PopulateScore(score); - - score.HitEvents.AddRange(hitEvents.Select(e => e).Cast()); - } - protected override JudgementResult CreateResult(HitObject hitObject, Judgement judgement) { switch (hitObject) @@ -63,42 +25,4 @@ namespace osu.Game.Rulesets.Osu.Scoring public override HitWindows CreateHitWindows() => new OsuHitWindows(); } - - public readonly struct HitEvent - { - /// - /// The time offset from the end of at which the event occurred. - /// - public readonly double TimeOffset; - - /// - /// The hit result. - /// - public readonly HitResult Result; - - /// - /// The on which the result occurred. - /// - public readonly HitObject HitObject; - - /// - /// The occurring prior to . - /// - [CanBeNull] - public readonly HitObject LastHitObject; - - /// - /// The player's position offset, if available, at the time of the event. - /// - public readonly Vector2? PositionOffset; - - public HitEvent(double timeOffset, HitResult result, HitObject hitObject, [CanBeNull] HitObject lastHitObject, Vector2? positionOffset) - { - TimeOffset = timeOffset; - Result = result; - HitObject = hitObject; - LastHitObject = lastHitObject; - PositionOffset = positionOffset; - } - } } diff --git a/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs b/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs index b648dd5e47..49d7f67b7f 100644 --- a/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs @@ -2,17 +2,14 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Framework.Layout; -using osu.Game.Beatmaps; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.Scoring; +using osu.Game.Scoring; using osuTK; using osuTK.Graphics; @@ -36,16 +33,11 @@ namespace osu.Game.Rulesets.Osu.Statistics private GridContainer pointGrid; - private readonly BeatmapInfo beatmap; - private readonly IReadOnlyList hitEvents; - private readonly LayoutValue sizeLayout = new LayoutValue(Invalidation.DrawSize); + private readonly ScoreInfo score; - public Heatmap(BeatmapInfo beatmap, IReadOnlyList hitEvents) + public Heatmap(ScoreInfo score) { - this.beatmap = beatmap; - this.hitEvents = hitEvents; - - AddLayout(sizeLayout); + this.score = score; } [BackgroundDependencyLoader] @@ -149,18 +141,18 @@ namespace osu.Game.Rulesets.Osu.Statistics pointGrid.Content = points; - if (hitEvents.Count > 0) + if (score.HitEvents == null || score.HitEvents.Count == 0) + return; + + // Todo: This should probably not be done like this. + float radius = OsuHitObject.OBJECT_RADIUS * (1.0f - 0.7f * (score.Beatmap.BaseDifficulty.CircleSize - 5) / 5) / 2; + + foreach (var e in score.HitEvents) { - // Todo: This should probably not be done like this. - float radius = OsuHitObject.OBJECT_RADIUS * (1.0f - 0.7f * (beatmap.BaseDifficulty.CircleSize - 5) / 5) / 2; + if (e.LastHitObject == null || e.PositionOffset == null) + continue; - foreach (var e in hitEvents) - { - if (e.LastHitObject == null || e.PositionOffset == null) - continue; - - AddPoint(((OsuHitObject)e.LastHitObject).StackedEndPosition, ((OsuHitObject)e.HitObject).StackedEndPosition, e.PositionOffset.Value, radius); - } + AddPoint(((OsuHitObject)e.LastHitObject).StackedEndPosition, ((OsuHitObject)e.HitObject).StackedEndPosition, e.PositionOffset.Value, radius); } } diff --git a/osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs b/osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs index 30d25f581f..f3ccb0630e 100644 --- a/osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs +++ b/osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; @@ -11,7 +10,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Rulesets.Osu.Scoring; +using osu.Game.Scoring; namespace osu.Game.Rulesets.Osu.Statistics { @@ -37,23 +36,23 @@ namespace osu.Game.Rulesets.Osu.Statistics /// private const float axis_points = 5; - private readonly List hitEvents; + private readonly ScoreInfo score; - public TimingDistributionGraph(List hitEvents) + public TimingDistributionGraph(ScoreInfo score) { - this.hitEvents = hitEvents; + this.score = score; } [BackgroundDependencyLoader] private void load() { - if (hitEvents.Count == 0) + if (score.HitEvents == null || score.HitEvents.Count == 0) return; int[] bins = new int[total_timing_distribution_bins]; - double binSize = hitEvents.Max(e => Math.Abs(e.TimeOffset)) / timing_distribution_bins; + double binSize = score.HitEvents.Max(e => Math.Abs(e.TimeOffset)) / timing_distribution_bins; - foreach (var e in hitEvents) + foreach (var e in score.HitEvents) { int binOffset = (int)(e.TimeOffset / binSize); bins[timing_distribution_centre_bin_index + binOffset]++; diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs index 52cc41fbd8..d8b0594803 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -10,10 +9,9 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Framework.Threading; using osu.Framework.Utils; -using osu.Game.Beatmaps; using osu.Game.Rulesets.Osu; -using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Osu.Statistics; +using osu.Game.Scoring; using osu.Game.Tests.Beatmaps; using osuTK; using osuTK.Graphics; @@ -50,7 +48,7 @@ namespace osu.Game.Tests.Visual.Ranking { Position = new Vector2(100, 300), }, - heatmap = new TestHeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, new List()) + heatmap = new TestHeatmap(new ScoreInfo { Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -93,8 +91,8 @@ namespace osu.Game.Tests.Visual.Ranking private class TestHeatmap : Heatmap { - public TestHeatmap(BeatmapInfo beatmap, List events) - : base(beatmap, events) + public TestHeatmap(ScoreInfo score) + : base(score) { } diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index cc3415a530..bcf8a19c61 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -18,7 +17,7 @@ namespace osu.Game.Tests.Visual.Ranking { var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { - HitEvents = TestSceneTimingDistributionGraph.CreateDistributedHitEvents().Cast().ToList(), + HitEvents = TestSceneTimingDistributionGraph.CreateDistributedHitEvents() }; loadPanel(score); diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs b/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs index 178d6d95b5..d5ee50e636 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs @@ -7,9 +7,9 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Osu.Statistics; using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; using osuTK; namespace osu.Game.Tests.Visual.Ranking @@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.Ranking RelativeSizeAxes = Axes.Both, Colour = Color4Extensions.FromHex("#333") }, - new TimingDistributionGraph(CreateDistributedHitEvents()) + new TimingDistributionGraph(new ScoreInfo { HitEvents = CreateDistributedHitEvents() }) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Rulesets/Scoring/HitEvent.cs b/osu.Game/Rulesets/Scoring/HitEvent.cs new file mode 100644 index 0000000000..908ac0c171 --- /dev/null +++ b/osu.Game/Rulesets/Scoring/HitEvent.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 JetBrains.Annotations; +using osu.Game.Rulesets.Objects; +using osuTK; + +namespace osu.Game.Rulesets.Scoring +{ + public readonly struct HitEvent + { + /// + /// The time offset from the end of at which the event occurred. + /// + public readonly double TimeOffset; + + /// + /// The hit result. + /// + public readonly HitResult Result; + + /// + /// The on which the result occurred. + /// + public readonly HitObject HitObject; + + /// + /// The occurring prior to . + /// + [CanBeNull] + public readonly HitObject LastHitObject; + + /// + /// The player's position offset, if available, at the time of the event. + /// + [CanBeNull] + public readonly Vector2? PositionOffset; + + public HitEvent(double timeOffset, HitResult result, HitObject hitObject, [CanBeNull] HitObject lastHitObject, [CanBeNull] Vector2? positionOffset) + { + TimeOffset = timeOffset; + Result = result; + HitObject = hitObject; + LastHitObject = lastHitObject; + PositionOffset = positionOffset; + } + } +} diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 619547aef4..b9f51dfad3 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -9,6 +9,7 @@ using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; using osu.Game.Scoring; namespace osu.Game.Rulesets.Scoring @@ -61,6 +62,9 @@ namespace osu.Game.Rulesets.Scoring private double baseScore; private double bonusScore; + private readonly List hitEvents = new List(); + private HitObject lastHitObject; + private double scoreMultiplier = 1; public ScoreProcessor() @@ -128,6 +132,9 @@ namespace osu.Game.Rulesets.Scoring rollingMaxBaseScore += result.Judgement.MaxNumericResult; } + hitEvents.Add(CreateHitEvent(result)); + lastHitObject = result.HitObject; + updateScore(); OnResultApplied(result); @@ -137,6 +144,9 @@ namespace osu.Game.Rulesets.Scoring { } + protected virtual HitEvent CreateHitEvent(JudgementResult result) + => new HitEvent(result.TimeOffset, result.Type, result.HitObject, lastHitObject, null); + protected sealed override void RevertResultInternal(JudgementResult result) { Combo.Value = result.ComboAtJudgement; @@ -159,6 +169,10 @@ namespace osu.Game.Rulesets.Scoring rollingMaxBaseScore -= result.Judgement.MaxNumericResult; } + Debug.Assert(hitEvents.Count > 0); + lastHitObject = hitEvents[^1].LastHitObject; + hitEvents.RemoveAt(hitEvents.Count - 1); + updateScore(); OnResultReverted(result); @@ -219,6 +233,8 @@ namespace osu.Game.Rulesets.Scoring base.Reset(storeResults); scoreResultCounts.Clear(); + hitEvents.Clear(); + lastHitObject = null; if (storeResults) { @@ -259,6 +275,8 @@ namespace osu.Game.Rulesets.Scoring foreach (var result in Enum.GetValues(typeof(HitResult)).OfType().Where(r => r > HitResult.None && hitWindows.IsHitResultAllowed(r))) score.Statistics[result] = GetStatistic(result); + + score.HitEvents = new List(hitEvents); } /// diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 6fc5892b3c..84c0d5b54e 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -168,7 +168,7 @@ namespace osu.Game.Scoring [NotMapped] [JsonIgnore] - public List HitEvents = new List(); + public List HitEvents { get; set; } [JsonIgnore] public List Files { get; set; } From 1cbbd6b4427130159832eade1e91ae557c4181e5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 20:03:18 +0900 Subject: [PATCH 2125/2376] Move timing distribution graph to osu.Game --- osu.Game.Rulesets.Osu/OsuRuleset.cs | 2 +- osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs | 2 +- .../Visual/Ranking/TestSceneTimingDistributionGraph.cs | 8 ++++---- .../Ranking/Statistics/HitEventTimingDistributionGraph.cs | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) rename osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs => osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs (96%) diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 45980cb3d5..d99fee3b15 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -200,7 +200,7 @@ namespace osu.Game.Rulesets.Osu { RelativeSizeAxes = Axes.X, Height = 130, - Child = new TimingDistributionGraph(score) + Child = new HitEventTimingDistributionGraph(score) { RelativeSizeAxes = Axes.Both } diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index bcf8a19c61..210abaef4e 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -17,7 +17,7 @@ namespace osu.Game.Tests.Visual.Ranking { var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { - HitEvents = TestSceneTimingDistributionGraph.CreateDistributedHitEvents() + HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents() }; loadPanel(score); diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs b/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs index d5ee50e636..bfdc216aa1 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs @@ -7,16 +7,16 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.Statistics; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; +using osu.Game.Screens.Ranking.Statistics; using osuTK; namespace osu.Game.Tests.Visual.Ranking { - public class TestSceneTimingDistributionGraph : OsuTestScene + public class TestSceneHitEventTimingDistributionGraph : OsuTestScene { - public TestSceneTimingDistributionGraph() + public TestSceneHitEventTimingDistributionGraph() { Children = new Drawable[] { @@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.Ranking RelativeSizeAxes = Axes.Both, Colour = Color4Extensions.FromHex("#333") }, - new TimingDistributionGraph(new ScoreInfo { HitEvents = CreateDistributedHitEvents() }) + new HitEventTimingDistributionGraph(new ScoreInfo { HitEvents = CreateDistributedHitEvents() }) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs similarity index 96% rename from osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs rename to osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs index f3ccb0630e..b258e92aeb 100644 --- a/osu.Game.Rulesets.Osu/Statistics/TimingDistributionGraph.cs +++ b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs @@ -12,9 +12,9 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Scoring; -namespace osu.Game.Rulesets.Osu.Statistics +namespace osu.Game.Screens.Ranking.Statistics { - public class TimingDistributionGraph : CompositeDrawable + public class HitEventTimingDistributionGraph : CompositeDrawable { /// /// The number of bins on each side of the timing distribution. @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.Statistics private readonly ScoreInfo score; - public TimingDistributionGraph(ScoreInfo score) + public HitEventTimingDistributionGraph(ScoreInfo score) { this.score = score; } From 83e6c3efdb32c23f2af26fb2c4661ae49f62275c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 20:31:52 +0900 Subject: [PATCH 2126/2376] Adjust API for returning statistics --- osu.Game.Rulesets.Osu/OsuRuleset.cs | 43 ++++++++----------- osu.Game.Rulesets.Osu/Statistics/Heatmap.cs | 3 +- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 18 ++++++++ .../TestSceneTimingDistributionGraph.cs | 3 +- osu.Game/Rulesets/Ruleset.cs | 1 + .../HitEventTimingDistributionGraph.cs | 26 +++++++---- .../Ranking/Statistics/StatisticContainer.cs | 2 +- .../Ranking/Statistics/StatisticItem.cs | 23 ++++++++++ .../Ranking/Statistics/StatisticRow.cs | 11 ++--- .../Ranking/Statistics/StatisticsPanel.cs | 5 ++- 10 files changed, 92 insertions(+), 43 deletions(-) create mode 100644 osu.Game/Screens/Ranking/Statistics/StatisticItem.cs diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index d99fee3b15..aa313c92b3 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -29,7 +29,9 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Skinning; using System; +using System.Linq; using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Statistics; using osu.Game.Screens.Ranking.Statistics; @@ -190,36 +192,29 @@ namespace osu.Game.Rulesets.Osu public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new OsuRulesetConfigManager(settings, RulesetInfo); - public override StatisticRow[] CreateStatistics(ScoreInfo score) => new[] + public override StatisticRow[] CreateStatistics(ScoreInfo score) { - new StatisticRow + var hitCircleEvents = score.HitEvents.Where(e => e.HitObject is HitCircle).ToList(); + + return new[] { - Content = new Drawable[] + new StatisticRow { - new StatisticContainer("Timing Distribution") + Columns = new[] { - RelativeSizeAxes = Axes.X, - Height = 130, - Child = new HitEventTimingDistributionGraph(score) + new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(hitCircleEvents) { - RelativeSizeAxes = Axes.Both - } - }, - new StatisticContainer("Accuracy Heatmap") - { - RelativeSizeAxes = Axes.Both, - Child = new Heatmap(score) + RelativeSizeAxes = Axes.X, + Height = 130 + }), + new StatisticItem("Accuracy Heatmap", new Heatmap(score) { - RelativeSizeAxes = Axes.Both - } - }, - }, - ColumnDimensions = new[] - { - new Dimension(), - new Dimension(GridSizeMode.Absolute, 130), + RelativeSizeAxes = Axes.X, + Height = 130 + }, new Dimension(GridSizeMode.Absolute, 130)), + } } - } - }; + }; + } } } diff --git a/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs b/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs index 49d7f67b7f..86cb8e682f 100644 --- a/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs @@ -3,6 +3,7 @@ using System; using System.Diagnostics; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -147,7 +148,7 @@ namespace osu.Game.Rulesets.Osu.Statistics // Todo: This should probably not be done like this. float radius = OsuHitObject.OBJECT_RADIUS * (1.0f - 0.7f * (score.Beatmap.BaseDifficulty.CircleSize - 5) / 5) / 2; - foreach (var e in score.HitEvents) + foreach (var e in score.HitEvents.Where(e => e.HitObject is HitCircle)) { if (e.LastHitObject == null || e.PositionOffset == null) continue; diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 4cdd1fbc24..cd4e699262 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -21,9 +21,12 @@ using osu.Game.Rulesets.Taiko.Difficulty; using osu.Game.Rulesets.Taiko.Scoring; using osu.Game.Scoring; using System; +using System.Linq; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Taiko.Edit; +using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Skinning; +using osu.Game.Screens.Ranking.Statistics; using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko @@ -155,5 +158,20 @@ namespace osu.Game.Rulesets.Taiko public int LegacyID => 1; public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame(); + + public override StatisticRow[] CreateStatistics(ScoreInfo score) => new[] + { + new StatisticRow + { + Columns = new[] + { + new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents.Where(e => e.HitObject is Hit).ToList()) + { + RelativeSizeAxes = Axes.X, + Height = 130 + }), + } + } + }; } } diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs b/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs index bfdc216aa1..b34529cca7 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; -using osu.Game.Scoring; using osu.Game.Screens.Ranking.Statistics; using osuTK; @@ -25,7 +24,7 @@ namespace osu.Game.Tests.Visual.Ranking RelativeSizeAxes = Axes.Both, Colour = Color4Extensions.FromHex("#333") }, - new HitEventTimingDistributionGraph(new ScoreInfo { HitEvents = CreateDistributedHitEvents() }) + new HitEventTimingDistributionGraph(CreateDistributedHitEvents()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index f05685b6e9..52784e354f 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -210,6 +210,7 @@ namespace osu.Game.Rulesets /// An empty frame for the current ruleset, or null if unsupported. public virtual IConvertibleReplayFrame CreateConvertibleReplayFrame() => null; + [NotNull] public virtual StatisticRow[] CreateStatistics(ScoreInfo score) => Array.Empty(); } } diff --git a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs index b258e92aeb..4acbc7da3c 100644 --- a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs +++ b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; @@ -10,10 +11,13 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Scoring; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Screens.Ranking.Statistics { + /// + /// A graph which displays the distribution of hit timing in a series of s. + /// public class HitEventTimingDistributionGraph : CompositeDrawable { /// @@ -32,27 +36,31 @@ namespace osu.Game.Screens.Ranking.Statistics private const int timing_distribution_centre_bin_index = timing_distribution_bins; /// - /// The number of data points shown on the axis below the graph. + /// The number of data points shown on each side of the axis below the graph. /// private const float axis_points = 5; - private readonly ScoreInfo score; + private readonly IReadOnlyList hitEvents; - public HitEventTimingDistributionGraph(ScoreInfo score) + /// + /// Creates a new . + /// + /// The s to display the timing distribution of. + public HitEventTimingDistributionGraph(IReadOnlyList hitEvents) { - this.score = score; + this.hitEvents = hitEvents; } [BackgroundDependencyLoader] private void load() { - if (score.HitEvents == null || score.HitEvents.Count == 0) + if (hitEvents == null || hitEvents.Count == 0) return; int[] bins = new int[total_timing_distribution_bins]; - double binSize = score.HitEvents.Max(e => Math.Abs(e.TimeOffset)) / timing_distribution_bins; + double binSize = hitEvents.Max(e => Math.Abs(e.TimeOffset)) / timing_distribution_bins; - foreach (var e in score.HitEvents) + foreach (var e in hitEvents) { int binOffset = (int)(e.TimeOffset / binSize); bins[timing_distribution_centre_bin_index + binOffset]++; @@ -67,6 +75,8 @@ namespace osu.Game.Screens.Ranking.Statistics InternalChild = new GridContainer { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, Width = 0.8f, Content = new[] diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs index d7b42c1c2f..b8dde8f85e 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs @@ -11,7 +11,7 @@ using osuTK; namespace osu.Game.Screens.Ranking.Statistics { - public class StatisticContainer : Container + internal class StatisticContainer : Container { protected override Container Content => content; diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs new file mode 100644 index 0000000000..2605ae9f1b --- /dev/null +++ b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using JetBrains.Annotations; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; + +namespace osu.Game.Screens.Ranking.Statistics +{ + public class StatisticItem + { + public readonly string Name; + public readonly Drawable Content; + public readonly Dimension Dimension; + + public StatisticItem([NotNull] string name, [NotNull] Drawable content, [CanBeNull] Dimension dimension = null) + { + Name = name; + Content = content; + Dimension = dimension; + } + } +} diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticRow.cs b/osu.Game/Screens/Ranking/Statistics/StatisticRow.cs index 5d39ef57b2..ebab148fc2 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticRow.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticRow.cs @@ -1,15 +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.Framework.Graphics; -using osu.Framework.Graphics.Containers; +using JetBrains.Annotations; namespace osu.Game.Screens.Ranking.Statistics { public class StatisticRow { - public Drawable[] Content = Array.Empty(); - public Dimension[] ColumnDimensions = Array.Empty(); + /// + /// The columns of this . + /// + [ItemCanBeNull] + public StatisticItem[] Columns; } } diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index acaf91246d..3d81229ac3 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.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 System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -80,8 +81,8 @@ namespace osu.Game.Screens.Ranking.Statistics { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Content = new[] { row.Content }, - ColumnDimensions = row.ColumnDimensions, + Content = new[] { row.Columns?.Select(c => c?.Content).ToArray() }, + ColumnDimensions = Enumerable.Range(0, row.Columns?.Length ?? 0).Select(i => row.Columns[i]?.Dimension ?? new Dimension()).ToArray(), RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) } }); } From ad3bc99e7c9bf6d39acb5f7595d97e1369ad4c56 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 20:48:53 +0900 Subject: [PATCH 2127/2376] Fix hit event position offset not being set --- osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs | 3 +++ osu.Game/Rulesets/Scoring/HitEvent.cs | 2 ++ 2 files changed, 5 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs index 231a24cac5..86ec76e373 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs @@ -11,6 +11,9 @@ namespace osu.Game.Rulesets.Osu.Scoring { public class OsuScoreProcessor : ScoreProcessor { + protected override HitEvent CreateHitEvent(JudgementResult result) + => base.CreateHitEvent(result).With((result as OsuHitCircleJudgementResult)?.CursorPositionAtHit); + protected override JudgementResult CreateResult(HitObject hitObject, Judgement judgement) { switch (hitObject) diff --git a/osu.Game/Rulesets/Scoring/HitEvent.cs b/osu.Game/Rulesets/Scoring/HitEvent.cs index 908ac0c171..a2770ec580 100644 --- a/osu.Game/Rulesets/Scoring/HitEvent.cs +++ b/osu.Game/Rulesets/Scoring/HitEvent.cs @@ -44,5 +44,7 @@ namespace osu.Game.Rulesets.Scoring LastHitObject = lastHitObject; PositionOffset = positionOffset; } + + public HitEvent With(Vector2? positionOffset) => new HitEvent(TimeOffset, Result, HitObject, LastHitObject, positionOffset); } } From 34a8fcfd2f6a3cd1d380400801c95e02930016c2 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 20:53:24 +0900 Subject: [PATCH 2128/2376] Fix potential off-by-one --- .../Ranking/Statistics/HitEventTimingDistributionGraph.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs index 4acbc7da3c..43de862007 100644 --- a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs +++ b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs @@ -58,7 +58,7 @@ namespace osu.Game.Screens.Ranking.Statistics return; int[] bins = new int[total_timing_distribution_bins]; - double binSize = hitEvents.Max(e => Math.Abs(e.TimeOffset)) / timing_distribution_bins; + double binSize = Math.Ceiling(hitEvents.Max(e => Math.Abs(e.TimeOffset)) / timing_distribution_bins); foreach (var e in hitEvents) { From 5ce2c712d343e46e939f62d36f9ad39e047e414d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 20:53:43 +0900 Subject: [PATCH 2129/2376] Fix statistics not being wrapped by containers --- osu.Game/Rulesets/Ruleset.cs | 5 +++++ .../Ranking/Statistics/StatisticContainer.cs | 10 ++++++++-- .../Ranking/Statistics/StatisticItem.cs | 20 +++++++++++++++++++ .../Ranking/Statistics/StatisticRow.cs | 5 ++++- .../Ranking/Statistics/StatisticsPanel.cs | 11 ++++++++-- 5 files changed, 46 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 52784e354f..a325e641a4 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -210,6 +210,11 @@ namespace osu.Game.Rulesets /// An empty frame for the current ruleset, or null if unsupported. public virtual IConvertibleReplayFrame CreateConvertibleReplayFrame() => null; + /// + /// Creates the statistics for a to be displayed in the results screen. + /// + /// The to create the statistics for. The score is guaranteed to have populated. + /// The s to display. Each may contain 0 or more . [NotNull] public virtual StatisticRow[] CreateStatistics(ScoreInfo score) => Array.Empty(); } diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs index b8dde8f85e..b063893633 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs @@ -19,9 +19,13 @@ namespace osu.Game.Screens.Ranking.Statistics public StatisticContainer(string name) { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + InternalChild = new GridContainer { - RelativeSizeAxes = Axes.Both, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, Content = new[] { new Drawable[] @@ -56,13 +60,15 @@ namespace osu.Game.Screens.Ranking.Statistics { content = new Container { - RelativeSizeAxes = Axes.Both + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, } }, }, RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), } }; } diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs index 2605ae9f1b..a3ef5bf99e 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs @@ -7,12 +7,32 @@ using osu.Framework.Graphics.Containers; namespace osu.Game.Screens.Ranking.Statistics { + /// + /// An item to be displayed in a row of statistics inside the results screen. + /// public class StatisticItem { + /// + /// The name of this item. + /// public readonly string Name; + + /// + /// The content to be displayed. + /// public readonly Drawable Content; + + /// + /// The of this row. This can be thought of as the column dimension of an encompassing . + /// public readonly Dimension Dimension; + /// + /// Creates a new , to be displayed inside a in the results screen. + /// + /// The name of this item. + /// The content to be displayed. + /// The of this row. This can be thought of as the column dimension of an encompassing . public StatisticItem([NotNull] string name, [NotNull] Drawable content, [CanBeNull] Dimension dimension = null) { Name = name; diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticRow.cs b/osu.Game/Screens/Ranking/Statistics/StatisticRow.cs index ebab148fc2..e1ca9799a3 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticRow.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticRow.cs @@ -5,12 +5,15 @@ using JetBrains.Annotations; namespace osu.Game.Screens.Ranking.Statistics { + /// + /// A row of statistics to be displayed in the results screen. + /// public class StatisticRow { /// /// The columns of this . /// - [ItemCanBeNull] + [ItemNotNull] public StatisticItem[] Columns; } } diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 3d81229ac3..328b6933a0 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -81,8 +81,15 @@ namespace osu.Game.Screens.Ranking.Statistics { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Content = new[] { row.Columns?.Select(c => c?.Content).ToArray() }, - ColumnDimensions = Enumerable.Range(0, row.Columns?.Length ?? 0).Select(i => row.Columns[i]?.Dimension ?? new Dimension()).ToArray(), + Content = new[] + { + row.Columns?.Select(c => new StatisticContainer(c.Name) + { + Child = c.Content + }).Cast().ToArray() + }, + ColumnDimensions = Enumerable.Range(0, row.Columns?.Length ?? 0) + .Select(i => row.Columns[i].Dimension ?? new Dimension()).ToArray(), RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) } }); } From 8aea8267fb989172c535a331aacddcfbe048e3b7 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 20:58:05 +0900 Subject: [PATCH 2130/2376] Add some padding --- osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs index b063893633..d9e5e1294a 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs @@ -62,6 +62,7 @@ namespace osu.Game.Screens.Ranking.Statistics { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Top = 15 } } }, }, From 89a863a3379262c600f9774728e63117c8037567 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 21:02:20 +0900 Subject: [PATCH 2131/2376] Refactor OsuRuleset --- osu.Game.Rulesets.Osu/OsuRuleset.cs | 33 ++++++++++++----------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index aa313c92b3..3fb8f574b3 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -192,29 +192,24 @@ namespace osu.Game.Rulesets.Osu public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new OsuRulesetConfigManager(settings, RulesetInfo); - public override StatisticRow[] CreateStatistics(ScoreInfo score) + public override StatisticRow[] CreateStatistics(ScoreInfo score) => new[] { - var hitCircleEvents = score.HitEvents.Where(e => e.HitObject is HitCircle).ToList(); - - return new[] + new StatisticRow { - new StatisticRow + Columns = new[] { - Columns = new[] + new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents.Where(e => e.HitObject is HitCircle).ToList()) { - new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(hitCircleEvents) - { - RelativeSizeAxes = Axes.X, - Height = 130 - }), - new StatisticItem("Accuracy Heatmap", new Heatmap(score) - { - RelativeSizeAxes = Axes.X, - Height = 130 - }, new Dimension(GridSizeMode.Absolute, 130)), - } + RelativeSizeAxes = Axes.X, + Height = 130 + }), + new StatisticItem("Accuracy Heatmap", new Heatmap(score) + { + RelativeSizeAxes = Axes.X, + Height = 130 + }, new Dimension(GridSizeMode.Absolute, 130)), } - }; - } + } + }; } } From 49997c54d01be791bcc1bdfe4d2ee52abf063edb Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 21:14:17 +0900 Subject: [PATCH 2132/2376] Remove unused class --- osu.Game.Rulesets.Osu/Scoring/HitOffset.cs | 23 ---------------------- 1 file changed, 23 deletions(-) delete mode 100644 osu.Game.Rulesets.Osu/Scoring/HitOffset.cs diff --git a/osu.Game.Rulesets.Osu/Scoring/HitOffset.cs b/osu.Game.Rulesets.Osu/Scoring/HitOffset.cs deleted file mode 100644 index e6a5a01b48..0000000000 --- a/osu.Game.Rulesets.Osu/Scoring/HitOffset.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osuTK; - -namespace osu.Game.Rulesets.Osu.Scoring -{ - public class HitOffset - { - public readonly Vector2 Position1; - public readonly Vector2 Position2; - public readonly Vector2 HitPosition; - public readonly float Radius; - - public HitOffset(Vector2 position1, Vector2 position2, Vector2 hitPosition, float radius) - { - Position1 = position1; - Position2 = position2; - HitPosition = hitPosition; - Radius = radius; - } - } -} From 863666f7c483d83dda88a3acf9b1bb1b33f70bce Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 21:14:31 +0900 Subject: [PATCH 2133/2376] Move accuracy heatmap to osu! ruleset, rename, remove magic number --- .../TestSceneAccuracyHeatmap.cs | 16 ++++++++-------- osu.Game.Rulesets.Osu/OsuRuleset.cs | 2 +- .../{Heatmap.cs => AccuracyHeatmap.cs} | 11 ++++++----- 3 files changed, 15 insertions(+), 14 deletions(-) rename {osu.Game.Tests/Visual/Ranking => osu.Game.Rulesets.Osu.Tests}/TestSceneAccuracyHeatmap.cs (86%) rename osu.Game.Rulesets.Osu/Statistics/{Heatmap.cs => AccuracyHeatmap.cs} (95%) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs similarity index 86% rename from osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs rename to osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs index d8b0594803..f2a36ea017 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs @@ -9,21 +9,21 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Framework.Threading; using osu.Framework.Utils; -using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Statistics; using osu.Game.Scoring; using osu.Game.Tests.Beatmaps; +using osu.Game.Tests.Visual; using osuTK; using osuTK.Graphics; -namespace osu.Game.Tests.Visual.Ranking +namespace osu.Game.Rulesets.Osu.Tests { public class TestSceneAccuracyHeatmap : OsuManualInputManagerTestScene { private Box background; private Drawable object1; private Drawable object2; - private TestHeatmap heatmap; + private TestAccuracyHeatmap accuracyHeatmap; private ScheduledDelegate automaticAdditionDelegate; [SetUp] @@ -48,7 +48,7 @@ namespace osu.Game.Tests.Visual.Ranking { Position = new Vector2(100, 300), }, - heatmap = new TestHeatmap(new ScoreInfo { Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }) + accuracyHeatmap = new TestAccuracyHeatmap(new ScoreInfo { Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -69,7 +69,7 @@ namespace osu.Game.Tests.Visual.Ranking RNG.NextSingle(object1.DrawPosition.Y - object1.DrawSize.Y / 2, object1.DrawPosition.Y + object1.DrawSize.Y / 2)); // The background is used for ToLocalSpace() since we need to go _inside_ the DrawSizePreservingContainer (Content of TestScene). - heatmap.AddPoint(object2.Position, object1.Position, randomPos, RNG.NextSingle(10, 500)); + accuracyHeatmap.AddPoint(object2.Position, object1.Position, randomPos, RNG.NextSingle(10, 500)); InputManager.MoveMouseTo(background.ToScreenSpace(randomPos)); }, 1, true); }); @@ -85,13 +85,13 @@ namespace osu.Game.Tests.Visual.Ranking protected override bool OnMouseDown(MouseDownEvent e) { - heatmap.AddPoint(object2.Position, object1.Position, background.ToLocalSpace(e.ScreenSpaceMouseDownPosition), 50); + accuracyHeatmap.AddPoint(object2.Position, object1.Position, background.ToLocalSpace(e.ScreenSpaceMouseDownPosition), 50); return true; } - private class TestHeatmap : Heatmap + private class TestAccuracyHeatmap : AccuracyHeatmap { - public TestHeatmap(ScoreInfo score) + public TestAccuracyHeatmap(ScoreInfo score) : base(score) { } diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 3fb8f574b3..65f26c0647 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -203,7 +203,7 @@ namespace osu.Game.Rulesets.Osu RelativeSizeAxes = Axes.X, Height = 130 }), - new StatisticItem("Accuracy Heatmap", new Heatmap(score) + new StatisticItem("Accuracy Heatmap", new AccuracyHeatmap(score) { RelativeSizeAxes = Axes.X, Height = 130 diff --git a/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs similarity index 95% rename from osu.Game.Rulesets.Osu/Statistics/Heatmap.cs rename to osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs index 86cb8e682f..10ca3eb9be 100644 --- a/osu.Game.Rulesets.Osu/Statistics/Heatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs @@ -9,6 +9,7 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Utils; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Scoring; using osuTK; @@ -16,17 +17,17 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Statistics { - public class Heatmap : CompositeDrawable + public class AccuracyHeatmap : CompositeDrawable { /// - /// Size of the inner circle containing the "hit" points, relative to the size of this . + /// Size of the inner circle containing the "hit" points, relative to the size of this . /// All other points outside of the inner circle are "miss" points. /// private const float inner_portion = 0.8f; /// /// Number of rows/columns of points. - /// 4px per point @ 128x128 size (the contents of the are always square). 1024 total points. + /// 4px per point @ 128x128 size (the contents of the are always square). 1024 total points. /// private const int points_per_dimension = 32; @@ -36,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Statistics private readonly ScoreInfo score; - public Heatmap(ScoreInfo score) + public AccuracyHeatmap(ScoreInfo score) { this.score = score; } @@ -170,7 +171,7 @@ namespace osu.Game.Rulesets.Osu.Statistics // Convert the above into the local search space. Vector2 localCentre = new Vector2(points_per_dimension) / 2; float localRadius = localCentre.X * inner_portion * normalisedDistance; // The radius inside the inner portion which of the heatmap which the closest point lies. - double localAngle = finalAngle + 3 * Math.PI / 4; // The angle inside the heatmap on which the closest point lies. + double localAngle = finalAngle + Math.PI - MathUtils.DegreesToRadians(rotation); // The angle inside the heatmap on which the closest point lies. Vector2 localPoint = localCentre + localRadius * new Vector2((float)Math.Cos(localAngle), (float)Math.Sin(localAngle)); // Find the most relevant hit point. From 81ad257a17ae2bab2df18ab9ceddb1e77202c149 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 21:18:58 +0900 Subject: [PATCH 2134/2376] Add timing distribution to mania ruleset --- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index a37aaa8cc4..b8725af856 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -30,6 +30,7 @@ using osu.Game.Rulesets.Mania.Skinning; using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; using osu.Game.Scoring; +using osu.Game.Screens.Ranking.Statistics; namespace osu.Game.Rulesets.Mania { @@ -307,6 +308,21 @@ namespace osu.Game.Rulesets.Mania { return (PlayfieldType)Enum.GetValues(typeof(PlayfieldType)).Cast().OrderByDescending(i => i).First(v => variant >= v); } + + public override StatisticRow[] CreateStatistics(ScoreInfo score) => new[] + { + new StatisticRow + { + Columns = new[] + { + new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents) + { + RelativeSizeAxes = Axes.X, + Height = 130 + }), + } + } + }; } public enum PlayfieldType From 25abdc290331e6d2ffea212259771d25cbf5b3b7 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 21:41:48 +0900 Subject: [PATCH 2135/2376] General cleanups --- osu.Game/Rulesets/Scoring/HitEvent.cs | 18 +++++++- osu.Game/Rulesets/Scoring/HitWindows.cs | 2 +- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 12 ------ osu.Game/Screens/Ranking/ScorePanel.cs | 18 +++++--- osu.Game/Screens/Ranking/ScorePanelList.cs | 43 ++++++++++++++----- .../Ranking/ScorePanelTrackingContainer.cs | 17 +++++++- .../Ranking/Statistics/StatisticContainer.cs | 23 ++++++---- .../Ranking/Statistics/StatisticItem.cs | 4 +- .../Ranking/Statistics/StatisticsPanel.cs | 9 ++-- 9 files changed, 98 insertions(+), 48 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/HitEvent.cs b/osu.Game/Rulesets/Scoring/HitEvent.cs index a2770ec580..ea2975a6c4 100644 --- a/osu.Game/Rulesets/Scoring/HitEvent.cs +++ b/osu.Game/Rulesets/Scoring/HitEvent.cs @@ -7,6 +7,9 @@ using osuTK; namespace osu.Game.Rulesets.Scoring { + /// + /// A generated by the containing extra statistics around a . + /// public readonly struct HitEvent { /// @@ -31,11 +34,19 @@ namespace osu.Game.Rulesets.Scoring public readonly HitObject LastHitObject; /// - /// The player's position offset, if available, at the time of the event. + /// A position offset, if available, at the time of the event. /// [CanBeNull] public readonly Vector2? PositionOffset; + /// + /// Creates a new . + /// + /// The time offset from the end of at which the event occurs. + /// The . + /// The that triggered the event. + /// The previous . + /// A positional offset. public HitEvent(double timeOffset, HitResult result, HitObject hitObject, [CanBeNull] HitObject lastHitObject, [CanBeNull] Vector2? positionOffset) { TimeOffset = timeOffset; @@ -45,6 +56,11 @@ namespace osu.Game.Rulesets.Scoring PositionOffset = positionOffset; } + /// + /// Creates a new with an optional positional offset. + /// + /// The positional offset. + /// The new . public HitEvent With(Vector2? positionOffset) => new HitEvent(TimeOffset, Result, HitObject, LastHitObject, positionOffset); } } diff --git a/osu.Game/Rulesets/Scoring/HitWindows.cs b/osu.Game/Rulesets/Scoring/HitWindows.cs index 77acbd4137..018b50bd3d 100644 --- a/osu.Game/Rulesets/Scoring/HitWindows.cs +++ b/osu.Game/Rulesets/Scoring/HitWindows.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Scoring /// Retrieves the with the largest hit window that produces a successful hit. /// /// The lowest allowed successful . - public HitResult LowestSuccessfulHitResult() + protected HitResult LowestSuccessfulHitResult() { for (var result = HitResult.Meh; result <= HitResult.Perfect; ++result) { diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index b9f51dfad3..22ec023f58 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -136,12 +136,6 @@ namespace osu.Game.Rulesets.Scoring lastHitObject = result.HitObject; updateScore(); - - OnResultApplied(result); - } - - protected virtual void OnResultApplied(JudgementResult result) - { } protected virtual HitEvent CreateHitEvent(JudgementResult result) @@ -174,12 +168,6 @@ namespace osu.Game.Rulesets.Scoring hitEvents.RemoveAt(hitEvents.Count - 1); updateScore(); - - OnResultReverted(result); - } - - protected virtual void OnResultReverted(JudgementResult result) - { } private void updateScore() diff --git a/osu.Game/Screens/Ranking/ScorePanel.cs b/osu.Game/Screens/Ranking/ScorePanel.cs index 257279bdc9..9633f5c533 100644 --- a/osu.Game/Screens/Ranking/ScorePanel.cs +++ b/osu.Game/Screens/Ranking/ScorePanel.cs @@ -76,12 +76,11 @@ namespace osu.Game.Screens.Ranking private static readonly Color4 contracted_middle_layer_colour = Color4Extensions.FromHex("#353535"); public event Action StateChanged; - public Action PostExpandAction; /// - /// Whether this can enter into an state. + /// An action to be invoked if this is clicked while in an expanded state. /// - public bool CanExpand = true; + public Action PostExpandAction; public readonly ScoreInfo Score; @@ -250,6 +249,7 @@ namespace osu.Game.Screens.Ranking { base.Size = value; + // Auto-size isn't used to avoid 1-frame issues and because the score panel is removed/re-added to the container. if (trackingContainer != null) trackingContainer.Size = value; } @@ -259,8 +259,7 @@ namespace osu.Game.Screens.Ranking { if (State == PanelState.Contracted) { - if (CanExpand) - State = PanelState.Expanded; + State = PanelState.Expanded; return true; } @@ -276,6 +275,15 @@ namespace osu.Game.Screens.Ranking private ScorePanelTrackingContainer trackingContainer; + /// + /// Creates a which this can reside inside. + /// The will track the size of this . + /// + /// + /// This is immediately added as a child of the . + /// + /// The . + /// If a already exists. public ScorePanelTrackingContainer CreateTrackingContainer() { if (trackingContainer != null) diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index 8f9064c2d1..9ebd7822c0 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -26,12 +26,13 @@ namespace osu.Game.Screens.Ranking /// private const float expanded_panel_spacing = 15; + /// + /// An action to be invoked if a is clicked while in an expanded state. + /// public Action PostExpandAction; public readonly Bindable SelectedScore = new Bindable(); - public float CurrentScrollPosition => scroll.Current; - private readonly Flow flow; private readonly Scroll scroll; private ScorePanel expandedPanel; @@ -47,16 +48,13 @@ namespace osu.Game.Screens.Ranking { RelativeSizeAxes = Axes.Both, HandleScroll = () => expandedPanel?.IsHovered != true, // handle horizontal scroll only when not hovering the expanded panel. - Children = new Drawable[] + Child = flow = new Flow { - flow = new Flow - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(panel_spacing, 0), - AutoSizeAxes = Axes.Both, - }, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(panel_spacing, 0), + AutoSizeAxes = Axes.Both, } }; } @@ -166,6 +164,10 @@ namespace osu.Game.Screens.Ranking private bool handleInput = true; + /// + /// Whether this or any of the s contained should handle scroll or click input. + /// Setting to false will also hide the scrollbar. + /// public bool HandleInput { get => handleInput; @@ -180,10 +182,24 @@ namespace osu.Game.Screens.Ranking public override bool PropagateNonPositionalInputSubTree => HandleInput && base.PropagateNonPositionalInputSubTree; + /// + /// Enumerates all s contained in this . + /// + /// public IEnumerable GetScorePanels() => flow.Select(t => t.Panel); + /// + /// Finds the corresponding to a . + /// + /// The to find the corresponding for. + /// The . public ScorePanel GetPanelForScore(ScoreInfo score) => flow.Single(t => t.Panel.Score == score).Panel; + /// + /// Detaches a from its , allowing the panel to be moved elsewhere in the hierarchy. + /// + /// The to detach. + /// If is not a part of this . public void Detach(ScorePanel panel) { var container = flow.FirstOrDefault(t => t.Panel == panel); @@ -193,6 +209,11 @@ namespace osu.Game.Screens.Ranking container.Detach(); } + /// + /// Attaches a to its in this . + /// + /// The to attach. + /// If is not a part of this . public void Attach(ScorePanel panel) { var container = flow.FirstOrDefault(t => t.Panel == panel); diff --git a/osu.Game/Screens/Ranking/ScorePanelTrackingContainer.cs b/osu.Game/Screens/Ranking/ScorePanelTrackingContainer.cs index f6f26d0f8a..c8010d1c32 100644 --- a/osu.Game/Screens/Ranking/ScorePanelTrackingContainer.cs +++ b/osu.Game/Screens/Ranking/ScorePanelTrackingContainer.cs @@ -6,16 +6,27 @@ using osu.Framework.Graphics.Containers; namespace osu.Game.Screens.Ranking { + /// + /// A which tracks the size of a , to which the can be added or removed. + /// public class ScorePanelTrackingContainer : CompositeDrawable { + /// + /// The that created this . + /// public readonly ScorePanel Panel; - public ScorePanelTrackingContainer(ScorePanel panel) + internal ScorePanelTrackingContainer(ScorePanel panel) { Panel = panel; Attach(); } + /// + /// Detaches the from this , removing it as a child. + /// This will continue tracking any size changes. + /// + /// If the is already detached. public void Detach() { if (InternalChildren.Count == 0) @@ -24,6 +35,10 @@ namespace osu.Game.Screens.Ranking RemoveInternal(Panel); } + /// + /// Attaches the to this , adding it as a child. + /// + /// If the is already attached. public void Attach() { if (InternalChildren.Count > 0) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs index d9e5e1294a..ed98698411 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Diagnostics.CodeAnalysis; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -11,13 +12,16 @@ using osuTK; namespace osu.Game.Screens.Ranking.Statistics { - internal class StatisticContainer : Container + /// + /// Wraps a to add a header and suitable layout for use in . + /// + internal class StatisticContainer : CompositeDrawable { - protected override Container Content => content; - - private readonly Container content; - - public StatisticContainer(string name) + /// + /// Creates a new . + /// + /// The to display. + public StatisticContainer([NotNull] StatisticItem item) { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; @@ -50,7 +54,7 @@ namespace osu.Game.Screens.Ranking.Statistics { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Text = name, + Text = item.Name, Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold), } } @@ -58,11 +62,12 @@ namespace osu.Game.Screens.Ranking.Statistics }, new Drawable[] { - content = new Container + new Container { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Margin = new MarginPadding { Top = 15 } + Margin = new MarginPadding { Top = 15 }, + Child = item.Content } }, }, diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs index a3ef5bf99e..e959ed24fc 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs @@ -30,9 +30,9 @@ namespace osu.Game.Screens.Ranking.Statistics /// /// Creates a new , to be displayed inside a in the results screen. /// - /// The name of this item. + /// The name of the item. /// The content to be displayed. - /// The of this row. This can be thought of as the column dimension of an encompassing . + /// The of this item. This can be thought of as the column dimension of an encompassing . public StatisticItem([NotNull] string name, [NotNull] Drawable content, [CanBeNull] Dimension dimension = null) { Name = name; diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 328b6933a0..c560cc9852 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -83,10 +83,7 @@ namespace osu.Game.Screens.Ranking.Statistics AutoSizeAxes = Axes.Y, Content = new[] { - row.Columns?.Select(c => new StatisticContainer(c.Name) - { - Child = c.Content - }).Cast().ToArray() + row.Columns?.Select(c => new StatisticContainer(c)).Cast().ToArray() }, ColumnDimensions = Enumerable.Range(0, row.Columns?.Length ?? 0) .Select(i => row.Columns[i].Dimension ?? new Dimension()).ToArray(), @@ -105,8 +102,8 @@ namespace osu.Game.Screens.Ranking.Statistics } } - protected override void PopIn() => this.FadeIn(); + protected override void PopIn() => this.FadeIn(150, Easing.OutQuint); - protected override void PopOut() => this.FadeOut(); + protected override void PopOut() => this.FadeOut(150, Easing.OutQuint); } } From 49bdd897758bf918224e8fafa8a0e02e9d0af70a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 21:54:09 +0900 Subject: [PATCH 2136/2376] Cleanup ReplayPlayer adjustments --- osu.Game/Screens/Play/Player.cs | 2 +- osu.Game/Screens/Play/ReplayPlayer.cs | 21 +++++++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 83991ad027..cfcef5155d 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -460,7 +460,7 @@ namespace osu.Game.Screens.Play { var score = new ScoreInfo { - Beatmap = Beatmap.Value.BeatmapInfo, + Beatmap = gameplayBeatmap.BeatmapInfo, Ruleset = rulesetInfo, Mods = Mods.Value.ToArray(), }; diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index d7580ea271..8a925958fd 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.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.Screens; using osu.Game.Scoring; using osu.Game.Screens.Ranking; @@ -25,16 +24,18 @@ namespace osu.Game.Screens.Play DrawableRuleset?.SetReplayScore(score); } - protected override void GotoRanking() - { - this.Push(CreateResults(CreateScore())); - } - protected override ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, false); - // protected override ScoreInfo CreateScore() - // { - // return score.ScoreInfo; - // } + protected override ScoreInfo CreateScore() + { + var baseScore = base.CreateScore(); + + // Since the replay score doesn't contain statistics, we'll pass them through here. + // We also have to pass in the beatmap to get the post-mod-application version. + score.ScoreInfo.Beatmap = baseScore.Beatmap; + score.ScoreInfo.HitEvents = baseScore.HitEvents; + + return score.ScoreInfo; + } } } From 740b01c049592483adb0dbc6f8e0b2d4cbf9e9d5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 22:05:58 +0900 Subject: [PATCH 2137/2376] Add xmldoc --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 22ec023f58..9c1bc35169 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -138,6 +138,11 @@ namespace osu.Game.Rulesets.Scoring updateScore(); } + /// + /// Creates the that describes a . + /// + /// The to describe. + /// The . protected virtual HitEvent CreateHitEvent(JudgementResult result) => new HitEvent(result.TimeOffset, result.Type, result.HitObject, lastHitObject, null); From 486b899e8f31c262b5ab911792bb5269c0edc880 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 22:11:29 +0900 Subject: [PATCH 2138/2376] Rename method --- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 2 +- osu.Game.Rulesets.Osu/OsuRuleset.cs | 2 +- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 2 +- osu.Game/Rulesets/Ruleset.cs | 2 +- osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index b8725af856..44e8f343d5 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -309,7 +309,7 @@ namespace osu.Game.Rulesets.Mania return (PlayfieldType)Enum.GetValues(typeof(PlayfieldType)).Cast().OrderByDescending(i => i).First(v => variant >= v); } - public override StatisticRow[] CreateStatistics(ScoreInfo score) => new[] + public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score) => new[] { new StatisticRow { diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 65f26c0647..8222eba339 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -192,7 +192,7 @@ namespace osu.Game.Rulesets.Osu public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new OsuRulesetConfigManager(settings, RulesetInfo); - public override StatisticRow[] CreateStatistics(ScoreInfo score) => new[] + public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score) => new[] { new StatisticRow { diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index cd4e699262..92b04e8397 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -159,7 +159,7 @@ namespace osu.Game.Rulesets.Taiko public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame(); - public override StatisticRow[] CreateStatistics(ScoreInfo score) => new[] + public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score) => new[] { new StatisticRow { diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index a325e641a4..f9c2b09be9 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -216,6 +216,6 @@ namespace osu.Game.Rulesets /// The to create the statistics for. The score is guaranteed to have populated. /// The s to display. Each may contain 0 or more . [NotNull] - public virtual StatisticRow[] CreateStatistics(ScoreInfo score) => Array.Empty(); + public virtual StatisticRow[] CreateStatisticsForScore(ScoreInfo score) => Array.Empty(); } } diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index c560cc9852..efb9397a23 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -75,7 +75,7 @@ namespace osu.Game.Screens.Ranking.Statistics Spacing = new Vector2(30, 15), }; - foreach (var row in newScore.Ruleset.CreateInstance().CreateStatistics(newScore)) + foreach (var row in newScore.Ruleset.CreateInstance().CreateStatisticsForScore(newScore)) { rows.Add(new GridContainer { From 4cb49cd606a57ec3aa4770ad8ab1caf5b11eeaee Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 22:21:34 +0900 Subject: [PATCH 2139/2376] Add minimum height to the timing distribution graph --- .../Ranking/Statistics/HitEventTimingDistributionGraph.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs index 43de862007..9b46bea2cb 100644 --- a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs +++ b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs @@ -69,7 +69,7 @@ namespace osu.Game.Screens.Ranking.Statistics int maxCount = bins.Max(); var bars = new Drawable[total_timing_distribution_bins]; for (int i = 0; i < bars.Length; i++) - bars[i] = new Bar { Height = (float)bins[i] / maxCount }; + bars[i] = new Bar { Height = Math.Max(0.05f, (float)bins[i] / maxCount) }; Container axisFlow; From 2814433d7cf572a78218db8da6c055e7139e1962 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 22:22:07 +0900 Subject: [PATCH 2140/2376] Rename test file --- ...butionGraph.cs => TestSceneHitEventTimingDistributionGraph.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename osu.Game.Tests/Visual/Ranking/{TestSceneTimingDistributionGraph.cs => TestSceneHitEventTimingDistributionGraph.cs} (100%) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs b/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs similarity index 100% rename from osu.Game.Tests/Visual/Ranking/TestSceneTimingDistributionGraph.cs rename to osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs From 22f3fd487b9cb7f346f068200b02f4fdf611e677 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 22:43:25 +0900 Subject: [PATCH 2141/2376] Mark test as headless --- osu.Game.Tests/Gameplay/TestSceneGameplayClockContainer.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/Gameplay/TestSceneGameplayClockContainer.cs b/osu.Game.Tests/Gameplay/TestSceneGameplayClockContainer.cs index a97566ba7b..cd3669f160 100644 --- a/osu.Game.Tests/Gameplay/TestSceneGameplayClockContainer.cs +++ b/osu.Game.Tests/Gameplay/TestSceneGameplayClockContainer.cs @@ -3,6 +3,7 @@ using System; using NUnit.Framework; +using osu.Framework.Testing; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Screens.Play; @@ -10,6 +11,7 @@ using osu.Game.Tests.Visual; namespace osu.Game.Tests.Gameplay { + [HeadlessTest] public class TestSceneGameplayClockContainer : OsuTestScene { [Test] From 037bd3b46330bc2f2942bf761f0e883455b02d5c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Jun 2020 22:47:55 +0900 Subject: [PATCH 2142/2376] Fix possible nullref --- osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs | 6 ++++++ osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs | 3 +++ 2 files changed, 9 insertions(+) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index 210abaef4e..8700fbeb42 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -29,6 +29,12 @@ namespace osu.Game.Tests.Visual.Ranking loadPanel(new TestScoreInfo(new OsuRuleset().RulesetInfo)); } + [Test] + public void TestNullScore() + { + loadPanel(null); + } + private void loadPanel(ScoreInfo score) => AddStep("load panel", () => { Child = new StatisticsPanel diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index efb9397a23..cac2bf866b 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -62,6 +62,9 @@ namespace osu.Game.Screens.Ranking.Statistics var newScore = score.NewValue; + if (newScore == null) + return; + if (newScore.HitEvents == null || newScore.HitEvents.Count == 0) content.Add(new MessagePlaceholder("Score has no statistics :(")); else From 3021bdf3059fc5d9ce2165d3594828a65b5ca4fc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 20 Jun 2020 00:34:01 +0900 Subject: [PATCH 2143/2376] 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 b95b794004..119c309675 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 6ec57e5100..bec3bc9d39 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -24,7 +24,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 0bfff24805..de5130b66a 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From 0046cc08e913572866146cebdb512aed9f2ec725 Mon Sep 17 00:00:00 2001 From: Ronnie Moir <7267697+H2n9@users.noreply.github.com> Date: Fri, 19 Jun 2020 18:40:36 +0100 Subject: [PATCH 2144/2376] Add test cases for different mods and rates. Cleanup test scene. --- .../Gameplay/TestSceneStoryboardSamples.cs | 44 ++++++++++++------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs index 60911d6792..0803da6678 100644 --- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs @@ -10,9 +10,8 @@ using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.IO.Stores; using osu.Framework.Testing; -using osu.Framework.Timing; using osu.Game.Audio; -using osu.Game.Beatmaps; +using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; @@ -73,26 +72,39 @@ namespace osu.Game.Tests.Gameplay AddUntilStep("sample playback succeeded", () => sample.LifetimeEnd < double.MaxValue); } - [Test] - public void TestSamplePlaybackWithRateMods() + [TestCase(typeof(OsuModDoubleTime), 1.5)] + [TestCase(typeof(OsuModHalfTime), 0.75)] + [TestCase(typeof(ModWindUp), 1.5)] + [TestCase(typeof(ModWindDown), 0.75)] + [TestCase(typeof(OsuModDoubleTime), 2)] + [TestCase(typeof(OsuModHalfTime), 0.5)] + [TestCase(typeof(ModWindUp), 2)] + [TestCase(typeof(ModWindDown), 0.5)] + public void TestSamplePlaybackWithRateMods(Type expectedMod, double expectedRate) { GameplayClockContainer gameplayContainer = null; TestDrawableStoryboardSample sample = null; - OsuModDoubleTime doubleTimeMod = null; + Mod testedMod = Activator.CreateInstance(expectedMod) as Mod; - AddStep("create container", () => + switch (testedMod) { - var beatmap = Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); + case ModRateAdjust m: + m.SpeedChange.Value = expectedRate; + break; - Add(gameplayContainer = new GameplayClockContainer(beatmap, new[] { doubleTimeMod = new OsuModDoubleTime() }, 0)); + case ModTimeRamp m: + m.SpeedChange.Value = expectedRate; + break; + } - SelectedMods.Value = new[] { doubleTimeMod }; - Beatmap.Value = new TestCustomSkinWorkingBeatmap(beatmap.Beatmap, gameplayContainer.GameplayClock, Audio); - }); - - AddStep("create storyboard sample", () => + AddStep("setup storyboard sample", () => { + Beatmap.Value = new TestCustomSkinWorkingBeatmap(new OsuRuleset().RulesetInfo, Audio); + SelectedMods.Value = new[] { testedMod }; + + Add(gameplayContainer = new GameplayClockContainer(Beatmap.Value, SelectedMods.Value, 0)); + gameplayContainer.Add(sample = new TestDrawableStoryboardSample(new StoryboardSampleInfo("test-sample", 1, 1)) { Clock = gameplayContainer.GameplayClock @@ -101,7 +113,7 @@ namespace osu.Game.Tests.Gameplay AddStep("start", () => gameplayContainer.Start()); - AddAssert("sample playback rate matches mod rates", () => sample.TestChannel.AggregateFrequency.Value == doubleTimeMod.SpeedChange.Value); + AddAssert("sample playback rate matches mod rates", () => sample.TestChannel.AggregateFrequency.Value == expectedRate); } private class TestSkin : LegacySkin @@ -138,8 +150,8 @@ namespace osu.Game.Tests.Gameplay { private readonly AudioManager audio; - public TestCustomSkinWorkingBeatmap(IBeatmap beatmap, IFrameBasedClock referenceClock, AudioManager audio) - : base(beatmap, null, referenceClock, audio) + public TestCustomSkinWorkingBeatmap(RulesetInfo ruleset, AudioManager audio) + : base(ruleset, null, audio) { this.audio = audio; } From 1d5084c35554939390b33db0c8d8191e0382724a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 19 Jun 2020 20:11:12 +0200 Subject: [PATCH 2145/2376] Use {Initial,Final}Rate instead of SpeedChange --- osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs index 0803da6678..295fcc5b58 100644 --- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs @@ -94,7 +94,7 @@ namespace osu.Game.Tests.Gameplay break; case ModTimeRamp m: - m.SpeedChange.Value = expectedRate; + m.InitialRate.Value = m.FinalRate.Value = expectedRate; break; } From 34476f6c2fa0d1e3ad922e46980fa9133302ee29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 19 Jun 2020 20:12:17 +0200 Subject: [PATCH 2146/2376] Delegate to base in a more consistent manner --- osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs index 295fcc5b58..b30870d057 100644 --- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs @@ -113,7 +113,7 @@ namespace osu.Game.Tests.Gameplay AddStep("start", () => gameplayContainer.Start()); - AddAssert("sample playback rate matches mod rates", () => sample.TestChannel.AggregateFrequency.Value == expectedRate); + AddAssert("sample playback rate matches mod rates", () => sample.Channel.AggregateFrequency.Value == expectedRate); } private class TestSkin : LegacySkin @@ -166,7 +166,7 @@ namespace osu.Game.Tests.Gameplay { } - public SampleChannel TestChannel => Channel; + public new SampleChannel Channel => base.Channel; } } } From 53861cdde81dec2aba2edfad6c92d89208a477ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 19 Jun 2020 20:13:43 +0200 Subject: [PATCH 2147/2376] Privatise setter --- osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs index 04df46410e..60cb9b94a6 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs @@ -21,7 +21,7 @@ namespace osu.Game.Storyboards.Drawables private readonly StoryboardSampleInfo sampleInfo; - protected SampleChannel Channel; + protected SampleChannel Channel { get; private set; } public override bool RemoveWhenNotAlive => false; From 470d5bfce3497c2c2a7048ee5db8a4a65249b153 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 19 Jun 2020 20:15:14 +0200 Subject: [PATCH 2148/2376] Invert if to reduce nesting --- .../Storyboards/Drawables/DrawableStoryboardSample.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs index 60cb9b94a6..8eaf9ac652 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs @@ -35,14 +35,13 @@ namespace osu.Game.Storyboards.Drawables private void load(IBindable beatmap, IBindable> mods) { Channel = beatmap.Value.Skin.GetSample(sampleInfo); + if (Channel == null) + return; - if (Channel != null) - { - Channel.Volume.Value = sampleInfo.Volume / 100.0; + Channel.Volume.Value = sampleInfo.Volume / 100.0; - foreach (var mod in mods.Value.OfType()) - mod.ApplyToSample(Channel); - } + foreach (var mod in mods.Value.OfType()) + mod.ApplyToSample(Channel); } protected override void Update() From 8298a2c8a936561714233e53e2111881dff91ba4 Mon Sep 17 00:00:00 2001 From: mcendu Date: Sat, 20 Jun 2020 14:53:25 +0800 Subject: [PATCH 2149/2376] inline stage light lookup and clarify behavior --- osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs | 2 +- osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs index 1a097405ac..b69827e2d9 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Mania.Skinning [BackgroundDependencyLoader] private void load(ISkinSource skin, IScrollingInfo scrollingInfo) { - string lightImage = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.LightImage, 0)?.Value + string lightImage = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.LightImage)?.Value ?? "mania-stage-light"; float leftLineWidth = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.LeftLineWidth) diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs index 7a2fa711e3..4114bf5628 100644 --- a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs @@ -128,7 +128,7 @@ namespace osu.Game.Rulesets.Mania.Skinning private Drawable getResult(HitResult result) { string filename = this.GetManiaSkinConfig(hitresult_mapping[result])?.Value - ?? default_hitresult_skin_filenames[result]; + ?? default_hitresult_skin_filenames[result]; return this.GetAnimation(filename, true, true); } From ca555a6a5271124ff0ff55d83185ba4f6e3a034b Mon Sep 17 00:00:00 2001 From: mcendu Date: Sat, 20 Jun 2020 14:56:39 +0800 Subject: [PATCH 2150/2376] rename per-column skin config retrieval to GetColumnSkinConfig Removed parameter "index"; all these cases should use extension instead --- osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs | 2 +- .../Skinning/LegacyColumnBackground.cs | 12 ++++++------ .../Skinning/LegacyHitExplosion.cs | 4 ++-- osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs | 4 ++-- .../Skinning/LegacyManiaColumnElement.cs | 4 ++-- osu.Game.Rulesets.Mania/Skinning/LegacyNotePiece.cs | 2 +- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs index 0c9bc97ba9..a749f80855 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Mania.Skinning [BackgroundDependencyLoader] private void load(ISkinSource skin, IScrollingInfo scrollingInfo, DrawableHitObject drawableObject) { - string imageName = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.HoldNoteBodyImage)?.Value + string imageName = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.HoldNoteBodyImage)?.Value ?? $"mania-note{FallbackColumnIndex}L"; sprite = skin.GetAnimation(imageName, true, true).With(d => diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs index b69827e2d9..64a7641421 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs @@ -35,25 +35,25 @@ namespace osu.Game.Rulesets.Mania.Skinning string lightImage = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.LightImage)?.Value ?? "mania-stage-light"; - float leftLineWidth = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.LeftLineWidth) + float leftLineWidth = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.LeftLineWidth) ?.Value ?? 1; - float rightLineWidth = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.RightLineWidth) + float rightLineWidth = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.RightLineWidth) ?.Value ?? 1; bool hasLeftLine = leftLineWidth > 0; bool hasRightLine = rightLineWidth > 0 && skin.GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value >= 2.4m || isLastColumn; - float lightPosition = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.LightPosition)?.Value + float lightPosition = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.LightPosition)?.Value ?? 0; - Color4 lineColour = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ColumnLineColour)?.Value + Color4 lineColour = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ColumnLineColour)?.Value ?? Color4.White; - Color4 backgroundColour = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour)?.Value + Color4 backgroundColour = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour)?.Value ?? Color4.Black; - Color4 lightColour = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ColumnLightColour)?.Value + Color4 lightColour = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ColumnLightColour)?.Value ?? Color4.White; InternalChildren = new Drawable[] diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs index ce0b9fe4b6..bc93bb2615 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs @@ -26,10 +26,10 @@ namespace osu.Game.Rulesets.Mania.Skinning [BackgroundDependencyLoader] private void load(ISkinSource skin, IScrollingInfo scrollingInfo) { - string imageName = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ExplosionImage)?.Value + string imageName = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ExplosionImage)?.Value ?? "lightingN"; - float explosionScale = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ExplosionScale)?.Value + float explosionScale = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ExplosionScale)?.Value ?? 1; // Create a temporary animation to retrieve the number of frames, in an effort to calculate the intended frame length. diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs index 7c8d1cd303..44f3e7d7b3 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs @@ -33,10 +33,10 @@ namespace osu.Game.Rulesets.Mania.Skinning [BackgroundDependencyLoader] private void load(ISkinSource skin, IScrollingInfo scrollingInfo) { - string upImage = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.KeyImage)?.Value + string upImage = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.KeyImage)?.Value ?? $"mania-key{FallbackColumnIndex}"; - string downImage = GetManiaSkinConfig(skin, LegacyManiaSkinConfigurationLookups.KeyImageDown)?.Value + string downImage = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.KeyImageDown)?.Value ?? $"mania-key{FallbackColumnIndex}D"; InternalChild = directionContainer = new Container diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs index 0c46a00bed..3c0c632c14 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Mania.Skinning } } - protected IBindable GetManiaSkinConfig(ISkin skin, LegacyManiaSkinConfigurationLookups lookup, int? index = null) - => skin.GetManiaSkinConfig(lookup, index ?? Column.Index); + protected IBindable GetColumnSkinConfig(ISkin skin, LegacyManiaSkinConfigurationLookups lookup) + => skin.GetManiaSkinConfig(lookup, Column.Index); } } diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyNotePiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyNotePiece.cs index 85523ae3c0..515c941d65 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyNotePiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyNotePiece.cs @@ -89,7 +89,7 @@ namespace osu.Game.Rulesets.Mania.Skinning break; } - string noteImage = GetManiaSkinConfig(skin, lookup)?.Value + string noteImage = GetColumnSkinConfig(skin, lookup)?.Value ?? $"mania-note{FallbackColumnIndex}{suffix}"; return skin.GetTexture(noteImage); From 19eb6fad7fca29af8f163ace52ba95e70383f542 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 21 Jun 2020 17:42:17 +0900 Subject: [PATCH 2151/2376] Make hold note ticks affect combo score rather than bonus --- osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs b/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs index 00b839f8ec..294aab1e4e 100644 --- a/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs +++ b/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs @@ -7,8 +7,6 @@ namespace osu.Game.Rulesets.Mania.Judgements { public class HoldNoteTickJudgement : ManiaJudgement { - public override bool AffectsCombo => false; - protected override int NumericResultFor(HitResult result) => 20; protected override double HealthIncreaseFor(HitResult result) From 44925b3951655b7a2659ac4b641da911f16461e0 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 21 Jun 2020 18:05:26 +0900 Subject: [PATCH 2152/2376] Reduce mania's HP drain by 20% --- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 2 ++ osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs | 10 +++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index a37aaa8cc4..6ddb052585 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -44,6 +44,8 @@ namespace osu.Game.Rulesets.Mania public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor(); + public override HealthProcessor CreateHealthProcessor(double drainStartTime) => new DrainingHealthProcessor(drainStartTime, 0.2); + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap, this); public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => new ManiaPerformanceCalculator(this, beatmap, score); diff --git a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs index 982f527517..2d3754841b 100644 --- a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs @@ -44,6 +44,7 @@ namespace osu.Game.Rulesets.Scoring private double gameplayEndTime; private readonly double drainStartTime; + private readonly double drainLenience; private readonly List<(double time, double health)> healthIncreases = new List<(double, double)>(); private double targetMinimumHealth; @@ -55,9 +56,14 @@ namespace osu.Game.Rulesets.Scoring /// Creates a new . /// /// The time after which draining should begin. - public DrainingHealthProcessor(double drainStartTime) + /// A lenience to apply to the default drain rate.
    + /// A value of 0 uses the default drain rate.
    + /// A value of 0.5 halves the drain rate.
    + /// A value of 1 completely removes drain. + public DrainingHealthProcessor(double drainStartTime, double drainLenience = 0) { this.drainStartTime = drainStartTime; + this.drainLenience = drainLenience; } protected override void Update() @@ -95,6 +101,8 @@ namespace osu.Game.Rulesets.Scoring ))); targetMinimumHealth = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, min_health_target, mid_health_target, max_health_target); + targetMinimumHealth += drainLenience * (1 - targetMinimumHealth); + targetMinimumHealth = Math.Min(1, targetMinimumHealth); base.ApplyBeatmap(beatmap); } From 9fbe2fa80a140eef3a8babf39c02bd003db56bb9 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 21 Jun 2020 19:31:00 +0900 Subject: [PATCH 2153/2376] Add comments, change to clamp --- osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs index 2d3754841b..ef341575fa 100644 --- a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs @@ -101,8 +101,12 @@ namespace osu.Game.Rulesets.Scoring ))); targetMinimumHealth = BeatmapDifficulty.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); - targetMinimumHealth = Math.Min(1, targetMinimumHealth); + + // Ensure the target HP is within an acceptable range. + targetMinimumHealth = Math.Clamp(targetMinimumHealth, 0, 1); base.ApplyBeatmap(beatmap); } From 599543acb6cf7fa44ff518d7f43eba47ec2e53b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 21 Jun 2020 18:02:09 +0200 Subject: [PATCH 2154/2376] Extract abstract hitobject sample test class --- .../Gameplay/HitObjectSampleTest.cs | 183 ++++++++++++++ .../Gameplay/TestSceneHitObjectSamples.cs | 239 +++--------------- 2 files changed, 216 insertions(+), 206 deletions(-) create mode 100644 osu.Game.Tests/Gameplay/HitObjectSampleTest.cs diff --git a/osu.Game.Tests/Gameplay/HitObjectSampleTest.cs b/osu.Game.Tests/Gameplay/HitObjectSampleTest.cs new file mode 100644 index 0000000000..6621344b6e --- /dev/null +++ b/osu.Game.Tests/Gameplay/HitObjectSampleTest.cs @@ -0,0 +1,183 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.IO.Stores; +using osu.Framework.Timing; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Formats; +using osu.Game.IO; +using osu.Game.Rulesets; +using osu.Game.Skinning; +using osu.Game.Storyboards; +using osu.Game.Tests.Resources; +using osu.Game.Tests.Visual; +using osu.Game.Users; + +namespace osu.Game.Tests.Gameplay +{ + public abstract class HitObjectSampleTest : PlayerTestScene + { + private readonly SkinInfo userSkinInfo = new SkinInfo(); + + private readonly BeatmapInfo beatmapInfo = new BeatmapInfo + { + BeatmapSet = new BeatmapSetInfo(), + Metadata = new BeatmapMetadata + { + Author = User.SYSTEM_USER + } + }; + + private readonly TestResourceStore userSkinResourceStore = new TestResourceStore(); + private readonly TestResourceStore beatmapSkinResourceStore = new TestResourceStore(); + private SkinSourceDependencyContainer dependencies; + private IBeatmap currentTestBeatmap; + protected override bool HasCustomSteps => true; + + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + => new DependencyContainer(dependencies = new SkinSourceDependencyContainer(base.CreateChildDependencies(parent))); + + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => currentTestBeatmap; + + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) + => new TestWorkingBeatmap(beatmapInfo, beatmapSkinResourceStore, beatmap, storyboard, Clock, Audio); + + protected void CreateTestWithBeatmap(string filename) + { + CreateTest(() => + { + AddStep("clear performed lookups", () => + { + userSkinResourceStore.PerformedLookups.Clear(); + beatmapSkinResourceStore.PerformedLookups.Clear(); + }); + + AddStep($"load {filename}", () => + { + using (var reader = new LineBufferedReader(TestResources.OpenResource($"SampleLookups/{filename}"))) + currentTestBeatmap = Decoder.GetDecoder(reader).Decode(reader); + }); + }); + } + + protected void SetupSkins(string beatmapFile, string userFile) + { + AddStep("setup skins", () => + { + userSkinInfo.Files = new List + { + new SkinFileInfo + { + Filename = userFile, + FileInfo = new IO.FileInfo { Hash = userFile } + } + }; + + beatmapInfo.BeatmapSet.Files = new List + { + new BeatmapSetFileInfo + { + Filename = beatmapFile, + FileInfo = new IO.FileInfo { Hash = beatmapFile } + } + }; + + // Need to refresh the cached skin source to refresh the skin resource store. + dependencies.SkinSource = new SkinProvidingContainer(new LegacySkin(userSkinInfo, userSkinResourceStore, Audio)); + }); + } + + protected void AssertBeatmapLookup(string name) => AddAssert($"\"{name}\" looked up from beatmap skin", + () => !userSkinResourceStore.PerformedLookups.Contains(name) && beatmapSkinResourceStore.PerformedLookups.Contains(name)); + + protected void AssertUserLookup(string name) => AddAssert($"\"{name}\" looked up from user skin", + () => !beatmapSkinResourceStore.PerformedLookups.Contains(name) && userSkinResourceStore.PerformedLookups.Contains(name)); + + protected class SkinSourceDependencyContainer : IReadOnlyDependencyContainer + { + public ISkinSource SkinSource; + + private readonly IReadOnlyDependencyContainer fallback; + + public SkinSourceDependencyContainer(IReadOnlyDependencyContainer fallback) + { + this.fallback = fallback; + } + + public object Get(Type type) + { + if (type == typeof(ISkinSource)) + return SkinSource; + + return fallback.Get(type); + } + + public object Get(Type type, CacheInfo info) + { + if (type == typeof(ISkinSource)) + return SkinSource; + + return fallback.Get(type, info); + } + + public void Inject(T instance) where T : class + { + // Never used directly + } + } + + protected class TestResourceStore : IResourceStore + { + public readonly List PerformedLookups = new List(); + + public byte[] Get(string name) + { + markLookup(name); + return Array.Empty(); + } + + public Task GetAsync(string name) + { + markLookup(name); + return Task.FromResult(Array.Empty()); + } + + public Stream GetStream(string name) + { + markLookup(name); + return new MemoryStream(); + } + + private void markLookup(string name) => PerformedLookups.Add(name.Substring(name.LastIndexOf(Path.DirectorySeparatorChar) + 1)); + + public IEnumerable GetAvailableResources() => Enumerable.Empty(); + + public void Dispose() + { + } + } + + private class TestWorkingBeatmap : ClockBackedTestWorkingBeatmap + { + private readonly BeatmapInfo skinBeatmapInfo; + private readonly IResourceStore resourceStore; + + public TestWorkingBeatmap(BeatmapInfo skinBeatmapInfo, IResourceStore resourceStore, IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock referenceClock, AudioManager audio, + double length = 60000) + : base(beatmap, storyboard, referenceClock, audio, length) + { + this.skinBeatmapInfo = skinBeatmapInfo; + this.resourceStore = resourceStore; + } + + protected override ISkin GetSkin() => new LegacyBeatmapSkin(skinBeatmapInfo, resourceStore, AudioManager); + } + } +} diff --git a/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs index acefaa006a..6144179d31 100644 --- a/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs @@ -1,52 +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; -using System.IO; -using System.Linq; -using System.Threading.Tasks; using NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Audio; -using osu.Framework.IO.Stores; using osu.Framework.Testing; -using osu.Framework.Timing; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.Formats; -using osu.Game.IO; using osu.Game.Rulesets; -using osu.Game.Skinning; -using osu.Game.Storyboards; -using osu.Game.Tests.Resources; -using osu.Game.Tests.Visual.Gameplay; -using osu.Game.Users; +using osu.Game.Rulesets.Osu; namespace osu.Game.Tests.Gameplay { [HeadlessTest] - public class TestSceneHitObjectSamples : OsuPlayerTestScene + public class TestSceneHitObjectSamples : HitObjectSampleTest { - private readonly SkinInfo userSkinInfo = new SkinInfo(); - - private readonly BeatmapInfo beatmapInfo = new BeatmapInfo - { - BeatmapSet = new BeatmapSetInfo(), - Metadata = new BeatmapMetadata - { - Author = User.SYSTEM_USER - } - }; - - private readonly TestResourceStore userSkinResourceStore = new TestResourceStore(); - private readonly TestResourceStore beatmapSkinResourceStore = new TestResourceStore(); - - protected override bool HasCustomSteps => true; - - private SkinSourceDependencyContainer dependencies; - - protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) - => new DependencyContainer(dependencies = new SkinSourceDependencyContainer(base.CreateChildDependencies(parent))); + protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); /// /// Tests that a hitobject which provides no custom sample set retrieves samples from the user skin. @@ -56,11 +21,11 @@ namespace osu.Game.Tests.Gameplay { const string expected_sample = "normal-hitnormal"; - setupSkins(expected_sample, expected_sample); + SetupSkins(expected_sample, expected_sample); - createTestWithBeatmap("hitobject-skin-sample.osu"); + CreateTestWithBeatmap("hitobject-skin-sample.osu"); - assertUserLookup(expected_sample); + AssertUserLookup(expected_sample); } /// @@ -71,11 +36,11 @@ namespace osu.Game.Tests.Gameplay { const string expected_sample = "normal-hitnormal"; - setupSkins(expected_sample, expected_sample); + SetupSkins(expected_sample, expected_sample); - createTestWithBeatmap("hitobject-beatmap-sample.osu"); + CreateTestWithBeatmap("hitobject-beatmap-sample.osu"); - assertBeatmapLookup(expected_sample); + AssertBeatmapLookup(expected_sample); } /// @@ -86,11 +51,11 @@ namespace osu.Game.Tests.Gameplay { const string expected_sample = "normal-hitnormal"; - setupSkins(null, expected_sample); + SetupSkins(null, expected_sample); - createTestWithBeatmap("hitobject-beatmap-sample.osu"); + CreateTestWithBeatmap("hitobject-beatmap-sample.osu"); - assertUserLookup(expected_sample); + AssertUserLookup(expected_sample); } /// @@ -102,11 +67,11 @@ namespace osu.Game.Tests.Gameplay [TestCase("normal-hitnormal")] public void TestDefaultCustomSampleFromBeatmap(string expectedSample) { - setupSkins(expectedSample, expectedSample); + SetupSkins(expectedSample, expectedSample); - createTestWithBeatmap("hitobject-beatmap-custom-sample.osu"); + CreateTestWithBeatmap("hitobject-beatmap-custom-sample.osu"); - assertBeatmapLookup(expectedSample); + AssertBeatmapLookup(expectedSample); } /// @@ -118,11 +83,11 @@ namespace osu.Game.Tests.Gameplay [TestCase("normal-hitnormal")] public void TestDefaultCustomSampleFromUserSkinFallback(string expectedSample) { - setupSkins(string.Empty, expectedSample); + SetupSkins(string.Empty, expectedSample); - createTestWithBeatmap("hitobject-beatmap-custom-sample.osu"); + CreateTestWithBeatmap("hitobject-beatmap-custom-sample.osu"); - assertUserLookup(expectedSample); + AssertUserLookup(expectedSample); } /// @@ -133,11 +98,11 @@ namespace osu.Game.Tests.Gameplay { const string expected_sample = "hit_1.wav"; - setupSkins(expected_sample, expected_sample); + SetupSkins(expected_sample, expected_sample); - createTestWithBeatmap("file-beatmap-sample.osu"); + CreateTestWithBeatmap("file-beatmap-sample.osu"); - assertBeatmapLookup(expected_sample); + AssertBeatmapLookup(expected_sample); } /// @@ -148,11 +113,11 @@ namespace osu.Game.Tests.Gameplay { const string expected_sample = "normal-hitnormal"; - setupSkins(expected_sample, expected_sample); + SetupSkins(expected_sample, expected_sample); - createTestWithBeatmap("controlpoint-skin-sample.osu"); + CreateTestWithBeatmap("controlpoint-skin-sample.osu"); - assertUserLookup(expected_sample); + AssertUserLookup(expected_sample); } /// @@ -163,11 +128,11 @@ namespace osu.Game.Tests.Gameplay { const string expected_sample = "normal-hitnormal"; - setupSkins(expected_sample, expected_sample); + SetupSkins(expected_sample, expected_sample); - createTestWithBeatmap("controlpoint-beatmap-sample.osu"); + CreateTestWithBeatmap("controlpoint-beatmap-sample.osu"); - assertBeatmapLookup(expected_sample); + AssertBeatmapLookup(expected_sample); } /// @@ -177,11 +142,11 @@ namespace osu.Game.Tests.Gameplay [TestCase("normal-hitnormal")] public void TestControlPointCustomSampleFromBeatmap(string sampleName) { - setupSkins(sampleName, sampleName); + SetupSkins(sampleName, sampleName); - createTestWithBeatmap("controlpoint-beatmap-custom-sample.osu"); + CreateTestWithBeatmap("controlpoint-beatmap-custom-sample.osu"); - assertBeatmapLookup(sampleName); + AssertBeatmapLookup(sampleName); } /// @@ -192,149 +157,11 @@ namespace osu.Game.Tests.Gameplay { const string expected_sample = "normal-hitnormal3"; - setupSkins(expected_sample, expected_sample); + SetupSkins(expected_sample, expected_sample); - createTestWithBeatmap("hitobject-beatmap-custom-sample-override.osu"); + CreateTestWithBeatmap("hitobject-beatmap-custom-sample-override.osu"); - assertBeatmapLookup(expected_sample); - } - - protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => currentTestBeatmap; - - protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) - => new TestWorkingBeatmap(beatmapInfo, beatmapSkinResourceStore, beatmap, storyboard, Clock, Audio); - - private IBeatmap currentTestBeatmap; - - private void createTestWithBeatmap(string filename) - { - CreateTest(() => - { - AddStep("clear performed lookups", () => - { - userSkinResourceStore.PerformedLookups.Clear(); - beatmapSkinResourceStore.PerformedLookups.Clear(); - }); - - AddStep($"load {filename}", () => - { - using (var reader = new LineBufferedReader(TestResources.OpenResource($"SampleLookups/{filename}"))) - currentTestBeatmap = Decoder.GetDecoder(reader).Decode(reader); - }); - }); - } - - private void setupSkins(string beatmapFile, string userFile) - { - AddStep("setup skins", () => - { - userSkinInfo.Files = new List - { - new SkinFileInfo - { - Filename = userFile, - FileInfo = new IO.FileInfo { Hash = userFile } - } - }; - - beatmapInfo.BeatmapSet.Files = new List - { - new BeatmapSetFileInfo - { - Filename = beatmapFile, - FileInfo = new IO.FileInfo { Hash = beatmapFile } - } - }; - - // Need to refresh the cached skin source to refresh the skin resource store. - dependencies.SkinSource = new SkinProvidingContainer(new LegacySkin(userSkinInfo, userSkinResourceStore, Audio)); - }); - } - - private void assertBeatmapLookup(string name) => AddAssert($"\"{name}\" looked up from beatmap skin", - () => !userSkinResourceStore.PerformedLookups.Contains(name) && beatmapSkinResourceStore.PerformedLookups.Contains(name)); - - private void assertUserLookup(string name) => AddAssert($"\"{name}\" looked up from user skin", - () => !beatmapSkinResourceStore.PerformedLookups.Contains(name) && userSkinResourceStore.PerformedLookups.Contains(name)); - - private class SkinSourceDependencyContainer : IReadOnlyDependencyContainer - { - public ISkinSource SkinSource; - - private readonly IReadOnlyDependencyContainer fallback; - - public SkinSourceDependencyContainer(IReadOnlyDependencyContainer fallback) - { - this.fallback = fallback; - } - - public object Get(Type type) - { - if (type == typeof(ISkinSource)) - return SkinSource; - - return fallback.Get(type); - } - - public object Get(Type type, CacheInfo info) - { - if (type == typeof(ISkinSource)) - return SkinSource; - - return fallback.Get(type, info); - } - - public void Inject(T instance) where T : class - { - // Never used directly - } - } - - private class TestResourceStore : IResourceStore - { - public readonly List PerformedLookups = new List(); - - public byte[] Get(string name) - { - markLookup(name); - return Array.Empty(); - } - - public Task GetAsync(string name) - { - markLookup(name); - return Task.FromResult(Array.Empty()); - } - - public Stream GetStream(string name) - { - markLookup(name); - return new MemoryStream(); - } - - private void markLookup(string name) => PerformedLookups.Add(name.Substring(name.LastIndexOf(Path.DirectorySeparatorChar) + 1)); - - public IEnumerable GetAvailableResources() => Enumerable.Empty(); - - public void Dispose() - { - } - } - - private class TestWorkingBeatmap : ClockBackedTestWorkingBeatmap - { - private readonly BeatmapInfo skinBeatmapInfo; - private readonly IResourceStore resourceStore; - - public TestWorkingBeatmap(BeatmapInfo skinBeatmapInfo, IResourceStore resourceStore, IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock referenceClock, AudioManager audio, - double length = 60000) - : base(beatmap, storyboard, referenceClock, audio, length) - { - this.skinBeatmapInfo = skinBeatmapInfo; - this.resourceStore = resourceStore; - } - - protected override ISkin GetSkin() => new LegacyBeatmapSkin(skinBeatmapInfo, resourceStore, AudioManager); + AssertBeatmapLookup(expected_sample); } } } From 4a8a673d41a276a540cab16806bd80beb29308e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 21 Jun 2020 18:19:33 +0200 Subject: [PATCH 2155/2376] Decouple abstract sample test from TestResources --- osu.Game.Tests/Gameplay/HitObjectSampleTest.cs | 5 +++-- osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs | 3 +++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Gameplay/HitObjectSampleTest.cs b/osu.Game.Tests/Gameplay/HitObjectSampleTest.cs index 6621344b6e..9c43690a95 100644 --- a/osu.Game.Tests/Gameplay/HitObjectSampleTest.cs +++ b/osu.Game.Tests/Gameplay/HitObjectSampleTest.cs @@ -16,7 +16,6 @@ using osu.Game.IO; using osu.Game.Rulesets; using osu.Game.Skinning; using osu.Game.Storyboards; -using osu.Game.Tests.Resources; using osu.Game.Tests.Visual; using osu.Game.Users; @@ -24,6 +23,8 @@ namespace osu.Game.Tests.Gameplay { public abstract class HitObjectSampleTest : PlayerTestScene { + protected abstract IResourceStore Resources { get; } + private readonly SkinInfo userSkinInfo = new SkinInfo(); private readonly BeatmapInfo beatmapInfo = new BeatmapInfo @@ -61,7 +62,7 @@ namespace osu.Game.Tests.Gameplay AddStep($"load {filename}", () => { - using (var reader = new LineBufferedReader(TestResources.OpenResource($"SampleLookups/{filename}"))) + using (var reader = new LineBufferedReader(Resources.GetStream($"Resources/SampleLookups/{filename}"))) currentTestBeatmap = Decoder.GetDecoder(reader).Decode(reader); }); }); diff --git a/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs index 6144179d31..78bdfeb80e 100644 --- a/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs @@ -2,9 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; +using osu.Framework.IO.Stores; using osu.Framework.Testing; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; +using osu.Game.Tests.Resources; namespace osu.Game.Tests.Gameplay { @@ -12,6 +14,7 @@ namespace osu.Game.Tests.Gameplay public class TestSceneHitObjectSamples : HitObjectSampleTest { protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); + protected override IResourceStore Resources => TestResources.GetStore(); /// /// Tests that a hitobject which provides no custom sample set retrieves samples from the user skin. From 4bba0c7359c857bc19d794c5b7b269e001d4d45e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 21 Jun 2020 18:20:36 +0200 Subject: [PATCH 2156/2376] Move abstract sample test to main game project --- osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs | 1 + .../Gameplay => osu.Game/Tests/Beatmaps}/HitObjectSampleTest.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) rename {osu.Game.Tests/Gameplay => osu.Game/Tests/Beatmaps}/HitObjectSampleTest.cs (99%) diff --git a/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs index 78bdfeb80e..ef6efb7fec 100644 --- a/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs @@ -6,6 +6,7 @@ using osu.Framework.IO.Stores; using osu.Framework.Testing; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; +using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Resources; namespace osu.Game.Tests.Gameplay diff --git a/osu.Game.Tests/Gameplay/HitObjectSampleTest.cs b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs similarity index 99% rename from osu.Game.Tests/Gameplay/HitObjectSampleTest.cs rename to osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs index 9c43690a95..91fca2c1bf 100644 --- a/osu.Game.Tests/Gameplay/HitObjectSampleTest.cs +++ b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs @@ -19,7 +19,7 @@ using osu.Game.Storyboards; using osu.Game.Tests.Visual; using osu.Game.Users; -namespace osu.Game.Tests.Gameplay +namespace osu.Game.Tests.Beatmaps { public abstract class HitObjectSampleTest : PlayerTestScene { From 07cbc3e68343b83b17b4e43f8302f84833417f99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 21 Jun 2020 23:04:59 +0200 Subject: [PATCH 2157/2376] Privatise and seal whatever possible --- osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs index 91fca2c1bf..b4ce322165 100644 --- a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs +++ b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs @@ -40,14 +40,14 @@ namespace osu.Game.Tests.Beatmaps private readonly TestResourceStore beatmapSkinResourceStore = new TestResourceStore(); private SkinSourceDependencyContainer dependencies; private IBeatmap currentTestBeatmap; - protected override bool HasCustomSteps => true; + protected sealed override bool HasCustomSteps => true; - protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + protected sealed override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => new DependencyContainer(dependencies = new SkinSourceDependencyContainer(base.CreateChildDependencies(parent))); - protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => currentTestBeatmap; + protected sealed override IBeatmap CreateBeatmap(RulesetInfo ruleset) => currentTestBeatmap; - protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) + protected sealed override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) => new TestWorkingBeatmap(beatmapInfo, beatmapSkinResourceStore, beatmap, storyboard, Clock, Audio); protected void CreateTestWithBeatmap(string filename) @@ -101,7 +101,7 @@ namespace osu.Game.Tests.Beatmaps protected void AssertUserLookup(string name) => AddAssert($"\"{name}\" looked up from user skin", () => !beatmapSkinResourceStore.PerformedLookups.Contains(name) && userSkinResourceStore.PerformedLookups.Contains(name)); - protected class SkinSourceDependencyContainer : IReadOnlyDependencyContainer + private class SkinSourceDependencyContainer : IReadOnlyDependencyContainer { public ISkinSource SkinSource; @@ -134,7 +134,7 @@ namespace osu.Game.Tests.Beatmaps } } - protected class TestResourceStore : IResourceStore + private class TestResourceStore : IResourceStore { public readonly List PerformedLookups = new List(); From ad85c5f538a162ca9973301331db8baa8110b17d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 21 Jun 2020 22:04:10 +0200 Subject: [PATCH 2158/2376] Add base legacy skin transformer --- .../Skinning/CatchLegacySkinTransformer.cs | 23 ++++------- .../Skinning/ManiaLegacySkinTransformer.cs | 26 +++++-------- .../Skinning/OsuLegacySkinTransformer.cs | 38 +++++++------------ .../Skinning/TaikoLegacySkinTransformer.cs | 17 +++------ osu.Game/Skinning/LegacySkinTransformer.cs | 35 +++++++++++++++++ 5 files changed, 71 insertions(+), 68 deletions(-) create mode 100644 osu.Game/Skinning/LegacySkinTransformer.cs diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs index 954f2dfc5f..d929da1a29 100644 --- a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs @@ -2,26 +2,21 @@ // See the LICENCE file in the repository root for full licence text. using Humanizer; -using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Textures; -using osu.Game.Audio; using osu.Game.Skinning; using osuTK; namespace osu.Game.Rulesets.Catch.Skinning { - public class CatchLegacySkinTransformer : ISkin + public class CatchLegacySkinTransformer : LegacySkinTransformer { - private readonly ISkin source; - - public CatchLegacySkinTransformer(ISkin source) + public CatchLegacySkinTransformer(ISkinSource source) + : base(source) { - this.source = source; } - public Drawable GetDrawableComponent(ISkinComponent component) + public override Drawable GetDrawableComponent(ISkinComponent component) { if (!(component is CatchSkinComponent catchSkinComponent)) return null; @@ -61,19 +56,15 @@ namespace osu.Game.Rulesets.Catch.Skinning return null; } - public Texture GetTexture(string componentName) => source.GetTexture(componentName); - - public SampleChannel GetSample(ISampleInfo sample) => source.GetSample(sample); - - public IBindable GetConfig(TLookup lookup) + public override IBindable GetConfig(TLookup lookup) { switch (lookup) { case CatchSkinColour colour: - return source.GetConfig(new SkinCustomColourLookup(colour)); + return Source.GetConfig(new SkinCustomColourLookup(colour)); } - return source.GetConfig(lookup); + return Source.GetConfig(lookup); } } } diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs index 4114bf5628..84e88a10be 100644 --- a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs @@ -3,11 +3,8 @@ using System; using osu.Framework.Graphics; -using osu.Framework.Graphics.Textures; -using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Game.Rulesets.Scoring; -using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Skinning; @@ -15,9 +12,8 @@ using System.Collections.Generic; namespace osu.Game.Rulesets.Mania.Skinning { - public class ManiaLegacySkinTransformer : ISkin + public class ManiaLegacySkinTransformer : LegacySkinTransformer { - private readonly ISkin source; private readonly ManiaBeatmap beatmap; /// @@ -59,23 +55,23 @@ namespace osu.Game.Rulesets.Mania.Skinning private Lazy hasKeyTexture; public ManiaLegacySkinTransformer(ISkinSource source, IBeatmap beatmap) + : base(source) { - this.source = source; this.beatmap = (ManiaBeatmap)beatmap; - source.SourceChanged += sourceChanged; + Source.SourceChanged += sourceChanged; sourceChanged(); } private void sourceChanged() { - isLegacySkin = new Lazy(() => source.GetConfig(LegacySkinConfiguration.LegacySetting.Version) != null); - hasKeyTexture = new Lazy(() => source.GetAnimation( + isLegacySkin = new Lazy(() => Source.GetConfig(LegacySkinConfiguration.LegacySetting.Version) != null); + hasKeyTexture = new Lazy(() => Source.GetAnimation( this.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.KeyImage, 0)?.Value ?? "mania-key1", true, true) != null); } - public Drawable GetDrawableComponent(ISkinComponent component) + public override Drawable GetDrawableComponent(ISkinComponent component) { switch (component) { @@ -133,16 +129,12 @@ namespace osu.Game.Rulesets.Mania.Skinning return this.GetAnimation(filename, true, true); } - public Texture GetTexture(string componentName) => source.GetTexture(componentName); - - public SampleChannel GetSample(ISampleInfo sample) => source.GetSample(sample); - - public IBindable GetConfig(TLookup lookup) + public override IBindable GetConfig(TLookup lookup) { if (lookup is ManiaSkinConfigurationLookup maniaLookup) - return source.GetConfig(new LegacyManiaSkinConfigurationLookup(beatmap.TotalColumns, maniaLookup.Lookup, maniaLookup.TargetColumn)); + return Source.GetConfig(new LegacyManiaSkinConfigurationLookup(beatmap.TotalColumns, maniaLookup.Lookup, maniaLookup.TargetColumn)); - return source.GetConfig(lookup); + return Source.GetConfig(lookup); } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs index ba0003b5cd..3e5758ca01 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs @@ -2,20 +2,15 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Textures; -using osu.Game.Audio; using osu.Game.Skinning; using osuTK; namespace osu.Game.Rulesets.Osu.Skinning { - public class OsuLegacySkinTransformer : ISkin + public class OsuLegacySkinTransformer : LegacySkinTransformer { - private readonly ISkin source; - private Lazy hasHitCircle; /// @@ -26,19 +21,18 @@ namespace osu.Game.Rulesets.Osu.Skinning public const float LEGACY_CIRCLE_RADIUS = 64 - 5; public OsuLegacySkinTransformer(ISkinSource source) + : base(source) { - this.source = source; - - source.SourceChanged += sourceChanged; + Source.SourceChanged += sourceChanged; sourceChanged(); } private void sourceChanged() { - hasHitCircle = new Lazy(() => source.GetTexture("hitcircle") != null); + hasHitCircle = new Lazy(() => Source.GetTexture("hitcircle") != null); } - public Drawable GetDrawableComponent(ISkinComponent component) + public override Drawable GetDrawableComponent(ISkinComponent component) { if (!(component is OsuSkinComponent osuComponent)) return null; @@ -85,13 +79,13 @@ namespace osu.Game.Rulesets.Osu.Skinning return null; case OsuSkinComponents.Cursor: - if (source.GetTexture("cursor") != null) + if (Source.GetTexture("cursor") != null) return new LegacyCursor(); return null; case OsuSkinComponents.CursorTrail: - if (source.GetTexture("cursortrail") != null) + if (Source.GetTexture("cursortrail") != null) return new LegacyCursorTrail(); return null; @@ -102,7 +96,7 @@ namespace osu.Game.Rulesets.Osu.Skinning return !hasFont(font) ? null - : new LegacySpriteText(source, font) + : new LegacySpriteText(Source, font) { // stable applies a blanket 0.8x scale to hitcircle fonts Scale = new Vector2(0.8f), @@ -113,16 +107,12 @@ namespace osu.Game.Rulesets.Osu.Skinning return null; } - public Texture GetTexture(string componentName) => source.GetTexture(componentName); - - public SampleChannel GetSample(ISampleInfo sample) => source.GetSample(sample); - - public IBindable GetConfig(TLookup lookup) + public override IBindable GetConfig(TLookup lookup) { switch (lookup) { case OsuSkinColour colour: - return source.GetConfig(new SkinCustomColourLookup(colour)); + return Source.GetConfig(new SkinCustomColourLookup(colour)); case OsuSkinConfiguration osuLookup: switch (osuLookup) @@ -136,16 +126,16 @@ namespace osu.Game.Rulesets.Osu.Skinning case OsuSkinConfiguration.HitCircleOverlayAboveNumber: // See https://osu.ppy.sh/help/wiki/Skinning/skin.ini#%5Bgeneral%5D // HitCircleOverlayAboveNumer (with typo) should still be supported for now. - return source.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumber) ?? - source.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumer); + return Source.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumber) ?? + Source.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumer); } break; } - return source.GetConfig(lookup); + return Source.GetConfig(lookup); } - private bool hasFont(string fontName) => source.GetTexture($"{fontName}-0") != null; + private bool hasFont(string fontName) => Source.GetTexture($"{fontName}-0") != null; } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs index 6e9a37eb93..23d675cfb0 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs @@ -6,23 +6,20 @@ using System.Collections.Generic; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Textures; using osu.Game.Audio; using osu.Game.Rulesets.Taiko.UI; using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.Skinning { - public class TaikoLegacySkinTransformer : ISkin + public class TaikoLegacySkinTransformer : LegacySkinTransformer { - private readonly ISkinSource source; - public TaikoLegacySkinTransformer(ISkinSource source) + : base(source) { - this.source = source; } - public Drawable GetDrawableComponent(ISkinComponent component) + public override Drawable GetDrawableComponent(ISkinComponent component) { if (!(component is TaikoSkinComponent taikoComponent)) return null; @@ -100,7 +97,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning return null; } - return source.GetDrawableComponent(component); + return Source.GetDrawableComponent(component); } private string getHitName(TaikoSkinComponents component) @@ -120,11 +117,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning throw new ArgumentOutOfRangeException(nameof(component), "Invalid result type"); } - public Texture GetTexture(string componentName) => source.GetTexture(componentName); + public override SampleChannel GetSample(ISampleInfo sampleInfo) => Source.GetSample(new LegacyTaikoSampleInfo(sampleInfo)); - public SampleChannel GetSample(ISampleInfo sampleInfo) => source.GetSample(new LegacyTaikoSampleInfo(sampleInfo)); - - public IBindable GetConfig(TLookup lookup) => source.GetConfig(lookup); + public override IBindable GetConfig(TLookup lookup) => Source.GetConfig(lookup); private class LegacyTaikoSampleInfo : ISampleInfo { diff --git a/osu.Game/Skinning/LegacySkinTransformer.cs b/osu.Game/Skinning/LegacySkinTransformer.cs new file mode 100644 index 0000000000..1131c93288 --- /dev/null +++ b/osu.Game/Skinning/LegacySkinTransformer.cs @@ -0,0 +1,35 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Audio.Sample; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Textures; +using osu.Game.Audio; + +namespace osu.Game.Skinning +{ + /// + /// Transformer used to handle support of legacy features for individual rulesets. + /// + public abstract class LegacySkinTransformer : ISkin + { + /// + /// Source of the which is being transformed. + /// + protected ISkinSource Source { get; } + + protected LegacySkinTransformer(ISkinSource source) + { + Source = source; + } + + public abstract Drawable GetDrawableComponent(ISkinComponent component); + + public Texture GetTexture(string componentName) => Source.GetTexture(componentName); + + public virtual SampleChannel GetSample(ISampleInfo sampleInfo) => Source.GetSample(sampleInfo); + + public abstract IBindable GetConfig(TLookup lookup); + } +} From 1bc5f3618417a15fe1fd0f44e705e4911d5adacd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 21 Jun 2020 22:05:10 +0200 Subject: [PATCH 2159/2376] Adjust test usage --- osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs index 7deeec527f..b570f090ca 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs @@ -17,7 +17,8 @@ namespace osu.Game.Rulesets.Catch.Tests { var store = new NamespacedResourceStore(new DllResourceStore(GetType().Assembly), "Resources/special-skin"); var rawSkin = new TestLegacySkin(new SkinInfo { Name = "special-skin" }, store); - var skin = new CatchLegacySkinTransformer(rawSkin); + var skinSource = new SkinProvidingContainer(rawSkin); + var skin = new CatchLegacySkinTransformer(skinSource); Assert.AreEqual(new Color4(232, 185, 35, 255), skin.GetConfig(CatchSkinColour.HyperDash)?.Value); Assert.AreEqual(new Color4(232, 74, 35, 255), skin.GetConfig(CatchSkinColour.HyperDashAfterImage)?.Value); From 3ede095b9c88e3d2f99a483c8b6171eb7f889f03 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 22 Jun 2020 15:42:55 +0900 Subject: [PATCH 2160/2376] Apply refactorings from review --- osu.Game/Screens/Ranking/ResultsScreen.cs | 20 ++++++++------------ osu.Game/Screens/Ranking/ScorePanelList.cs | 4 ++-- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 133efd6e7b..193d975e42 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -236,13 +236,11 @@ namespace osu.Game.Screens.Ranking scorePanelList.Detach(expandedPanel); detachedPanelContainer.Add(expandedPanel); - // Move into its original location in the local container. + // Move into its original location in the local container first, then to the final location. var origLocation = detachedPanelContainer.ToLocalSpace(screenSpacePos); - expandedPanel.MoveTo(origLocation); - expandedPanel.MoveToX(origLocation.X); - - // Move into the final location. - expandedPanel.MoveToX(StatisticsPanel.SIDE_PADDING, 150, Easing.OutQuint); + expandedPanel.MoveTo(origLocation) + .Then() + .MoveTo(new Vector2(StatisticsPanel.SIDE_PADDING, origLocation.Y), 150, Easing.OutQuint); // Hide contracted panels. foreach (var contracted in scorePanelList.GetScorePanels().Where(p => p.State == PanelState.Contracted)) @@ -262,13 +260,11 @@ namespace osu.Game.Screens.Ranking detachedPanelContainer.Remove(detachedPanel); scorePanelList.Attach(detachedPanel); - // Move into its original location in the attached container. + // Move into its original location in the attached container first, then to the final location. var origLocation = detachedPanel.Parent.ToLocalSpace(screenSpacePos); - detachedPanel.MoveTo(origLocation); - detachedPanel.MoveToX(origLocation.X); - - // Move into the final location. - detachedPanel.MoveToX(0, 150, Easing.OutQuint); + detachedPanel.MoveTo(origLocation) + .Then() + .MoveTo(new Vector2(0, origLocation.Y), 150, Easing.OutQuint); // Show contracted panels. foreach (var contracted in scorePanelList.GetScorePanels().Where(p => p.State == PanelState.Contracted)) diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index 9ebd7822c0..0f8bc82ac0 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -202,7 +202,7 @@ namespace osu.Game.Screens.Ranking /// If is not a part of this . public void Detach(ScorePanel panel) { - var container = flow.FirstOrDefault(t => t.Panel == panel); + var container = flow.SingleOrDefault(t => t.Panel == panel); if (container == null) throw new InvalidOperationException("Panel is not contained by the score panel list."); @@ -216,7 +216,7 @@ namespace osu.Game.Screens.Ranking /// If is not a part of this . public void Attach(ScorePanel panel) { - var container = flow.FirstOrDefault(t => t.Panel == panel); + var container = flow.SingleOrDefault(t => t.Panel == panel); if (container == null) throw new InvalidOperationException("Panel is not contained by the score panel list."); From 21f776e51feafd8a06e397e90ef88bace4900d0c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 22 Jun 2020 15:48:42 +0900 Subject: [PATCH 2161/2376] Simplify/optimise heatmap point additoin --- .../Statistics/AccuracyHeatmap.cs | 22 +++---------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs index 10ca3eb9be..f05bfce8d7 100644 --- a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; @@ -175,25 +174,10 @@ namespace osu.Game.Rulesets.Osu.Statistics Vector2 localPoint = localCentre + localRadius * new Vector2((float)Math.Cos(localAngle), (float)Math.Sin(localAngle)); // Find the most relevant hit point. - double minDist = double.PositiveInfinity; - HitPoint point = null; + int r = Math.Clamp((int)Math.Round(localPoint.Y), 0, points_per_dimension - 1); + int c = Math.Clamp((int)Math.Round(localPoint.X), 0, points_per_dimension - 1); - for (int r = 0; r < points_per_dimension; r++) - { - for (int c = 0; c < points_per_dimension; c++) - { - float dist = Vector2.Distance(new Vector2(c, r), localPoint); - - if (dist < minDist) - { - minDist = dist; - point = (HitPoint)pointGrid.Content[r][c]; - } - } - } - - Debug.Assert(point != null); - point.Increment(); + ((HitPoint)pointGrid.Content[r][c]).Increment(); } private class HitPoint : Circle From e91c2ee5e275bcdc398bcc117055a468ea2f8348 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Jun 2020 16:19:38 +0900 Subject: [PATCH 2162/2376] Simplify logic by considering all buttons equally --- osu.Game/Graphics/Cursor/MenuCursor.cs | 36 +++++++++----------------- 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/osu.Game/Graphics/Cursor/MenuCursor.cs b/osu.Game/Graphics/Cursor/MenuCursor.cs index ff28dddd40..fd8f016860 100644 --- a/osu.Game/Graphics/Cursor/MenuCursor.cs +++ b/osu.Game/Graphics/Cursor/MenuCursor.cs @@ -13,7 +13,6 @@ using JetBrains.Annotations; using osu.Framework.Bindables; using osu.Framework.Graphics.Textures; using osu.Framework.Input.Events; -using osuTK.Input; using osu.Framework.Utils; namespace osu.Game.Graphics.Cursor @@ -74,23 +73,17 @@ namespace osu.Game.Graphics.Cursor protected override bool OnMouseDown(MouseDownEvent e) { // only trigger animation for main mouse buttons - if (e.Button <= MouseButton.Right) - { - activeCursor.Scale = new Vector2(1); - activeCursor.ScaleTo(0.90f, 800, Easing.OutQuint); + activeCursor.Scale = new Vector2(1); + activeCursor.ScaleTo(0.90f, 800, Easing.OutQuint); - activeCursor.AdditiveLayer.Alpha = 0; - activeCursor.AdditiveLayer.FadeInFromZero(800, Easing.OutQuint); - } + activeCursor.AdditiveLayer.Alpha = 0; + activeCursor.AdditiveLayer.FadeInFromZero(800, Easing.OutQuint); - if (shouldKeepRotating(e)) + if (cursorRotate.Value && dragRotationState != DragRotationState.Rotating) { // if cursor is already rotating don't reset its rotate origin - if (dragRotationState != DragRotationState.Rotating) - { - dragRotationState = DragRotationState.DragStarted; - positionMouseDown = e.MousePosition; - } + dragRotationState = DragRotationState.DragStarted; + positionMouseDown = e.MousePosition; } return base.OnMouseDown(e); @@ -98,17 +91,16 @@ namespace osu.Game.Graphics.Cursor protected override void OnMouseUp(MouseUpEvent e) { - if (!anyMainButtonPressed(e)) + if (!e.HasAnyButtonPressed) { activeCursor.AdditiveLayer.FadeOutFromOne(500, Easing.OutQuint); activeCursor.ScaleTo(1, 500, Easing.OutElastic); - } - if (!shouldKeepRotating(e)) - { - if (dragRotationState == DragRotationState.Rotating) + if (dragRotationState != DragRotationState.NotDragging) + { activeCursor.RotateTo(0, 600 * (1 + Math.Abs(activeCursor.Rotation / 720)), Easing.OutElasticHalf); - dragRotationState = DragRotationState.NotDragging; + dragRotationState = DragRotationState.NotDragging; + } } base.OnMouseUp(e); @@ -126,10 +118,6 @@ namespace osu.Game.Graphics.Cursor activeCursor.ScaleTo(0.6f, 250, Easing.In); } - private bool shouldKeepRotating(MouseEvent e) => cursorRotate.Value && (anyMainButtonPressed(e)); - - private static bool anyMainButtonPressed(MouseEvent e) => e.IsPressed(MouseButton.Left) || e.IsPressed(MouseButton.Middle) || e.IsPressed(MouseButton.Right); - public class Cursor : Container { private Container cursorContainer; From 2d121b4e3dd8f10bf70d2f6051167d848fdc4fef Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Jun 2020 16:32:27 +0900 Subject: [PATCH 2163/2376] Simplify lookup fallback code --- osu.Game.Tournament/IPC/FileBasedIPC.cs | 32 +++++-------------------- 1 file changed, 6 insertions(+), 26 deletions(-) diff --git a/osu.Game.Tournament/IPC/FileBasedIPC.cs b/osu.Game.Tournament/IPC/FileBasedIPC.cs index a17491bf2d..42be6ea119 100644 --- a/osu.Game.Tournament/IPC/FileBasedIPC.cs +++ b/osu.Game.Tournament/IPC/FileBasedIPC.cs @@ -4,7 +4,6 @@ using System; using System.IO; using System.Linq; -using System.Collections.Generic; using JetBrains.Annotations; using Microsoft.Win32; using osu.Framework.Allocation; @@ -187,32 +186,13 @@ namespace osu.Game.Tournament.IPC [CanBeNull] private string findStablePath() { - string stableInstallPath = string.Empty; + var stableInstallPath = findFromEnvVar() ?? + findFromRegistry() ?? + findFromLocalAppData() ?? + findFromDotFolder(); - try - { - List> stableFindMethods = new List> - { - findFromEnvVar, - findFromRegistry, - findFromLocalAppData, - findFromDotFolder - }; - - foreach (var r in stableFindMethods) - { - stableInstallPath = r.Invoke(); - - if (stableInstallPath != null) - return stableInstallPath; - } - - return null; - } - finally - { - Logger.Log($"Stable path for tourney usage: {stableInstallPath}"); - } + Logger.Log($"Stable path for tourney usage: {stableInstallPath}"); + return stableInstallPath; } private string findFromEnvVar() From fc31d4962938dc29c22e43c63e403be0530c909d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Jun 2020 16:34:04 +0900 Subject: [PATCH 2164/2376] try-catch registry lookup to avoid crashes on non-windows platforms --- osu.Game.Tournament/IPC/FileBasedIPC.cs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tournament/IPC/FileBasedIPC.cs b/osu.Game.Tournament/IPC/FileBasedIPC.cs index 42be6ea119..999ce61ac8 100644 --- a/osu.Game.Tournament/IPC/FileBasedIPC.cs +++ b/osu.Game.Tournament/IPC/FileBasedIPC.cs @@ -238,13 +238,19 @@ namespace osu.Game.Tournament.IPC { Logger.Log("Trying to find stable in registry"); - string stableInstallPath; + try + { + string stableInstallPath; - using (RegistryKey key = Registry.ClassesRoot.OpenSubKey("osu")) - stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty).ToString().Split('"')[1].Replace("osu!.exe", ""); + using (RegistryKey key = Registry.ClassesRoot.OpenSubKey("osu")) + stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty).ToString().Split('"')[1].Replace("osu!.exe", ""); - if (ipcFileExistsInDirectory(stableInstallPath)) - return stableInstallPath; + if (ipcFileExistsInDirectory(stableInstallPath)) + return stableInstallPath; + } + catch + { + } return null; } From 628e05f655efc53043448f5cc6f33e0bac117347 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Jun 2020 17:09:22 +0900 Subject: [PATCH 2165/2376] 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 119c309675..192be999eb 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 bec3bc9d39..911292c6ae 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -25,7 +25,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index de5130b66a..18249b40ca 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + From 8d3ed0584878edf8914df04f7265028492cbf740 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Jun 2020 17:42:54 +0900 Subject: [PATCH 2166/2376] Update welcome text sprite location --- osu.Game/Screens/Menu/IntroWelcome.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/IntroWelcome.cs b/osu.Game/Screens/Menu/IntroWelcome.cs index 711c7b64e4..92c844db8b 100644 --- a/osu.Game/Screens/Menu/IntroWelcome.cs +++ b/osu.Game/Screens/Menu/IntroWelcome.cs @@ -124,7 +124,7 @@ namespace osu.Game.Screens.Menu Scale = new Vector2(0.1f), Height = 156, Alpha = 0, - Texture = textures.Get(@"Welcome/welcome_text") + Texture = textures.Get(@"Intro/Welcome/welcome_text") }, }; } From 533d6e72eb5dc403408b0917af2ca431e2890e86 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 22 Jun 2020 18:05:21 +0900 Subject: [PATCH 2167/2376] Refactor + comment angle math --- .../Statistics/AccuracyHeatmap.cs | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs index f05bfce8d7..f8ab03aad0 100644 --- a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs @@ -167,11 +167,28 @@ namespace osu.Game.Rulesets.Osu.Statistics double finalAngle = angle2 - angle1; // Angle between start, end, and hit points. float normalisedDistance = Vector2.Distance(hitPoint, end) / radius; - // Convert the above into the local search space. + // Consider two objects placed horizontally, with the start on the left and the end on the right. + // The above calculated the angle between {end, start}, and the angle between {end, hitPoint}, in the form: + // +pi | 0 + // O --------- O -----> Note: Math.Atan2 has a range (-pi <= theta <= +pi) + // -pi | 0 + // E.g. If the hit point was directly above end, it would have an angle pi/2. + // + // It also calculated the angle separating hitPoint from the line joining {start, end}, that is anti-clockwise in the form: + // 0 | pi + // O --------- O -----> + // 2pi | pi + // + // However keep in mind that cos(0)=1 and cos(2pi)=1, whereas we actually want these values to appear on the left, so the x-coordinate needs to be inverted. + // Likewise sin(pi/2)=1 and sin(3pi/2)=-1, whereas we actually want these values to appear on the bottom/top respectively, so the y-coordinate also needs to be inverted. + // + // We also need to apply the anti-clockwise rotation. + var rotatedAngle = finalAngle - MathUtils.DegreesToRadians(rotation); + var rotatedCoordinate = -1 * new Vector2((float)Math.Cos(rotatedAngle), (float)Math.Sin(rotatedAngle)); + Vector2 localCentre = new Vector2(points_per_dimension) / 2; float localRadius = localCentre.X * inner_portion * normalisedDistance; // The radius inside the inner portion which of the heatmap which the closest point lies. - double localAngle = finalAngle + Math.PI - MathUtils.DegreesToRadians(rotation); // The angle inside the heatmap on which the closest point lies. - Vector2 localPoint = localCentre + localRadius * new Vector2((float)Math.Cos(localAngle), (float)Math.Sin(localAngle)); + Vector2 localPoint = localCentre + localRadius * rotatedCoordinate; // Find the most relevant hit point. int r = Math.Clamp((int)Math.Round(localPoint.Y), 0, points_per_dimension - 1); From 9dbd230ad30f9f4e8564ae986c0f66f99415b131 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 22 Jun 2020 18:06:52 +0900 Subject: [PATCH 2168/2376] Don't consider slider tails in timing distribution --- osu.Game.Rulesets.Osu/OsuRuleset.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 8222eba339..a164265290 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -198,7 +198,7 @@ namespace osu.Game.Rulesets.Osu { Columns = new[] { - new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents.Where(e => e.HitObject is HitCircle).ToList()) + new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)).ToList()) { RelativeSizeAxes = Axes.X, Height = 130 From 261adfc4e682973738960c6ace4c284f502811fd Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 22 Jun 2020 18:38:41 +0900 Subject: [PATCH 2169/2376] Create a local playable beatmap instead --- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 2 +- .../TestSceneAccuracyHeatmap.cs | 6 +- osu.Game.Rulesets.Osu/OsuRuleset.cs | 4 +- .../Statistics/AccuracyHeatmap.cs | 7 +- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 2 +- osu.Game/Rulesets/Ruleset.cs | 3 +- osu.Game/Screens/Play/Player.cs | 2 +- osu.Game/Screens/Play/ReplayPlayer.cs | 2 - .../Ranking/Statistics/StatisticsPanel.cs | 67 ++++++++++++------- 9 files changed, 57 insertions(+), 38 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index f8fa5d4c40..411956e120 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -311,7 +311,7 @@ namespace osu.Game.Rulesets.Mania return (PlayfieldType)Enum.GetValues(typeof(PlayfieldType)).Cast().OrderByDescending(i => i).First(v => variant >= v); } - public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score) => new[] + public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[] { new StatisticRow { diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs index f2a36ea017..49b469ba24 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs @@ -39,12 +39,12 @@ namespace osu.Game.Rulesets.Osu.Tests RelativeSizeAxes = Axes.Both, Colour = Color4Extensions.FromHex("#333"), }, - object1 = new BorderCircle + object2 = new BorderCircle { Position = new Vector2(256, 192), Colour = Color4.Yellow, }, - object2 = new BorderCircle + object1 = new BorderCircle { Position = new Vector2(100, 300), }, @@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Osu.Tests private class TestAccuracyHeatmap : AccuracyHeatmap { public TestAccuracyHeatmap(ScoreInfo score) - : base(score) + : base(score, new TestBeatmap(new OsuRuleset().RulesetInfo)) { } diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index a164265290..2ba2f4b097 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -192,7 +192,7 @@ namespace osu.Game.Rulesets.Osu public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new OsuRulesetConfigManager(settings, RulesetInfo); - public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score) => new[] + public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[] { new StatisticRow { @@ -203,7 +203,7 @@ namespace osu.Game.Rulesets.Osu RelativeSizeAxes = Axes.X, Height = 130 }), - new StatisticItem("Accuracy Heatmap", new AccuracyHeatmap(score) + new StatisticItem("Accuracy Heatmap", new AccuracyHeatmap(score, playableBeatmap) { RelativeSizeAxes = Axes.X, Height = 130 diff --git a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs index f8ab03aad0..58089553a4 100644 --- a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Utils; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Scoring; using osuTK; @@ -35,10 +36,12 @@ namespace osu.Game.Rulesets.Osu.Statistics private GridContainer pointGrid; private readonly ScoreInfo score; + private readonly IBeatmap playableBeatmap; - public AccuracyHeatmap(ScoreInfo score) + public AccuracyHeatmap(ScoreInfo score, IBeatmap playableBeatmap) { this.score = score; + this.playableBeatmap = playableBeatmap; } [BackgroundDependencyLoader] @@ -146,7 +149,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 * (score.Beatmap.BaseDifficulty.CircleSize - 5) / 5) / 2; + float radius = OsuHitObject.OBJECT_RADIUS * (1.0f - 0.7f * (playableBeatmap.BeatmapInfo.BaseDifficulty.CircleSize - 5) / 5) / 2; foreach (var e in score.HitEvents.Where(e => e.HitObject is HitCircle)) { diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 92b04e8397..17d0800228 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -159,7 +159,7 @@ namespace osu.Game.Rulesets.Taiko public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame(); - public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score) => new[] + public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[] { new StatisticRow { diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index f9c2b09be9..3a7f433a37 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -214,8 +214,9 @@ namespace osu.Game.Rulesets /// Creates the statistics for a to be displayed in the results screen. /// /// The to create the statistics for. The score is guaranteed to have populated. + /// The , converted for this with all relevant s applied. /// The s to display. Each may contain 0 or more . [NotNull] - public virtual StatisticRow[] CreateStatisticsForScore(ScoreInfo score) => Array.Empty(); + public virtual StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => Array.Empty(); } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index c2bb75b8f3..d3b88e56ae 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -463,7 +463,7 @@ namespace osu.Game.Screens.Play { var score = new ScoreInfo { - Beatmap = gameplayBeatmap.BeatmapInfo, + Beatmap = Beatmap.Value.BeatmapInfo, Ruleset = rulesetInfo, Mods = Mods.Value.ToArray(), }; diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index 8a925958fd..7f5c17a265 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -31,8 +31,6 @@ namespace osu.Game.Screens.Play var baseScore = base.CreateScore(); // Since the replay score doesn't contain statistics, we'll pass them through here. - // We also have to pass in the beatmap to get the post-mod-application version. - score.ScoreInfo.Beatmap = baseScore.Beatmap; score.ScoreInfo.HitEvents = baseScore.HitEvents; return score.ScoreInfo; diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index cac2bf866b..8aceaa335c 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -1,14 +1,18 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using System.Threading; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Placeholders; +using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osuTK; @@ -22,6 +26,9 @@ namespace osu.Game.Screens.Ranking.Statistics protected override bool StartHidden => true; + [Resolved] + private BeatmapManager beatmapManager { get; set; } + private readonly Container content; private readonly LoadingSpinner spinner; @@ -71,37 +78,47 @@ namespace osu.Game.Screens.Ranking.Statistics { spinner.Show(); - var rows = new FillFlowContainer - { - RelativeSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Spacing = new Vector2(30, 15), - }; + var localCancellationSource = loadCancellation = new CancellationTokenSource(); + IBeatmap playableBeatmap = null; - foreach (var row in newScore.Ruleset.CreateInstance().CreateStatisticsForScore(newScore)) + // 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(() => { - rows.Add(new GridContainer + playableBeatmap = beatmapManager.GetWorkingBeatmap(newScore.Beatmap).GetPlayableBeatmap(newScore.Ruleset, newScore.Mods ?? Array.Empty()); + }, loadCancellation.Token).ContinueWith(t => + { + var rows = new FillFlowContainer { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Content = new[] + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(30, 15), + }; + + foreach (var row in newScore.Ruleset.CreateInstance().CreateStatisticsForScore(newScore, playableBeatmap)) + { + rows.Add(new GridContainer { - row.Columns?.Select(c => new StatisticContainer(c)).Cast().ToArray() - }, - ColumnDimensions = Enumerable.Range(0, row.Columns?.Length ?? 0) - .Select(i => row.Columns[i].Dimension ?? new Dimension()).ToArray(), - RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) } - }); - } + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Content = new[] + { + row.Columns?.Select(c => new StatisticContainer(c)).Cast().ToArray() + }, + ColumnDimensions = Enumerable.Range(0, row.Columns?.Length ?? 0) + .Select(i => row.Columns[i].Dimension ?? new Dimension()).ToArray(), + RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) } + }); + } - LoadComponentAsync(rows, d => - { - if (Score.Value != newScore) - return; + LoadComponentAsync(rows, d => + { + if (Score.Value != newScore) + return; - spinner.Hide(); - content.Add(d); - }, (loadCancellation = new CancellationTokenSource()).Token); + spinner.Hide(); + content.Add(d); + }, localCancellationSource.Token); + }, localCancellationSource.Token); } } From 2b7fb2b71d352a97031b7477315c50f0a9f8ba70 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 22 Jun 2020 19:04:51 +0900 Subject: [PATCH 2170/2376] Rename to Position --- osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs | 4 ++-- osu.Game/Rulesets/Scoring/HitEvent.cs | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs index 58089553a4..40bdeeaa88 100644 --- a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs @@ -153,10 +153,10 @@ namespace osu.Game.Rulesets.Osu.Statistics foreach (var e in score.HitEvents.Where(e => e.HitObject is HitCircle)) { - if (e.LastHitObject == null || e.PositionOffset == null) + if (e.LastHitObject == null || e.Position == null) continue; - AddPoint(((OsuHitObject)e.LastHitObject).StackedEndPosition, ((OsuHitObject)e.HitObject).StackedEndPosition, e.PositionOffset.Value, radius); + AddPoint(((OsuHitObject)e.LastHitObject).StackedEndPosition, ((OsuHitObject)e.HitObject).StackedEndPosition, e.Position.Value, radius); } } diff --git a/osu.Game/Rulesets/Scoring/HitEvent.cs b/osu.Game/Rulesets/Scoring/HitEvent.cs index ea2975a6c4..0ebbec62ba 100644 --- a/osu.Game/Rulesets/Scoring/HitEvent.cs +++ b/osu.Game/Rulesets/Scoring/HitEvent.cs @@ -34,10 +34,10 @@ namespace osu.Game.Rulesets.Scoring public readonly HitObject LastHitObject; /// - /// A position offset, if available, at the time of the event. + /// A position, if available, at the time of the event. /// [CanBeNull] - public readonly Vector2? PositionOffset; + public readonly Vector2? Position; /// /// Creates a new . @@ -46,14 +46,14 @@ namespace osu.Game.Rulesets.Scoring /// The . /// The that triggered the event. /// The previous . - /// A positional offset. - public HitEvent(double timeOffset, HitResult result, HitObject hitObject, [CanBeNull] HitObject lastHitObject, [CanBeNull] Vector2? positionOffset) + /// A position corresponding to the event. + public HitEvent(double timeOffset, HitResult result, HitObject hitObject, [CanBeNull] HitObject lastHitObject, [CanBeNull] Vector2? position) { TimeOffset = timeOffset; Result = result; HitObject = hitObject; LastHitObject = lastHitObject; - PositionOffset = positionOffset; + Position = position; } /// From 30aa6ec2d3ff6dc63c0bf9b3d33cb5aef820c35c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 22 Jun 2020 19:05:41 +0900 Subject: [PATCH 2171/2376] Don't consider slider tails in accuracy heatmap --- osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs index 40bdeeaa88..cba753e003 100644 --- a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs @@ -151,7 +151,7 @@ namespace osu.Game.Rulesets.Osu.Statistics // 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; - foreach (var e in score.HitEvents.Where(e => e.HitObject is HitCircle)) + foreach (var e in score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle))) { if (e.LastHitObject == null || e.Position == null) continue; From 988baad16f296bb4f3df9f2c5e3478651057ceff Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 22 Jun 2020 19:20:43 +0900 Subject: [PATCH 2172/2376] Expand statistics to fill more of the screen --- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 2 +- osu.Game.Rulesets.Osu/OsuRuleset.cs | 13 +++++++++---- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 2 +- .../Screens/Ranking/Statistics/StatisticsPanel.cs | 8 +++++++- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 411956e120..a27485dd06 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -320,7 +320,7 @@ namespace osu.Game.Rulesets.Mania new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents) { RelativeSizeAxes = Axes.X, - Height = 130 + Height = 250 }), } } diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 2ba2f4b097..e488ba65c8 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -30,7 +30,6 @@ using osu.Game.Scoring; using osu.Game.Skinning; using System; using System.Linq; -using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Statistics; using osu.Game.Screens.Ranking.Statistics; @@ -201,13 +200,19 @@ namespace osu.Game.Rulesets.Osu new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)).ToList()) { RelativeSizeAxes = Axes.X, - Height = 130 + Height = 250 }), + } + }, + new StatisticRow + { + Columns = new[] + { new StatisticItem("Accuracy Heatmap", new AccuracyHeatmap(score, playableBeatmap) { RelativeSizeAxes = Axes.X, - Height = 130 - }, new Dimension(GridSizeMode.Absolute, 130)), + Height = 250 + }), } } }; diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 17d0800228..156905fa9c 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -168,7 +168,7 @@ namespace osu.Game.Rulesets.Taiko new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents.Where(e => e.HitObject is Hit).ToList()) { RelativeSizeAxes = Axes.X, - Height = 130 + Height = 250 }), } } diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 8aceaa335c..d2d2adb2f4 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -98,11 +98,17 @@ namespace osu.Game.Screens.Ranking.Statistics { rows.Add(new GridContainer { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Content = new[] { - row.Columns?.Select(c => new StatisticContainer(c)).Cast().ToArray() + row.Columns?.Select(c => new StatisticContainer(c) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }).Cast().ToArray() }, ColumnDimensions = Enumerable.Range(0, row.Columns?.Length ?? 0) .Select(i => row.Columns[i].Dimension ?? new Dimension()).ToArray(), From 4d30761ce3131eccaab114285018f5ab0cc54a79 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 22 Jun 2020 19:49:38 +0900 Subject: [PATCH 2173/2376] Fix 1M score being possible with only GREATs in mania --- osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs b/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs index c2f8fb8678..53db676a54 100644 --- a/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs +++ b/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs @@ -25,8 +25,10 @@ namespace osu.Game.Rulesets.Mania.Judgements return 200; case HitResult.Great: - case HitResult.Perfect: return 300; + + case HitResult.Perfect: + return 320; } } } From 5c4df2e32c0a1cd8f79f8d5e1e99e9fef6bfd228 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 22 Jun 2020 20:20:42 +0900 Subject: [PATCH 2174/2376] Cancel load on dispose --- osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index d2d2adb2f4..651cdc4b0f 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -63,6 +63,7 @@ namespace osu.Game.Screens.Ranking.Statistics private void populateStatistics(ValueChangedEvent score) { loadCancellation?.Cancel(); + loadCancellation = null; foreach (var child in content) child.FadeOut(150).Expire(); @@ -131,5 +132,12 @@ namespace osu.Game.Screens.Ranking.Statistics protected override void PopIn() => this.FadeIn(150, Easing.OutQuint); protected override void PopOut() => this.FadeOut(150, Easing.OutQuint); + + protected override void Dispose(bool isDisposing) + { + loadCancellation?.Cancel(); + + base.Dispose(isDisposing); + } } } From ff2f3a8484022a209b09d5c72343561e26e4128a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 22 Jun 2020 20:32:04 +0900 Subject: [PATCH 2175/2376] Fix div-by-zero errors with autoplay --- ...estSceneHitEventTimingDistributionGraph.cs | 28 ++++++++++++++++--- .../HitEventTimingDistributionGraph.cs | 4 +++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs b/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs index b34529cca7..7ca1fc842f 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs @@ -3,6 +3,8 @@ using System; using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; @@ -15,7 +17,25 @@ namespace osu.Game.Tests.Visual.Ranking { public class TestSceneHitEventTimingDistributionGraph : OsuTestScene { - public TestSceneHitEventTimingDistributionGraph() + [Test] + public void TestManyDistributedEvents() + { + createTest(CreateDistributedHitEvents()); + } + + [Test] + public void TestZeroTimeOffset() + { + createTest(Enumerable.Range(0, 100).Select(_ => new HitEvent(0, HitResult.Perfect, new HitCircle(), new HitCircle(), null)).ToList()); + } + + [Test] + public void TestNoEvents() + { + createTest(new List()); + } + + private void createTest(List events) => AddStep("create test", () => { Children = new Drawable[] { @@ -24,14 +44,14 @@ namespace osu.Game.Tests.Visual.Ranking RelativeSizeAxes = Axes.Both, Colour = Color4Extensions.FromHex("#333") }, - new HitEventTimingDistributionGraph(CreateDistributedHitEvents()) + new HitEventTimingDistributionGraph(events) { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(400, 130) + Size = new Vector2(600, 130) } }; - } + }); public static List CreateDistributedHitEvents() { diff --git a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs index 9b46bea2cb..8ec7e863b1 100644 --- a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs +++ b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs @@ -58,8 +58,12 @@ namespace osu.Game.Screens.Ranking.Statistics return; int[] bins = new int[total_timing_distribution_bins]; + double binSize = Math.Ceiling(hitEvents.Max(e => Math.Abs(e.TimeOffset)) / timing_distribution_bins); + // Prevent div-by-0 by enforcing a minimum bin size + binSize = Math.Max(1, binSize); + foreach (var e in hitEvents) { int binOffset = (int)(e.TimeOffset / binSize); From 6afd6efdeba5fe08b9598c9de1c089a162bcc467 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 22 Jun 2020 20:33:08 +0900 Subject: [PATCH 2176/2376] Return default beatmap if local beatmap can't be retrieved --- osu.Game/Beatmaps/BeatmapManager.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 2cf3a21975..637833fb5d 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -240,6 +240,9 @@ namespace osu.Game.Beatmaps beatmapInfo = QueryBeatmap(b => b.ID == info.ID); } + if (beatmapInfo == null) + return DefaultBeatmap; + lock (workingCache) { var working = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == beatmapInfo.ID); From 983f0ada2da68527f5892ffd6ff0202c05d1d439 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 22 Jun 2020 20:44:39 +0900 Subject: [PATCH 2177/2376] Increase number of points to ensure there's a centre --- osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs index cba753e003..23539f3a12 100644 --- a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs @@ -27,9 +27,9 @@ namespace osu.Game.Rulesets.Osu.Statistics /// /// Number of rows/columns of points. - /// 4px per point @ 128x128 size (the contents of the are always square). 1024 total points. + /// ~4px per point @ 128x128 size (the contents of the are always square). 1089 total points. /// - private const int points_per_dimension = 32; + private const int points_per_dimension = 33; private const float rotation = 45; From 1aec1ea53fc9566e384f03fd96e5e5af72f3a2be Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 22 Jun 2020 20:45:44 +0900 Subject: [PATCH 2178/2376] Fix off-by-one causing auto to not be centred --- osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs index 23539f3a12..0d6d05292a 100644 --- a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs @@ -189,7 +189,7 @@ namespace osu.Game.Rulesets.Osu.Statistics var rotatedAngle = finalAngle - MathUtils.DegreesToRadians(rotation); var rotatedCoordinate = -1 * new Vector2((float)Math.Cos(rotatedAngle), (float)Math.Sin(rotatedAngle)); - Vector2 localCentre = new Vector2(points_per_dimension) / 2; + Vector2 localCentre = new Vector2(points_per_dimension - 1) / 2; float localRadius = localCentre.X * inner_portion * normalisedDistance; // The radius inside the inner portion which of the heatmap which the closest point lies. Vector2 localPoint = localCentre + localRadius * rotatedCoordinate; From beb6e6ea88af3dbf213808accea49d247e358ce8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 22 Jun 2020 21:00:13 +0900 Subject: [PATCH 2179/2376] Buffer the accuracy heatmap for performance --- .../Statistics/AccuracyHeatmap.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs index 0d6d05292a..6e1b6ef9b5 100644 --- a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs @@ -33,6 +33,7 @@ namespace osu.Game.Rulesets.Osu.Statistics private const float rotation = 45; + private BufferedContainer bufferedGrid; private GridContainer pointGrid; private readonly ScoreInfo score; @@ -112,10 +113,16 @@ namespace osu.Game.Rulesets.Osu.Statistics } } }, - pointGrid = new GridContainer + bufferedGrid = new BufferedContainer { - RelativeSizeAxes = Axes.Both - } + RelativeSizeAxes = Axes.Both, + CacheDrawnFrameBuffer = true, + BackgroundColour = Color4Extensions.FromHex("#202624").Opacity(0), + Child = pointGrid = new GridContainer + { + RelativeSizeAxes = Axes.Both + } + }, } }; @@ -198,6 +205,8 @@ namespace osu.Game.Rulesets.Osu.Statistics int c = Math.Clamp((int)Math.Round(localPoint.X), 0, points_per_dimension - 1); ((HitPoint)pointGrid.Content[r][c]).Increment(); + + bufferedGrid.ForceRedraw(); } private class HitPoint : Circle From b3e200ee7face7f246ef82ee56faba604a216740 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 22 Jun 2020 21:00:35 +0900 Subject: [PATCH 2180/2376] Re-invert test --- osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs index 49b469ba24..10d9d7ffde 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs @@ -39,12 +39,12 @@ namespace osu.Game.Rulesets.Osu.Tests RelativeSizeAxes = Axes.Both, Colour = Color4Extensions.FromHex("#333"), }, - object2 = new BorderCircle + object1 = new BorderCircle { Position = new Vector2(256, 192), Colour = Color4.Yellow, }, - object1 = new BorderCircle + object2 = new BorderCircle { Position = new Vector2(100, 300), }, From cb03e6faa9cf975719a8d3753ca063bb986ed8bb Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 22 Jun 2020 21:09:47 +0900 Subject: [PATCH 2181/2376] Improve visual display of arrow --- .../Statistics/AccuracyHeatmap.cs | 53 +++++++++++-------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs index 6e1b6ef9b5..94d47ecb32 100644 --- a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs @@ -74,41 +74,52 @@ namespace osu.Game.Rulesets.Osu.Statistics new Container { RelativeSizeAxes = Axes.Both, - Masking = true, Children = new Drawable[] { - new Box + new Container { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Y, - Height = 2, // We're rotating along a diagonal - we don't really care how big this is. - Width = 1f, - Rotation = -rotation, - Alpha = 0.3f, - }, - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Y, - Height = 2, // We're rotating along a diagonal - we don't really care how big this is. - Width = 1f, - Rotation = rotation + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(1), + Child = new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + Children = new Drawable[] + { + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Height = 2, // We're rotating along a diagonal - we don't really care how big this is. + Width = 1f, + Rotation = -rotation, + Alpha = 0.3f, + }, + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Height = 2, // We're rotating along a diagonal - we don't really care how big this is. + Width = 1f, + Rotation = rotation + }, + } + }, }, new Box { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, Width = 10, - Height = 2f, + Height = 2, }, new Box { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, - Y = -1, - Width = 2f, + Width = 2, Height = 10, } } From f60a80b2635f4beb8d45f5a8432abbb2bf36e278 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Jun 2020 18:01:08 +0900 Subject: [PATCH 2182/2376] Fix animations and general code quality --- osu.Game/Screens/Menu/IntroWelcome.cs | 73 +++++++++++---------------- 1 file changed, 29 insertions(+), 44 deletions(-) diff --git a/osu.Game/Screens/Menu/IntroWelcome.cs b/osu.Game/Screens/Menu/IntroWelcome.cs index 92c844db8b..7714ec6ee1 100644 --- a/osu.Game/Screens/Menu/IntroWelcome.cs +++ b/osu.Game/Screens/Menu/IntroWelcome.cs @@ -69,63 +69,47 @@ namespace osu.Game.Screens.Menu private class WelcomeIntroSequence : Container { private Sprite welcomeText; + private Container scaleContainer; [BackgroundDependencyLoader] private void load(TextureStore textures) { Origin = Anchor.Centre; Anchor = Anchor.Centre; + Children = new Drawable[] { - new Container + scaleContainer = new Container { + AutoSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, - AutoSizeAxes = Axes.Both, Children = new Drawable[] { - new Container + new LogoVisualisation { - AutoSizeAxes = Axes.Both, - Children = new Drawable[] - { - new LogoVisualisation - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Alpha = 0.5f, - AccentColour = Color4.DarkBlue, - Size = new Vector2(0.96f) - }, - new Container - { - AutoSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Circle - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(480), - Colour = Color4.Black - } - } - } - } - } + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Alpha = 0.5f, + AccentColour = Color4.DarkBlue, + Size = new Vector2(0.96f) + }, + new Circle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(480), + Colour = Color4.Black + }, + welcomeText = new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Texture = textures.Get(@"Intro/Welcome/welcome_text") + }, } }, - welcomeText = new Sprite - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.X, - Scale = new Vector2(0.1f), - Height = 156, - Alpha = 0, - Texture = textures.Get(@"Intro/Welcome/welcome_text") - }, }; } @@ -135,9 +119,10 @@ namespace osu.Game.Screens.Menu using (BeginDelayedSequence(0, true)) { - welcomeText.ResizeHeightTo(welcomeText.Height * 2, 500, Easing.In); - welcomeText.FadeIn(delay_step_two); - welcomeText.ScaleTo(welcomeText.Scale + new Vector2(0.1f), delay_step_two, Easing.Out).OnComplete(_ => Expire()); + scaleContainer.ScaleTo(0.9f).ScaleTo(1, delay_step_two).OnComplete(_ => Expire()); + scaleContainer.FadeInFromZero(1800); + + welcomeText.ScaleTo(new Vector2(1, 0)).ScaleTo(Vector2.One, 400, Easing.Out); } } } From 1bf00e0c820c8c391a0b7574b8cfc28ea0374c43 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 22 Jun 2020 23:22:49 +0900 Subject: [PATCH 2183/2376] Schedule continuation --- osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 651cdc4b0f..77f3bd7b5c 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -86,7 +86,7 @@ namespace osu.Game.Screens.Ranking.Statistics Task.Run(() => { playableBeatmap = beatmapManager.GetWorkingBeatmap(newScore.Beatmap).GetPlayableBeatmap(newScore.Ruleset, newScore.Mods ?? Array.Empty()); - }, loadCancellation.Token).ContinueWith(t => + }, loadCancellation.Token).ContinueWith(t => Schedule(() => { var rows = new FillFlowContainer { @@ -125,7 +125,7 @@ namespace osu.Game.Screens.Ranking.Statistics spinner.Hide(); content.Add(d); }, localCancellationSource.Token); - }, localCancellationSource.Token); + }), localCancellationSource.Token); } } From 7a48ab1774cfaca8697f47806651ec6e3cd6c8ff Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2020 17:19:46 +0000 Subject: [PATCH 2184/2376] Bump ppy.osu.Game.Resources from 2020.602.0 to 2020.622.1 Bumps [ppy.osu.Game.Resources](https://github.com/ppy/osu-resources) from 2020.602.0 to 2020.622.1. - [Release notes](https://github.com/ppy/osu-resources/releases) - [Commits](https://github.com/ppy/osu-resources/compare/2020.602.0...2020.622.1) Signed-off-by: dependabot-preview[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 119c309675..192be999eb 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 bec3bc9d39..911292c6ae 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -25,7 +25,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index de5130b66a..18249b40ca 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + From e827b14abf5212aa0809256b4830456acda994e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 21 Jun 2020 16:40:05 +0200 Subject: [PATCH 2185/2376] Add LayeredHitSamples skin config lookup --- osu.Game/Skinning/GlobalSkinConfiguration.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Skinning/GlobalSkinConfiguration.cs b/osu.Game/Skinning/GlobalSkinConfiguration.cs index 8774fe5a97..d405702ea5 100644 --- a/osu.Game/Skinning/GlobalSkinConfiguration.cs +++ b/osu.Game/Skinning/GlobalSkinConfiguration.cs @@ -5,6 +5,7 @@ namespace osu.Game.Skinning { public enum GlobalSkinConfiguration { - AnimationFramerate + AnimationFramerate, + LayeredHitSounds, } } From c5049b51c5835ab6950d1f9244d0d355157439a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 21 Jun 2020 16:43:21 +0200 Subject: [PATCH 2186/2376] Mark normal-hitnormal sample as layered --- .../Objects/Legacy/ConvertHitObjectParser.cs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 9e936c7717..77075b2abe 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -12,6 +12,7 @@ using System.Linq; using JetBrains.Annotations; using osu.Framework.Utils; using osu.Game.Beatmaps.Legacy; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Objects.Legacy { @@ -356,7 +357,10 @@ namespace osu.Game.Rulesets.Objects.Legacy Bank = bankInfo.Normal, Name = HitSampleInfo.HIT_NORMAL, Volume = bankInfo.Volume, - CustomSampleBank = bankInfo.CustomSampleBank + CustomSampleBank = bankInfo.CustomSampleBank, + // if the sound type doesn't have the Normal flag set, attach it anyway as a layered sample. + // None also counts as a normal non-layered sample: https://osu.ppy.sh/help/wiki/osu!_File_Formats/Osu_(file_format)#hitsounds + IsLayered = type != LegacyHitSoundType.None && !type.HasFlag(LegacyHitSoundType.Normal) } }; @@ -409,7 +413,7 @@ namespace osu.Game.Rulesets.Objects.Legacy public SampleBankInfo Clone() => (SampleBankInfo)MemberwiseClone(); } - internal class LegacyHitSampleInfo : HitSampleInfo + public class LegacyHitSampleInfo : HitSampleInfo { private int customSampleBank; @@ -424,6 +428,15 @@ namespace osu.Game.Rulesets.Objects.Legacy Suffix = value.ToString(); } } + + /// + /// Whether this hit sample is layered. + /// + /// + /// Layered hit samples are automatically added in all modes (except osu!mania), but can be disabled + /// using the skin config option. + /// + public bool IsLayered { get; set; } } private class FileHitSampleInfo : LegacyHitSampleInfo From c7d2ce12eb1cbf0cd6a8f5d1c72ac482d6ed62a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 21 Jun 2020 18:52:15 +0200 Subject: [PATCH 2187/2376] Add failing test cases --- ...a-hitobject-beatmap-custom-sample-bank.osu | 10 ++++ ...a-hitobject-beatmap-normal-sample-bank.osu | 10 ++++ .../TestSceneManiaHitObjectSamples.cs | 49 +++++++++++++++ .../Gameplay/TestSceneHitObjectSamples.cs | 60 +++++++++++++++++++ .../hitobject-beatmap-custom-sample-bank.osu | 7 +++ .../Tests/Beatmaps/HitObjectSampleTest.cs | 12 +++- 6 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Rulesets.Mania.Tests/Resources/SampleLookups/mania-hitobject-beatmap-custom-sample-bank.osu create mode 100644 osu.Game.Rulesets.Mania.Tests/Resources/SampleLookups/mania-hitobject-beatmap-normal-sample-bank.osu create mode 100644 osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectSamples.cs create mode 100644 osu.Game.Tests/Resources/SampleLookups/hitobject-beatmap-custom-sample-bank.osu diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/SampleLookups/mania-hitobject-beatmap-custom-sample-bank.osu b/osu.Game.Rulesets.Mania.Tests/Resources/SampleLookups/mania-hitobject-beatmap-custom-sample-bank.osu new file mode 100644 index 0000000000..4f8e1b68dd --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Resources/SampleLookups/mania-hitobject-beatmap-custom-sample-bank.osu @@ -0,0 +1,10 @@ +osu file format v14 + +[General] +Mode: 3 + +[TimingPoints] +0,300,4,0,2,100,1,0 + +[HitObjects] +444,320,1000,5,2,0:0:0:0: diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/SampleLookups/mania-hitobject-beatmap-normal-sample-bank.osu b/osu.Game.Rulesets.Mania.Tests/Resources/SampleLookups/mania-hitobject-beatmap-normal-sample-bank.osu new file mode 100644 index 0000000000..f22901e304 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Resources/SampleLookups/mania-hitobject-beatmap-normal-sample-bank.osu @@ -0,0 +1,10 @@ +osu file format v14 + +[General] +Mode: 3 + +[TimingPoints] +0,300,4,0,2,100,1,0 + +[HitObjects] +444,320,1000,5,1,0:0:0:0: diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectSamples.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectSamples.cs new file mode 100644 index 0000000000..0d726e1a50 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectSamples.cs @@ -0,0 +1,49 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Reflection; +using NUnit.Framework; +using osu.Framework.IO.Stores; +using osu.Game.Tests.Beatmaps; + +namespace osu.Game.Rulesets.Mania.Tests +{ + public class TestSceneManiaHitObjectSamples : HitObjectSampleTest + { + protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset(); + protected override IResourceStore Resources => new DllResourceStore(Assembly.GetAssembly(typeof(TestSceneManiaHitObjectSamples))); + + /// + /// Tests that when a normal sample bank is used, the normal hitsound will be looked up. + /// + [Test] + public void TestManiaHitObjectNormalSampleBank() + { + const string expected_sample = "normal-hitnormal2"; + + SetupSkins(expected_sample, expected_sample); + + CreateTestWithBeatmap("mania-hitobject-beatmap-normal-sample-bank.osu"); + + AssertBeatmapLookup(expected_sample); + } + + /// + /// Tests that when a custom sample bank is used, layered hitsounds are not played + /// (only the sample from the custom bank is looked up). + /// + [Test] + public void TestManiaHitObjectCustomSampleBank() + { + const string expected_sample = "normal-hitwhistle2"; + const string unwanted_sample = "normal-hitnormal2"; + + SetupSkins(expected_sample, unwanted_sample); + + CreateTestWithBeatmap("mania-hitobject-beatmap-custom-sample-bank.osu"); + + AssertBeatmapLookup(expected_sample); + AssertNoLookup(unwanted_sample); + } + } +} diff --git a/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs index ef6efb7fec..737946e1e0 100644 --- a/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs @@ -6,6 +6,7 @@ using osu.Framework.IO.Stores; using osu.Framework.Testing; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; +using osu.Game.Skinning; using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Resources; @@ -167,5 +168,64 @@ namespace osu.Game.Tests.Gameplay AssertBeatmapLookup(expected_sample); } + + /// + /// Tests that when a custom sample bank is used, both the normal and additional sounds will be looked up. + /// + [Test] + public void TestHitObjectCustomSampleBank() + { + string[] expectedSamples = + { + "normal-hitnormal2", + "normal-hitwhistle2" + }; + + SetupSkins(expectedSamples[0], expectedSamples[1]); + + CreateTestWithBeatmap("hitobject-beatmap-custom-sample-bank.osu"); + + AssertBeatmapLookup(expectedSamples[0]); + AssertUserLookup(expectedSamples[1]); + } + + /// + /// Tests that when a custom sample bank is used, but is disabled, + /// only the additional sound will be looked up. + /// + [Test] + public void TestHitObjectCustomSampleBankWithoutLayered() + { + const string expected_sample = "normal-hitwhistle2"; + const string unwanted_sample = "normal-hitnormal2"; + + SetupSkins(expected_sample, unwanted_sample); + disableLayeredHitSounds(); + + CreateTestWithBeatmap("hitobject-beatmap-custom-sample-bank.osu"); + + AssertBeatmapLookup(expected_sample); + AssertNoLookup(unwanted_sample); + } + + /// + /// Tests that when a normal sample bank is used and is disabled, + /// the normal sound will be looked up anyway. + /// + [Test] + public void TestHitObjectNormalSampleBankWithoutLayered() + { + const string expected_sample = "normal-hitnormal"; + + SetupSkins(expected_sample, expected_sample); + disableLayeredHitSounds(); + + CreateTestWithBeatmap("hitobject-beatmap-sample.osu"); + + AssertBeatmapLookup(expected_sample); + } + + private void disableLayeredHitSounds() + => AddStep("set LayeredHitSounds to false", () => Skin.Configuration.ConfigDictionary[GlobalSkinConfiguration.LayeredHitSounds.ToString()] = "0"); } } diff --git a/osu.Game.Tests/Resources/SampleLookups/hitobject-beatmap-custom-sample-bank.osu b/osu.Game.Tests/Resources/SampleLookups/hitobject-beatmap-custom-sample-bank.osu new file mode 100644 index 0000000000..c50c921839 --- /dev/null +++ b/osu.Game.Tests/Resources/SampleLookups/hitobject-beatmap-custom-sample-bank.osu @@ -0,0 +1,7 @@ +osu file format v14 + +[TimingPoints] +0,300,4,0,2,100,1,0 + +[HitObjects] +444,320,1000,5,2,0:0:0:0: diff --git a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs index b4ce322165..ab4fb38657 100644 --- a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs +++ b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs @@ -24,6 +24,10 @@ namespace osu.Game.Tests.Beatmaps public abstract class HitObjectSampleTest : PlayerTestScene { protected abstract IResourceStore Resources { get; } + protected LegacySkin Skin { get; private set; } + + [Resolved] + private RulesetStore rulesetStore { get; set; } private readonly SkinInfo userSkinInfo = new SkinInfo(); @@ -64,6 +68,9 @@ namespace osu.Game.Tests.Beatmaps { using (var reader = new LineBufferedReader(Resources.GetStream($"Resources/SampleLookups/{filename}"))) currentTestBeatmap = Decoder.GetDecoder(reader).Decode(reader); + + // populate ruleset for beatmap converters that require it to be present. + currentTestBeatmap.BeatmapInfo.Ruleset = rulesetStore.GetRuleset(currentTestBeatmap.BeatmapInfo.RulesetID); }); }); } @@ -91,7 +98,7 @@ namespace osu.Game.Tests.Beatmaps }; // Need to refresh the cached skin source to refresh the skin resource store. - dependencies.SkinSource = new SkinProvidingContainer(new LegacySkin(userSkinInfo, userSkinResourceStore, Audio)); + dependencies.SkinSource = new SkinProvidingContainer(Skin = new LegacySkin(userSkinInfo, userSkinResourceStore, Audio)); }); } @@ -101,6 +108,9 @@ namespace osu.Game.Tests.Beatmaps protected void AssertUserLookup(string name) => AddAssert($"\"{name}\" looked up from user skin", () => !beatmapSkinResourceStore.PerformedLookups.Contains(name) && userSkinResourceStore.PerformedLookups.Contains(name)); + protected void AssertNoLookup(string name) => AddAssert($"\"{name}\" not looked up", + () => !beatmapSkinResourceStore.PerformedLookups.Contains(name) && !userSkinResourceStore.PerformedLookups.Contains(name)); + private class SkinSourceDependencyContainer : IReadOnlyDependencyContainer { public ISkinSource SkinSource; From 8233f5fbc4e532cedc6a02b54d453ab106f5bf64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 21 Jun 2020 22:44:35 +0200 Subject: [PATCH 2188/2376] Check skin option in skin transformers --- .../Skinning/ManiaLegacySkinTransformer.cs | 12 ++++++++++++ osu.Game/Skinning/LegacySkinTransformer.cs | 13 ++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs index 84e88a10be..e167135556 100644 --- a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs @@ -9,6 +9,9 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Skinning; using System.Collections.Generic; +using osu.Framework.Audio.Sample; +using osu.Game.Audio; +using osu.Game.Rulesets.Objects.Legacy; namespace osu.Game.Rulesets.Mania.Skinning { @@ -129,6 +132,15 @@ namespace osu.Game.Rulesets.Mania.Skinning return this.GetAnimation(filename, true, true); } + public override SampleChannel GetSample(ISampleInfo sampleInfo) + { + // layered hit sounds never play in mania + if (sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample && legacySample.IsLayered) + return new SampleChannelVirtual(); + + return Source.GetSample(sampleInfo); + } + public override IBindable GetConfig(TLookup lookup) { if (lookup is ManiaSkinConfigurationLookup maniaLookup) diff --git a/osu.Game/Skinning/LegacySkinTransformer.cs b/osu.Game/Skinning/LegacySkinTransformer.cs index 1131c93288..94a7a32f05 100644 --- a/osu.Game/Skinning/LegacySkinTransformer.cs +++ b/osu.Game/Skinning/LegacySkinTransformer.cs @@ -6,6 +6,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Textures; using osu.Game.Audio; +using osu.Game.Rulesets.Objects.Legacy; namespace osu.Game.Skinning { @@ -28,7 +29,17 @@ namespace osu.Game.Skinning public Texture GetTexture(string componentName) => Source.GetTexture(componentName); - public virtual SampleChannel GetSample(ISampleInfo sampleInfo) => Source.GetSample(sampleInfo); + public virtual SampleChannel GetSample(ISampleInfo sampleInfo) + { + if (!(sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample)) + return Source.GetSample(sampleInfo); + + var playLayeredHitSounds = GetConfig(GlobalSkinConfiguration.LayeredHitSounds); + if (legacySample.IsLayered && playLayeredHitSounds?.Value == false) + return new SampleChannelVirtual(); + + return Source.GetSample(sampleInfo); + } public abstract IBindable GetConfig(TLookup lookup); } From a2a2bf4f787fbecb798074e90aed86ae5a8197bd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Jun 2020 10:05:28 +0900 Subject: [PATCH 2189/2376] Don't activate run tool window on rider run --- .../.idea/runConfigurations/CatchRuleset__Tests_.xml | 6 +++--- .../.idea/runConfigurations/ManiaRuleset__Tests_.xml | 6 +++--- .../.idea/runConfigurations/OsuRuleset__Tests_.xml | 6 +++--- .../.idea/runConfigurations/TaikoRuleset__Tests_.xml | 6 +++--- .../.idea/runConfigurations/Tournament.xml | 6 +++--- .../.idea/runConfigurations/Tournament__Tests_.xml | 6 +++--- .idea/.idea.osu.Desktop/.idea/runConfigurations/osu_.xml | 6 +++--- .../.idea.osu.Desktop/.idea/runConfigurations/osu_SDL.xml | 8 ++++---- .../.idea/runConfigurations/osu___Tests_.xml | 6 +++--- 9 files changed, 28 insertions(+), 28 deletions(-) diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/CatchRuleset__Tests_.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/CatchRuleset__Tests_.xml index a4154623b6..512ac4393a 100644 --- a/.idea/.idea.osu.Desktop/.idea/runConfigurations/CatchRuleset__Tests_.xml +++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/CatchRuleset__Tests_.xml @@ -1,8 +1,8 @@ - + \ No newline at end of file diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/ManiaRuleset__Tests_.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/ManiaRuleset__Tests_.xml index 080dc04001..dec1ef717f 100644 --- a/.idea/.idea.osu.Desktop/.idea/runConfigurations/ManiaRuleset__Tests_.xml +++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/ManiaRuleset__Tests_.xml @@ -1,8 +1,8 @@ - + \ No newline at end of file diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/OsuRuleset__Tests_.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/OsuRuleset__Tests_.xml index 3de6a7e609..d9370d5440 100644 --- a/.idea/.idea.osu.Desktop/.idea/runConfigurations/OsuRuleset__Tests_.xml +++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/OsuRuleset__Tests_.xml @@ -1,8 +1,8 @@ - + \ No newline at end of file diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/TaikoRuleset__Tests_.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/TaikoRuleset__Tests_.xml index da14c2a29e..def4940bb1 100644 --- a/.idea/.idea.osu.Desktop/.idea/runConfigurations/TaikoRuleset__Tests_.xml +++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/TaikoRuleset__Tests_.xml @@ -1,8 +1,8 @@ - + \ No newline at end of file diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/Tournament.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/Tournament.xml index 45d1ce25e9..1ffa73c257 100644 --- a/.idea/.idea.osu.Desktop/.idea/runConfigurations/Tournament.xml +++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/Tournament.xml @@ -1,8 +1,8 @@ - + \ No newline at end of file diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/Tournament__Tests_.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/Tournament__Tests_.xml index ba80f7c100..e64da796b7 100644 --- a/.idea/.idea.osu.Desktop/.idea/runConfigurations/Tournament__Tests_.xml +++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/Tournament__Tests_.xml @@ -1,8 +1,8 @@ - + \ No newline at end of file diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu_.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu_.xml index 911c3ed9b7..22105e1de2 100644 --- a/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu_.xml +++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu_.xml @@ -1,8 +1,8 @@ - + \ No newline at end of file diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu_SDL.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu_SDL.xml index d85a0ae44c..31f1fda09d 100644 --- a/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu_SDL.xml +++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu_SDL.xml @@ -1,8 +1,8 @@ - + \ No newline at end of file diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu___Tests_.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu___Tests_.xml index ec3c81f4cd..cc243f6901 100644 --- a/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu___Tests_.xml +++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu___Tests_.xml @@ -1,8 +1,8 @@ - + \ No newline at end of file From b289beca53e68647d1fd38f57d0a15e94edbaa41 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Jun 2020 13:33:33 +0900 Subject: [PATCH 2190/2376] Fix samples being played too early --- osu.Game/Screens/Menu/IntroWelcome.cs | 33 +++++++++++++++------------ 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/Menu/IntroWelcome.cs b/osu.Game/Screens/Menu/IntroWelcome.cs index 7714ec6ee1..7ab74cbf22 100644 --- a/osu.Game/Screens/Menu/IntroWelcome.cs +++ b/osu.Game/Screens/Menu/IntroWelcome.cs @@ -39,24 +39,27 @@ namespace osu.Game.Screens.Menu if (!resuming) { - welcome?.Play(); - pianoReverb?.Play(); - - Scheduler.AddDelayed(() => - { - StartTrack(); - PrepareMenuLoad(); - - logo.ScaleTo(1); - logo.FadeIn(); - - Scheduler.Add(LoadMenu); - }, delay_step_two); - LoadComponentAsync(new WelcomeIntroSequence { RelativeSizeAxes = Axes.Both - }, AddInternal); + }, intro => + { + AddInternal(intro); + + welcome?.Play(); + pianoReverb?.Play(); + + Scheduler.AddDelayed(() => + { + StartTrack(); + PrepareMenuLoad(); + + logo.ScaleTo(1); + logo.FadeIn(); + + Scheduler.Add(LoadMenu); + }, delay_step_two); + }); } } From 4554a7db3362d3e477cbbaee424c7b490578efb1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Jun 2020 13:49:18 +0900 Subject: [PATCH 2191/2376] Update naming --- .../Objects/Drawables/Pieces/ReverseArrowPiece.cs | 2 +- .../Objects/Drawables/Pieces/CirclePiece.cs | 4 ++-- .../Skinning/TaikoLegacyPlayfieldBackgroundRight.cs | 2 +- osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs | 2 +- osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs | 2 +- .../Visual/UserInterface/TestSceneBeatSyncedContainer.cs | 2 +- osu.Game/Graphics/Containers/BeatSyncedContainer.cs | 8 ++++---- osu.Game/Graphics/UserInterface/OsuTextBox.cs | 2 +- osu.Game/Graphics/UserInterface/TwoLayerButton.cs | 2 +- osu.Game/Rulesets/Mods/ModNightcore.cs | 2 +- osu.Game/Screens/Menu/Button.cs | 4 ++-- osu.Game/Screens/Menu/MenuSideFlashes.cs | 6 +++--- osu.Game/Screens/Menu/OsuLogo.cs | 2 +- 13 files changed, 20 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ReverseArrowPiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ReverseArrowPiece.cs index 1a5195acf8..ae43006e76 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ReverseArrowPiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ReverseArrowPiece.cs @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces }; } - protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) { if (!drawableRepeat.IsHit) Child.ScaleTo(1.3f).ScaleTo(1f, timingPoint.BeatLength, Easing.Out); diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs index b5471e6976..f515a35c18 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.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.Audio.Track; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -8,7 +9,6 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Graphics.Backgrounds; using osuTK.Graphics; using osu.Game.Beatmaps.ControlPoints; -using osu.Framework.Audio.Track; using osu.Framework.Graphics.Effects; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -148,7 +148,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces }; } - protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) { if (!effectPoint.KiaiMode) return; diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacyPlayfieldBackgroundRight.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacyPlayfieldBackgroundRight.cs index 7508c75231..4bbb6be6b1 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacyPlayfieldBackgroundRight.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacyPlayfieldBackgroundRight.cs @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning }; } - protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) { base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs index 407ab30e12..b937beae3c 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs @@ -83,7 +83,7 @@ namespace osu.Game.Rulesets.Taiko.UI lastObjectHit = result.IsHit; } - protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) { kiaiMode = effectPoint.KiaiMode; } diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs b/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs index cce2be7758..6f25a5f662 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs @@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Taiko.UI textureAnimation.Seek(0); } - protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) { // assume that if the animation is playing on its own, it's independent from the beat and doesn't need to be touched. if (textureAnimation.FrameCount == 0 || textureAnimation.IsPlaying) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs index 4c32e995e8..dd5ceec739 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs @@ -177,7 +177,7 @@ namespace osu.Game.Tests.Visual.UserInterface timeSinceLastBeat.Value = TimeSinceLastBeat; } - protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) { base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs index 5a613d1a54..c37fcc043d 100644 --- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs +++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs @@ -18,7 +18,7 @@ namespace osu.Game.Graphics.Containers private TimingControlPoint lastTimingPoint; /// - /// The amount of time before a beat we should fire . + /// The amount of time before a beat we should fire . /// This allows for adding easing to animations that may be synchronised to the beat. /// protected double EarlyActivationMilliseconds; @@ -50,7 +50,7 @@ namespace osu.Game.Graphics.Containers private TimingControlPoint defaultTiming; private EffectControlPoint defaultEffect; - private TrackAmplitudes defaultAmplitudes; + private ChannelAmplitudes defaultAmplitudes; protected bool IsBeatSyncedWithTrack { get; private set; } @@ -129,7 +129,7 @@ namespace osu.Game.Graphics.Containers OmitFirstBarLine = false }; - defaultAmplitudes = new TrackAmplitudes + defaultAmplitudes = new ChannelAmplitudes { FrequencyAmplitudes = new float[256], LeftChannel = 0, @@ -137,7 +137,7 @@ namespace osu.Game.Graphics.Containers }; } - protected virtual void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) + protected virtual void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) { } } diff --git a/osu.Game/Graphics/UserInterface/OsuTextBox.cs b/osu.Game/Graphics/UserInterface/OsuTextBox.cs index 6f440d8138..06c46fbb91 100644 --- a/osu.Game/Graphics/UserInterface/OsuTextBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuTextBox.cs @@ -147,7 +147,7 @@ namespace osu.Game.Graphics.UserInterface }; } - protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) { if (!hasSelection) this.FadeTo(0.7f).FadeTo(0.4f, timingPoint.BeatLength, Easing.InOutSine); diff --git a/osu.Game/Graphics/UserInterface/TwoLayerButton.cs b/osu.Game/Graphics/UserInterface/TwoLayerButton.cs index aa96796cf1..120149d8c1 100644 --- a/osu.Game/Graphics/UserInterface/TwoLayerButton.cs +++ b/osu.Game/Graphics/UserInterface/TwoLayerButton.cs @@ -230,7 +230,7 @@ namespace osu.Game.Graphics.UserInterface }; } - protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) { base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); diff --git a/osu.Game/Rulesets/Mods/ModNightcore.cs b/osu.Game/Rulesets/Mods/ModNightcore.cs index 1df2aeb348..ed8eb2fb66 100644 --- a/osu.Game/Rulesets/Mods/ModNightcore.cs +++ b/osu.Game/Rulesets/Mods/ModNightcore.cs @@ -78,7 +78,7 @@ namespace osu.Game.Rulesets.Mods private const int bars_per_segment = 4; - protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) { base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); diff --git a/osu.Game/Screens/Menu/Button.cs b/osu.Game/Screens/Menu/Button.cs index 6708ce0ba0..be6ed9700c 100644 --- a/osu.Game/Screens/Menu/Button.cs +++ b/osu.Game/Screens/Menu/Button.cs @@ -6,6 +6,7 @@ using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; +using osu.Framework.Audio.Track; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -15,7 +16,6 @@ using osuTK.Graphics; using osuTK.Input; using osu.Framework.Extensions.Color4Extensions; using osu.Game.Graphics.Containers; -using osu.Framework.Audio.Track; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; @@ -132,7 +132,7 @@ namespace osu.Game.Screens.Menu private bool rightward; - protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) { base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); diff --git a/osu.Game/Screens/Menu/MenuSideFlashes.cs b/osu.Game/Screens/Menu/MenuSideFlashes.cs index 321381ac8d..2ff8132d47 100644 --- a/osu.Game/Screens/Menu/MenuSideFlashes.cs +++ b/osu.Game/Screens/Menu/MenuSideFlashes.cs @@ -3,7 +3,6 @@ using osuTK.Graphics; using osu.Framework.Allocation; -using osu.Framework.Audio.Track; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; @@ -16,6 +15,7 @@ using osu.Game.Skinning; using osu.Game.Online.API; using osu.Game.Users; using System; +using osu.Framework.Audio.Track; using osu.Framework.Bindables; namespace osu.Game.Screens.Menu @@ -89,7 +89,7 @@ namespace osu.Game.Screens.Menu skin.BindValueChanged(_ => updateColour(), true); } - protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) { if (beatIndex < 0) return; @@ -100,7 +100,7 @@ namespace osu.Game.Screens.Menu flash(rightBox, timingPoint.BeatLength, effectPoint.KiaiMode, amplitudes); } - private void flash(Drawable d, double beatLength, bool kiai, TrackAmplitudes amplitudes) + private void flash(Drawable d, double beatLength, bool kiai, ChannelAmplitudes amplitudes) { d.FadeTo(Math.Max(0, ((ReferenceEquals(d, leftBox) ? amplitudes.LeftChannel : amplitudes.RightChannel) - amplitude_dead_zone) / (kiai ? kiai_multiplier : alpha_multiplier)), box_fade_in_time) .Then() diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index 9cadfd7df6..089906c342 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -264,7 +264,7 @@ namespace osu.Game.Screens.Menu private int lastBeatIndex; - protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) { base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); From 49d3511063a14ad024ff8bb9da2932b08ca8fdd1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Jun 2020 13:55:44 +0900 Subject: [PATCH 2192/2376] Read amplitudes from piano reverb source --- osu.Game/Screens/Menu/IntroWelcome.cs | 6 ++- osu.Game/Screens/Menu/LogoVisualisation.cs | 59 ++++++++++++++++------ 2 files changed, 49 insertions(+), 16 deletions(-) diff --git a/osu.Game/Screens/Menu/IntroWelcome.cs b/osu.Game/Screens/Menu/IntroWelcome.cs index 7ab74cbf22..81e473dc04 100644 --- a/osu.Game/Screens/Menu/IntroWelcome.cs +++ b/osu.Game/Screens/Menu/IntroWelcome.cs @@ -44,6 +44,8 @@ namespace osu.Game.Screens.Menu RelativeSizeAxes = Axes.Both }, intro => { + intro.LogoVisualisation.AddAmplitudeSource(pianoReverb); + AddInternal(intro); welcome?.Play(); @@ -74,6 +76,8 @@ namespace osu.Game.Screens.Menu private Sprite welcomeText; private Container scaleContainer; + public LogoVisualisation LogoVisualisation { get; private set; } + [BackgroundDependencyLoader] private void load(TextureStore textures) { @@ -89,7 +93,7 @@ namespace osu.Game.Screens.Menu Origin = Anchor.Centre, Children = new Drawable[] { - new LogoVisualisation + LogoVisualisation = new LogoVisualisation { RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, diff --git a/osu.Game/Screens/Menu/LogoVisualisation.cs b/osu.Game/Screens/Menu/LogoVisualisation.cs index 6a28740d4e..dcbfe15210 100644 --- a/osu.Game/Screens/Menu/LogoVisualisation.cs +++ b/osu.Game/Screens/Menu/LogoVisualisation.cs @@ -13,7 +13,10 @@ using osu.Framework.Graphics.Textures; using osu.Game.Beatmaps; using osu.Game.Graphics; using System; +using System.Collections.Generic; +using JetBrains.Annotations; using osu.Framework.Allocation; +using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Framework.Utils; @@ -65,6 +68,11 @@ namespace osu.Game.Screens.Menu public Color4 AccentColour { get; set; } + /// + /// The relative movement of bars based on input amplification. Defaults to 1. + /// + public float Magnitude { get; set; } = 1; + private readonly float[] frequencyAmplitudes = new float[256]; private IShader shader; @@ -76,6 +84,13 @@ namespace osu.Game.Screens.Menu Blending = BlendingParameters.Additive; } + private readonly List amplitudeSources = new List(); + + public void AddAmplitudeSource(IHasAmplitudes amplitudeSource) + { + amplitudeSources.Add(amplitudeSource); + } + [BackgroundDependencyLoader] private void load(ShaderManager shaders, IBindable beatmap) { @@ -83,27 +98,28 @@ namespace osu.Game.Screens.Menu shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED); } + private readonly float[] temporalAmplitudes = new float[256]; + private void updateAmplitudes() { - var track = beatmap.Value.TrackLoaded ? beatmap.Value.Track : null; - var effect = beatmap.Value.BeatmapLoaded ? beatmap.Value.Beatmap?.ControlPointInfo.EffectPointAt(track?.CurrentTime ?? Time.Current) : null; + var effect = beatmap.Value.BeatmapLoaded && beatmap.Value.TrackLoaded + ? beatmap.Value.Beatmap?.ControlPointInfo.EffectPointAt(beatmap.Value.Track.CurrentTime) + : null; - float[] temporalAmplitudes = track?.CurrentAmplitudes.FrequencyAmplitudes; + for (int i = 0; i < temporalAmplitudes.Length; i++) + temporalAmplitudes[i] = 0; + + if (beatmap.Value.TrackLoaded) + addAmplitudesFromSource(beatmap.Value.Track); + + foreach (var source in amplitudeSources) + addAmplitudesFromSource(source); for (int i = 0; i < bars_per_visualiser; i++) { - if (track?.IsRunning ?? false) - { - float targetAmplitude = (temporalAmplitudes?[(i + indexOffset) % bars_per_visualiser] ?? 0) * (effect?.KiaiMode == true ? 1 : 0.5f); - if (targetAmplitude > frequencyAmplitudes[i]) - frequencyAmplitudes[i] = targetAmplitude; - } - else - { - int index = (i + index_change) % bars_per_visualiser; - if (frequencyAmplitudes[index] > frequencyAmplitudes[i]) - frequencyAmplitudes[i] = frequencyAmplitudes[index]; - } + float targetAmplitude = Magnitude * (temporalAmplitudes[(i + indexOffset) % bars_per_visualiser]) * (effect?.KiaiMode == true ? 1 : 0.5f); + if (targetAmplitude > frequencyAmplitudes[i]) + frequencyAmplitudes[i] = targetAmplitude; } indexOffset = (indexOffset + index_change) % bars_per_visualiser; @@ -136,6 +152,19 @@ namespace osu.Game.Screens.Menu protected override DrawNode CreateDrawNode() => new VisualisationDrawNode(this); + private void addAmplitudesFromSource([NotNull] IHasAmplitudes source) + { + if (source == null) throw new ArgumentNullException(nameof(source)); + + var amplitudes = source.CurrentAmplitudes.FrequencyAmplitudes; + + for (int i = 0; i < amplitudes.Length; i++) + { + if (i < temporalAmplitudes.Length) + temporalAmplitudes[i] += amplitudes[i]; + } + } + private class VisualisationDrawNode : DrawNode { protected new LogoVisualisation Source => (LogoVisualisation)base.Source; From 6d19fd936ef5e05fd0e95fc4aa12417e29d3f36c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Jun 2020 15:13:30 +0900 Subject: [PATCH 2193/2376] Change test scene to not inherit unused ScreenTestScene --- osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index ac364b5233..f5c5a4d75c 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -25,7 +25,7 @@ using osuTK.Input; namespace osu.Game.Tests.Visual.Ranking { [TestFixture] - public class TestSceneResultsScreen : ScreenTestScene + public class TestSceneResultsScreen : OsuManualInputManagerTestScene { private BeatmapManager beatmaps; From 6bcc693c2f8fc80649e5af944bf87c1e8c946145 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Jun 2020 15:21:23 +0900 Subject: [PATCH 2194/2376] Add ability to close statistics by clicking anywhere --- .../Visual/Ranking/TestSceneResultsScreen.cs | 40 +++++++++++++++++++ .../Ranking/Statistics/StatisticsPanel.cs | 7 ++++ 2 files changed, 47 insertions(+) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index f5c5a4d75c..74808bc2f5 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -20,6 +20,7 @@ using osu.Game.Screens; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking.Statistics; +using osuTK; using osuTK.Input; namespace osu.Game.Tests.Visual.Ranking @@ -87,6 +88,45 @@ namespace osu.Game.Tests.Visual.Ranking AddAssert("retry overlay present", () => screen.RetryOverlay != null); } + [Test] + public void TestShowHideStatisticsViaOutsideClick() + { + TestResultsScreen screen = null; + + AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen())); + AddUntilStep("wait for loaded", () => screen.IsLoaded); + + AddStep("click expanded panel", () => + { + var expandedPanel = this.ChildrenOfType().Single(p => p.State == PanelState.Expanded); + InputManager.MoveMouseTo(expandedPanel); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("statistics shown", () => this.ChildrenOfType().Single().State.Value == Visibility.Visible); + + AddUntilStep("expanded panel at the left of the screen", () => + { + var expandedPanel = this.ChildrenOfType().Single(p => p.State == PanelState.Expanded); + return expandedPanel.ScreenSpaceDrawQuad.TopLeft.X - screen.ScreenSpaceDrawQuad.TopLeft.X < 150; + }); + + AddStep("click to right of panel", () => + { + var expandedPanel = this.ChildrenOfType().Single(p => p.State == PanelState.Expanded); + InputManager.MoveMouseTo(expandedPanel.ScreenSpaceDrawQuad.TopRight + new Vector2(100, 0)); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("statistics hidden", () => this.ChildrenOfType().Single().State.Value == Visibility.Hidden); + + AddUntilStep("expanded panel in centre of screen", () => + { + var expandedPanel = this.ChildrenOfType().Single(p => p.State == PanelState.Expanded); + return Precision.AlmostEquals(expandedPanel.ScreenSpaceDrawQuad.Centre.X, screen.ScreenSpaceDrawQuad.Centre.X, 1); + }); + } + [Test] public void TestShowHideStatistics() { diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 77f3bd7b5c..7f406331cd 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -9,6 +9,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Events; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Placeholders; @@ -129,6 +130,12 @@ namespace osu.Game.Screens.Ranking.Statistics } } + protected override bool OnClick(ClickEvent e) + { + ToggleVisibility(); + return true; + } + protected override void PopIn() => this.FadeIn(150, Easing.OutQuint); protected override void PopOut() => this.FadeOut(150, Easing.OutQuint); From a6c6e391caaa93f2c024fb5832b4db90ff4c95e2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Jun 2020 17:38:30 +0900 Subject: [PATCH 2195/2376] Fix player not exiting immediately on Alt-F4 --- osu.Game.Tests/Visual/Gameplay/TestScenePause.cs | 4 +--- osu.Game/Screens/Play/Player.cs | 6 ------ 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index 387ac42f67..1961a224c1 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -174,9 +174,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestExitFromGameplay() { - AddStep("exit", () => Player.Exit()); - confirmPaused(); - + // an externally triggered exit should immediately exit, skipping all pause logic. AddStep("exit", () => Player.Exit()); confirmExited(); } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index d3b88e56ae..541275cf55 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -656,12 +656,6 @@ namespace osu.Game.Screens.Play return true; } - if (canPause) - { - Pause(); - return true; - } - // GameplayClockContainer performs seeks / start / stop operations on the beatmap's track. // as we are no longer the current screen, we cannot guarantee the track is still usable. GameplayClockContainer?.StopUsingBeatmapClock(); From 53d542546e1ae25edbf4afe3e777eee2b5038a92 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Jun 2020 18:04:50 +0900 Subject: [PATCH 2196/2376] Fix editor drag selection not continuing to select unless the mouse is moved --- .../Edit/Compose/Components/DragBox.cs | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/DragBox.cs b/osu.Game/Screens/Edit/Compose/Components/DragBox.cs index c5f1bd1575..0615ebfc20 100644 --- a/osu.Game/Screens/Edit/Compose/Components/DragBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/DragBox.cs @@ -53,6 +53,8 @@ namespace osu.Game.Screens.Edit.Compose.Components } }; + private RectangleF? dragRectangle; + /// /// Handle a forwarded mouse event. /// @@ -66,15 +68,14 @@ namespace osu.Game.Screens.Edit.Compose.Components var dragQuad = new Quad(dragStartPosition.X, dragStartPosition.Y, dragPosition.X - dragStartPosition.X, dragPosition.Y - dragStartPosition.Y); // We use AABBFloat instead of RectangleF since it handles negative sizes for us - var dragRectangle = dragQuad.AABBFloat; + var rec = dragQuad.AABBFloat; + dragRectangle = rec; - var topLeft = ToLocalSpace(dragRectangle.TopLeft); - var bottomRight = ToLocalSpace(dragRectangle.BottomRight); + var topLeft = ToLocalSpace(rec.TopLeft); + var bottomRight = ToLocalSpace(rec.BottomRight); Box.Position = topLeft; Box.Size = bottomRight - topLeft; - - PerformSelection?.Invoke(dragRectangle); return true; } @@ -93,7 +94,19 @@ namespace osu.Game.Screens.Edit.Compose.Components } } - public override void Hide() => State = Visibility.Hidden; + protected override void Update() + { + base.Update(); + + if (dragRectangle != null) + PerformSelection?.Invoke(dragRectangle.Value); + } + + public override void Hide() + { + State = Visibility.Hidden; + dragRectangle = null; + } public override void Show() => State = Visibility.Visible; From a5eac716ec8bc3ae84da15aaca17457a78fcbf1b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Jun 2020 18:42:56 +0900 Subject: [PATCH 2197/2376] Make work for all editors based on track running state --- .../Compose/Components/BlueprintContainer.cs | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index cc417bbb10..4aa235ba50 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics.Primitives; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; @@ -320,10 +321,22 @@ namespace osu.Game.Screens.Edit.Compose.Components { foreach (var blueprint in SelectionBlueprints) { - if (blueprint.IsAlive && blueprint.IsPresent && rect.Contains(blueprint.ScreenSpaceSelectionPoint)) - blueprint.Select(); - else - blueprint.Deselect(); + // only run when utmost necessary to avoid unnecessary rect computations. + bool isValidForSelection() => blueprint.IsAlive && blueprint.IsPresent && rect.Contains(blueprint.ScreenSpaceSelectionPoint); + + switch (blueprint.State) + { + case SelectionState.NotSelected: + if (isValidForSelection()) + blueprint.Select(); + break; + + case SelectionState.Selected: + // if the editor is playing, we generally don't want to deselect objects even if outside the selection area. + if (!editorClock.IsRunning && !isValidForSelection()) + blueprint.Deselect(); + break; + } } } From e7238e25f96de3c9de8c622d3026a04d34039cc8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 23 Jun 2020 20:36:09 +0900 Subject: [PATCH 2198/2376] Fix exception when dragging after deleting object --- .../Screens/Edit/Compose/Components/BlueprintContainer.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index cc417bbb10..767f60cf71 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -82,6 +82,7 @@ namespace osu.Game.Screens.Edit.Compose.Components case NotifyCollectionChangedAction.Remove: foreach (var o in args.OldItems) SelectionBlueprints.FirstOrDefault(b => b.HitObject == o)?.Deselect(); + break; } }; @@ -250,6 +251,9 @@ namespace osu.Game.Screens.Edit.Compose.Components blueprint.Deselected -= onBlueprintDeselected; SelectionBlueprints.Remove(blueprint); + + if (movementBlueprint == blueprint) + finishSelectionMovement(); } protected virtual void AddBlueprintFor(HitObject hitObject) From 61c4ed327c6c7f54be05430644ba8bc9ba479fb9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Jun 2020 21:26:41 +0900 Subject: [PATCH 2199/2376] 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 192be999eb..493b1f5529 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 911292c6ae..26d81a1004 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -24,7 +24,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 18249b40ca..72f09ee287 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From 14ad3835ff01e51c0992f3f2d09072fe1bc5b8fe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Jun 2020 13:49:18 +0900 Subject: [PATCH 2200/2376] Update naming --- .../Objects/Drawables/Pieces/ReverseArrowPiece.cs | 2 +- .../Objects/Drawables/Pieces/CirclePiece.cs | 4 ++-- .../Skinning/TaikoLegacyPlayfieldBackgroundRight.cs | 2 +- osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs | 2 +- osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs | 2 +- .../Visual/UserInterface/TestSceneBeatSyncedContainer.cs | 2 +- osu.Game/Graphics/Containers/BeatSyncedContainer.cs | 8 ++++---- osu.Game/Graphics/UserInterface/OsuTextBox.cs | 2 +- osu.Game/Graphics/UserInterface/TwoLayerButton.cs | 2 +- osu.Game/Rulesets/Mods/ModNightcore.cs | 2 +- osu.Game/Screens/Menu/Button.cs | 4 ++-- osu.Game/Screens/Menu/MenuSideFlashes.cs | 6 +++--- osu.Game/Screens/Menu/OsuLogo.cs | 2 +- 13 files changed, 20 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ReverseArrowPiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ReverseArrowPiece.cs index 1a5195acf8..ae43006e76 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ReverseArrowPiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ReverseArrowPiece.cs @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces }; } - protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) { if (!drawableRepeat.IsHit) Child.ScaleTo(1.3f).ScaleTo(1f, timingPoint.BeatLength, Easing.Out); diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs index b5471e6976..f515a35c18 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.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.Audio.Track; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -8,7 +9,6 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Graphics.Backgrounds; using osuTK.Graphics; using osu.Game.Beatmaps.ControlPoints; -using osu.Framework.Audio.Track; using osu.Framework.Graphics.Effects; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -148,7 +148,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces }; } - protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) { if (!effectPoint.KiaiMode) return; diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacyPlayfieldBackgroundRight.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacyPlayfieldBackgroundRight.cs index 7508c75231..4bbb6be6b1 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacyPlayfieldBackgroundRight.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacyPlayfieldBackgroundRight.cs @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning }; } - protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) { base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs index 407ab30e12..b937beae3c 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs @@ -83,7 +83,7 @@ namespace osu.Game.Rulesets.Taiko.UI lastObjectHit = result.IsHit; } - protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) { kiaiMode = effectPoint.KiaiMode; } diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs b/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs index cce2be7758..6f25a5f662 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs @@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Taiko.UI textureAnimation.Seek(0); } - protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) { // assume that if the animation is playing on its own, it's independent from the beat and doesn't need to be touched. if (textureAnimation.FrameCount == 0 || textureAnimation.IsPlaying) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs index 4c32e995e8..dd5ceec739 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs @@ -177,7 +177,7 @@ namespace osu.Game.Tests.Visual.UserInterface timeSinceLastBeat.Value = TimeSinceLastBeat; } - protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) { base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs index 5a613d1a54..c37fcc043d 100644 --- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs +++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs @@ -18,7 +18,7 @@ namespace osu.Game.Graphics.Containers private TimingControlPoint lastTimingPoint; /// - /// The amount of time before a beat we should fire . + /// The amount of time before a beat we should fire . /// This allows for adding easing to animations that may be synchronised to the beat. /// protected double EarlyActivationMilliseconds; @@ -50,7 +50,7 @@ namespace osu.Game.Graphics.Containers private TimingControlPoint defaultTiming; private EffectControlPoint defaultEffect; - private TrackAmplitudes defaultAmplitudes; + private ChannelAmplitudes defaultAmplitudes; protected bool IsBeatSyncedWithTrack { get; private set; } @@ -129,7 +129,7 @@ namespace osu.Game.Graphics.Containers OmitFirstBarLine = false }; - defaultAmplitudes = new TrackAmplitudes + defaultAmplitudes = new ChannelAmplitudes { FrequencyAmplitudes = new float[256], LeftChannel = 0, @@ -137,7 +137,7 @@ namespace osu.Game.Graphics.Containers }; } - protected virtual void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) + protected virtual void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) { } } diff --git a/osu.Game/Graphics/UserInterface/OsuTextBox.cs b/osu.Game/Graphics/UserInterface/OsuTextBox.cs index 6f440d8138..06c46fbb91 100644 --- a/osu.Game/Graphics/UserInterface/OsuTextBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuTextBox.cs @@ -147,7 +147,7 @@ namespace osu.Game.Graphics.UserInterface }; } - protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) { if (!hasSelection) this.FadeTo(0.7f).FadeTo(0.4f, timingPoint.BeatLength, Easing.InOutSine); diff --git a/osu.Game/Graphics/UserInterface/TwoLayerButton.cs b/osu.Game/Graphics/UserInterface/TwoLayerButton.cs index aa96796cf1..120149d8c1 100644 --- a/osu.Game/Graphics/UserInterface/TwoLayerButton.cs +++ b/osu.Game/Graphics/UserInterface/TwoLayerButton.cs @@ -230,7 +230,7 @@ namespace osu.Game.Graphics.UserInterface }; } - protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) { base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); diff --git a/osu.Game/Rulesets/Mods/ModNightcore.cs b/osu.Game/Rulesets/Mods/ModNightcore.cs index 1df2aeb348..ed8eb2fb66 100644 --- a/osu.Game/Rulesets/Mods/ModNightcore.cs +++ b/osu.Game/Rulesets/Mods/ModNightcore.cs @@ -78,7 +78,7 @@ namespace osu.Game.Rulesets.Mods private const int bars_per_segment = 4; - protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) { base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); diff --git a/osu.Game/Screens/Menu/Button.cs b/osu.Game/Screens/Menu/Button.cs index 6708ce0ba0..be6ed9700c 100644 --- a/osu.Game/Screens/Menu/Button.cs +++ b/osu.Game/Screens/Menu/Button.cs @@ -6,6 +6,7 @@ using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; +using osu.Framework.Audio.Track; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -15,7 +16,6 @@ using osuTK.Graphics; using osuTK.Input; using osu.Framework.Extensions.Color4Extensions; using osu.Game.Graphics.Containers; -using osu.Framework.Audio.Track; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; @@ -132,7 +132,7 @@ namespace osu.Game.Screens.Menu private bool rightward; - protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) { base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); diff --git a/osu.Game/Screens/Menu/MenuSideFlashes.cs b/osu.Game/Screens/Menu/MenuSideFlashes.cs index 321381ac8d..2ff8132d47 100644 --- a/osu.Game/Screens/Menu/MenuSideFlashes.cs +++ b/osu.Game/Screens/Menu/MenuSideFlashes.cs @@ -3,7 +3,6 @@ using osuTK.Graphics; using osu.Framework.Allocation; -using osu.Framework.Audio.Track; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; @@ -16,6 +15,7 @@ using osu.Game.Skinning; using osu.Game.Online.API; using osu.Game.Users; using System; +using osu.Framework.Audio.Track; using osu.Framework.Bindables; namespace osu.Game.Screens.Menu @@ -89,7 +89,7 @@ namespace osu.Game.Screens.Menu skin.BindValueChanged(_ => updateColour(), true); } - protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) { if (beatIndex < 0) return; @@ -100,7 +100,7 @@ namespace osu.Game.Screens.Menu flash(rightBox, timingPoint.BeatLength, effectPoint.KiaiMode, amplitudes); } - private void flash(Drawable d, double beatLength, bool kiai, TrackAmplitudes amplitudes) + private void flash(Drawable d, double beatLength, bool kiai, ChannelAmplitudes amplitudes) { d.FadeTo(Math.Max(0, ((ReferenceEquals(d, leftBox) ? amplitudes.LeftChannel : amplitudes.RightChannel) - amplitude_dead_zone) / (kiai ? kiai_multiplier : alpha_multiplier)), box_fade_in_time) .Then() diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index 9cadfd7df6..089906c342 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -264,7 +264,7 @@ namespace osu.Game.Screens.Menu private int lastBeatIndex; - protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) { base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); From f2735a77975db231e525117a1092ce5756ba8353 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Jun 2020 21:30:37 +0900 Subject: [PATCH 2201/2376] Use new empty ChannelAmplitudes spec --- osu.Game/Graphics/Containers/BeatSyncedContainer.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs index c37fcc043d..dd5c41285a 100644 --- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs +++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs @@ -50,7 +50,6 @@ namespace osu.Game.Graphics.Containers private TimingControlPoint defaultTiming; private EffectControlPoint defaultEffect; - private ChannelAmplitudes defaultAmplitudes; protected bool IsBeatSyncedWithTrack { get; private set; } @@ -107,7 +106,7 @@ namespace osu.Game.Graphics.Containers return; using (BeginDelayedSequence(-TimeSinceLastBeat, true)) - OnNewBeat(beatIndex, timingPoint, effectPoint, track?.CurrentAmplitudes ?? defaultAmplitudes); + OnNewBeat(beatIndex, timingPoint, effectPoint, track?.CurrentAmplitudes ?? ChannelAmplitudes.Empty); lastBeat = beatIndex; lastTimingPoint = timingPoint; @@ -128,13 +127,6 @@ namespace osu.Game.Graphics.Containers KiaiMode = false, OmitFirstBarLine = false }; - - defaultAmplitudes = new ChannelAmplitudes - { - FrequencyAmplitudes = new float[256], - LeftChannel = 0, - RightChannel = 0 - }; } protected virtual void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) From 5cdabbc8bb7f888ae3f8e0d9f270ea9e2b4dc365 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Jun 2020 21:33:03 +0900 Subject: [PATCH 2202/2376] Update access to FrequencyAmplitudes via span --- osu.Game/Screens/Menu/LogoVisualisation.cs | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/Menu/LogoVisualisation.cs b/osu.Game/Screens/Menu/LogoVisualisation.cs index 6a28740d4e..cbed1d2e0e 100644 --- a/osu.Game/Screens/Menu/LogoVisualisation.cs +++ b/osu.Game/Screens/Menu/LogoVisualisation.cs @@ -14,6 +14,7 @@ using osu.Game.Beatmaps; using osu.Game.Graphics; using System; using osu.Framework.Allocation; +using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Utils; @@ -88,22 +89,13 @@ namespace osu.Game.Screens.Menu var track = beatmap.Value.TrackLoaded ? beatmap.Value.Track : null; var effect = beatmap.Value.BeatmapLoaded ? beatmap.Value.Beatmap?.ControlPointInfo.EffectPointAt(track?.CurrentTime ?? Time.Current) : null; - float[] temporalAmplitudes = track?.CurrentAmplitudes.FrequencyAmplitudes; + ReadOnlySpan temporalAmplitudes = (track?.CurrentAmplitudes ?? ChannelAmplitudes.Empty).FrequencyAmplitudes.Span; for (int i = 0; i < bars_per_visualiser; i++) { - if (track?.IsRunning ?? false) - { - float targetAmplitude = (temporalAmplitudes?[(i + indexOffset) % bars_per_visualiser] ?? 0) * (effect?.KiaiMode == true ? 1 : 0.5f); - if (targetAmplitude > frequencyAmplitudes[i]) - frequencyAmplitudes[i] = targetAmplitude; - } - else - { - int index = (i + index_change) % bars_per_visualiser; - if (frequencyAmplitudes[index] > frequencyAmplitudes[i]) - frequencyAmplitudes[i] = frequencyAmplitudes[index]; - } + float targetAmplitude = (temporalAmplitudes[(i + indexOffset) % bars_per_visualiser]) * (effect?.KiaiMode == true ? 1 : 0.5f); + if (targetAmplitude > frequencyAmplitudes[i]) + frequencyAmplitudes[i] = targetAmplitude; } indexOffset = (indexOffset + index_change) % bars_per_visualiser; From 9d753a4fc2966e048ebdbeb3eaa2127e1569694e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Jun 2020 21:34:57 +0900 Subject: [PATCH 2203/2376] Update intro resource locations --- osu.Game/Screens/Menu/IntroCircles.cs | 2 +- osu.Game/Screens/Menu/IntroScreen.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Menu/IntroCircles.cs b/osu.Game/Screens/Menu/IntroCircles.cs index aa9cee969c..d4cd073b7a 100644 --- a/osu.Game/Screens/Menu/IntroCircles.cs +++ b/osu.Game/Screens/Menu/IntroCircles.cs @@ -24,7 +24,7 @@ namespace osu.Game.Screens.Menu private void load(AudioManager audio) { if (MenuVoice.Value) - welcome = audio.Samples.Get(@"welcome"); + welcome = audio.Samples.Get(@"Intro/welcome"); } protected override void LogoArriving(OsuLogo logo, bool resuming) diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index b99d8ae9d1..20964549f5 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -72,7 +72,7 @@ namespace osu.Game.Screens.Menu MenuVoice = config.GetBindable(OsuSetting.MenuVoice); MenuMusic = config.GetBindable(OsuSetting.MenuMusic); - seeya = audio.Samples.Get(@"seeya"); + seeya = audio.Samples.Get(@"Intro/seeya"); BeatmapSetInfo setInfo = null; From ccb27082d52c2bda1bfd01e4a5ca3583e49c3035 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 Jun 2020 11:08:32 +0900 Subject: [PATCH 2204/2376] Fix background appearing too late --- osu.Game/Screens/Menu/IntroWelcome.cs | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Menu/IntroWelcome.cs b/osu.Game/Screens/Menu/IntroWelcome.cs index 81e473dc04..abd4a68d4f 100644 --- a/osu.Game/Screens/Menu/IntroWelcome.cs +++ b/osu.Game/Screens/Menu/IntroWelcome.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; +using osu.Game.Screens.Backgrounds; using osuTK.Graphics; namespace osu.Game.Screens.Menu @@ -24,6 +25,13 @@ namespace osu.Game.Screens.Menu private SampleChannel pianoReverb; protected override string SeeyaSampleName => "Intro/Welcome/seeya"; + protected override BackgroundScreen CreateBackground() => background = new BackgroundScreenDefault(false) + { + Alpha = 0, + }; + + private BackgroundScreenDefault background; + [BackgroundDependencyLoader] private void load(AudioManager audio) { @@ -44,6 +52,8 @@ namespace osu.Game.Screens.Menu RelativeSizeAxes = Axes.Both }, intro => { + PrepareMenuLoad(); + intro.LogoVisualisation.AddAmplitudeSource(pianoReverb); AddInternal(intro); @@ -54,21 +64,24 @@ namespace osu.Game.Screens.Menu Scheduler.AddDelayed(() => { StartTrack(); - PrepareMenuLoad(); + + const float fade_in_time = 200; logo.ScaleTo(1); - logo.FadeIn(); + logo.FadeIn(fade_in_time); - Scheduler.Add(LoadMenu); + background.FadeIn(fade_in_time); + + LoadMenu(); }, delay_step_two); }); } } - public override void OnSuspending(IScreen next) + public override void OnResuming(IScreen last) { - this.FadeOut(300); - base.OnSuspending(next); + base.OnResuming(last); + background.FadeOut(100); } private class WelcomeIntroSequence : Container From 1387a9e2c63a0d46200a4ea7ef71e28cdb68c893 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 Jun 2020 16:57:17 +0900 Subject: [PATCH 2205/2376] Move all tournament tests to using placeholder data rather than reading from bracket --- .../Components/TestSceneMatchScoreDisplay.cs | 2 +- osu.Game.Tournament.Tests/LadderTestScene.cs | 146 ------------------ .../Screens/TestSceneLadderEditorScreen.cs | 2 +- .../Screens/TestSceneLadderScreen.cs | 2 +- .../Screens/TestSceneMapPoolScreen.cs | 2 +- .../Screens/TestSceneRoundEditorScreen.cs | 2 +- .../Screens/TestSceneSeedingEditorScreen.cs | 2 +- .../Screens/TestSceneSeedingScreen.cs | 2 +- .../Screens/TestSceneTeamEditorScreen.cs | 2 +- .../Screens/TestSceneTeamIntroScreen.cs | 2 +- .../Screens/TestSceneTeamWinScreen.cs | 2 +- .../TournamentTestScene.cs | 141 +++++++++++++++++ 12 files changed, 151 insertions(+), 156 deletions(-) delete mode 100644 osu.Game.Tournament.Tests/LadderTestScene.cs diff --git a/osu.Game.Tournament.Tests/Components/TestSceneMatchScoreDisplay.cs b/osu.Game.Tournament.Tests/Components/TestSceneMatchScoreDisplay.cs index 77119f7a60..acd5d53310 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneMatchScoreDisplay.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneMatchScoreDisplay.cs @@ -9,7 +9,7 @@ using osu.Game.Tournament.Screens.Gameplay.Components; namespace osu.Game.Tournament.Tests.Components { - public class TestSceneMatchScoreDisplay : LadderTestScene + public class TestSceneMatchScoreDisplay : TournamentTestScene { [Cached(Type = typeof(MatchIPCInfo))] private MatchIPCInfo matchInfo = new MatchIPCInfo(); diff --git a/osu.Game.Tournament.Tests/LadderTestScene.cs b/osu.Game.Tournament.Tests/LadderTestScene.cs deleted file mode 100644 index 2f4373679c..0000000000 --- a/osu.Game.Tournament.Tests/LadderTestScene.cs +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Linq; -using NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Utils; -using osu.Game.Beatmaps; -using osu.Game.Rulesets; -using osu.Game.Tournament.Models; -using osu.Game.Users; - -namespace osu.Game.Tournament.Tests -{ - [TestFixture] - public abstract class LadderTestScene : TournamentTestScene - { - [Cached] - protected LadderInfo Ladder { get; private set; } = new LadderInfo(); - - [Resolved] - private RulesetStore rulesetStore { get; set; } - - [BackgroundDependencyLoader] - private void load() - { - Ladder.Ruleset.Value ??= rulesetStore.AvailableRulesets.First(); - - Ruleset.BindTo(Ladder.Ruleset); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - TournamentMatch match = CreateSampleMatch(); - - Ladder.Rounds.Add(match.Round.Value); - Ladder.Matches.Add(match); - Ladder.Teams.Add(match.Team1.Value); - Ladder.Teams.Add(match.Team2.Value); - - Ladder.CurrentMatch.Value = match; - } - - public static TournamentMatch CreateSampleMatch() => new TournamentMatch - { - Team1 = - { - Value = new TournamentTeam - { - FlagName = { Value = "JP" }, - FullName = { Value = "Japan" }, - LastYearPlacing = { Value = 10 }, - Seed = { Value = "Low" }, - SeedingResults = - { - new SeedingResult - { - Mod = { Value = "NM" }, - Seed = { Value = 10 }, - Beatmaps = - { - new SeedingBeatmap - { - BeatmapInfo = CreateSampleBeatmapInfo(), - Score = 12345672, - Seed = { Value = 24 }, - }, - new SeedingBeatmap - { - BeatmapInfo = CreateSampleBeatmapInfo(), - Score = 1234567, - Seed = { Value = 12 }, - }, - new SeedingBeatmap - { - BeatmapInfo = CreateSampleBeatmapInfo(), - Score = 1234567, - Seed = { Value = 16 }, - } - } - }, - new SeedingResult - { - Mod = { Value = "DT" }, - Seed = { Value = 5 }, - Beatmaps = - { - new SeedingBeatmap - { - BeatmapInfo = CreateSampleBeatmapInfo(), - Score = 234567, - Seed = { Value = 3 }, - }, - new SeedingBeatmap - { - BeatmapInfo = CreateSampleBeatmapInfo(), - Score = 234567, - Seed = { Value = 6 }, - }, - new SeedingBeatmap - { - BeatmapInfo = CreateSampleBeatmapInfo(), - Score = 234567, - Seed = { Value = 12 }, - } - } - } - }, - Players = - { - new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 12 } } }, - new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 16 } } }, - new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 20 } } }, - new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 24 } } }, - new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 30 } } }, - } - } - }, - Team2 = - { - Value = new TournamentTeam - { - FlagName = { Value = "US" }, - FullName = { Value = "United States" }, - Players = - { - new User { Username = "Hello" }, - new User { Username = "Hello" }, - new User { Username = "Hello" }, - new User { Username = "Hello" }, - new User { Username = "Hello" }, - } - } - }, - Round = - { - Value = new TournamentRound { Name = { Value = "Quarterfinals" } } - } - }; - - public static BeatmapInfo CreateSampleBeatmapInfo() => - new BeatmapInfo { Metadata = new BeatmapMetadata { Title = "Test Title", Artist = "Test Artist", ID = RNG.Next(0, 1000000) } }; - } -} diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneLadderEditorScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneLadderEditorScreen.cs index a45c5de2bd..bceb3e6b74 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneLadderEditorScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneLadderEditorScreen.cs @@ -8,7 +8,7 @@ using osu.Game.Tournament.Screens.Editors; namespace osu.Game.Tournament.Tests.Screens { - public class TestSceneLadderEditorScreen : LadderTestScene + public class TestSceneLadderEditorScreen : TournamentTestScene { [BackgroundDependencyLoader] private void load() diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneLadderScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneLadderScreen.cs index 2be0564c82..c4c100d506 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneLadderScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneLadderScreen.cs @@ -8,7 +8,7 @@ using osu.Game.Tournament.Screens.Ladder; namespace osu.Game.Tournament.Tests.Screens { - public class TestSceneLadderScreen : LadderTestScene + public class TestSceneLadderScreen : TournamentTestScene { [BackgroundDependencyLoader] private void load() diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs index a4538be384..f4032fdd54 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs @@ -12,7 +12,7 @@ using osu.Game.Tournament.Screens.MapPool; namespace osu.Game.Tournament.Tests.Screens { - public class TestSceneMapPoolScreen : LadderTestScene + public class TestSceneMapPoolScreen : TournamentTestScene { private MapPoolScreen screen; diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneRoundEditorScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneRoundEditorScreen.cs index e15ac416b0..5c2b59df3a 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneRoundEditorScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneRoundEditorScreen.cs @@ -5,7 +5,7 @@ using osu.Game.Tournament.Screens.Editors; namespace osu.Game.Tournament.Tests.Screens { - public class TestSceneRoundEditorScreen : LadderTestScene + public class TestSceneRoundEditorScreen : TournamentTestScene { public TestSceneRoundEditorScreen() { diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneSeedingEditorScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneSeedingEditorScreen.cs index 8d12d5393d..2722021216 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneSeedingEditorScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneSeedingEditorScreen.cs @@ -7,7 +7,7 @@ using osu.Game.Tournament.Screens.Editors; namespace osu.Game.Tournament.Tests.Screens { - public class TestSceneSeedingEditorScreen : LadderTestScene + public class TestSceneSeedingEditorScreen : TournamentTestScene { [Cached] private readonly LadderInfo ladder = new LadderInfo(); diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneSeedingScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneSeedingScreen.cs index 4269f8f56a..d414d8e36e 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneSeedingScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneSeedingScreen.cs @@ -8,7 +8,7 @@ using osu.Game.Tournament.Screens.TeamIntro; namespace osu.Game.Tournament.Tests.Screens { - public class TestSceneSeedingScreen : LadderTestScene + public class TestSceneSeedingScreen : TournamentTestScene { [Cached] private readonly LadderInfo ladder = new LadderInfo(); diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneTeamEditorScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneTeamEditorScreen.cs index 097bad4a02..fc6574ec8a 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneTeamEditorScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneTeamEditorScreen.cs @@ -5,7 +5,7 @@ using osu.Game.Tournament.Screens.Editors; namespace osu.Game.Tournament.Tests.Screens { - public class TestSceneTeamEditorScreen : LadderTestScene + public class TestSceneTeamEditorScreen : TournamentTestScene { public TestSceneTeamEditorScreen() { diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneTeamIntroScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneTeamIntroScreen.cs index e36b594ff2..b3f78c92d9 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneTeamIntroScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneTeamIntroScreen.cs @@ -9,7 +9,7 @@ using osu.Game.Tournament.Screens.TeamIntro; namespace osu.Game.Tournament.Tests.Screens { - public class TestSceneTeamIntroScreen : LadderTestScene + public class TestSceneTeamIntroScreen : TournamentTestScene { [Cached] private readonly LadderInfo ladder = new LadderInfo(); diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneTeamWinScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneTeamWinScreen.cs index 1a2faa76c1..6873fb0f4b 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneTeamWinScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneTeamWinScreen.cs @@ -9,7 +9,7 @@ using osu.Game.Tournament.Screens.TeamWin; namespace osu.Game.Tournament.Tests.Screens { - public class TestSceneTeamWinScreen : LadderTestScene + public class TestSceneTeamWinScreen : TournamentTestScene { [Cached] private readonly LadderInfo ladder = new LadderInfo(); diff --git a/osu.Game.Tournament.Tests/TournamentTestScene.cs b/osu.Game.Tournament.Tests/TournamentTestScene.cs index 18ac3230da..a7b141cf43 100644 --- a/osu.Game.Tournament.Tests/TournamentTestScene.cs +++ b/osu.Game.Tournament.Tests/TournamentTestScene.cs @@ -1,13 +1,154 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Platform; using osu.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; using osu.Game.Tests.Visual; +using osu.Game.Tournament.IPC; +using osu.Game.Tournament.Models; +using osu.Game.Users; namespace osu.Game.Tournament.Tests { public abstract class TournamentTestScene : OsuTestScene { + [Cached] + protected LadderInfo Ladder { get; private set; } = new LadderInfo(); + + [Resolved] + private RulesetStore rulesetStore { get; set; } + + [Cached] + protected MatchIPCInfo IPCInfo { get; private set; } = new MatchIPCInfo(); + + [BackgroundDependencyLoader] + private void load(Storage storage) + { + Ladder.Ruleset.Value ??= rulesetStore.AvailableRulesets.First(); + + Ruleset.BindTo(Ladder.Ruleset); + Dependencies.CacheAs(new StableInfo(storage)); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + TournamentMatch match = CreateSampleMatch(); + + Ladder.Rounds.Add(match.Round.Value); + Ladder.Matches.Add(match); + Ladder.Teams.Add(match.Team1.Value); + Ladder.Teams.Add(match.Team2.Value); + + Ladder.CurrentMatch.Value = match; + } + + public static TournamentMatch CreateSampleMatch() => new TournamentMatch + { + Team1 = + { + Value = new TournamentTeam + { + FlagName = { Value = "JP" }, + FullName = { Value = "Japan" }, + LastYearPlacing = { Value = 10 }, + Seed = { Value = "Low" }, + SeedingResults = + { + new SeedingResult + { + Mod = { Value = "NM" }, + Seed = { Value = 10 }, + Beatmaps = + { + new SeedingBeatmap + { + BeatmapInfo = CreateSampleBeatmapInfo(), + Score = 12345672, + Seed = { Value = 24 }, + }, + new SeedingBeatmap + { + BeatmapInfo = CreateSampleBeatmapInfo(), + Score = 1234567, + Seed = { Value = 12 }, + }, + new SeedingBeatmap + { + BeatmapInfo = CreateSampleBeatmapInfo(), + Score = 1234567, + Seed = { Value = 16 }, + } + } + }, + new SeedingResult + { + Mod = { Value = "DT" }, + Seed = { Value = 5 }, + Beatmaps = + { + new SeedingBeatmap + { + BeatmapInfo = CreateSampleBeatmapInfo(), + Score = 234567, + Seed = { Value = 3 }, + }, + new SeedingBeatmap + { + BeatmapInfo = CreateSampleBeatmapInfo(), + Score = 234567, + Seed = { Value = 6 }, + }, + new SeedingBeatmap + { + BeatmapInfo = CreateSampleBeatmapInfo(), + Score = 234567, + Seed = { Value = 12 }, + } + } + } + }, + Players = + { + new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 12 } } }, + new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 16 } } }, + new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 20 } } }, + new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 24 } } }, + new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 30 } } }, + } + } + }, + Team2 = + { + Value = new TournamentTeam + { + FlagName = { Value = "US" }, + FullName = { Value = "United States" }, + Players = + { + new User { Username = "Hello" }, + new User { Username = "Hello" }, + new User { Username = "Hello" }, + new User { Username = "Hello" }, + new User { Username = "Hello" }, + } + } + }, + Round = + { + Value = new TournamentRound { Name = { Value = "Quarterfinals" } } + } + }; + + public static BeatmapInfo CreateSampleBeatmapInfo() => + new BeatmapInfo { Metadata = new BeatmapMetadata { Title = "Test Title", Artist = "Test Artist", ID = RNG.Next(0, 1000000) } }; + protected override ITestSceneTestRunner CreateRunner() => new TournamentTestSceneTestRunner(); public class TournamentTestSceneTestRunner : TournamentGameBase, ITestSceneTestRunner From 92e272ebb6f301a5b896ea2a3166d2cbf761be99 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 Jun 2020 16:57:40 +0900 Subject: [PATCH 2206/2376] Remove unnecessary prefixes --- osu.Game.Tournament/TournamentSceneManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tournament/TournamentSceneManager.cs b/osu.Game.Tournament/TournamentSceneManager.cs index 23fcb01db7..2c539cdd43 100644 --- a/osu.Game.Tournament/TournamentSceneManager.cs +++ b/osu.Game.Tournament/TournamentSceneManager.cs @@ -37,7 +37,7 @@ namespace osu.Game.Tournament public const float STREAM_AREA_WIDTH = 1366; - public const double REQUIRED_WIDTH = TournamentSceneManager.CONTROL_AREA_WIDTH * 2 + TournamentSceneManager.STREAM_AREA_WIDTH; + public const double REQUIRED_WIDTH = CONTROL_AREA_WIDTH * 2 + STREAM_AREA_WIDTH; [Cached] private TournamentMatchChatDisplay chat = new TournamentMatchChatDisplay(); From eb3e1b2b2698ab5bc843bd5a90c945ca01cd7d5f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 Jun 2020 17:03:22 +0900 Subject: [PATCH 2207/2376] Fix incorrect inheritance on remaining test scene --- .../Components/TestSceneTournamentBeatmapPanel.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Tournament.Tests/Components/TestSceneTournamentBeatmapPanel.cs b/osu.Game.Tournament.Tests/Components/TestSceneTournamentBeatmapPanel.cs index 77fa411058..bc32a12ab7 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneTournamentBeatmapPanel.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneTournamentBeatmapPanel.cs @@ -8,12 +8,11 @@ using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; -using osu.Game.Tests.Visual; using osu.Game.Tournament.Components; namespace osu.Game.Tournament.Tests.Components { - public class TestSceneTournamentBeatmapPanel : OsuTestScene + public class TestSceneTournamentBeatmapPanel : TournamentTestScene { [Resolved] private IAPIProvider api { get; set; } From 5fd6246d1b5e3b0d650cf4117d10df84b6d9f5de Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 Jun 2020 17:57:07 +0900 Subject: [PATCH 2208/2376] Fix remaining test scenes --- .../Screens/TestSceneTeamWinScreen.cs | 10 ++-------- osu.Game.Tournament.Tests/TournamentTestScene.cs | 13 +++++-------- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneTeamWinScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneTeamWinScreen.cs index 6873fb0f4b..3ca58dcaf4 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneTeamWinScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneTeamWinScreen.cs @@ -4,25 +4,19 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Game.Tournament.Models; using osu.Game.Tournament.Screens.TeamWin; namespace osu.Game.Tournament.Tests.Screens { public class TestSceneTeamWinScreen : TournamentTestScene { - [Cached] - private readonly LadderInfo ladder = new LadderInfo(); - [BackgroundDependencyLoader] private void load() { - var match = new TournamentMatch(); - match.Team1.Value = Ladder.Teams.FirstOrDefault(t => t.Acronym.Value == "USA"); - match.Team2.Value = Ladder.Teams.FirstOrDefault(t => t.Acronym.Value == "JPN"); + var match = Ladder.CurrentMatch.Value; + match.Round.Value = Ladder.Rounds.FirstOrDefault(g => g.Name.Value == "Finals"); match.Completed.Value = true; - ladder.CurrentMatch.Value = match; Add(new TeamWinScreen { diff --git a/osu.Game.Tournament.Tests/TournamentTestScene.cs b/osu.Game.Tournament.Tests/TournamentTestScene.cs index a7b141cf43..d22da25f9d 100644 --- a/osu.Game.Tournament.Tests/TournamentTestScene.cs +++ b/osu.Game.Tournament.Tests/TournamentTestScene.cs @@ -31,14 +31,6 @@ namespace osu.Game.Tournament.Tests { Ladder.Ruleset.Value ??= rulesetStore.AvailableRulesets.First(); - Ruleset.BindTo(Ladder.Ruleset); - Dependencies.CacheAs(new StableInfo(storage)); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - TournamentMatch match = CreateSampleMatch(); Ladder.Rounds.Add(match.Round.Value); @@ -47,6 +39,9 @@ namespace osu.Game.Tournament.Tests Ladder.Teams.Add(match.Team2.Value); Ladder.CurrentMatch.Value = match; + + Ruleset.BindTo(Ladder.Ruleset); + Dependencies.CacheAs(new StableInfo(storage)); } public static TournamentMatch CreateSampleMatch() => new TournamentMatch @@ -55,6 +50,7 @@ namespace osu.Game.Tournament.Tests { Value = new TournamentTeam { + Acronym = { Value = "JPN" }, FlagName = { Value = "JP" }, FullName = { Value = "Japan" }, LastYearPlacing = { Value = 10 }, @@ -128,6 +124,7 @@ namespace osu.Game.Tournament.Tests { Value = new TournamentTeam { + Acronym = { Value = "USA" }, FlagName = { Value = "US" }, FullName = { Value = "United States" }, Players = From 9e1bf71233b66a88b2419339db6cf181a7705534 Mon Sep 17 00:00:00 2001 From: Viktor Rosvall Date: Wed, 24 Jun 2020 11:29:38 +0200 Subject: [PATCH 2209/2376] Added text explaining a second copy will be made --- osu.Game/Screens/Select/ImportFromStablePopup.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/ImportFromStablePopup.cs b/osu.Game/Screens/Select/ImportFromStablePopup.cs index 20494829ae..272f9566d5 100644 --- a/osu.Game/Screens/Select/ImportFromStablePopup.cs +++ b/osu.Game/Screens/Select/ImportFromStablePopup.cs @@ -12,7 +12,7 @@ namespace osu.Game.Screens.Select public ImportFromStablePopup(Action importFromStable) { HeaderText = @"You have no beatmaps!"; - BodyText = "An existing copy of osu! was found, though.\nWould you like to import your beatmaps, skins and scores?"; + BodyText = "An existing copy of osu! was found, though.\nWould you like to import your beatmaps, skins and scores?\nThis will create a second copy of all files on disk."; Icon = FontAwesome.Solid.Plane; From 6bc507d49ed2bc76cbbaffc5dc9ddac087a5bd8f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 24 Jun 2020 18:53:52 +0900 Subject: [PATCH 2210/2376] Increase coordinate parsing limits --- osu.Game/Beatmaps/Formats/Parsing.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Formats/Parsing.cs b/osu.Game/Beatmaps/Formats/Parsing.cs index c3efb8c760..c4795a6931 100644 --- a/osu.Game/Beatmaps/Formats/Parsing.cs +++ b/osu.Game/Beatmaps/Formats/Parsing.cs @@ -11,7 +11,7 @@ namespace osu.Game.Beatmaps.Formats /// public static class Parsing { - public const int MAX_COORDINATE_VALUE = 65536; + public const int MAX_COORDINATE_VALUE = 131072; public const double MAX_PARSE_VALUE = int.MaxValue; From 0d3bc1ac29685628e57cceddd94d525b6ef48ea2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 Jun 2020 22:29:30 +0900 Subject: [PATCH 2211/2376] Add basic heatmap colour scaling based on peak value --- .../Statistics/AccuracyHeatmap.cs | 59 ++++++++++++++++--- 1 file changed, 50 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs index 94d47ecb32..eeb8b519ca 100644 --- a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; @@ -39,6 +40,11 @@ namespace osu.Game.Rulesets.Osu.Statistics private readonly ScoreInfo score; private readonly IBeatmap playableBeatmap; + /// + /// The highest count of any point currently being displayed. + /// + protected float PeakValue { get; private set; } + public AccuracyHeatmap(ScoreInfo score, IBeatmap playableBeatmap) { this.score = score; @@ -152,7 +158,7 @@ namespace osu.Game.Rulesets.Osu.Statistics ? HitPointType.Hit : HitPointType.Miss; - var point = new HitPoint(pointType) + var point = new HitPoint(pointType, this) { Colour = pointType == HitPointType.Hit ? new Color4(102, 255, 204, 255) : new Color4(255, 102, 102, 255) }; @@ -215,7 +221,7 @@ namespace osu.Game.Rulesets.Osu.Statistics int r = Math.Clamp((int)Math.Round(localPoint.Y), 0, points_per_dimension - 1); int c = Math.Clamp((int)Math.Round(localPoint.X), 0, points_per_dimension - 1); - ((HitPoint)pointGrid.Content[r][c]).Increment(); + PeakValue = Math.Max(PeakValue, ((HitPoint)pointGrid.Content[r][c]).Increment()); bufferedGrid.ForceRedraw(); } @@ -223,21 +229,56 @@ namespace osu.Game.Rulesets.Osu.Statistics private class HitPoint : Circle { private readonly HitPointType pointType; + private readonly AccuracyHeatmap heatmap; - public HitPoint(HitPointType pointType) + public override bool IsPresent => count > 0; + + public HitPoint(HitPointType pointType, AccuracyHeatmap heatmap) { this.pointType = pointType; + this.heatmap = heatmap; RelativeSizeAxes = Axes.Both; - Alpha = 0; + Alpha = 1; } - public void Increment() + private int count; + + /// + /// Increment the value of this point by one. + /// + /// The value after incrementing. + public int Increment() { - if (Alpha < 1) - Alpha += 0.1f; - else if (pointType == HitPointType.Hit) - Colour = ((Color4)Colour).Lighten(0.1f); + return ++count; + } + + protected override void Update() + { + base.Update(); + + // the point at which alpha is saturated and we begin to adjust colour lightness. + const float lighten_cutoff = 0.95f; + + // the amount of lightness to attribute regardless of relative value to peak point. + const float non_relative_portion = 0.2f; + + float amount = 0; + + // give some amount of alpha regardless of relative count + amount += non_relative_portion * Math.Min(1, count / 10f); + + // add relative portion + amount += (1 - non_relative_portion) * (count / heatmap.PeakValue); + + // apply easing + amount = (float)Interpolation.ApplyEasing(Easing.OutQuint, Math.Min(1, amount)); + + Debug.Assert(amount <= 1); + + Alpha = Math.Min(amount / lighten_cutoff, 1); + if (pointType == HitPointType.Hit) + Colour = ((Color4)Colour).Lighten(Math.Max(0, amount - lighten_cutoff)); } } From 4c283476866d7cf9d276a8e3219fa3a069f7604f Mon Sep 17 00:00:00 2001 From: Ronnie Moir <7267697+H2n9@users.noreply.github.com> Date: Wed, 24 Jun 2020 15:34:20 +0100 Subject: [PATCH 2212/2376] Adjust sample rate by UserPlaybackRate --- osu.Game/Screens/Play/Player.cs | 2 +- osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 83991ad027..71a97da5c2 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -95,7 +95,7 @@ namespace osu.Game.Screens.Play public bool LoadedBeatmapSuccessfully => DrawableRuleset?.Objects.Any() == true; - protected GameplayClockContainer GameplayClockContainer { get; private set; } + public GameplayClockContainer GameplayClockContainer { get; private set; } public DimmableStoryboard DimmableStoryboard { get; private set; } diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs index 8eaf9ac652..3dc7eab968 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs @@ -4,11 +4,13 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; +using osu.Game.Screens.Play; namespace osu.Game.Storyboards.Drawables { @@ -32,7 +34,7 @@ namespace osu.Game.Storyboards.Drawables } [BackgroundDependencyLoader] - private void load(IBindable beatmap, IBindable> mods) + private void load(IBindable beatmap, IBindable> mods, Player player) { Channel = beatmap.Value.Skin.GetSample(sampleInfo); if (Channel == null) @@ -42,6 +44,8 @@ namespace osu.Game.Storyboards.Drawables foreach (var mod in mods.Value.OfType()) mod.ApplyToSample(Channel); + + Channel.AddAdjustment(AdjustableProperty.Frequency, player.GameplayClockContainer.UserPlaybackRate); } protected override void Update() From 992ada46700d6f6420a6f22b0904376bb7ec7c58 Mon Sep 17 00:00:00 2001 From: Ronnie Moir <7267697+H2n9@users.noreply.github.com> Date: Wed, 24 Jun 2020 16:17:18 +0100 Subject: [PATCH 2213/2376] Revert UserPlaybackRate changes --- osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs index 3dc7eab968..5aeadb2e1f 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs @@ -34,7 +34,7 @@ namespace osu.Game.Storyboards.Drawables } [BackgroundDependencyLoader] - private void load(IBindable beatmap, IBindable> mods, Player player) + private void load(IBindable beatmap, IBindable> mods) { Channel = beatmap.Value.Skin.GetSample(sampleInfo); if (Channel == null) @@ -44,8 +44,6 @@ namespace osu.Game.Storyboards.Drawables foreach (var mod in mods.Value.OfType()) mod.ApplyToSample(Channel); - - Channel.AddAdjustment(AdjustableProperty.Frequency, player.GameplayClockContainer.UserPlaybackRate); } protected override void Update() From f2a48a339ea7e643ab5156764f99450d53f30bf2 Mon Sep 17 00:00:00 2001 From: Ronnie Moir <7267697+H2n9@users.noreply.github.com> Date: Wed, 24 Jun 2020 16:33:19 +0100 Subject: [PATCH 2214/2376] Remove unused usings --- osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs index 5aeadb2e1f..8eaf9ac652 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs @@ -4,13 +4,11 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; -using osu.Game.Screens.Play; namespace osu.Game.Storyboards.Drawables { From ac5cd8f25a3a1f08c3adc3fe562e0fbdc5b0585c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 25 Jun 2020 13:40:26 +0900 Subject: [PATCH 2215/2376] Fix colours with 0 alpha being invisible in legacy skins --- osu.Game/Skinning/LegacySkin.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 0b2b723440..bbc64a24e7 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -271,7 +271,15 @@ namespace osu.Game.Skinning } private IBindable getCustomColour(IHasCustomColours source, string lookup) - => source.CustomColours.TryGetValue(lookup, out var col) ? new Bindable(col) : null; + { + if (!source.CustomColours.TryGetValue(lookup, out var col)) + return null; + + if (col.A == 0) + col.A = 1; + + return new Bindable(col); + } private IBindable getManiaImage(LegacyManiaSkinConfiguration source, string lookup) => source.ImageLookups.TryGetValue(lookup, out var image) ? new Bindable(image) : null; From 4c601af207c3374eb37fd588e989e2ee9f129acb Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 25 Jun 2020 13:43:14 +0900 Subject: [PATCH 2216/2376] Match condition --- osu.Game/Skinning/LegacySkin.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index bbc64a24e7..be6d694efe 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -275,7 +275,7 @@ namespace osu.Game.Skinning if (!source.CustomColours.TryGetValue(lookup, out var col)) return null; - if (col.A == 0) + if (col.A <= 0 || col.A >= 255) col.A = 1; return new Bindable(col); From 8b84aa454d61cd61390fcce8f8f79be4e8ab1ebb Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 25 Jun 2020 13:43:56 +0900 Subject: [PATCH 2217/2376] Fix incorrect upper bound --- osu.Game/Skinning/LegacySkin.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index be6d694efe..ea630b9b8d 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -275,7 +275,7 @@ namespace osu.Game.Skinning if (!source.CustomColours.TryGetValue(lookup, out var col)) return null; - if (col.A <= 0 || col.A >= 255) + if (col.A <= 0 || col.A >= 1) col.A = 1; return new Bindable(col); From 4ff9a910121d85957cdb94011a8ed5dba653c3ad Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 25 Jun 2020 14:15:26 +0900 Subject: [PATCH 2218/2376] Adjust at parse time instead --- osu.Game/Beatmaps/Formats/LegacyDecoder.cs | 7 ++++++- osu.Game/Skinning/LegacySkin.cs | 10 +--------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index 6406bd88a5..a0e83554a3 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -103,7 +103,12 @@ namespace osu.Game.Beatmaps.Formats try { - colour = new Color4(byte.Parse(split[0]), byte.Parse(split[1]), byte.Parse(split[2]), split.Length == 4 ? byte.Parse(split[3]) : (byte)255); + byte alpha = split.Length == 4 ? byte.Parse(split[3]) : (byte)255; + + if (alpha == 0) + alpha = 255; + + colour = new Color4(byte.Parse(split[0]), byte.Parse(split[1]), byte.Parse(split[2]), alpha); } catch { diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index ea630b9b8d..0b2b723440 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -271,15 +271,7 @@ namespace osu.Game.Skinning } private IBindable getCustomColour(IHasCustomColours source, string lookup) - { - if (!source.CustomColours.TryGetValue(lookup, out var col)) - return null; - - if (col.A <= 0 || col.A >= 1) - col.A = 1; - - return new Bindable(col); - } + => source.CustomColours.TryGetValue(lookup, out var col) ? new Bindable(col) : null; private IBindable getManiaImage(LegacyManiaSkinConfiguration source, string lookup) => source.ImageLookups.TryGetValue(lookup, out var image) ? new Bindable(image) : null; From 531a69650f390ef3825f449daf46f0a32149895a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 25 Jun 2020 14:22:40 +0900 Subject: [PATCH 2219/2376] Add test --- osu.Game.Tests/Resources/skin-zero-alpha-colour.ini | 5 +++++ osu.Game.Tests/Skins/LegacySkinDecoderTest.cs | 10 ++++++++++ 2 files changed, 15 insertions(+) create mode 100644 osu.Game.Tests/Resources/skin-zero-alpha-colour.ini diff --git a/osu.Game.Tests/Resources/skin-zero-alpha-colour.ini b/osu.Game.Tests/Resources/skin-zero-alpha-colour.ini new file mode 100644 index 0000000000..3c0dae6b13 --- /dev/null +++ b/osu.Game.Tests/Resources/skin-zero-alpha-colour.ini @@ -0,0 +1,5 @@ +[General] +Version: latest + +[Colours] +Combo1: 255,255,255,0 \ No newline at end of file diff --git a/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs b/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs index aedf26ee75..c408d2f182 100644 --- a/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs +++ b/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs @@ -108,5 +108,15 @@ namespace osu.Game.Tests.Skins using (var stream = new LineBufferedReader(resStream)) Assert.That(decoder.Decode(stream).LegacyVersion, Is.EqualTo(1.0m)); } + + [Test] + public void TestDecodeColourWithZeroAlpha() + { + var decoder = new LegacySkinDecoder(); + + using (var resStream = TestResources.OpenResource("skin-zero-alpha-colour.ini")) + using (var stream = new LineBufferedReader(resStream)) + Assert.That(decoder.Decode(stream).ComboColours[0].A, Is.EqualTo(1.0f)); + } } } From fd13c0a6ddb5c8ea3260e64f956af86ab63bee5e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Jun 2020 18:44:04 +0900 Subject: [PATCH 2220/2376] Standardise line thickness --- osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs index eeb8b519ca..89707b3ebb 100644 --- a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs @@ -40,6 +40,8 @@ namespace osu.Game.Rulesets.Osu.Statistics private readonly ScoreInfo score; private readonly IBeatmap playableBeatmap; + private const float line_thickness = 2; + /// /// The highest count of any point currently being displayed. /// @@ -69,7 +71,7 @@ namespace osu.Game.Rulesets.Osu.Statistics RelativeSizeAxes = Axes.Both, Size = new Vector2(inner_portion), Masking = true, - BorderThickness = 2f, + BorderThickness = line_thickness, BorderColour = Color4.White, Child = new Box { @@ -98,7 +100,7 @@ namespace osu.Game.Rulesets.Osu.Statistics Origin = Anchor.Centre, RelativeSizeAxes = Axes.Y, Height = 2, // We're rotating along a diagonal - we don't really care how big this is. - Width = 1f, + Width = line_thickness, Rotation = -rotation, Alpha = 0.3f, }, @@ -108,7 +110,7 @@ namespace osu.Game.Rulesets.Osu.Statistics Origin = Anchor.Centre, RelativeSizeAxes = Axes.Y, Height = 2, // We're rotating along a diagonal - we don't really care how big this is. - Width = 1f, + Width = line_thickness, Rotation = rotation }, } From c095753f2444ce052b8fa7588c9cfb1eb0956cef Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Jun 2020 19:02:04 +0900 Subject: [PATCH 2221/2376] Add better line smoothing --- osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs index 89707b3ebb..20adbc1c02 100644 --- a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs @@ -98,9 +98,10 @@ namespace osu.Game.Rulesets.Osu.Statistics { Anchor = Anchor.Centre, Origin = Anchor.Centre, + EdgeSmoothness = new Vector2(1), RelativeSizeAxes = Axes.Y, Height = 2, // We're rotating along a diagonal - we don't really care how big this is. - Width = line_thickness, + Width = line_thickness / 2, Rotation = -rotation, Alpha = 0.3f, }, @@ -108,9 +109,10 @@ namespace osu.Game.Rulesets.Osu.Statistics { Anchor = Anchor.Centre, Origin = Anchor.Centre, + EdgeSmoothness = new Vector2(1), RelativeSizeAxes = Axes.Y, Height = 2, // We're rotating along a diagonal - we don't really care how big this is. - Width = line_thickness, + Width = line_thickness / 2, // adjust for edgesmoothness Rotation = rotation }, } @@ -121,13 +123,15 @@ namespace osu.Game.Rulesets.Osu.Statistics Anchor = Anchor.TopRight, Origin = Anchor.TopRight, Width = 10, - Height = 2, + EdgeSmoothness = new Vector2(1), + Height = line_thickness / 2, // adjust for edgesmoothness }, new Box { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, - Width = 2, + EdgeSmoothness = new Vector2(1), + Width = line_thickness / 2, // adjust for edgesmoothness Height = 10, } } From d7742766d054ca1d036985b6ca6c62ab946851c4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Jun 2020 19:47:23 +0900 Subject: [PATCH 2222/2376] Add key/press repeat support to carousel --- osu.Game/Screens/Select/BeatmapCarousel.cs | 63 ++++++++++++++++++++-- 1 file changed, 59 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index e174c46610..6611955cce 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -452,32 +452,49 @@ namespace osu.Game.Screens.Select /// public void ScrollToSelected() => scrollPositionCache.Invalidate(); + #region Key / button selection logic + protected override bool OnKeyDown(KeyDownEvent e) { switch (e.Key) { case Key.Left: - SelectNext(-1, true); + if (!e.Repeat) + beginRepeatSelection(() => SelectNext(-1, true), e.Key); return true; case Key.Right: - SelectNext(1, true); + if (!e.Repeat) + beginRepeatSelection(() => SelectNext(1, true), e.Key); return true; } return false; } + protected override void OnKeyUp(KeyUpEvent e) + { + switch (e.Key) + { + case Key.Left: + case Key.Right: + endRepeatSelection(e.Key); + break; + } + + base.OnKeyUp(e); + } + public bool OnPressed(GlobalAction action) { switch (action) { case GlobalAction.SelectNext: - SelectNext(1, false); + beginRepeatSelection(() => SelectNext(1, false), action); return true; case GlobalAction.SelectPrevious: - SelectNext(-1, false); + beginRepeatSelection(() => SelectNext(-1, false), action); return true; } @@ -486,8 +503,46 @@ namespace osu.Game.Screens.Select public void OnReleased(GlobalAction action) { + switch (action) + { + case GlobalAction.SelectNext: + case GlobalAction.SelectPrevious: + endRepeatSelection(action); + break; + } } + private const double repeat_interval = 120; + + private ScheduledDelegate repeatDelegate; + private object lastRepeatSource; + + /// + /// Begin repeating the specified selection action. + /// + /// The action to perform. + /// The source of the action. Used in conjunction with to only cancel the correct action (most recently pressed key). + private void beginRepeatSelection(Action action, object source) + { + endRepeatSelection(); + + lastRepeatSource = source; + Scheduler.Add(repeatDelegate = new ScheduledDelegate(action, Time.Current, repeat_interval)); + } + + private void endRepeatSelection(object source = null) + { + // only the most recent source should be able to cancel the current action. + if (source != null && !EqualityComparer.Default.Equals(lastRepeatSource, source)) + return; + + repeatDelegate?.Cancel(); + repeatDelegate = null; + lastRepeatSource = null; + } + + #endregion + protected override void Update() { base.Update(); From c36d9d4fc3fa87aef05600dae76ada50bf5c076d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Jun 2020 20:01:29 +0900 Subject: [PATCH 2223/2376] Add test coverage --- .../SongSelect/TestSceneBeatmapCarousel.cs | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 2f12194ede..073d75692e 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -17,11 +17,12 @@ using osu.Game.Rulesets; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Carousel; using osu.Game.Screens.Select.Filter; +using osuTK.Input; namespace osu.Game.Tests.Visual.SongSelect { [TestFixture] - public class TestSceneBeatmapCarousel : OsuTestScene + public class TestSceneBeatmapCarousel : OsuManualInputManagerTestScene { private TestBeatmapCarousel carousel; private RulesetStore rulesets; @@ -39,6 +40,43 @@ namespace osu.Game.Tests.Visual.SongSelect this.rulesets = rulesets; } + [Test] + public void TestKeyRepeat() + { + loadBeatmaps(); + advanceSelection(false); + + AddStep("press down arrow", () => InputManager.PressKey(Key.Down)); + + BeatmapInfo selection = null; + + checkSelectionIterating(true); + + AddStep("press up arrow", () => InputManager.PressKey(Key.Up)); + + checkSelectionIterating(true); + + AddStep("release down arrow", () => InputManager.ReleaseKey(Key.Down)); + + checkSelectionIterating(true); + + AddStep("release up arrow", () => InputManager.ReleaseKey(Key.Up)); + + checkSelectionIterating(false); + + void checkSelectionIterating(bool isIterating) + { + for (int i = 0; i < 3; i++) + { + AddStep("store selection", () => selection = carousel.SelectedBeatmap); + if (isIterating) + AddUntilStep("selection changed", () => carousel.SelectedBeatmap != selection); + else + AddUntilStep("selection not changed", () => carousel.SelectedBeatmap == selection); + } + } + } + [Test] public void TestRecommendedSelection() { From 54f087b933ef1c6df225508271c3d5c634454d69 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 25 Jun 2020 18:59:14 +0900 Subject: [PATCH 2224/2376] Re-layout match subscreen columns --- .../TestSceneMatchLeaderboardChatDisplay.cs | 32 ------ .../Multiplayer/TestSceneMatchSubScreen.cs | 16 +++ .../Components/LeaderboardChatDisplay.cs | 100 ------------------ .../Match/Components/OverlinedChatDisplay.cs | 20 ++++ .../Match/Components/OverlinedLeaderboard.cs | 24 +++++ .../Screens/Multi/Match/MatchSubScreen.cs | 60 +++-------- osu.Game/Tests/Visual/ModTestScene.cs | 13 --- osu.Game/Tests/Visual/ScreenTestScene.cs | 4 +- 8 files changed, 77 insertions(+), 192 deletions(-) delete mode 100644 osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboardChatDisplay.cs delete mode 100644 osu.Game/Screens/Multi/Match/Components/LeaderboardChatDisplay.cs create mode 100644 osu.Game/Screens/Multi/Match/Components/OverlinedChatDisplay.cs create mode 100644 osu.Game/Screens/Multi/Match/Components/OverlinedLeaderboard.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboardChatDisplay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboardChatDisplay.cs deleted file mode 100644 index 72bbc11cd0..0000000000 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboardChatDisplay.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Screens.Multi.Match.Components; -using osuTK; - -namespace osu.Game.Tests.Visual.Multiplayer -{ - public class TestSceneMatchLeaderboardChatDisplay : MultiplayerTestScene - { - protected override bool UseOnlineAPI => true; - - public TestSceneMatchLeaderboardChatDisplay() - { - Room.RoomID.Value = 7; - - Add(new Container - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(500), - Child = new LeaderboardChatDisplay - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - } - }); - } - } -} diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs index b687724105..8c54f49b8f 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs @@ -58,6 +58,22 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for load", () => match.IsCurrentScreen()); } + [Test] + public void TestLoadSimpleMatch() + { + AddStep("set room properties", () => + { + Room.RoomID.Value = 1; + Room.Name.Value = "my awesome room"; + Room.Host.Value = new User { Id = 2, Username = "peppy" }; + Room.Playlist.Add(new PlaylistItem + { + Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, + Ruleset = { Value = new OsuRuleset().RulesetInfo } + }); + }); + } + [Test] public void TestPlaylistItemSelectedOnCreate() { diff --git a/osu.Game/Screens/Multi/Match/Components/LeaderboardChatDisplay.cs b/osu.Game/Screens/Multi/Match/Components/LeaderboardChatDisplay.cs deleted file mode 100644 index de02b7f605..0000000000 --- a/osu.Game/Screens/Multi/Match/Components/LeaderboardChatDisplay.cs +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.UserInterface; -using osu.Game.Graphics; -using osu.Game.Graphics.UserInterface; - -namespace osu.Game.Screens.Multi.Match.Components -{ - public class LeaderboardChatDisplay : MultiplayerComposite - { - private const double fade_duration = 100; - - private readonly OsuTabControl tabControl; - private readonly MatchLeaderboard leaderboard; - private readonly MatchChatDisplay chat; - - public LeaderboardChatDisplay() - { - RelativeSizeAxes = Axes.Both; - - InternalChild = new GridContainer - { - RelativeSizeAxes = Axes.Both, - Content = new[] - { - new Drawable[] - { - tabControl = new DisplayModeTabControl - { - RelativeSizeAxes = Axes.X, - Height = 24, - } - }, - new Drawable[] - { - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Top = 10 }, - Children = new Drawable[] - { - leaderboard = new MatchLeaderboard { RelativeSizeAxes = Axes.Both }, - chat = new MatchChatDisplay - { - RelativeSizeAxes = Axes.Both, - Alpha = 0 - } - } - } - }, - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - } - }; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - tabControl.AccentColour = colours.Yellow; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - tabControl.Current.BindValueChanged(changeTab); - } - - public void RefreshScores() => leaderboard.RefreshScores(); - - private void changeTab(ValueChangedEvent mode) - { - chat.FadeTo(mode.NewValue == DisplayMode.Chat ? 1 : 0, fade_duration); - leaderboard.FadeTo(mode.NewValue == DisplayMode.Leaderboard ? 1 : 0, fade_duration); - } - - private class DisplayModeTabControl : OsuTabControl - { - protected override TabItem CreateTabItem(DisplayMode value) => base.CreateTabItem(value).With(d => - { - d.Anchor = Anchor.Centre; - d.Origin = Anchor.Centre; - }); - } - - private enum DisplayMode - { - Leaderboard, - Chat, - } - } -} diff --git a/osu.Game/Screens/Multi/Match/Components/OverlinedChatDisplay.cs b/osu.Game/Screens/Multi/Match/Components/OverlinedChatDisplay.cs new file mode 100644 index 0000000000..a8d898385a --- /dev/null +++ b/osu.Game/Screens/Multi/Match/Components/OverlinedChatDisplay.cs @@ -0,0 +1,20 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Game.Screens.Multi.Components; + +namespace osu.Game.Screens.Multi.Match.Components +{ + public class OverlinedChatDisplay : OverlinedDisplay + { + public OverlinedChatDisplay() + : base("Chat") + { + Content.Add(new MatchChatDisplay + { + RelativeSizeAxes = Axes.Both + }); + } + } +} diff --git a/osu.Game/Screens/Multi/Match/Components/OverlinedLeaderboard.cs b/osu.Game/Screens/Multi/Match/Components/OverlinedLeaderboard.cs new file mode 100644 index 0000000000..bda2cd70d7 --- /dev/null +++ b/osu.Game/Screens/Multi/Match/Components/OverlinedLeaderboard.cs @@ -0,0 +1,24 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Game.Screens.Multi.Components; + +namespace osu.Game.Screens.Multi.Match.Components +{ + public class OverlinedLeaderboard : OverlinedDisplay + { + private readonly MatchLeaderboard leaderboard; + + public OverlinedLeaderboard() + : base("Leaderboard") + { + Content.Add(leaderboard = new MatchLeaderboard + { + RelativeSizeAxes = Axes.Both + }); + } + + public void RefreshScores() => leaderboard.RefreshScores(); + } +} diff --git a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs index f837a407a5..a2a8816b13 100644 --- a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs +++ b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs @@ -11,7 +11,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Game.Audio; using osu.Game.Beatmaps; -using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.GameTypes; @@ -52,8 +51,8 @@ namespace osu.Game.Screens.Multi.Match protected readonly Bindable SelectedItem = new Bindable(); - private LeaderboardChatDisplay leaderboardChatDisplay; private MatchSettingsOverlay settingsOverlay; + private OverlinedLeaderboard leaderboard; private IBindable> managerUpdated; @@ -87,7 +86,10 @@ namespace osu.Game.Screens.Multi.Match RelativeSizeAxes = Axes.Both, Content = new[] { - new Drawable[] { new Components.Header() }, + new Drawable[] + { + new Components.Header() + }, new Drawable[] { new Container @@ -96,12 +98,6 @@ namespace osu.Game.Screens.Multi.Match Padding = new MarginPadding { Top = 65 }, Child = new GridContainer { - ColumnDimensions = new[] - { - new Dimension(minSize: 160), - new Dimension(minSize: 360), - new Dimension(minSize: 400), - }, RelativeSizeAxes = Axes.Both, Content = new[] { @@ -111,49 +107,23 @@ namespace osu.Game.Screens.Multi.Match { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Right = 5 }, - Child = new OverlinedParticipants(Direction.Vertical) { RelativeSizeAxes = Axes.Both } - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Horizontal = 5 }, - Child = new GridContainer + Child = new OverlinedPlaylist(true) // Temporarily always allow selection { RelativeSizeAxes = Axes.Both, - Content = new[] - { - new Drawable[] - { - new OverlinedPlaylist(true) // Temporarily always allow selection - { - RelativeSizeAxes = Axes.Both, - SelectedItem = { BindTarget = SelectedItem } - } - }, - null, - new Drawable[] - { - new TriangleButton - { - RelativeSizeAxes = Axes.X, - Text = "Show beatmap results", - Action = showBeatmapResults - } - } - }, - RowDimensions = new[] - { - new Dimension(), - new Dimension(GridSizeMode.Absolute, 5), - new Dimension(GridSizeMode.AutoSize) - } + SelectedItem = { BindTarget = SelectedItem } } }, new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Horizontal = 5 }, + Child = leaderboard = new OverlinedLeaderboard { RelativeSizeAxes = Axes.Both }, + }, + new Container { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Left = 5 }, - Child = leaderboardChatDisplay = new LeaderboardChatDisplay() + Child = new OverlinedChatDisplay { RelativeSizeAxes = Axes.Both } } }, } @@ -261,7 +231,7 @@ namespace osu.Game.Screens.Multi.Match case GameTypeTimeshift _: multiplayer?.Push(new PlayerLoader(() => new TimeshiftPlayer(SelectedItem.Value) { - Exited = () => leaderboardChatDisplay.RefreshScores() + Exited = () => leaderboard.RefreshScores() })); break; } diff --git a/osu.Game/Tests/Visual/ModTestScene.cs b/osu.Game/Tests/Visual/ModTestScene.cs index 23b5ad0bd8..add851ebf3 100644 --- a/osu.Game/Tests/Visual/ModTestScene.cs +++ b/osu.Game/Tests/Visual/ModTestScene.cs @@ -21,19 +21,6 @@ namespace osu.Game.Tests.Visual AddStep("set test data", () => currentTestData = testData); }); - public override void TearDownSteps() - { - AddUntilStep("test passed", () => - { - if (currentTestData == null) - return true; - - return currentTestData.PassCondition?.Invoke() ?? false; - }); - - base.TearDownSteps(); - } - protected sealed override IBeatmap CreateBeatmap(RulesetInfo ruleset) => currentTestData?.Beatmap ?? base.CreateBeatmap(ruleset); protected sealed override TestPlayer CreatePlayer(Ruleset ruleset) diff --git a/osu.Game/Tests/Visual/ScreenTestScene.cs b/osu.Game/Tests/Visual/ScreenTestScene.cs index 33cc00e748..067d8faf54 100644 --- a/osu.Game/Tests/Visual/ScreenTestScene.cs +++ b/osu.Game/Tests/Visual/ScreenTestScene.cs @@ -33,8 +33,8 @@ namespace osu.Game.Tests.Visual [SetUpSteps] public virtual void SetUpSteps() => addExitAllScreensStep(); - [TearDownSteps] - public virtual void TearDownSteps() => addExitAllScreensStep(); + // [TearDownSteps] + // public virtual void TearDownSteps() => addExitAllScreensStep(); private void addExitAllScreensStep() { From 01fa664b7dc57587357a29a7ac6c812da294de67 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 25 Jun 2020 20:53:48 +0900 Subject: [PATCH 2225/2376] Add recent participants --- .../Multiplayer/TestSceneMatchSubScreen.cs | 1 + .../Multi/Components/OverlinedDisplay.cs | 12 ++++ .../Screens/Multi/Match/MatchSubScreen.cs | 63 +++++++++++-------- 3 files changed, 50 insertions(+), 26 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs index 8c54f49b8f..66091f5679 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs @@ -66,6 +66,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Room.RoomID.Value = 1; Room.Name.Value = "my awesome room"; Room.Host.Value = new User { Id = 2, Username = "peppy" }; + Room.RecentParticipants.Add(Room.Host.Value); Room.Playlist.Add(new PlaylistItem { Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, diff --git a/osu.Game/Screens/Multi/Components/OverlinedDisplay.cs b/osu.Game/Screens/Multi/Components/OverlinedDisplay.cs index 8d8d4cc404..6aeb6c94df 100644 --- a/osu.Game/Screens/Multi/Components/OverlinedDisplay.cs +++ b/osu.Game/Screens/Multi/Components/OverlinedDisplay.cs @@ -35,6 +35,18 @@ namespace osu.Game.Screens.Multi.Components } } + private bool showLine = true; + + public bool ShowLine + { + get => showLine; + set + { + showLine = value; + line.Alpha = value ? 1 : 0; + } + } + protected string Details { set => details.Text = value; diff --git a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs index a2a8816b13..8216f64872 100644 --- a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs +++ b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs @@ -94,45 +94,56 @@ namespace osu.Game.Screens.Multi.Match { new Container { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Top = 65 }, - Child = new GridContainer + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Top = 10 }, + Child = new OverlinedParticipants(Direction.Horizontal) { - RelativeSizeAxes = Axes.Both, - Content = new[] + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + ShowLine = false + } + } + }, + new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] { - new Drawable[] + new Container { - new Container + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Right = 5 }, + Child = new OverlinedPlaylist(true) // Temporarily always allow selection { RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Right = 5 }, - Child = new OverlinedPlaylist(true) // Temporarily always allow selection - { - RelativeSizeAxes = Axes.Both, - SelectedItem = { BindTarget = SelectedItem } - } - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Horizontal = 5 }, - Child = leaderboard = new OverlinedLeaderboard { RelativeSizeAxes = Axes.Both }, - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Left = 5 }, - Child = new OverlinedChatDisplay { RelativeSizeAxes = Axes.Both } + SelectedItem = { BindTarget = SelectedItem } } }, - } + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Horizontal = 5 }, + Child = leaderboard = new OverlinedLeaderboard { RelativeSizeAxes = Axes.Both }, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = 5 }, + Child = new OverlinedChatDisplay { RelativeSizeAxes = Axes.Both } + } + }, } } } }, RowDimensions = new[] { + new Dimension(GridSizeMode.AutoSize), new Dimension(GridSizeMode.AutoSize), new Dimension(), } From d704a4597dc60d882d4b8f54d6245cbca81ab67f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Jun 2020 21:33:02 +0900 Subject: [PATCH 2226/2376] Use existing helper function for key repeat --- osu.Game/Screens/Select/BeatmapCarousel.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 6611955cce..ad19c9661f 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -19,6 +19,7 @@ using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Beatmaps; +using osu.Game.Extensions; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; using osu.Game.Input.Bindings; @@ -527,7 +528,7 @@ namespace osu.Game.Screens.Select endRepeatSelection(); lastRepeatSource = source; - Scheduler.Add(repeatDelegate = new ScheduledDelegate(action, Time.Current, repeat_interval)); + Scheduler.Add(repeatDelegate = this.BeginKeyRepeat(Scheduler, action)); } private void endRepeatSelection(object source = null) From 7c1dd43899d1369106890723eda0c8c671991274 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 25 Jun 2020 21:58:40 +0900 Subject: [PATCH 2227/2376] Re-style multiplayer header --- osu.Game/Screens/Multi/Header.cs | 101 +++++++++++++++---------------- 1 file changed, 50 insertions(+), 51 deletions(-) diff --git a/osu.Game/Screens/Multi/Header.cs b/osu.Game/Screens/Multi/Header.cs index 5b8e8a7fd9..2cdd082068 100644 --- a/osu.Game/Screens/Multi/Header.cs +++ b/osu.Game/Screens/Multi/Header.cs @@ -1,12 +1,14 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; +using Humanizer; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Screens; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; @@ -19,41 +21,41 @@ namespace osu.Game.Screens.Multi { public class Header : Container { - public const float HEIGHT = 121; - - private readonly HeaderBreadcrumbControl breadcrumbs; + public const float HEIGHT = 100; public Header(ScreenStack stack) { - MultiHeaderTitle title; RelativeSizeAxes = Axes.X; Height = HEIGHT; + HeaderBreadcrumbControl breadcrumbs; + MultiHeaderTitle title; + Children = new Drawable[] { new Box { RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex(@"2f2043"), + Colour = Color4Extensions.FromHex(@"#1f1921"), }, new Container { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Horizontal = SearchableListOverlay.WIDTH_PADDING + OsuScreen.HORIZONTAL_OVERFLOW_PADDING }, + Padding = new MarginPadding { Left = SearchableListOverlay.WIDTH_PADDING + OsuScreen.HORIZONTAL_OVERFLOW_PADDING }, Children = new Drawable[] { title = new MultiHeaderTitle { Anchor = Anchor.CentreLeft, - Origin = Anchor.BottomLeft, - X = -MultiHeaderTitle.ICON_WIDTH, + Origin = Anchor.CentreLeft, }, breadcrumbs = new HeaderBreadcrumbControl(stack) { Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.X, - }, + Origin = Anchor.BottomLeft + } }, }, }; @@ -62,37 +64,26 @@ namespace osu.Game.Screens.Multi { if (screen.NewValue is IMultiplayerSubScreen multiScreen) title.Screen = multiScreen; + + if (breadcrumbs.Items.Any() && screen.NewValue == breadcrumbs.Items.First()) + breadcrumbs.FadeOut(500, Easing.OutQuint); + else + breadcrumbs.FadeIn(500, Easing.OutQuint); }; breadcrumbs.Current.TriggerChange(); } - [BackgroundDependencyLoader] - private void load(OsuColour colours) + private class MultiHeaderTitle : CompositeDrawable { - breadcrumbs.StripColour = colours.Green; - } - - private class MultiHeaderTitle : CompositeDrawable, IHasAccentColour - { - public const float ICON_WIDTH = icon_size + spacing; - - private const float icon_size = 25; private const float spacing = 6; - private const int text_offset = 2; - private readonly SpriteIcon iconSprite; - private readonly OsuSpriteText title, pageText; + private readonly OsuSpriteText dot; + private readonly OsuSpriteText pageTitle; public IMultiplayerSubScreen Screen { - set => pageText.Text = value.ShortTitle.ToLowerInvariant(); - } - - public Color4 AccentColour - { - get => pageText.Colour; - set => pageText.Colour = value; + set => pageTitle.Text = value.ShortTitle.Titleize(); } public MultiHeaderTitle() @@ -108,32 +99,26 @@ namespace osu.Game.Screens.Multi Direction = FillDirection.Horizontal, Children = new Drawable[] { - iconSprite = new SpriteIcon - { - Size = new Vector2(icon_size), - Anchor = Anchor.Centre, - Origin = Anchor.Centre - }, - title = new OsuSpriteText + new OsuSpriteText { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Font = OsuFont.GetFont(size: 20, weight: FontWeight.Bold), - Margin = new MarginPadding { Bottom = text_offset } + Font = OsuFont.GetFont(size: 24), + Text = "Multiplayer" }, - new Circle + dot = new OsuSpriteText { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(4), - Colour = Color4.Gray, + Font = OsuFont.GetFont(size: 24), + Text = "·" }, - pageText = new OsuSpriteText + pageTitle = new OsuSpriteText { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Font = OsuFont.GetFont(size: 20), - Margin = new MarginPadding { Bottom = text_offset } + Font = OsuFont.GetFont(size: 24), + Text = "Lounge" } } }, @@ -143,9 +128,7 @@ namespace osu.Game.Screens.Multi [BackgroundDependencyLoader] private void load(OsuColour colours) { - title.Text = "multi"; - iconSprite.Icon = OsuIcon.Multi; - AccentColour = colours.Yellow; + pageTitle.Colour = dot.Colour = colours.Yellow; } } @@ -154,12 +137,28 @@ namespace osu.Game.Screens.Multi public HeaderBreadcrumbControl(ScreenStack stack) : base(stack) { + RelativeSizeAxes = Axes.X; + StripColour = Color4.Transparent; } protected override void LoadComplete() { base.LoadComplete(); - AccentColour = Color4.White; + AccentColour = Color4Extensions.FromHex("#e35c99"); + } + + protected override TabItem CreateTabItem(IScreen value) => new HeaderBreadcrumbTabItem(value) + { + AccentColour = AccentColour + }; + + private class HeaderBreadcrumbTabItem : BreadcrumbTabItem + { + public HeaderBreadcrumbTabItem(IScreen value) + : base(value) + { + Bar.Colour = Color4.Transparent; + } } } } From 20092c58ff6cac5194016236ff6a72cd8574fa92 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 25 Jun 2020 21:59:36 +0900 Subject: [PATCH 2228/2376] Reduce spacing between recent participants tiles --- osu.Game/Screens/Multi/Components/ParticipantsList.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Multi/Components/ParticipantsList.cs b/osu.Game/Screens/Multi/Components/ParticipantsList.cs index 79d130adf5..7978b4eaab 100644 --- a/osu.Game/Screens/Multi/Components/ParticipantsList.cs +++ b/osu.Game/Screens/Multi/Components/ParticipantsList.cs @@ -79,7 +79,7 @@ namespace osu.Game.Screens.Multi.Components Direction = Direction, AutoSizeAxes = AutoSizeAxes, RelativeSizeAxes = RelativeSizeAxes, - Spacing = new Vector2(10) + Spacing = Vector2.One }; for (int i = 0; i < RecentParticipants.Count; i++) From 668105dd6ee9857824066dd236106bd2b88cea02 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 25 Jun 2020 22:06:28 +0900 Subject: [PATCH 2229/2376] Adjust boldening --- osu.Game/Screens/Multi/Components/OverlinedDisplay.cs | 4 ++-- osu.Game/Screens/Multi/Match/Components/Header.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Multi/Components/OverlinedDisplay.cs b/osu.Game/Screens/Multi/Components/OverlinedDisplay.cs index 6aeb6c94df..d2bb3c4876 100644 --- a/osu.Game/Screens/Multi/Components/OverlinedDisplay.cs +++ b/osu.Game/Screens/Multi/Components/OverlinedDisplay.cs @@ -84,9 +84,9 @@ namespace osu.Game.Screens.Multi.Components new OsuSpriteText { Text = title, - Font = OsuFont.GetFont(size: 14) + Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold) }, - details = new OsuSpriteText { Font = OsuFont.GetFont(size: 14) }, + details = new OsuSpriteText { Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold) }, } }, }, diff --git a/osu.Game/Screens/Multi/Match/Components/Header.cs b/osu.Game/Screens/Multi/Match/Components/Header.cs index ddbaab1706..134a0b3f2e 100644 --- a/osu.Game/Screens/Multi/Match/Components/Header.cs +++ b/osu.Game/Screens/Multi/Match/Components/Header.cs @@ -52,7 +52,7 @@ namespace osu.Game.Screens.Multi.Match.Components Font = OsuFont.GetFont(size: 30), Current = { BindTarget = RoomName } }, - hostText = new LinkFlowContainer(s => s.Font = OsuFont.GetFont(size: 20, weight: FontWeight.SemiBold)) + hostText = new LinkFlowContainer(s => s.Font = OsuFont.GetFont(size: 20)) { AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, @@ -71,7 +71,7 @@ namespace osu.Game.Screens.Multi.Match.Components if (host.NewValue != null) { hostText.AddText("hosted by "); - hostText.AddUserLink(host.NewValue); + hostText.AddUserLink(host.NewValue, s => s.Font = s.Font.With(weight: FontWeight.SemiBold)); } }, true); } From b7f5a89f82b9216231ae079112922bbe41e78984 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 25 Jun 2020 22:13:39 +0900 Subject: [PATCH 2230/2376] Reduce background fade opacity --- osu.Game/Screens/Multi/Multiplayer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Multi/Multiplayer.cs b/osu.Game/Screens/Multi/Multiplayer.cs index e724152e08..3178e35581 100644 --- a/osu.Game/Screens/Multi/Multiplayer.cs +++ b/osu.Game/Screens/Multi/Multiplayer.cs @@ -117,7 +117,7 @@ namespace osu.Game.Screens.Multi Child = new Box { RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientVertical(backgroundColour.Opacity(0.7f), backgroundColour) + Colour = ColourInfo.GradientVertical(backgroundColour.Opacity(0.5f), backgroundColour) }, } } From 23f569351a11aaf945d53202438fb6dbb02009ed Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 25 Jun 2020 22:22:19 +0900 Subject: [PATCH 2231/2376] Add back missing beatmap results button --- .../Screens/Multi/Match/MatchSubScreen.cs | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs index 8216f64872..1b2fdffa5e 100644 --- a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs +++ b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Game.Audio; using osu.Game.Beatmaps; +using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.GameTypes; @@ -118,10 +119,36 @@ namespace osu.Game.Screens.Multi.Match { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Right = 5 }, - Child = new OverlinedPlaylist(true) // Temporarily always allow selection + Child = new GridContainer { RelativeSizeAxes = Axes.Both, - SelectedItem = { BindTarget = SelectedItem } + Content = new[] + { + new Drawable[] + { + new OverlinedPlaylist(true) // Temporarily always allow selection + { + RelativeSizeAxes = Axes.Both, + SelectedItem = { BindTarget = SelectedItem } + } + }, + null, + new Drawable[] + { + new TriangleButton + { + RelativeSizeAxes = Axes.X, + Text = "Show beatmap results", + Action = showBeatmapResults + } + } + }, + RowDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.Absolute, 5), + new Dimension(GridSizeMode.AutoSize) + } } }, new Container From 44a8039e924b9a614bea16a2dbcaf8a52dfe03b1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 25 Jun 2020 22:22:24 +0900 Subject: [PATCH 2232/2376] Reduce header further --- osu.Game/Screens/Multi/Header.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Multi/Header.cs b/osu.Game/Screens/Multi/Header.cs index 2cdd082068..f5f429a37d 100644 --- a/osu.Game/Screens/Multi/Header.cs +++ b/osu.Game/Screens/Multi/Header.cs @@ -21,7 +21,7 @@ namespace osu.Game.Screens.Multi { public class Header : Container { - public const float HEIGHT = 100; + public const float HEIGHT = 80; public Header(ScreenStack stack) { @@ -49,7 +49,7 @@ namespace osu.Game.Screens.Multi title = new MultiHeaderTitle { Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, + Origin = Anchor.BottomLeft, }, breadcrumbs = new HeaderBreadcrumbControl(stack) { From 8d47c908ad2909211383b2e0e5d39110c13a24b4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 25 Jun 2020 22:28:31 +0900 Subject: [PATCH 2233/2376] Remove breadcrumb fade --- osu.Game/Screens/Multi/Header.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/osu.Game/Screens/Multi/Header.cs b/osu.Game/Screens/Multi/Header.cs index f5f429a37d..e27fa154af 100644 --- a/osu.Game/Screens/Multi/Header.cs +++ b/osu.Game/Screens/Multi/Header.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Linq; using Humanizer; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; @@ -64,11 +63,6 @@ namespace osu.Game.Screens.Multi { if (screen.NewValue is IMultiplayerSubScreen multiScreen) title.Screen = multiScreen; - - if (breadcrumbs.Items.Any() && screen.NewValue == breadcrumbs.Items.First()) - breadcrumbs.FadeOut(500, Easing.OutQuint); - else - breadcrumbs.FadeIn(500, Easing.OutQuint); }; breadcrumbs.Current.TriggerChange(); From 65a2fc3bfc052a150d67d0b14a19fece40cf47cb Mon Sep 17 00:00:00 2001 From: Ronnie Moir <7267697+H2n9@users.noreply.github.com> Date: Thu, 25 Jun 2020 17:53:14 +0100 Subject: [PATCH 2234/2376] Revert access modifier --- 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 71a97da5c2..83991ad027 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -95,7 +95,7 @@ namespace osu.Game.Screens.Play public bool LoadedBeatmapSuccessfully => DrawableRuleset?.Objects.Any() == true; - public GameplayClockContainer GameplayClockContainer { get; private set; } + protected GameplayClockContainer GameplayClockContainer { get; private set; } public DimmableStoryboard DimmableStoryboard { get; private set; } From e3d654d33f2f5d1daeed1f72e263e1e943104ed3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 26 Jun 2020 20:14:02 +0900 Subject: [PATCH 2235/2376] Cleanup --- osu.Game/Screens/Select/BeatmapCarousel.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index ad19c9661f..c58b34f9f2 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -513,8 +513,6 @@ namespace osu.Game.Screens.Select } } - private const double repeat_interval = 120; - private ScheduledDelegate repeatDelegate; private object lastRepeatSource; From 1b4c31a84f3e2e4654ea065f0dee3095bcae47c0 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 26 Jun 2020 20:14:08 +0900 Subject: [PATCH 2236/2376] Remove double schedule --- osu.Game/Screens/Select/BeatmapCarousel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index c58b34f9f2..5fbe917943 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -526,7 +526,7 @@ namespace osu.Game.Screens.Select endRepeatSelection(); lastRepeatSource = source; - Scheduler.Add(repeatDelegate = this.BeginKeyRepeat(Scheduler, action)); + repeatDelegate = this.BeginKeyRepeat(Scheduler, action); } private void endRepeatSelection(object source = null) From 8f6d52550f6ddfb46a7f9f8384fa1bd7fbc5c34b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 26 Jun 2020 20:32:13 +0900 Subject: [PATCH 2237/2376] Fix potential exception if button is pressed before selection --- osu.Game/Screens/Select/BeatmapCarousel.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 5fbe917943..71ccd6fada 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -279,6 +279,9 @@ namespace osu.Game.Screens.Select /// Whether to skip individual difficulties and only increment over full groups. public void SelectNext(int direction = 1, bool skipDifficulties = true) { + if (selectedBeatmap == null) + return; + if (beatmapSets.All(s => s.Filtered.Value)) return; From 099416b4c3d981d7e844ca4b57833597f8e7715a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 26 Jun 2020 21:03:34 +0900 Subject: [PATCH 2238/2376] Move check inside next difficulty selection --- osu.Game/Screens/Select/BeatmapCarousel.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 71ccd6fada..6f913a3177 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -279,9 +279,6 @@ namespace osu.Game.Screens.Select /// Whether to skip individual difficulties and only increment over full groups. public void SelectNext(int direction = 1, bool skipDifficulties = true) { - if (selectedBeatmap == null) - return; - if (beatmapSets.All(s => s.Filtered.Value)) return; @@ -305,6 +302,9 @@ namespace osu.Game.Screens.Select private void selectNextDifficulty(int direction) { + if (selectedBeatmap == null) + return; + var unfilteredDifficulties = selectedBeatmapSet.Children.Where(s => !s.Filtered.Value).ToList(); int index = unfilteredDifficulties.IndexOf(selectedBeatmap); From 97a212a7f6a35a17a098e9ae11ff2a9b27833666 Mon Sep 17 00:00:00 2001 From: Power Maker Date: Fri, 26 Jun 2020 14:32:01 +0200 Subject: [PATCH 2239/2376] Hide red tint based on "Show health display even when you can't fail" setting --- osu.Game/Screens/Play/HUD/FailingLayer.cs | 25 ++++++++++++++++++++++- osu.Game/Screens/Play/HUDOverlay.cs | 5 +++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/FailingLayer.cs b/osu.Game/Screens/Play/HUD/FailingLayer.cs index a49aa89a7c..6fda5a1214 100644 --- a/osu.Game/Screens/Play/HUD/FailingLayer.cs +++ b/osu.Game/Screens/Play/HUD/FailingLayer.cs @@ -31,6 +31,7 @@ namespace osu.Game.Screens.Play.HUD /// public double LowHealthThreshold = 0.20f; + public readonly Bindable HUDEnabled = new Bindable(); private readonly Bindable enabled = new Bindable(); private readonly Container boxes; @@ -74,7 +75,7 @@ namespace osu.Game.Screens.Play.HUD boxes.Colour = color.Red; configEnabled = config.GetBindable(OsuSetting.FadePlayfieldWhenHealthLow); - enabled.BindValueChanged(e => this.FadeTo(e.NewValue ? 1 : 0, fade_time, Easing.OutQuint), true); + enabled.BindValueChanged(e => TryToFade(fade_time, Easing.OutQuint, e.NewValue ? true : false), true); } protected override void LoadComplete() @@ -105,6 +106,28 @@ namespace osu.Game.Screens.Play.HUD enabled.Value = false; } + /// + /// Tries to fade based on "Fade playfield when health is low" setting + /// + /// Duration of the fade + /// Type of easing + /// True when you want to fade in, false when you want to fade out + public void TryToFade(float fadeDuration, Easing easing, bool fadeIn) + { + if (HUDEnabled.Value) + { + if (fadeIn) + { + if (enabled.Value) + this.FadeIn(fadeDuration, easing); + } + else + this.FadeOut(fadeDuration, easing); + } + else + this.FadeOut(fadeDuration, easing); + } + protected override void Update() { double target = Math.Clamp(max_alpha * (1 - Current.Value / LowHealthThreshold), 0, max_alpha); diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 5114efd9a9..73b93582ef 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using Microsoft.Diagnostics.Runtime.Interop; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; @@ -153,6 +154,8 @@ namespace osu.Game.Screens.Play // start all elements hidden hideTargets.ForEach(d => d.Hide()); + + FailingLayer.HUDEnabled.BindTo(ShowHealthbar); } public override void Hide() => throw new InvalidOperationException($"{nameof(HUDOverlay)} should not be hidden as it will remove the ability of a user to quit. Use {nameof(ShowHud)} instead."); @@ -168,11 +171,13 @@ namespace osu.Game.Screens.Play if (healthBar.NewValue) { HealthDisplay.FadeIn(fade_duration, fade_easing); + FailingLayer.TryToFade(fade_duration, fade_easing, true); topScoreContainer.MoveToY(30, fade_duration, fade_easing); } else { HealthDisplay.FadeOut(fade_duration, fade_easing); + FailingLayer.TryToFade(fade_duration, fade_easing, false); topScoreContainer.MoveToY(0, fade_duration, fade_easing); } }, true); From efeaa1cc10ddd6d0b80ebd16651908a4be7c818a Mon Sep 17 00:00:00 2001 From: Power Maker Date: Fri, 26 Jun 2020 14:58:42 +0200 Subject: [PATCH 2240/2376] Make some changes, fix and add tests --- .../Visual/Gameplay/TestSceneFailingLayer.cs | 27 +++++++++++++++++++ osu.Game/Screens/Play/HUD/FailingLayer.cs | 3 ++- osu.Game/Screens/Play/HUDOverlay.cs | 2 -- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs index a95e806862..83d9e888f1 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs @@ -3,6 +3,7 @@ using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Testing; using osu.Game.Configuration; using osu.Game.Rulesets.Scoring; @@ -14,6 +15,8 @@ namespace osu.Game.Tests.Visual.Gameplay { private FailingLayer layer; + private Bindable enabledHUD = new Bindable(); + [Resolved] private OsuConfigManager config { get; set; } @@ -24,8 +27,10 @@ namespace osu.Game.Tests.Visual.Gameplay { Child = layer = new FailingLayer(); layer.BindHealthProcessor(new DrainingHealthProcessor(1)); + layer.HUDEnabled.BindTo(enabledHUD); }); + AddStep("enable HUDOverlay", () => enabledHUD.Value = true); AddStep("enable layer", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, true)); AddUntilStep("layer is visible", () => layer.IsPresent); } @@ -69,5 +74,27 @@ namespace osu.Game.Tests.Visual.Gameplay AddWaitStep("wait for potential fade", 10); AddAssert("layer is still visible", () => layer.IsPresent); } + + [Test] + public void TestLayerVisibilityWithDifferentOptions() + { + AddStep("set health to 0.10", () => layer.Current.Value = 0.1); + + AddStep("disable HUDOverlay", () => enabledHUD.Value = false); + AddStep("disable FadePlayfieldWhenHealthLow", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, false)); + AddUntilStep("layer fade is invisible", () => !layer.IsPresent); + + AddStep("disable HUDOverlay", () => enabledHUD.Value = false); + AddStep("enable FadePlayfieldWhenHealthLow", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, true)); + AddUntilStep("layer fade is invisible", () => !layer.IsPresent); + + AddStep("enable HUDOverlay", () => enabledHUD.Value = true); + AddStep("disable FadePlayfieldWhenHealthLow", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, false)); + AddUntilStep("layer fade is invisible", () => !layer.IsPresent); + + AddStep("enable HUDOverlay", () => enabledHUD.Value = true); + AddStep("enable FadePlayfieldWhenHealthLow", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, true)); + AddUntilStep("layer fade is visible", () => layer.IsPresent); + } } } diff --git a/osu.Game/Screens/Play/HUD/FailingLayer.cs b/osu.Game/Screens/Play/HUD/FailingLayer.cs index 6fda5a1214..d982764c30 100644 --- a/osu.Game/Screens/Play/HUD/FailingLayer.cs +++ b/osu.Game/Screens/Play/HUD/FailingLayer.cs @@ -75,7 +75,8 @@ namespace osu.Game.Screens.Play.HUD boxes.Colour = color.Red; configEnabled = config.GetBindable(OsuSetting.FadePlayfieldWhenHealthLow); - enabled.BindValueChanged(e => TryToFade(fade_time, Easing.OutQuint, e.NewValue ? true : false), true); + enabled.BindValueChanged(e => TryToFade(fade_time, Easing.OutQuint, e.NewValue), true); + HUDEnabled.BindValueChanged(e => TryToFade(fade_time, Easing.OutQuint, e.NewValue), true); } protected override void LoadComplete() diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 73b93582ef..d4c548dce7 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -171,13 +171,11 @@ namespace osu.Game.Screens.Play if (healthBar.NewValue) { HealthDisplay.FadeIn(fade_duration, fade_easing); - FailingLayer.TryToFade(fade_duration, fade_easing, true); topScoreContainer.MoveToY(30, fade_duration, fade_easing); } else { HealthDisplay.FadeOut(fade_duration, fade_easing); - FailingLayer.TryToFade(fade_duration, fade_easing, false); topScoreContainer.MoveToY(0, fade_duration, fade_easing); } }, true); From 798e8e7a8deea5d1ac665bc9491604c0f082e5ed Mon Sep 17 00:00:00 2001 From: Power Maker Date: Fri, 26 Jun 2020 15:12:01 +0200 Subject: [PATCH 2241/2376] Fix CI fail --- osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs | 2 +- osu.Game/Screens/Play/HUDOverlay.cs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs index 83d9e888f1..3eda47627b 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs @@ -15,7 +15,7 @@ namespace osu.Game.Tests.Visual.Gameplay { private FailingLayer layer; - private Bindable enabledHUD = new Bindable(); + private readonly Bindable enabledHUD = new Bindable(); [Resolved] private OsuConfigManager config { get; set; } diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index d4c548dce7..b55a93db1f 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using Microsoft.Diagnostics.Runtime.Interop; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; From bd1f38cc3ef41c0ca8bbd586c81b1cbf8b6a6d9f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 26 Jun 2020 23:21:44 +0900 Subject: [PATCH 2242/2376] Fix crash due to unsafe mod deserialisation --- .../Online/TestAPIModSerialization.cs | 59 ++++++++++++++++++- osu.Game/Rulesets/Mods/ModTimeRamp.cs | 4 +- 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Online/TestAPIModSerialization.cs b/osu.Game.Tests/Online/TestAPIModSerialization.cs index d9318aa822..5948582d77 100644 --- a/osu.Game.Tests/Online/TestAPIModSerialization.cs +++ b/osu.Game.Tests/Online/TestAPIModSerialization.cs @@ -49,9 +49,32 @@ namespace osu.Game.Tests.Online Assert.That(converted.TestSetting.Value, Is.EqualTo(2)); } + [Test] + public void TestDeserialiseTimeRampMod() + { + // Create the mod with values different from default. + var apiMod = new APIMod(new TestModTimeRamp + { + AdjustPitch = { Value = false }, + InitialRate = { Value = 1.25 }, + FinalRate = { Value = 0.25 } + }); + + var deserialised = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(apiMod)); + var converted = (TestModTimeRamp)deserialised.ToMod(new TestRuleset()); + + Assert.That(converted.AdjustPitch.Value, Is.EqualTo(false)); + Assert.That(converted.InitialRate.Value, Is.EqualTo(1.25)); + Assert.That(converted.FinalRate.Value, Is.EqualTo(0.25)); + } + private class TestRuleset : Ruleset { - public override IEnumerable GetModsFor(ModType type) => new[] { new TestMod() }; + public override IEnumerable GetModsFor(ModType type) => new Mod[] + { + new TestMod(), + new TestModTimeRamp(), + }; public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => throw new System.NotImplementedException(); @@ -78,5 +101,39 @@ namespace osu.Game.Tests.Online Precision = 0.01, }; } + + private class TestModTimeRamp : ModTimeRamp + { + public override string Name => "Test Mod"; + public override string Acronym => "TMTR"; + public override double ScoreMultiplier => 1; + + [SettingSource("Initial rate", "The starting speed of the track")] + public override BindableNumber InitialRate { get; } = new BindableDouble + { + MinValue = 1, + MaxValue = 2, + Default = 1.5, + Value = 1.5, + Precision = 0.01, + }; + + [SettingSource("Final rate", "The speed increase to ramp towards")] + public override BindableNumber FinalRate { get; } = new BindableDouble + { + MinValue = 0, + MaxValue = 1, + Default = 0.5, + Value = 0.5, + Precision = 0.01, + }; + + [SettingSource("Adjust pitch", "Should pitch be adjusted with speed")] + public override BindableBool AdjustPitch { get; } = new BindableBool + { + Default = true, + Value = true + }; + } } } diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index cbd07efa97..839d97f04e 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -89,9 +89,9 @@ namespace osu.Game.Rulesets.Mods private void applyPitchAdjustment(ValueChangedEvent adjustPitchSetting) { // remove existing old adjustment - track.RemoveAdjustment(adjustmentForPitchSetting(adjustPitchSetting.OldValue), SpeedChange); + track?.RemoveAdjustment(adjustmentForPitchSetting(adjustPitchSetting.OldValue), SpeedChange); - track.AddAdjustment(adjustmentForPitchSetting(adjustPitchSetting.NewValue), SpeedChange); + track?.AddAdjustment(adjustmentForPitchSetting(adjustPitchSetting.NewValue), SpeedChange); } private AdjustableProperty adjustmentForPitchSetting(bool adjustPitchSettingValue) From c233dc476800e7df39ee242a7515cc6bc8beb5e9 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 27 Jun 2020 00:16:16 +0900 Subject: [PATCH 2243/2376] Add some global error handling --- osu.Game/Screens/Multi/RoomManager.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Multi/RoomManager.cs b/osu.Game/Screens/Multi/RoomManager.cs index 4d6ac46c84..b8c969a845 100644 --- a/osu.Game/Screens/Multi/RoomManager.cs +++ b/osu.Game/Screens/Multi/RoomManager.cs @@ -166,8 +166,16 @@ namespace osu.Game.Screens.Multi var r = listing[i]; r.Position.Value = i; - update(r, r); - addRoom(r); + try + { + update(r, r); + addRoom(r); + } + catch (Exception ex) + { + Logger.Error(ex, $"Failed to update room: {r.Name.Value}."); + rooms.Remove(r); + } } RoomsUpdated?.Invoke(); From e8d36bc3cbb5c6aa2c0f0fd6dde7d18d34799590 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 27 Jun 2020 00:19:22 +0900 Subject: [PATCH 2244/2376] Don't trigger the same exception multiple times --- osu.Game/Screens/Multi/RoomManager.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Multi/RoomManager.cs b/osu.Game/Screens/Multi/RoomManager.cs index b8c969a845..5083fb2ee3 100644 --- a/osu.Game/Screens/Multi/RoomManager.cs +++ b/osu.Game/Screens/Multi/RoomManager.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; @@ -142,6 +143,8 @@ namespace osu.Game.Screens.Multi joinedRoom = null; } + private readonly List roomsFailedUpdate = new List(); + /// /// Invoked when the listing of all s is received from the server. /// @@ -173,7 +176,14 @@ namespace osu.Game.Screens.Multi } catch (Exception ex) { - Logger.Error(ex, $"Failed to update room: {r.Name.Value}."); + Debug.Assert(r.RoomID.Value != null); + + if (!roomsFailedUpdate.Contains(r.RoomID.Value.Value)) + { + Logger.Error(ex, $"Failed to update room: {r.Name.Value}."); + roomsFailedUpdate.Add(r.RoomID.Value.Value); + } + rooms.Remove(r); } } From 3783fe8d6a2797925e4ad77525c676226fcf9bcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 26 Jun 2020 19:03:41 +0200 Subject: [PATCH 2245/2376] Rename fields for clarity --- .../Visual/Gameplay/TestSceneFailingLayer.cs | 14 +++++------ osu.Game/Screens/Play/HUD/FailingLayer.cs | 23 ++++++++++--------- osu.Game/Screens/Play/HUDOverlay.cs | 2 +- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs index 3eda47627b..1c55595c97 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs @@ -15,7 +15,7 @@ namespace osu.Game.Tests.Visual.Gameplay { private FailingLayer layer; - private readonly Bindable enabledHUD = new Bindable(); + private readonly Bindable showHealth = new Bindable(); [Resolved] private OsuConfigManager config { get; set; } @@ -27,10 +27,10 @@ namespace osu.Game.Tests.Visual.Gameplay { Child = layer = new FailingLayer(); layer.BindHealthProcessor(new DrainingHealthProcessor(1)); - layer.HUDEnabled.BindTo(enabledHUD); + layer.ShowHealth.BindTo(showHealth); }); - AddStep("enable HUDOverlay", () => enabledHUD.Value = true); + AddStep("show health", () => showHealth.Value = true); AddStep("enable layer", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, true)); AddUntilStep("layer is visible", () => layer.IsPresent); } @@ -80,19 +80,19 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("set health to 0.10", () => layer.Current.Value = 0.1); - AddStep("disable HUDOverlay", () => enabledHUD.Value = false); + AddStep("don't show health", () => showHealth.Value = false); AddStep("disable FadePlayfieldWhenHealthLow", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, false)); AddUntilStep("layer fade is invisible", () => !layer.IsPresent); - AddStep("disable HUDOverlay", () => enabledHUD.Value = false); + AddStep("don't show health", () => showHealth.Value = false); AddStep("enable FadePlayfieldWhenHealthLow", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, true)); AddUntilStep("layer fade is invisible", () => !layer.IsPresent); - AddStep("enable HUDOverlay", () => enabledHUD.Value = true); + AddStep("show health", () => showHealth.Value = true); AddStep("disable FadePlayfieldWhenHealthLow", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, false)); AddUntilStep("layer fade is invisible", () => !layer.IsPresent); - AddStep("enable HUDOverlay", () => enabledHUD.Value = true); + AddStep("show health", () => showHealth.Value = true); AddStep("enable FadePlayfieldWhenHealthLow", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, true)); AddUntilStep("layer fade is visible", () => layer.IsPresent); } diff --git a/osu.Game/Screens/Play/HUD/FailingLayer.cs b/osu.Game/Screens/Play/HUD/FailingLayer.cs index d982764c30..e8c99c2ed8 100644 --- a/osu.Game/Screens/Play/HUD/FailingLayer.cs +++ b/osu.Game/Screens/Play/HUD/FailingLayer.cs @@ -31,11 +31,12 @@ namespace osu.Game.Screens.Play.HUD /// public double LowHealthThreshold = 0.20f; - public readonly Bindable HUDEnabled = new Bindable(); - private readonly Bindable enabled = new Bindable(); + public readonly Bindable ShowHealth = new Bindable(); + + private readonly Bindable fadePlayfieldWhenHealthLow = new Bindable(); private readonly Container boxes; - private Bindable configEnabled; + private Bindable fadePlayfieldWhenHealthLowSetting; private HealthProcessor healthProcessor; public FailingLayer() @@ -74,9 +75,9 @@ namespace osu.Game.Screens.Play.HUD { boxes.Colour = color.Red; - configEnabled = config.GetBindable(OsuSetting.FadePlayfieldWhenHealthLow); - enabled.BindValueChanged(e => TryToFade(fade_time, Easing.OutQuint, e.NewValue), true); - HUDEnabled.BindValueChanged(e => TryToFade(fade_time, Easing.OutQuint, e.NewValue), true); + fadePlayfieldWhenHealthLowSetting = config.GetBindable(OsuSetting.FadePlayfieldWhenHealthLow); + fadePlayfieldWhenHealthLow.BindValueChanged(e => TryToFade(fade_time, Easing.OutQuint, e.NewValue), true); + ShowHealth.BindValueChanged(e => TryToFade(fade_time, Easing.OutQuint, e.NewValue), true); } protected override void LoadComplete() @@ -98,13 +99,13 @@ namespace osu.Game.Screens.Play.HUD if (LoadState < LoadState.Ready) return; - enabled.UnbindBindings(); + fadePlayfieldWhenHealthLow.UnbindBindings(); // Don't display ever if the ruleset is not using a draining health display. if (healthProcessor is DrainingHealthProcessor) - enabled.BindTo(configEnabled); + fadePlayfieldWhenHealthLow.BindTo(fadePlayfieldWhenHealthLowSetting); else - enabled.Value = false; + fadePlayfieldWhenHealthLow.Value = false; } /// @@ -115,11 +116,11 @@ namespace osu.Game.Screens.Play.HUD /// True when you want to fade in, false when you want to fade out public void TryToFade(float fadeDuration, Easing easing, bool fadeIn) { - if (HUDEnabled.Value) + if (ShowHealth.Value) { if (fadeIn) { - if (enabled.Value) + if (fadePlayfieldWhenHealthLow.Value) this.FadeIn(fadeDuration, easing); } else diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index b55a93db1f..96e9625f76 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -154,7 +154,7 @@ namespace osu.Game.Screens.Play // start all elements hidden hideTargets.ForEach(d => d.Hide()); - FailingLayer.HUDEnabled.BindTo(ShowHealthbar); + FailingLayer.ShowHealth.BindTo(ShowHealthbar); } public override void Hide() => throw new InvalidOperationException($"{nameof(HUDOverlay)} should not be hidden as it will remove the ability of a user to quit. Use {nameof(ShowHud)} instead."); From 415e1c05ff7c83f9a2ef0f7981a80ecf24f60d9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 26 Jun 2020 19:06:41 +0200 Subject: [PATCH 2246/2376] Simplify implementation --- osu.Game/Screens/Play/HUD/FailingLayer.cs | 26 +++++------------------ 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/FailingLayer.cs b/osu.Game/Screens/Play/HUD/FailingLayer.cs index e8c99c2ed8..22b7950d31 100644 --- a/osu.Game/Screens/Play/HUD/FailingLayer.cs +++ b/osu.Game/Screens/Play/HUD/FailingLayer.cs @@ -76,8 +76,8 @@ namespace osu.Game.Screens.Play.HUD boxes.Colour = color.Red; fadePlayfieldWhenHealthLowSetting = config.GetBindable(OsuSetting.FadePlayfieldWhenHealthLow); - fadePlayfieldWhenHealthLow.BindValueChanged(e => TryToFade(fade_time, Easing.OutQuint, e.NewValue), true); - ShowHealth.BindValueChanged(e => TryToFade(fade_time, Easing.OutQuint, e.NewValue), true); + fadePlayfieldWhenHealthLow.BindValueChanged(_ => updateState(), true); + ShowHealth.BindValueChanged(_ => updateState(), true); } protected override void LoadComplete() @@ -108,26 +108,10 @@ namespace osu.Game.Screens.Play.HUD fadePlayfieldWhenHealthLow.Value = false; } - /// - /// Tries to fade based on "Fade playfield when health is low" setting - /// - /// Duration of the fade - /// Type of easing - /// True when you want to fade in, false when you want to fade out - public void TryToFade(float fadeDuration, Easing easing, bool fadeIn) + private void updateState() { - if (ShowHealth.Value) - { - if (fadeIn) - { - if (fadePlayfieldWhenHealthLow.Value) - this.FadeIn(fadeDuration, easing); - } - else - this.FadeOut(fadeDuration, easing); - } - else - this.FadeOut(fadeDuration, easing); + var showLayer = fadePlayfieldWhenHealthLow.Value && ShowHealth.Value; + this.FadeTo(showLayer ? 1 : 0, fade_time, Easing.OutQuint); } protected override void Update() From a63b6a3ddf571bb941b858347fe903a4b82a1c5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 26 Jun 2020 19:22:30 +0200 Subject: [PATCH 2247/2376] Simplify binding --- osu.Game/Screens/Play/HUDOverlay.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 96e9625f76..f09745cf71 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -153,8 +153,6 @@ namespace osu.Game.Screens.Play // start all elements hidden hideTargets.ForEach(d => d.Hide()); - - FailingLayer.ShowHealth.BindTo(ShowHealthbar); } public override void Hide() => throw new InvalidOperationException($"{nameof(HUDOverlay)} should not be hidden as it will remove the ability of a user to quit. Use {nameof(ShowHud)} instead."); @@ -264,7 +262,10 @@ namespace osu.Game.Screens.Play Margin = new MarginPadding { Top = 20 } }; - protected virtual FailingLayer CreateFailingLayer() => new FailingLayer(); + protected virtual FailingLayer CreateFailingLayer() => new FailingLayer + { + ShowHealth = { BindTarget = ShowHealthbar } + }; protected virtual KeyCounterDisplay CreateKeyCounter() => new KeyCounterDisplay { From 02f590309d9b67c40e582a1c4f4302ee216204f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 26 Jun 2020 19:22:45 +0200 Subject: [PATCH 2248/2376] Add xmldoc for public property --- osu.Game/Screens/Play/HUD/FailingLayer.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Play/HUD/FailingLayer.cs b/osu.Game/Screens/Play/HUD/FailingLayer.cs index 22b7950d31..d4faa4bbb7 100644 --- a/osu.Game/Screens/Play/HUD/FailingLayer.cs +++ b/osu.Game/Screens/Play/HUD/FailingLayer.cs @@ -31,6 +31,9 @@ namespace osu.Game.Screens.Play.HUD /// public double LowHealthThreshold = 0.20f; + /// + /// Whether the current player health should be shown on screen. + /// public readonly Bindable ShowHealth = new Bindable(); private readonly Bindable fadePlayfieldWhenHealthLow = new Bindable(); From 3637bf2f9bc4929cd58cffb4aeb8830e1ceee690 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 26 Jun 2020 19:23:42 +0200 Subject: [PATCH 2249/2376] Clean up member order & access modifiers --- osu.Game/Screens/Play/HUD/FailingLayer.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/FailingLayer.cs b/osu.Game/Screens/Play/HUD/FailingLayer.cs index d4faa4bbb7..b96cfd170e 100644 --- a/osu.Game/Screens/Play/HUD/FailingLayer.cs +++ b/osu.Game/Screens/Play/HUD/FailingLayer.cs @@ -18,10 +18,15 @@ using osuTK.Graphics; namespace osu.Game.Screens.Play.HUD { /// - /// An overlay layer on top of the playfield which fades to red when the current player health falls below a certain threshold defined by . + /// An overlay layer on top of the playfield which fades to red when the current player health falls below a certain threshold defined by . /// public class FailingLayer : HealthDisplay { + /// + /// Whether the current player health should be shown on screen. + /// + public readonly Bindable ShowHealth = new Bindable(); + private const float max_alpha = 0.4f; private const int fade_time = 400; private const float gradient_size = 0.3f; @@ -29,12 +34,7 @@ namespace osu.Game.Screens.Play.HUD /// /// The threshold under which the current player life should be considered low and the layer should start fading in. /// - public double LowHealthThreshold = 0.20f; - - /// - /// Whether the current player health should be shown on screen. - /// - public readonly Bindable ShowHealth = new Bindable(); + private const double low_health_threshold = 0.20f; private readonly Bindable fadePlayfieldWhenHealthLow = new Bindable(); private readonly Container boxes; @@ -119,7 +119,7 @@ namespace osu.Game.Screens.Play.HUD protected override void Update() { - double target = Math.Clamp(max_alpha * (1 - Current.Value / LowHealthThreshold), 0, max_alpha); + double target = Math.Clamp(max_alpha * (1 - Current.Value / low_health_threshold), 0, max_alpha); boxes.Alpha = (float)Interpolation.Lerp(boxes.Alpha, target, Clock.ElapsedFrameTime * 0.01f); From c47f762f24c007fe504144694d93945565fbeafc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 27 Jun 2020 15:59:26 +0200 Subject: [PATCH 2250/2376] Update test scene to allow checking samples --- .../ManiaBeatmapSampleConversionTest.cs | 20 +++++++++++++------ .../convert-samples-expected-conversion.json | 9 ++++++--- .../Testing/Beatmaps/convert-samples.osu | 2 +- .../mania-samples-expected-conversion.json | 6 ++++-- 4 files changed, 25 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapSampleConversionTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapSampleConversionTest.cs index d8f87195d1..dd1b2e1745 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapSampleConversionTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapSampleConversionTest.cs @@ -29,13 +29,16 @@ namespace osu.Game.Rulesets.Mania.Tests StartTime = hitObject.StartTime, EndTime = hitObject.GetEndTime(), Column = ((ManiaHitObject)hitObject).Column, - NodeSamples = getSampleNames((hitObject as HoldNote)?.NodeSamples) + Samples = getSampleNames(hitObject.Samples), + NodeSamples = getNodeSampleNames((hitObject as HoldNote)?.NodeSamples) }; } - private IList> getSampleNames(List> hitSampleInfo) - => hitSampleInfo?.Select(samples => - (IList)samples.Select(sample => sample.LookupNames.First()).ToList()) + private IList getSampleNames(IList hitSampleInfo) + => hitSampleInfo.Select(sample => sample.LookupNames.First()).ToList(); + + private IList> getNodeSampleNames(List> hitSampleInfo) + => hitSampleInfo?.Select(getSampleNames) .ToList(); protected override Ruleset CreateRuleset() => new ManiaRuleset(); @@ -51,14 +54,19 @@ namespace osu.Game.Rulesets.Mania.Tests public double StartTime; public double EndTime; public int Column; + public IList Samples; public IList> NodeSamples; public bool Equals(SampleConvertValue other) => Precision.AlmostEquals(StartTime, other.StartTime, conversion_lenience) && Precision.AlmostEquals(EndTime, other.EndTime, conversion_lenience) - && samplesEqual(NodeSamples, other.NodeSamples); + && samplesEqual(Samples, other.Samples) + && nodeSamplesEqual(NodeSamples, other.NodeSamples); - private static bool samplesEqual(ICollection> firstSampleList, ICollection> secondSampleList) + private static bool samplesEqual(ICollection firstSampleList, ICollection secondSampleList) + => firstSampleList.SequenceEqual(secondSampleList); + + private static bool nodeSamplesEqual(ICollection> firstSampleList, ICollection> secondSampleList) { if (firstSampleList == null && secondSampleList == null) return true; diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples-expected-conversion.json b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples-expected-conversion.json index b8ce85eef5..fec1360b26 100644 --- a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples-expected-conversion.json +++ b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples-expected-conversion.json @@ -9,7 +9,8 @@ ["normal-hitnormal"], ["soft-hitnormal"], ["drum-hitnormal"] - ] + ], + "Samples": ["drum-hitnormal"] }, { "StartTime": 1875.0, "EndTime": 2750.0, @@ -17,14 +18,16 @@ "NodeSamples": [ ["soft-hitnormal"], ["drum-hitnormal"] - ] + ], + "Samples": ["drum-hitnormal"] }] }, { "StartTime": 3750.0, "Objects": [{ "StartTime": 3750.0, "EndTime": 3750.0, - "Column": 3 + "Column": 3, + "Samples": ["normal-hitnormal"] }] }] } \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples.osu b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples.osu index 16b73992d2..fea1de6614 100644 --- a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples.osu +++ b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples.osu @@ -13,4 +13,4 @@ SliderTickRate:1 [HitObjects] 88,99,1000,6,0,L|306:259,2,245,0|0|0,1:0|2:0|3:0,0:0:0:0: -259,118,3750,1,0,0:0:0:0: +259,118,3750,1,0,1:0:0:0: diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples-expected-conversion.json b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples-expected-conversion.json index e22540614d..1aca75a796 100644 --- a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples-expected-conversion.json +++ b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples-expected-conversion.json @@ -8,7 +8,8 @@ "NodeSamples": [ ["normal-hitnormal"], [] - ] + ], + "Samples": ["normal-hitnormal"] }] }, { "StartTime": 2000.0, @@ -19,7 +20,8 @@ "NodeSamples": [ ["drum-hitnormal"], [] - ] + ], + "Samples": ["drum-hitnormal"] }] }] } \ No newline at end of file From 5e92809401122afcd4504ebf99ad17e234c6dbd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 27 Jun 2020 16:46:43 +0200 Subject: [PATCH 2251/2376] Add failing test case --- .../ManiaBeatmapSampleConversionTest.cs | 1 + ...r-convert-samples-expected-conversion.json | 21 +++++++++++++++++++ .../Beatmaps/slider-convert-samples.osu | 15 +++++++++++++ 3 files changed, 37 insertions(+) create mode 100644 osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/slider-convert-samples-expected-conversion.json create mode 100644 osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/slider-convert-samples.osu diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapSampleConversionTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapSampleConversionTest.cs index dd1b2e1745..c8feb4ae24 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapSampleConversionTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapSampleConversionTest.cs @@ -20,6 +20,7 @@ namespace osu.Game.Rulesets.Mania.Tests [TestCase("convert-samples")] [TestCase("mania-samples")] + [TestCase("slider-convert-samples")] public void Test(string name) => base.Test(name); protected override IEnumerable CreateConvertValue(HitObject hitObject) diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/slider-convert-samples-expected-conversion.json b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/slider-convert-samples-expected-conversion.json new file mode 100644 index 0000000000..e3768a90d7 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/slider-convert-samples-expected-conversion.json @@ -0,0 +1,21 @@ +{ + "Mappings": [{ + "StartTime": 8470.0, + "Objects": [{ + "StartTime": 8470.0, + "EndTime": 8470.0, + "Column": 0, + "Samples": ["normal-hitnormal", "normal-hitclap"] + }, { + "StartTime": 8626.470587768974, + "EndTime": 8626.470587768974, + "Column": 1, + "Samples": ["normal-hitnormal"] + }, { + "StartTime": 8782.941175537948, + "EndTime": 8782.941175537948, + "Column": 2, + "Samples": ["normal-hitnormal", "normal-hitclap"] + }] + }] +} diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/slider-convert-samples.osu b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/slider-convert-samples.osu new file mode 100644 index 0000000000..08e90ce807 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/slider-convert-samples.osu @@ -0,0 +1,15 @@ +osu file format v14 + +[Difficulty] +HPDrainRate:6 +CircleSize:4 +OverallDifficulty:8 +ApproachRate:9.5 +SliderMultiplier:2.00000000596047 +SliderTickRate:1 + +[TimingPoints] +0,312.941176470588,4,1,0,100,1,0 + +[HitObjects] +82,216,8470,6,0,P|52:161|99:113,2,100,8|0|8,1:0|1:0|1:0,0:0:0:0: From 1551c42c122119172a67c9a0900ef8d8376284fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 27 Jun 2020 16:49:14 +0200 Subject: [PATCH 2252/2376] Avoid division when slicing node sample list --- .../Patterns/Legacy/DistanceObjectPatternGenerator.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs index 9fbdf58e21..a09ef6d5b6 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs @@ -483,9 +483,12 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy if (!(HitObject is IHasPathWithRepeats curveData)) return null; - double segmentTime = (EndTime - HitObject.StartTime) / spanCount; - - int index = (int)(segmentTime == 0 ? 0 : (time - HitObject.StartTime) / segmentTime); + // mathematically speaking this could be done by calculating (time - HitObject.StartTime) / SegmentDuration + // however, floating-point operations can introduce inaccuracies - therefore resort to iterated addition + // (all callers use this method to calculate repeat point times, so this way is consistent and deterministic) + int index = 0; + for (double nodeTime = HitObject.StartTime; nodeTime < time; nodeTime += SegmentDuration) + index += 1; // avoid slicing the list & creating copies, if at all possible. return index == 0 ? curveData.NodeSamples : curveData.NodeSamples.Skip(index).ToList(); From 082c94f98dfd7b00515846a06045e7b3949205b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 28 Jun 2020 13:14:46 +0200 Subject: [PATCH 2253/2376] Temporarily disable masking of tournament song bar --- osu.Game.Tournament/Components/SongBar.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tournament/Components/SongBar.cs b/osu.Game.Tournament/Components/SongBar.cs index fc7fcef892..cafec0a88b 100644 --- a/osu.Game.Tournament/Components/SongBar.cs +++ b/osu.Game.Tournament/Components/SongBar.cs @@ -6,6 +6,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Game.Beatmaps; @@ -66,6 +67,9 @@ namespace osu.Game.Tournament.Components } } + // Todo: This is a hack for https://github.com/ppy/osu-framework/issues/3617 since this container is at the very edge of the screen and potentially initially masked away. + protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false; + [BackgroundDependencyLoader] private void load() { @@ -77,8 +81,6 @@ namespace osu.Game.Tournament.Components flow = new FillFlowContainer { RelativeSizeAxes = Axes.X, - // Todo: This is a hack for https://github.com/ppy/osu-framework/issues/3617 since this container is at the very edge of the screen and potentially initially masked away. - Height = 1, AutoSizeAxes = Axes.Y, LayoutDuration = 500, LayoutEasing = Easing.OutQuint, From 006adf0fb50a903c67c7317b3c721ff653615506 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 28 Jun 2020 22:45:13 +0900 Subject: [PATCH 2254/2376] Change logic to ignore rooms completely after first error --- osu.Game/Screens/Multi/RoomManager.cs | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/Multi/RoomManager.cs b/osu.Game/Screens/Multi/RoomManager.cs index 5083fb2ee3..642378d8d5 100644 --- a/osu.Game/Screens/Multi/RoomManager.cs +++ b/osu.Game/Screens/Multi/RoomManager.cs @@ -143,7 +143,7 @@ namespace osu.Game.Screens.Multi joinedRoom = null; } - private readonly List roomsFailedUpdate = new List(); + private readonly HashSet ignoredRooms = new HashSet(); /// /// Invoked when the listing of all s is received from the server. @@ -166,25 +166,26 @@ namespace osu.Game.Screens.Multi continue; } - var r = listing[i]; - r.Position.Value = i; + var room = listing[i]; + + Debug.Assert(room.RoomID.Value != null); + + if (ignoredRooms.Contains(room.RoomID.Value.Value)) + continue; + + room.Position.Value = i; try { - update(r, r); - addRoom(r); + update(room, room); + addRoom(room); } catch (Exception ex) { - Debug.Assert(r.RoomID.Value != null); + Logger.Error(ex, $"Failed to update room: {room.Name.Value}."); - if (!roomsFailedUpdate.Contains(r.RoomID.Value.Value)) - { - Logger.Error(ex, $"Failed to update room: {r.Name.Value}."); - roomsFailedUpdate.Add(r.RoomID.Value.Value); - } - - rooms.Remove(r); + ignoredRooms.Add(room.RoomID.Value.Value); + rooms.Remove(room); } } From 678767918e29dee961730f5f8f18a2a0e6e98c5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 28 Jun 2020 23:32:04 +0200 Subject: [PATCH 2255/2376] Centralise logic further --- osu.Game/Screens/Play/HUD/FailingLayer.cs | 30 ++++++----------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/FailingLayer.cs b/osu.Game/Screens/Play/HUD/FailingLayer.cs index b96cfd170e..84dbb35f68 100644 --- a/osu.Game/Screens/Play/HUD/FailingLayer.cs +++ b/osu.Game/Screens/Play/HUD/FailingLayer.cs @@ -36,10 +36,9 @@ namespace osu.Game.Screens.Play.HUD /// private const double low_health_threshold = 0.20f; - private readonly Bindable fadePlayfieldWhenHealthLow = new Bindable(); private readonly Container boxes; - private Bindable fadePlayfieldWhenHealthLowSetting; + private Bindable fadePlayfieldWhenHealthLow; private HealthProcessor healthProcessor; public FailingLayer() @@ -78,15 +77,15 @@ namespace osu.Game.Screens.Play.HUD { boxes.Colour = color.Red; - fadePlayfieldWhenHealthLowSetting = config.GetBindable(OsuSetting.FadePlayfieldWhenHealthLow); - fadePlayfieldWhenHealthLow.BindValueChanged(_ => updateState(), true); - ShowHealth.BindValueChanged(_ => updateState(), true); + fadePlayfieldWhenHealthLow = config.GetBindable(OsuSetting.FadePlayfieldWhenHealthLow); + fadePlayfieldWhenHealthLow.BindValueChanged(_ => updateState()); + ShowHealth.BindValueChanged(_ => updateState()); } protected override void LoadComplete() { base.LoadComplete(); - updateBindings(); + updateState(); } public override void BindHealthProcessor(HealthProcessor processor) @@ -94,26 +93,13 @@ namespace osu.Game.Screens.Play.HUD base.BindHealthProcessor(processor); healthProcessor = processor; - updateBindings(); - } - - private void updateBindings() - { - if (LoadState < LoadState.Ready) - return; - - fadePlayfieldWhenHealthLow.UnbindBindings(); - - // Don't display ever if the ruleset is not using a draining health display. - if (healthProcessor is DrainingHealthProcessor) - fadePlayfieldWhenHealthLow.BindTo(fadePlayfieldWhenHealthLowSetting); - else - fadePlayfieldWhenHealthLow.Value = false; + updateState(); } private void updateState() { - var showLayer = fadePlayfieldWhenHealthLow.Value && ShowHealth.Value; + // Don't display ever if the ruleset is not using a draining health display. + var showLayer = healthProcessor is DrainingHealthProcessor && fadePlayfieldWhenHealthLow.Value && ShowHealth.Value; this.FadeTo(showLayer ? 1 : 0, fade_time, Easing.OutQuint); } From ffbce61ca884320098351a331bcd658041fe79b2 Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 29 Jun 2020 00:39:49 +0200 Subject: [PATCH 2256/2376] Add the option to loop the intro in the main menu --- osu.Game/Configuration/OsuConfigManager.cs | 2 ++ .../Overlays/Settings/Sections/Audio/MainMenuSettings.cs | 5 +++++ osu.Game/Screens/Menu/IntroScreen.cs | 4 ++++ 3 files changed, 11 insertions(+) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 9d31bc9bba..aa9b5340f6 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -55,6 +55,7 @@ namespace osu.Game.Configuration Set(OsuSetting.VolumeInactive, 0.25, 0, 1, 0.01); Set(OsuSetting.MenuVoice, true); + Set(OsuSetting.MenuMusicLoop, true); Set(OsuSetting.MenuMusic, true); Set(OsuSetting.AudioOffset, 0, -500.0, 500.0, 1); @@ -191,6 +192,7 @@ namespace osu.Game.Configuration AudioOffset, VolumeInactive, MenuMusic, + MenuMusicLoop, MenuVoice, CursorRotation, MenuParallax, diff --git a/osu.Game/Overlays/Settings/Sections/Audio/MainMenuSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/MainMenuSettings.cs index a303f93b34..7ec123c04c 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/MainMenuSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/MainMenuSettings.cs @@ -28,6 +28,11 @@ namespace osu.Game.Overlays.Settings.Sections.Audio LabelText = "osu! music theme", Bindable = config.GetBindable(OsuSetting.MenuMusic) }, + new SettingsCheckbox + { + LabelText = "loop the music theme", + Bindable = config.GetBindable(OsuSetting.MenuMusicLoop) + }, new SettingsDropdown { LabelText = "Intro sequence", diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index 88d18d0073..57f93690a8 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -40,6 +40,7 @@ namespace osu.Game.Screens.Menu protected IBindable MenuVoice { get; private set; } protected IBindable MenuMusic { get; private set; } + private IBindable menuMusicLoop { get; set; } private WorkingBeatmap initialBeatmap; @@ -73,6 +74,7 @@ namespace osu.Game.Screens.Menu MenuVoice = config.GetBindable(OsuSetting.MenuVoice); MenuMusic = config.GetBindable(OsuSetting.MenuMusic); + menuMusicLoop = config.GetBindable(OsuSetting.MenuMusicLoop); seeya = audio.Samples.Get(SeeyaSampleName); @@ -152,6 +154,8 @@ namespace osu.Game.Screens.Menu // Only start the current track if it is the menu music. A beatmap's track is started when entering the Main Menu. if (UsingThemedIntro) Track.Restart(); + if (menuMusicLoop.Value) + Track.Looping = true; } protected override void LogoArriving(OsuLogo logo, bool resuming) From 5689f279871de69937a37342e10efcb98e5232e1 Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 29 Jun 2020 00:54:06 +0200 Subject: [PATCH 2257/2376] Make sure it only loops for themed intros if true --- osu.Game/Screens/Menu/IntroScreen.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index 57f93690a8..fa8a641203 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -152,8 +152,10 @@ namespace osu.Game.Screens.Menu protected void StartTrack() { // Only start the current track if it is the menu music. A beatmap's track is started when entering the Main Menu. - if (UsingThemedIntro) - Track.Restart(); + if (!UsingThemedIntro) + return; + + Track.Restart(); if (menuMusicLoop.Value) Track.Looping = true; } From 270384e71e1fe41226eaf4864b6955fe5abcc4b1 Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 29 Jun 2020 00:59:44 +0200 Subject: [PATCH 2258/2376] Remove redundant get set --- osu.Game/Screens/Menu/IntroScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index fa8a641203..8ef7ebe5e6 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -40,7 +40,7 @@ namespace osu.Game.Screens.Menu protected IBindable MenuVoice { get; private set; } protected IBindable MenuMusic { get; private set; } - private IBindable menuMusicLoop { get; set; } + private IBindable menuMusicLoop; private WorkingBeatmap initialBeatmap; From 24dceb9f84e49bf778ad575076f917065e6d5f67 Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 29 Jun 2020 01:41:47 +0200 Subject: [PATCH 2259/2376] Make only "Welcome" loop --- osu.Game/Configuration/OsuConfigManager.cs | 2 -- .../Settings/Sections/Audio/MainMenuSettings.cs | 5 ----- osu.Game/Screens/Menu/IntroScreen.cs | 11 ++--------- osu.Game/Screens/Menu/IntroWelcome.cs | 2 ++ 4 files changed, 4 insertions(+), 16 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index aa9b5340f6..9d31bc9bba 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -55,7 +55,6 @@ namespace osu.Game.Configuration Set(OsuSetting.VolumeInactive, 0.25, 0, 1, 0.01); Set(OsuSetting.MenuVoice, true); - Set(OsuSetting.MenuMusicLoop, true); Set(OsuSetting.MenuMusic, true); Set(OsuSetting.AudioOffset, 0, -500.0, 500.0, 1); @@ -192,7 +191,6 @@ namespace osu.Game.Configuration AudioOffset, VolumeInactive, MenuMusic, - MenuMusicLoop, MenuVoice, CursorRotation, MenuParallax, diff --git a/osu.Game/Overlays/Settings/Sections/Audio/MainMenuSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/MainMenuSettings.cs index 7ec123c04c..a303f93b34 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/MainMenuSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/MainMenuSettings.cs @@ -28,11 +28,6 @@ namespace osu.Game.Overlays.Settings.Sections.Audio LabelText = "osu! music theme", Bindable = config.GetBindable(OsuSetting.MenuMusic) }, - new SettingsCheckbox - { - LabelText = "loop the music theme", - Bindable = config.GetBindable(OsuSetting.MenuMusicLoop) - }, new SettingsDropdown { LabelText = "Intro sequence", diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index 8ef7ebe5e6..5f91aaad15 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -40,7 +40,6 @@ namespace osu.Game.Screens.Menu protected IBindable MenuVoice { get; private set; } protected IBindable MenuMusic { get; private set; } - private IBindable menuMusicLoop; private WorkingBeatmap initialBeatmap; @@ -74,8 +73,6 @@ namespace osu.Game.Screens.Menu MenuVoice = config.GetBindable(OsuSetting.MenuVoice); MenuMusic = config.GetBindable(OsuSetting.MenuMusic); - menuMusicLoop = config.GetBindable(OsuSetting.MenuMusicLoop); - seeya = audio.Samples.Get(SeeyaSampleName); BeatmapSetInfo setInfo = null; @@ -152,12 +149,8 @@ namespace osu.Game.Screens.Menu protected void StartTrack() { // Only start the current track if it is the menu music. A beatmap's track is started when entering the Main Menu. - if (!UsingThemedIntro) - return; - - Track.Restart(); - if (menuMusicLoop.Value) - Track.Looping = true; + if (UsingThemedIntro) + Track.Restart(); } protected override void LogoArriving(OsuLogo logo, bool resuming) diff --git a/osu.Game/Screens/Menu/IntroWelcome.cs b/osu.Game/Screens/Menu/IntroWelcome.cs index abd4a68d4f..bf42e36e8c 100644 --- a/osu.Game/Screens/Menu/IntroWelcome.cs +++ b/osu.Game/Screens/Menu/IntroWelcome.cs @@ -39,6 +39,8 @@ namespace osu.Game.Screens.Menu welcome = audio.Samples.Get(@"Intro/Welcome/welcome"); pianoReverb = audio.Samples.Get(@"Intro/Welcome/welcome_piano"); + + Track.Looping = true; } protected override void LogoArriving(OsuLogo logo, bool resuming) From 444504f2b9c7765d6219b643e1b10464b8810240 Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 29 Jun 2020 02:10:40 +0200 Subject: [PATCH 2260/2376] Expose MainMenu Track as internal get private set --- osu.Game/Screens/Menu/MainMenu.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index f0da2482d6..9245df2a7d 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -6,6 +6,7 @@ using System.Linq; using osuTK; using osuTK.Graphics; using osu.Framework.Allocation; +using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; @@ -63,6 +64,8 @@ namespace osu.Game.Screens.Menu protected override BackgroundScreen CreateBackground() => background; + internal Track Track { get; private set; } + private Bindable holdDelay; private Bindable loginDisplayed; @@ -173,15 +176,15 @@ namespace osu.Game.Screens.Menu base.OnEntering(last); buttons.FadeInFromZero(500); - var track = Beatmap.Value.Track; + Track = Beatmap.Value.Track; var metadata = Beatmap.Value.Metadata; - if (last is IntroScreen && track != null) + if (last is IntroScreen && Track != null) { - if (!track.IsRunning) + if (!Track.IsRunning) { - track.Seek(metadata.PreviewTime != -1 ? metadata.PreviewTime : 0.4f * track.Length); - track.Start(); + Track.Seek(metadata.PreviewTime != -1 ? metadata.PreviewTime : 0.4f * Track.Length); + Track.Start(); } } } From 0c4b06b48562fc15ef0ccdec53932ffaefb9d109 Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 29 Jun 2020 02:16:19 +0200 Subject: [PATCH 2261/2376] Add visualtest to check if Track loops in Welcome --- osu.Game.Tests/Visual/Menus/IntroTestScene.cs | 12 ++++++------ .../Visual/Menus/TestSceneIntroWelcome.cs | 13 +++++++++++++ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/IntroTestScene.cs b/osu.Game.Tests/Visual/Menus/IntroTestScene.cs index 2d2f1a1618..f71d13ed35 100644 --- a/osu.Game.Tests/Visual/Menus/IntroTestScene.cs +++ b/osu.Game.Tests/Visual/Menus/IntroTestScene.cs @@ -19,10 +19,10 @@ namespace osu.Game.Tests.Visual.Menus [Cached] private OsuLogo logo; + protected OsuScreenStack IntroStack; + protected IntroTestScene() { - OsuScreenStack introStack = null; - Children = new Drawable[] { new Box @@ -45,17 +45,17 @@ namespace osu.Game.Tests.Visual.Menus logo.FinishTransforms(); logo.IsTracking = false; - introStack?.Expire(); + IntroStack?.Expire(); - Add(introStack = new OsuScreenStack + Add(IntroStack = new OsuScreenStack { RelativeSizeAxes = Axes.Both, }); - introStack.Push(CreateScreen()); + IntroStack.Push(CreateScreen()); }); - AddUntilStep("wait for menu", () => introStack.CurrentScreen is MainMenu); + AddUntilStep("wait for menu", () => IntroStack.CurrentScreen is MainMenu); } protected abstract IScreen CreateScreen(); diff --git a/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs b/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs index 905f17ef0b..1347bae2ad 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs @@ -11,5 +11,18 @@ namespace osu.Game.Tests.Visual.Menus public class TestSceneIntroWelcome : IntroTestScene { protected override IScreen CreateScreen() => new IntroWelcome(); + + public TestSceneIntroWelcome() + { + AddAssert("check if menu music loops", () => + { + var menu = IntroStack?.CurrentScreen as MainMenu; + + if (menu == null) + return false; + + return menu.Track.Looping; + }); + } } } From af7494b2325e31a4a3fef36fc263b19a9b3e9dfb Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 29 Jun 2020 13:58:35 +0900 Subject: [PATCH 2262/2376] Improve quality of song select beatmap wedge --- osu.Game/Screens/Select/BeatmapInfoWedge.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index 7a8a1593b9..27ce9e82dd 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -155,7 +155,6 @@ namespace osu.Game.Screens.Select var metadata = beatmapInfo.Metadata ?? beatmap.BeatmapSetInfo?.Metadata ?? new BeatmapMetadata(); CacheDrawnFrameBuffer = true; - RedrawOnScale = false; RelativeSizeAxes = Axes.Both; From 5db103dc613d238413b56ea3b2d31312b68e67cc Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 29 Jun 2020 14:38:50 +0900 Subject: [PATCH 2263/2376] Improve quality of taiko hit target --- osu.Game.Rulesets.Taiko/UI/TaikoHitTarget.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoHitTarget.cs b/osu.Game.Rulesets.Taiko/UI/TaikoHitTarget.cs index 7de1593ab6..caddc8b122 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoHitTarget.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoHitTarget.cs @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Taiko.UI Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - Scale = new Vector2(TaikoHitObject.DEFAULT_STRONG_SIZE), + Size = new Vector2(TaikoHitObject.DEFAULT_STRONG_SIZE), Masking = true, BorderColour = Color4.White, BorderThickness = border_thickness, @@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Taiko.UI Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - Scale = new Vector2(TaikoHitObject.DEFAULT_SIZE), + Size = new Vector2(TaikoHitObject.DEFAULT_SIZE), Masking = true, BorderColour = Color4.White, BorderThickness = border_thickness, From bb81f908fb163f0d77d2d2fb74c54be563cbb859 Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Mon, 29 Jun 2020 15:44:10 +0800 Subject: [PATCH 2264/2376] Exclude EmptyHitWindow from being considered in TimingDistributionGraph --- .../Ranking/Statistics/HitEventTimingDistributionGraph.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs index 8ec7e863b1..527da429ed 100644 --- a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs +++ b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs @@ -48,7 +48,7 @@ namespace osu.Game.Screens.Ranking.Statistics /// The s to display the timing distribution of. public HitEventTimingDistributionGraph(IReadOnlyList hitEvents) { - this.hitEvents = hitEvents; + this.hitEvents = hitEvents.Where(e => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows)).ToList(); } [BackgroundDependencyLoader] From 51f5083c2d71a87eb8fcda3f6aa1b1a748f86b47 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2020 17:17:52 +0000 Subject: [PATCH 2265/2376] Bump Sentry from 2.1.3 to 2.1.4 Bumps [Sentry](https://github.com/getsentry/sentry-dotnet) from 2.1.3 to 2.1.4. - [Release notes](https://github.com/getsentry/sentry-dotnet/releases) - [Commits](https://github.com/getsentry/sentry-dotnet/compare/2.1.3...2.1.4) Signed-off-by: dependabot-preview[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 26d81a1004..5f326a361d 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -26,7 +26,7 @@ - + From 1701c844a6a34f035e09c20b3c7a62950290be9e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 30 Jun 2020 16:36:53 +0900 Subject: [PATCH 2266/2376] Fix scroll container height on smaller ui scales --- osu.Game/Screens/Ranking/ResultsScreen.cs | 68 +++++++++++++---------- 1 file changed, 39 insertions(+), 29 deletions(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 193d975e42..968b446df9 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -70,41 +70,33 @@ namespace osu.Game.Screens.Ranking { new Drawable[] { - new Container + new VerticalScrollContainer { RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + ScrollbarVisible = false, + Child = new Container { - new OsuScrollContainer + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - ScrollbarVisible = false, - Child = new Container + scorePanelList = new ScorePanelList { - RelativeSizeAxes = Axes.X, - Height = screen_height, - Children = new Drawable[] - { - scorePanelList = new ScorePanelList - { - RelativeSizeAxes = Axes.Both, - SelectedScore = { BindTarget = SelectedScore }, - PostExpandAction = () => statisticsPanel.ToggleVisibility() - }, - detachedPanelContainer = new Container - { - RelativeSizeAxes = Axes.Both - }, - statisticsPanel = new StatisticsPanel - { - RelativeSizeAxes = Axes.Both, - Score = { BindTarget = SelectedScore } - }, - } - } - }, + RelativeSizeAxes = Axes.Both, + SelectedScore = { BindTarget = SelectedScore }, + PostExpandAction = () => statisticsPanel.ToggleVisibility() + }, + detachedPanelContainer = new Container + { + RelativeSizeAxes = Axes.Both + }, + statisticsPanel = new StatisticsPanel + { + RelativeSizeAxes = Axes.Both, + Score = { BindTarget = SelectedScore } + }, + } } - } + }, }, new[] { @@ -277,5 +269,23 @@ namespace osu.Game.Screens.Ranking detachedPanel = null; } } + + private class VerticalScrollContainer : OsuScrollContainer + { + protected override Container Content => content; + + private readonly Container content; + + public VerticalScrollContainer() + { + base.Content.Add(content = new Container { RelativeSizeAxes = Axes.X }); + } + + protected override void Update() + { + base.Update(); + content.Height = Math.Max(screen_height, DrawHeight); + } + } } } From 85c42456f25c9a27cd4871fb2cc71e1e3caf1b17 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 30 Jun 2020 21:38:51 +0900 Subject: [PATCH 2267/2376] Improve performance of sequential scrolling algorithm --- .../Algorithms/SequentialScrollAlgorithm.cs | 164 +++++++++++------- 1 file changed, 104 insertions(+), 60 deletions(-) diff --git a/osu.Game/Rulesets/UI/Scrolling/Algorithms/SequentialScrollAlgorithm.cs b/osu.Game/Rulesets/UI/Scrolling/Algorithms/SequentialScrollAlgorithm.cs index 0052c877f6..a1f68d7201 100644 --- a/osu.Game/Rulesets/UI/Scrolling/Algorithms/SequentialScrollAlgorithm.cs +++ b/osu.Game/Rulesets/UI/Scrolling/Algorithms/SequentialScrollAlgorithm.cs @@ -3,21 +3,26 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using JetBrains.Annotations; using osu.Game.Rulesets.Timing; namespace osu.Game.Rulesets.UI.Scrolling.Algorithms { public class SequentialScrollAlgorithm : IScrollAlgorithm { - private readonly Dictionary positionCache; + private static readonly IComparer by_position_comparer = Comparer.Create((c1, c2) => c1.Position.CompareTo(c2.Position)); private readonly IReadOnlyList controlPoints; + /// + /// Stores a mapping of time -> position for each control point. + /// + private readonly List positionMappings = new List(); + public SequentialScrollAlgorithm(IReadOnlyList controlPoints) { this.controlPoints = controlPoints; - - positionCache = new Dictionary(); } public double GetDisplayStartTime(double originTime, float offset, double timeRange, float scrollLength) @@ -27,55 +32,31 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms public float GetLength(double startTime, double endTime, double timeRange, float scrollLength) { - var objectLength = relativePositionAtCached(endTime, timeRange) - relativePositionAtCached(startTime, timeRange); + var objectLength = relativePositionAt(endTime, timeRange) - relativePositionAt(startTime, timeRange); return (float)(objectLength * scrollLength); } public float PositionAt(double time, double currentTime, double timeRange, float scrollLength) { - // Caching is not used here as currentTime is unlikely to have been previously cached - double timelinePosition = relativePositionAt(currentTime, timeRange); - return (float)((relativePositionAtCached(time, timeRange) - timelinePosition) * scrollLength); + double timelineLength = relativePositionAt(time, timeRange) - relativePositionAt(currentTime, timeRange); + return (float)(timelineLength * scrollLength); } public double TimeAt(float position, double currentTime, double timeRange, float scrollLength) { - // Convert the position to a length relative to time = 0 - double length = position / scrollLength + relativePositionAt(currentTime, timeRange); + if (controlPoints.Count == 0) + return position * timeRange; - // We need to consider all timing points until the specified time and not just the currently-active one, - // since each timing point individually affects the positions of _all_ hitobjects after its start time - for (int i = 0; i < controlPoints.Count; i++) - { - var current = controlPoints[i]; - var next = i < controlPoints.Count - 1 ? controlPoints[i + 1] : null; + // Find the position at the current time, and the given length. + double relativePosition = relativePositionAt(currentTime, timeRange) + position / scrollLength; - // Duration of the current control point - var currentDuration = (next?.StartTime ?? double.PositiveInfinity) - current.StartTime; + var positionMapping = findControlPointMapping(timeRange, new PositionMapping(0, null, relativePosition), by_position_comparer); - // Figure out the length of control point - var currentLength = currentDuration / timeRange * current.Multiplier; - - if (currentLength > length) - { - // The point is within this control point - return current.StartTime + length * timeRange / current.Multiplier; - } - - length -= currentLength; - } - - return 0; // Should never occur + // Begin at the control point's time and add the remaining time to reach the given position. + return positionMapping.Time + (relativePosition - positionMapping.Position) * timeRange / positionMapping.ControlPoint.Multiplier; } - private double relativePositionAtCached(double time, double timeRange) - { - if (!positionCache.TryGetValue(time, out double existing)) - positionCache[time] = existing = relativePositionAt(time, timeRange); - return existing; - } - - public void Reset() => positionCache.Clear(); + public void Reset() => positionMappings.Clear(); /// /// Finds the position which corresponds to a point in time. @@ -84,37 +65,100 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms /// The time to find the position at. /// The amount of time visualised by the scrolling area. /// A positive value indicating the position at . - private double relativePositionAt(double time, double timeRange) + private double relativePositionAt(in double time, in double timeRange) { if (controlPoints.Count == 0) return time / timeRange; - double length = 0; + var mapping = findControlPointMapping(timeRange, new PositionMapping(time)); - // We need to consider all timing points until the specified time and not just the currently-active one, - // since each timing point individually affects the positions of _all_ hitobjects after its start time - for (int i = 0; i < controlPoints.Count; i++) + // Begin at the control point's position and add the remaining distance to reach the given time. + return mapping.Position + (time - mapping.Time) / timeRange * mapping.ControlPoint.Multiplier; + } + + /// + /// Finds a 's that is relevant to a given . + /// + /// + /// This is used to find the last occuring prior to a time value, or prior to a position value (if is used). + /// + /// The time range. + /// The to find the closest to. + /// The comparison. If null, the default comparer is used (by time). + /// The 's that is relevant for . + private PositionMapping findControlPointMapping(in double timeRange, in PositionMapping search, IComparer comparer = null) + { + generatePositionMappings(timeRange); + + var mappingIndex = positionMappings.BinarySearch(search, comparer ?? Comparer.Default); + + if (mappingIndex < 0) { - var current = controlPoints[i]; - var next = i < controlPoints.Count - 1 ? controlPoints[i + 1] : null; + // If the search value isn't found, the _next_ control point is returned, but we actually want the _previous_ control point. + // In doing so, we must make sure to not underflow the position mapping list (i.e. always use the 0th control point for time < first_control_point_time). + mappingIndex = Math.Max(0, ~mappingIndex - 1); - // We don't need to consider any control points beyond the current time, since it will not yet - // affect any hitobjects - if (i > 0 && current.StartTime > time) - continue; - - // Duration of the current control point - var currentDuration = (next?.StartTime ?? double.PositiveInfinity) - current.StartTime; - - // We want to consider the minimal amount of time that this control point has affected, - // which may be either its duration, or the amount of time that has passed within it - var durationInCurrent = Math.Min(currentDuration, time - current.StartTime); - - // Figure out how much of the time range the duration represents, and adjust it by the speed multiplier - length += durationInCurrent / timeRange * current.Multiplier; + Debug.Assert(mappingIndex < positionMappings.Count); } - return length; + var mapping = positionMappings[mappingIndex]; + Debug.Assert(mapping.ControlPoint != null); + + return mapping; + } + + /// + /// Generates the mapping of (and their respective start times) to their relative position from 0. + /// + /// The time range. + private void generatePositionMappings(in double timeRange) + { + if (positionMappings.Count > 0) + return; + + if (controlPoints.Count == 0) + return; + + positionMappings.Add(new PositionMapping(controlPoints[0].StartTime, controlPoints[0])); + + for (int i = 0; i < controlPoints.Count - 1; i++) + { + var current = controlPoints[i]; + var next = controlPoints[i + 1]; + + // Figure out how much of the time range the duration represents, and adjust it by the speed multiplier + float length = (float)((next.StartTime - current.StartTime) / timeRange * current.Multiplier); + + positionMappings.Add(new PositionMapping(next.StartTime, next, positionMappings[^1].Position + length)); + } + } + + private readonly struct PositionMapping : IComparable + { + /// + /// The time corresponding to this position. + /// + public readonly double Time; + + /// + /// The at . + /// + [CanBeNull] + public readonly MultiplierControlPoint ControlPoint; + + /// + /// The relative position from 0 of . + /// + public readonly double Position; + + public PositionMapping(double time, MultiplierControlPoint controlPoint = null, double position = default) + { + Time = time; + ControlPoint = controlPoint; + Position = position; + } + + public int CompareTo(PositionMapping other) => Time.CompareTo(other.Time); } } } From 508d34fd3ac6d48dae4e5aa578e0c112f609cb3a Mon Sep 17 00:00:00 2001 From: Lucas A Date: Tue, 30 Jun 2020 19:51:10 +0200 Subject: [PATCH 2268/2376] Fix notification redirecting to the old log folder when game installation has been migrated to another location. --- osu.Game/OsuGame.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index b0d7b14d34..92233f143d 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -767,7 +767,7 @@ namespace osu.Game Text = "Subsequent messages have been logged. Click to view log files.", Activated = () => { - Host.Storage.GetStorageForDirectory("logs").OpenInNativeExplorer(); + Storage.GetStorageForDirectory("logs").OpenInNativeExplorer(); return true; } })); From 39cfbb67ad7962f1b75beb72fd793e445de66512 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 30 Jun 2020 20:16:19 +0200 Subject: [PATCH 2269/2376] Replace iterated addition with rounding --- .../Patterns/Legacy/DistanceObjectPatternGenerator.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs index a09ef6d5b6..d03eb0b3c9 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs @@ -483,12 +483,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy if (!(HitObject is IHasPathWithRepeats curveData)) return null; - // mathematically speaking this could be done by calculating (time - HitObject.StartTime) / SegmentDuration - // however, floating-point operations can introduce inaccuracies - therefore resort to iterated addition - // (all callers use this method to calculate repeat point times, so this way is consistent and deterministic) - int index = 0; - for (double nodeTime = HitObject.StartTime; nodeTime < time; nodeTime += SegmentDuration) - index += 1; + // mathematically speaking this should be a whole number always, but floating-point arithmetic is not so kind + var index = (int)Math.Round(SegmentDuration == 0 ? 0 : (time - HitObject.StartTime) / SegmentDuration, MidpointRounding.AwayFromZero); // avoid slicing the list & creating copies, if at all possible. return index == 0 ? curveData.NodeSamples : curveData.NodeSamples.Skip(index).ToList(); From ab15b6031d662fb660149f0c9085be99f5c33b59 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 1 Jul 2020 17:12:07 +0900 Subject: [PATCH 2270/2376] Update with framework-side storage changes --- osu.Game/IO/OsuStorage.cs | 6 +++--- osu.Game/OsuGameBase.cs | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index 499bcb4063..f5ce1c0105 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.cs @@ -24,12 +24,12 @@ namespace osu.Game.IO "storage.ini" }; - public OsuStorage(GameHost host) - : base(host.Storage, string.Empty) + public OsuStorage(GameHost host, Storage defaultStorage) + : base(defaultStorage, string.Empty) { this.host = host; - storageConfig = new StorageConfigManager(host.Storage); + storageConfig = new StorageConfigManager(defaultStorage); var customStoragePath = storageConfig.Get(StorageConfig.FullPath); diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 3e7311092e..c79f710151 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -312,11 +312,13 @@ namespace osu.Game base.SetHost(host); // may be non-null for certain tests - Storage ??= new OsuStorage(host); + Storage ??= host.Storage; LocalConfig ??= new OsuConfigManager(Storage); } + protected override Storage CreateStorage(GameHost host, Storage defaultStorage) => new OsuStorage(host, defaultStorage); + private readonly List fileImporters = new List(); public async Task Import(params string[] paths) From cdcad94e9f0a8ce75c6be8572408795aaa6bde16 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 1 Jul 2020 17:47:29 +0900 Subject: [PATCH 2271/2376] Handle exception thrown due to custom stoage on startup --- osu.Game/IO/OsuStorage.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index f5ce1c0105..8bcc0941c1 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.cs @@ -34,7 +34,17 @@ namespace osu.Game.IO var customStoragePath = storageConfig.Get(StorageConfig.FullPath); if (!string.IsNullOrEmpty(customStoragePath)) - ChangeTargetStorage(host.GetStorage(customStoragePath)); + { + try + { + ChangeTargetStorage(host.GetStorage(customStoragePath)); + } + catch (Exception ex) + { + Logger.Log($"Couldn't use custom storage path ({customStoragePath}): {ex}. Using default path.", LoggingTarget.Runtime, LogLevel.Error); + ChangeTargetStorage(defaultStorage); + } + } } protected override void ChangeTargetStorage(Storage newStorage) From 5f577797a7dd491d8ccecd49b28967ca826eb038 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 1 Jul 2020 18:41:00 +0900 Subject: [PATCH 2272/2376] Expose transform helpers in SkinnableSound --- osu.Game/Skinning/SkinnableSound.cs | 30 +++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index 30320c89a6..24d6648273 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -7,8 +7,10 @@ using osu.Framework.Allocation; using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Graphics; using osu.Framework.Graphics.Audio; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Transforms; using osu.Game.Audio; namespace osu.Game.Skinning @@ -43,6 +45,34 @@ namespace osu.Game.Skinning public BindableNumber Tempo => samplesContainer.Tempo; + /// + /// Smoothly adjusts over time. + /// + /// A to which further transforms can be added. + public TransformSequence VolumeTo(double newVolume, double duration = 0, Easing easing = Easing.None) => + samplesContainer.VolumeTo(newVolume, duration, easing); + + /// + /// Smoothly adjusts over time. + /// + /// A to which further transforms can be added. + public TransformSequence BalanceTo(double newBalance, double duration = 0, Easing easing = Easing.None) => + samplesContainer.BalanceTo(newBalance, duration, easing); + + /// + /// Smoothly adjusts over time. + /// + /// A to which further transforms can be added. + public TransformSequence FrequencyTo(double newFrequency, double duration = 0, Easing easing = Easing.None) => + samplesContainer.FrequencyTo(newFrequency, duration, easing); + + /// + /// Smoothly adjusts over time. + /// + /// A to which further transforms can be added. + public TransformSequence TempoTo(double newTempo, double duration = 0, Easing easing = Easing.None) => + samplesContainer.TempoTo(newTempo, duration, easing); + public bool Looping { get => looping; From 6f6376d53c56e5f592a6ae253349b4cc1923f5e8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 1 Jul 2020 18:52:05 +0900 Subject: [PATCH 2273/2376] Update framework --- .idea/.idea.osu.Desktop/.idea/modules.xml | 1 - osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.idea/.idea.osu.Desktop/.idea/modules.xml b/.idea/.idea.osu.Desktop/.idea/modules.xml index 366f172c30..fe63f5faf3 100644 --- a/.idea/.idea.osu.Desktop/.idea/modules.xml +++ b/.idea/.idea.osu.Desktop/.idea/modules.xml @@ -2,7 +2,6 @@ - diff --git a/osu.Android.props b/osu.Android.props index 493b1f5529..a2c97ead2f 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 5f326a361d..3ef53a2a53 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -24,7 +24,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 72f09ee287..492bf89fab 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From 49aa839872b5291e2df9011c410f8d72edf3823b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 1 Jul 2020 18:54:11 +0900 Subject: [PATCH 2274/2376] Update RulesetInputManager to use new method --- osu.Game/Rulesets/UI/RulesetInputManager.cs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index ba30fe28d5..f2ac61eaf4 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -18,9 +18,6 @@ using osu.Game.Input.Handlers; using osu.Game.Screens.Play; using osuTK.Input; using static osu.Game.Input.Handlers.ReplayInputHandler; -using JoystickState = osu.Framework.Input.States.JoystickState; -using KeyboardState = osu.Framework.Input.States.KeyboardState; -using MouseState = osu.Framework.Input.States.MouseState; namespace osu.Game.Rulesets.UI { @@ -42,11 +39,7 @@ namespace osu.Game.Rulesets.UI } } - protected override InputState CreateInitialState() - { - var state = base.CreateInitialState(); - return new RulesetInputManagerInputState(state.Mouse, state.Keyboard, state.Joystick); - } + protected override InputState CreateInitialState() => new RulesetInputManagerInputState(base.CreateInitialState()); protected readonly KeyBindingContainer KeyBindingContainer; @@ -203,8 +196,8 @@ namespace osu.Game.Rulesets.UI { public ReplayState LastReplayState; - public RulesetInputManagerInputState(MouseState mouse = null, KeyboardState keyboard = null, JoystickState joystick = null) - : base(mouse, keyboard, joystick) + public RulesetInputManagerInputState(InputState state = null) + : base(state) { } } From 4e839e4f1fb595740caa29f901f7072fc2858f23 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 1 Jul 2020 19:02:05 +0900 Subject: [PATCH 2275/2376] Fix "welcome" intro test failure due to no wait logic --- .../Visual/Menus/TestSceneIntroWelcome.cs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs b/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs index 1347bae2ad..8f20e38494 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; +using osu.Framework.Audio.Track; using osu.Framework.Screens; using osu.Game.Screens.Menu; @@ -14,15 +15,11 @@ namespace osu.Game.Tests.Visual.Menus public TestSceneIntroWelcome() { - AddAssert("check if menu music loops", () => - { - var menu = IntroStack?.CurrentScreen as MainMenu; + AddUntilStep("wait for load", () => getTrack() != null); - if (menu == null) - return false; - - return menu.Track.Looping; - }); + AddAssert("check if menu music loops", () => getTrack().Looping); } + + private Track getTrack() => (IntroStack?.CurrentScreen as MainMenu)?.Track; } } From 1edfac4923623a1d78b1379c4f2c7e8e4177a01b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 1 Jul 2020 23:21:08 +0900 Subject: [PATCH 2276/2376] Fix test failing --- osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs index f3d54d876a..8ea0e34214 100644 --- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs @@ -127,6 +127,9 @@ namespace osu.Game.Tests.NonVisual var osu = loadOsu(host); var storage = osu.Dependencies.Get(); + // Store the current storage's path. We'll need to refer to this for assertions in the original directory after the migration completes. + string originalDirectory = storage.GetFullPath("."); + // ensure we perform a save host.Dependencies.Get().Save(); @@ -145,25 +148,25 @@ namespace osu.Game.Tests.NonVisual Assert.That(storage.GetFullPath("."), Is.EqualTo(customPath)); // ensure cache was not moved - Assert.That(host.Storage.ExistsDirectory("cache")); + Assert.That(Directory.Exists(Path.Combine(originalDirectory, "cache"))); // ensure nested cache was moved - Assert.That(!host.Storage.ExistsDirectory(Path.Combine("test-nested", "cache"))); + Assert.That(!Directory.Exists(Path.Combine(originalDirectory, "test-nested", "cache"))); Assert.That(storage.ExistsDirectory(Path.Combine("test-nested", "cache"))); foreach (var file in OsuStorage.IGNORE_FILES) { - Assert.That(host.Storage.Exists(file), Is.True); + Assert.That(File.Exists(Path.Combine(originalDirectory, file))); Assert.That(storage.Exists(file), Is.False); } foreach (var dir in OsuStorage.IGNORE_DIRECTORIES) { - Assert.That(host.Storage.ExistsDirectory(dir), Is.True); + Assert.That(Directory.Exists(Path.Combine(originalDirectory, dir))); Assert.That(storage.ExistsDirectory(dir), Is.False); } - Assert.That(new StreamReader(host.Storage.GetStream("storage.ini")).ReadToEnd().Contains($"FullPath = {customPath}")); + Assert.That(new StreamReader(Path.Combine(originalDirectory, "storage.ini")).ReadToEnd().Contains($"FullPath = {customPath}")); } finally { From 3278a1d7d821e4fd5cfe9d4bde6125ef9a77a09c Mon Sep 17 00:00:00 2001 From: ekrctb Date: Thu, 2 Jul 2020 00:21:45 +0900 Subject: [PATCH 2277/2376] Standardize osu!catch coordinate system There were two coordinate systems used: - 0..512 (used in osu!stable) - 0..1 (relative coordinate) This commit replaces the usage of the relative coordinate system to the coordinate system of 0..512. --- .../CatchBeatmapConversionTest.cs | 3 +-- .../TestSceneAutoJuiceStream.cs | 6 +++--- .../TestSceneCatchStacker.cs | 10 +++++++++- .../TestSceneCatcherArea.cs | 4 ++-- .../TestSceneDrawableHitObjects.cs | 4 ++-- .../TestSceneHyperDash.cs | 8 ++++---- .../TestSceneJuiceStream.cs | 5 +++-- .../Beatmaps/CatchBeatmapConverter.cs | 5 ++--- .../Beatmaps/CatchBeatmapProcessor.cs | 14 +++++++------- .../Preprocessing/CatchDifficultyHitObject.cs | 5 ++--- .../Difficulty/Skills/Movement.cs | 5 ++--- osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs | 4 ++++ .../Objects/Drawables/DrawableCatchHitObject.cs | 4 ++-- osu.Game.Rulesets.Catch/Objects/JuiceStream.cs | 9 ++++----- .../Replays/CatchAutoGenerator.cs | 4 ++-- .../Replays/CatchReplayFrame.cs | 5 ++--- osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs | 11 ++++++++++- .../UI/CatchPlayfieldAdjustmentContainer.cs | 2 +- osu.Game.Rulesets.Catch/UI/Catcher.cs | 13 +++++-------- osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 10 ++-------- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 2 +- 21 files changed, 70 insertions(+), 63 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs index f4749be370..df54df7b01 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs @@ -8,7 +8,6 @@ using NUnit.Framework; using osu.Framework.Utils; using osu.Game.Rulesets.Catch.Mods; using osu.Game.Rulesets.Catch.Objects; -using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Objects; using osu.Game.Tests.Beatmaps; @@ -83,7 +82,7 @@ namespace osu.Game.Rulesets.Catch.Tests public float Position { - get => HitObject?.X * CatchPlayfield.BASE_WIDTH ?? position; + get => HitObject?.X ?? position; set => position = value; } diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs index 7c2304694f..d6bba3d55e 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs @@ -27,15 +27,15 @@ namespace osu.Game.Rulesets.Catch.Tests for (int i = 0; i < 100; i++) { - float width = (i % 10 + 1) / 20f; + float width = (i % 10 + 1) / 20f * CatchPlayfield.WIDTH; beatmap.HitObjects.Add(new JuiceStream { - X = 0.5f - width / 2, + X = CatchPlayfield.CENTER_X - width / 2, Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, - new Vector2(width * CatchPlayfield.BASE_WIDTH, 0) + new Vector2(width, 0) }), StartTime = i * 2000, NewCombo = i % 8 == 0 diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchStacker.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchStacker.cs index 44672b6526..1ff31697b8 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchStacker.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchStacker.cs @@ -4,6 +4,7 @@ using NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Catch.UI; namespace osu.Game.Rulesets.Catch.Tests { @@ -22,7 +23,14 @@ namespace osu.Game.Rulesets.Catch.Tests }; for (int i = 0; i < 512; i++) - beatmap.HitObjects.Add(new Fruit { X = 0.5f + i / 2048f * (i % 10 - 5), StartTime = i * 100, NewCombo = i % 8 == 0 }); + { + beatmap.HitObjects.Add(new Fruit + { + X = (0.5f + i / 2048f * (i % 10 - 5)) * CatchPlayfield.WIDTH, + StartTime = i * 100, + NewCombo = i % 8 == 0 + }); + } return beatmap; } diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs index 2b30edb70b..fbb22a8498 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs @@ -76,8 +76,8 @@ namespace osu.Game.Rulesets.Catch.Tests RelativeSizeAxes = Axes.Both, Child = new TestCatcherArea(new BeatmapDifficulty { CircleSize = size }) { - Anchor = Anchor.CentreLeft, - Origin = Anchor.TopLeft, + Anchor = Anchor.Centre, + Origin = Anchor.TopCentre, CreateDrawableRepresentation = ((DrawableRuleset)catchRuleset.CreateInstance().CreateDrawableRulesetWith(new CatchBeatmap())).CreateDrawableRepresentation }, }); diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs index a7094c00be..d35f828e28 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs @@ -158,8 +158,8 @@ namespace osu.Game.Rulesets.Catch.Tests private float getXCoords(bool hit) { - const float x_offset = 0.2f; - float xCoords = drawableRuleset.Playfield.Width / 2; + const float x_offset = 0.2f * CatchPlayfield.WIDTH; + float xCoords = CatchPlayfield.CENTER_X; if (drawableRuleset.Playfield is CatchPlayfield catchPlayfield) catchPlayfield.CatcherArea.MovableCatcher.X = xCoords - x_offset; diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs index a0dcb86d57..ad24adf352 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs @@ -47,13 +47,13 @@ namespace osu.Game.Rulesets.Catch.Tests }; // Should produce a hyper-dash (edge case test) - beatmap.HitObjects.Add(new Fruit { StartTime = 1816, X = 56 / 512f, NewCombo = true }); - beatmap.HitObjects.Add(new Fruit { StartTime = 2008, X = 308 / 512f, NewCombo = true }); + beatmap.HitObjects.Add(new Fruit { StartTime = 1816, X = 56, NewCombo = true }); + beatmap.HitObjects.Add(new Fruit { StartTime = 2008, X = 308, NewCombo = true }); double startTime = 3000; - const float left_x = 0.02f; - const float right_x = 0.98f; + const float left_x = 0.02f * CatchPlayfield.WIDTH; + const float right_x = 0.98f * CatchPlayfield.WIDTH; createObjects(() => new Fruit { X = left_x }); createObjects(() => new TestJuiceStream(right_x), 1); diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.cs index ffcf61a4bf..269e783899 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osuTK; @@ -30,7 +31,7 @@ namespace osu.Game.Rulesets.Catch.Tests { new JuiceStream { - X = 0.5f, + X = CatchPlayfield.CENTER_X, Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, @@ -40,7 +41,7 @@ namespace osu.Game.Rulesets.Catch.Tests }, new Banana { - X = 0.5f, + X = CatchPlayfield.CENTER_X, StartTime = 1000, NewCombo = true } diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs index 0de2060e2d..145a40f5f5 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs @@ -5,7 +5,6 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Objects; using System.Collections.Generic; using System.Linq; -using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects; using osu.Framework.Extensions.IEnumerableExtensions; @@ -36,7 +35,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps Path = curveData.Path, NodeSamples = curveData.NodeSamples, RepeatCount = curveData.RepeatCount, - X = (positionData?.X ?? 0) / CatchPlayfield.BASE_WIDTH, + X = positionData?.X ?? 0, NewCombo = comboData?.NewCombo ?? false, ComboOffset = comboData?.ComboOffset ?? 0, LegacyLastTickOffset = (obj as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0 @@ -59,7 +58,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps Samples = obj.Samples, NewCombo = comboData?.NewCombo ?? false, ComboOffset = comboData?.ComboOffset ?? 0, - X = (positionData?.X ?? 0) / CatchPlayfield.BASE_WIDTH + X = positionData?.X ?? 0 }.Yield(); } } diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs index 7c81bcdf0c..bb14988414 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs @@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps case BananaShower bananaShower: foreach (var banana in bananaShower.NestedHitObjects.OfType()) { - banana.XOffset = (float)rng.NextDouble(); + banana.XOffset = (float)(rng.NextDouble() * CatchPlayfield.WIDTH); rng.Next(); // osu!stable retrieved a random banana type rng.Next(); // osu!stable retrieved a random banana rotation rng.Next(); // osu!stable retrieved a random banana colour @@ -75,7 +75,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps case JuiceStream juiceStream: // Todo: BUG!! Stable used the last control point as the final position of the path, but it should use the computed path instead. - lastPosition = juiceStream.X + juiceStream.Path.ControlPoints[^1].Position.Value.X / CatchPlayfield.BASE_WIDTH; + lastPosition = juiceStream.X + juiceStream.Path.ControlPoints[^1].Position.Value.X; // Todo: BUG!! Stable attempted to use the end time of the stream, but referenced it too early in execution and used the start time instead. lastStartTime = juiceStream.StartTime; @@ -86,7 +86,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps catchObject.XOffset = 0; if (catchObject is TinyDroplet) - catchObject.XOffset = Math.Clamp(rng.Next(-20, 20) / CatchPlayfield.BASE_WIDTH, -catchObject.X, 1 - catchObject.X); + catchObject.XOffset = Math.Clamp(rng.Next(-20, 20), -catchObject.X, CatchPlayfield.WIDTH - catchObject.X); else if (catchObject is Droplet) rng.Next(); // osu!stable retrieved a random droplet rotation } @@ -131,7 +131,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps } // ReSharper disable once PossibleLossOfFraction - if (Math.Abs(positionDiff * CatchPlayfield.BASE_WIDTH) < timeDiff / 3) + if (Math.Abs(positionDiff) < timeDiff / 3) applyOffset(ref offsetPosition, positionDiff); hitObject.XOffset = offsetPosition - hitObject.X; @@ -149,12 +149,12 @@ namespace osu.Game.Rulesets.Catch.Beatmaps private static void applyRandomOffset(ref float position, double maxOffset, FastRandom rng) { bool right = rng.NextBool(); - float rand = Math.Min(20, (float)rng.Next(0, Math.Max(0, maxOffset))) / CatchPlayfield.BASE_WIDTH; + float rand = Math.Min(20, (float)rng.Next(0, Math.Max(0, maxOffset))); if (right) { // Clamp to the right bound - if (position + rand <= 1) + if (position + rand <= CatchPlayfield.WIDTH) position += rand; else position -= rand; @@ -211,7 +211,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps objectWithDroplets.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime)); - double halfCatcherWidth = CatcherArea.GetCatcherSize(beatmap.BeatmapInfo.BaseDifficulty) / 2; + double halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) / 2; int lastDirection = 0; double lastExcess = halfCatcherWidth; diff --git a/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs b/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs index 360af1a8c9..3e21b8fbaf 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs @@ -3,7 +3,6 @@ using System; using osu.Game.Rulesets.Catch.Objects; -using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Objects; @@ -33,8 +32,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing // We will scale everything by this factor, so we can assume a uniform CircleSize among beatmaps. var scalingFactor = normalized_hitobject_radius / halfCatcherWidth; - NormalizedPosition = BaseObject.X * CatchPlayfield.BASE_WIDTH * scalingFactor; - LastNormalizedPosition = LastObject.X * CatchPlayfield.BASE_WIDTH * scalingFactor; + NormalizedPosition = BaseObject.X * scalingFactor; + LastNormalizedPosition = LastObject.X * scalingFactor; // Every strain interval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure StrainTime = Math.Max(40, DeltaTime); diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs index 918ed77683..e679231638 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs @@ -3,7 +3,6 @@ using System; using osu.Game.Rulesets.Catch.Difficulty.Preprocessing; -using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; @@ -68,7 +67,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills } // Bonus for edge dashes. - if (catchCurrent.LastObject.DistanceToHyperDash <= 20.0f / CatchPlayfield.BASE_WIDTH) + if (catchCurrent.LastObject.DistanceToHyperDash <= 20.0f) { if (!catchCurrent.LastObject.HyperDash) edgeDashBonus += 5.7; @@ -78,7 +77,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills playerPosition = catchCurrent.NormalizedPosition; } - distanceAddition *= 1.0 + edgeDashBonus * ((20 - catchCurrent.LastObject.DistanceToHyperDash * CatchPlayfield.BASE_WIDTH) / 20) * Math.Pow((Math.Min(catchCurrent.StrainTime * catchCurrent.ClockRate, 265) / 265), 1.5); // Edge Dashes are easier at lower ms values + distanceAddition *= 1.0 + edgeDashBonus * ((20 - catchCurrent.LastObject.DistanceToHyperDash) / 20) * Math.Pow((Math.Min(catchCurrent.StrainTime * catchCurrent.ClockRate, 265) / 265), 1.5); // Edge Dashes are easier at lower ms values } lastPlayerPosition = playerPosition; diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs index f3b566f340..04932ecdbb 100644 --- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs @@ -5,6 +5,7 @@ using osu.Framework.Bindables; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Catch.Beatmaps; +using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; @@ -17,6 +18,9 @@ namespace osu.Game.Rulesets.Catch.Objects private float x; + /// + /// The horizontal position of the fruit between 0 and . + /// public float X { get => x + XOffset; diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs index b12cdd4ccb..c6345a9df7 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Catch.UI; using osuTK; using osuTK.Graphics; @@ -70,12 +71,11 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables public float DisplayRadius => DrawSize.X / 2 * Scale.X * HitObject.Scale; - protected override float SamplePlaybackPosition => HitObject.X; + protected override float SamplePlaybackPosition => HitObject.X / CatchPlayfield.WIDTH; protected DrawableCatchHitObject(CatchHitObject hitObject) : base(hitObject) { - RelativePositionAxes = Axes.X; X = hitObject.X; } diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index 2c96ee2b19..6b8b70ed54 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -7,7 +7,6 @@ using System.Threading; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; @@ -80,7 +79,7 @@ namespace osu.Game.Rulesets.Catch.Objects { StartTime = t + lastEvent.Value.Time, X = X + Path.PositionAt( - lastEvent.Value.PathProgress + (t / sinceLastTick) * (e.PathProgress - lastEvent.Value.PathProgress)).X / CatchPlayfield.BASE_WIDTH, + lastEvent.Value.PathProgress + (t / sinceLastTick) * (e.PathProgress - lastEvent.Value.PathProgress)).X, }); } } @@ -97,7 +96,7 @@ namespace osu.Game.Rulesets.Catch.Objects { Samples = dropletSamples, StartTime = e.Time, - X = X + Path.PositionAt(e.PathProgress).X / CatchPlayfield.BASE_WIDTH, + X = X + Path.PositionAt(e.PathProgress).X, }); break; @@ -108,14 +107,14 @@ namespace osu.Game.Rulesets.Catch.Objects { Samples = Samples, StartTime = e.Time, - X = X + Path.PositionAt(e.PathProgress).X / CatchPlayfield.BASE_WIDTH, + X = X + Path.PositionAt(e.PathProgress).X, }); break; } } } - public float EndX => X + this.CurvePositionAt(1).X / CatchPlayfield.BASE_WIDTH; + public float EndX => X + this.CurvePositionAt(1).X; public double Duration { diff --git a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs index 7a33cb0577..5d11c574b1 100644 --- a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs +++ b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Catch.Replays // todo: add support for HT DT const double dash_speed = Catcher.BASE_SPEED; const double movement_speed = dash_speed / 2; - float lastPosition = 0.5f; + float lastPosition = CatchPlayfield.CENTER_X; double lastTime = 0; void moveToNext(CatchHitObject h) @@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Catch.Replays bool impossibleJump = speedRequired > movement_speed * 2; // todo: get correct catcher size, based on difficulty CS. - const float catcher_width_half = CatcherArea.CATCHER_SIZE / CatchPlayfield.BASE_WIDTH * 0.3f * 0.5f; + const float catcher_width_half = CatcherArea.CATCHER_SIZE * 0.3f * 0.5f; if (lastPosition - catcher_width_half < h.X && lastPosition + catcher_width_half > h.X) { diff --git a/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs b/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs index 9dab3ed630..7efd832f62 100644 --- a/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs +++ b/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using osu.Game.Beatmaps; using osu.Game.Replays.Legacy; -using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays.Types; @@ -41,7 +40,7 @@ namespace osu.Game.Rulesets.Catch.Replays public void FromLegacy(LegacyReplayFrame currentFrame, IBeatmap beatmap, ReplayFrame lastFrame = null) { - Position = currentFrame.Position.X / CatchPlayfield.BASE_WIDTH; + Position = currentFrame.Position.X; Dashing = currentFrame.ButtonState == ReplayButtonState.Left1; if (Dashing) @@ -63,7 +62,7 @@ namespace osu.Game.Rulesets.Catch.Replays if (Actions.Contains(CatchAction.Dash)) state |= ReplayButtonState.Left1; - return new LegacyReplayFrame(Time, Position * CatchPlayfield.BASE_WIDTH, null, state); + return new LegacyReplayFrame(Time, Position, null, state); } } } diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs index 2319c5ac1f..d034f3c7d4 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs @@ -16,7 +16,16 @@ namespace osu.Game.Rulesets.Catch.UI { public class CatchPlayfield : ScrollingPlayfield { - public const float BASE_WIDTH = 512; + /// + /// The width of the playfield. + /// The horizontal movement of the catcher is confined in the area of this width. + /// + public const float WIDTH = 512; + + /// + /// The center position of the playfield. + /// + public const float CENTER_X = WIDTH / 2; internal readonly CatcherArea CatcherArea; diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs index b8d3dc9017..8ee23461ba 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Catch.UI { base.Update(); - Scale = new Vector2(Parent.ChildSize.X / CatchPlayfield.BASE_WIDTH); + Scale = new Vector2(Parent.ChildSize.X / CatchPlayfield.WIDTH); Size = Vector2.Divide(Vector2.One, Scale); } } diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 9cce46d730..82cbbefcca 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Catch.UI /// /// The relative space to cover in 1 millisecond. based on 1 game pixel per millisecond as in osu-stable. /// - public const double BASE_SPEED = 1.0 / 512; + public const double BASE_SPEED = 1.0; public Container ExplodingFruitTarget; @@ -104,9 +104,6 @@ namespace osu.Game.Rulesets.Catch.UI { this.trailsTarget = trailsTarget; - RelativePositionAxes = Axes.X; - X = 0.5f; - Origin = Anchor.TopCentre; Size = new Vector2(CatcherArea.CATCHER_SIZE); @@ -209,8 +206,8 @@ namespace osu.Game.Rulesets.Catch.UI var halfCatchWidth = catchWidth * 0.5f; // this stuff wil disappear once we move fruit to non-relative coordinate space in the future. - var catchObjectPosition = fruit.X * CatchPlayfield.BASE_WIDTH; - var catcherPosition = Position.X * CatchPlayfield.BASE_WIDTH; + var catchObjectPosition = fruit.X; + var catcherPosition = Position.X; var validCatch = catchObjectPosition >= catcherPosition - halfCatchWidth && @@ -224,7 +221,7 @@ namespace osu.Game.Rulesets.Catch.UI { var target = fruit.HyperDashTarget; var timeDifference = target.StartTime - fruit.StartTime; - double positionDifference = target.X * CatchPlayfield.BASE_WIDTH - catcherPosition; + double positionDifference = target.X - catcherPosition; var velocity = positionDifference / Math.Max(1.0, timeDifference - 1000.0 / 60.0); SetHyperDashState(Math.Abs(velocity), target.X); @@ -331,7 +328,7 @@ namespace osu.Game.Rulesets.Catch.UI public void UpdatePosition(float position) { - position = Math.Clamp(position, 0, 1); + position = Math.Clamp(position, 0, CatchPlayfield.WIDTH); if (position == X) return; diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 37d177b936..bf1ac5bc0e 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -31,14 +31,8 @@ namespace osu.Game.Rulesets.Catch.UI public CatcherArea(BeatmapDifficulty difficulty = null) { - RelativeSizeAxes = Axes.X; - Height = CATCHER_SIZE; - Child = MovableCatcher = new Catcher(this, difficulty); - } - - public static float GetCatcherSize(BeatmapDifficulty difficulty) - { - return CATCHER_SIZE / CatchPlayfield.BASE_WIDTH * (1.0f - 0.7f * (difficulty.CircleSize - 5) / 5); + Size = new Vector2(CatchPlayfield.WIDTH, CATCHER_SIZE); + Child = MovableCatcher = new Catcher(this, difficulty) { X = CatchPlayfield.CENTER_X }; } public void OnResult(DrawableCatchHitObject fruit, JudgementResult result) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index cefb47893c..57555cce90 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -218,7 +218,7 @@ namespace osu.Game.Beatmaps.Formats break; case 2: - position.X = ((IHasXPosition)hitObject).X * 512; + position.X = ((IHasXPosition)hitObject).X; break; case 3: From 5c1f1ab622c8a4e862a7652564b557c76b9514ab Mon Sep 17 00:00:00 2001 From: Joehu Date: Wed, 1 Jul 2020 14:31:06 -0700 Subject: [PATCH 2278/2376] Fix avatar in score panel being unclickable when statistics panel is visible --- osu.Game/Screens/Ranking/ResultsScreen.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 968b446df9..49ce07b708 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -79,6 +79,11 @@ namespace osu.Game.Screens.Ranking RelativeSizeAxes = Axes.Both, Children = new Drawable[] { + statisticsPanel = new StatisticsPanel + { + RelativeSizeAxes = Axes.Both, + Score = { BindTarget = SelectedScore } + }, scorePanelList = new ScorePanelList { RelativeSizeAxes = Axes.Both, @@ -89,11 +94,6 @@ namespace osu.Game.Screens.Ranking { RelativeSizeAxes = Axes.Both }, - statisticsPanel = new StatisticsPanel - { - RelativeSizeAxes = Axes.Both, - Score = { BindTarget = SelectedScore } - }, } } }, From fa252d5e950d685699632b9cfb78ea7d25a95c58 Mon Sep 17 00:00:00 2001 From: Joehu Date: Wed, 1 Jul 2020 17:37:38 -0700 Subject: [PATCH 2279/2376] Fix score panel not showing silver s/ss badges on hd/fl plays --- .../Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs index ee53ee9879..213c1692ee 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -9,6 +10,7 @@ using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Utils; using osu.Game.Graphics; +using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osuTK; @@ -191,8 +193,8 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy Padding = new MarginPadding { Vertical = -15, Horizontal = -20 }, Children = new[] { - new RankBadge(1f, ScoreRank.X), - new RankBadge(0.95f, ScoreRank.S), + new RankBadge(1f, score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.XH : ScoreRank.X), + new RankBadge(0.95f, score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.SH : ScoreRank.S), new RankBadge(0.9f, ScoreRank.A), new RankBadge(0.8f, ScoreRank.B), new RankBadge(0.7f, ScoreRank.C), From 718f06c69075b9199ac07de2db66e6b8124182e5 Mon Sep 17 00:00:00 2001 From: Joehu Date: Thu, 2 Jul 2020 12:35:32 -0700 Subject: [PATCH 2280/2376] Use Mod.AdjustRank() instead --- .../Expanded/Accuracy/AccuracyCircle.cs | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs index 213c1692ee..45da23f1f9 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs @@ -193,18 +193,26 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy Padding = new MarginPadding { Vertical = -15, Horizontal = -20 }, Children = new[] { - new RankBadge(1f, score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.XH : ScoreRank.X), - new RankBadge(0.95f, score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.SH : ScoreRank.S), - new RankBadge(0.9f, ScoreRank.A), - new RankBadge(0.8f, ScoreRank.B), - new RankBadge(0.7f, ScoreRank.C), - new RankBadge(0.35f, ScoreRank.D), + new RankBadge(1f, getRank(ScoreRank.X)), + new RankBadge(0.95f, getRank(ScoreRank.S)), + new RankBadge(0.9f, getRank(ScoreRank.A)), + new RankBadge(0.8f, getRank(ScoreRank.B)), + new RankBadge(0.7f, getRank(ScoreRank.C)), + new RankBadge(0.35f, getRank(ScoreRank.D)), } }, rankText = new RankText(score.Rank) }; } + private ScoreRank getRank(ScoreRank rank) + { + foreach (var mod in score.Mods.OfType()) + rank = mod.AdjustRank(rank, score.Accuracy); + + return rank; + } + protected override void LoadComplete() { base.LoadComplete(); From d66b97868c4db2e057bf7340200d49eace3dd16f Mon Sep 17 00:00:00 2001 From: Joehu Date: Thu, 2 Jul 2020 12:39:37 -0700 Subject: [PATCH 2281/2376] Adjust rank when flashlight is enabled --- osu.Game/Rulesets/Mods/ModFlashlight.cs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/ModFlashlight.cs b/osu.Game/Rulesets/Mods/ModFlashlight.cs index 35a8334237..6e94a84e7d 100644 --- a/osu.Game/Rulesets/Mods/ModFlashlight.cs +++ b/osu.Game/Rulesets/Mods/ModFlashlight.cs @@ -47,9 +47,25 @@ namespace osu.Game.Rulesets.Mods public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor) { Combo.BindTo(scoreProcessor.Combo); + + // Default value of ScoreProcessor's Rank in Flashlight Mod should be SS+ + scoreProcessor.Rank.Value = ScoreRank.XH; } - public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank; + public ScoreRank AdjustRank(ScoreRank rank, double accuracy) + { + switch (rank) + { + case ScoreRank.X: + return ScoreRank.XH; + + case ScoreRank.S: + return ScoreRank.SH; + + default: + return rank; + } + } public virtual void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { From cb69d1a86537fe5904229e8c0b7291952dc7aece Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 3 Jul 2020 16:47:14 +0900 Subject: [PATCH 2282/2376] Fix crash when changing tabs in changelog --- osu.Game/Graphics/UserInterface/BreadcrumbControl.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs b/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs index 84429bf5bd..fb5ff4aad3 100644 --- a/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs +++ b/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs @@ -27,6 +27,8 @@ namespace osu.Game.Graphics.UserInterface { Height = 32; TabContainer.Spacing = new Vector2(padding, 0f); + SwitchTabOnRemove = false; + Current.ValueChanged += index => { foreach (var t in TabContainer.Children.OfType()) From ffec4298a7b9c535db5b5dca6c8c2c4074cbf1e2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 4 Jul 2020 16:45:46 +0900 Subject: [PATCH 2283/2376] Use DrawablePool for DrawableJudgements --- .../Objects/Drawables/DrawableOsuJudgement.cs | 35 +++++--- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 20 +++-- .../Rulesets/Judgements/DrawableJudgement.cs | 83 ++++++++++++++----- 3 files changed, 99 insertions(+), 39 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs index 022e9ea12b..9d0c406295 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs @@ -24,10 +24,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { } + public DrawableOsuJudgement() + { + } + [BackgroundDependencyLoader] private void load(OsuConfigManager config) { - if (config.Get(OsuSetting.HitLighting) && Result.Type != HitResult.Miss) + if (config.Get(OsuSetting.HitLighting)) { AddInternal(lighting = new SkinnableSprite("lighting") { @@ -36,16 +40,23 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Blending = BlendingParameters.Additive, Depth = float.MaxValue }); + } + } - if (JudgedObject != null) - { - lightingColour = JudgedObject.AccentColour.GetBoundCopy(); - lightingColour.BindValueChanged(colour => lighting.Colour = colour.NewValue, true); - } - else - { - lighting.Colour = Color4.White; - } + protected override void PrepareForUse() + { + base.PrepareForUse(); + + lightingColour?.UnbindAll(); + + if (JudgedObject != null) + { + lightingColour = JudgedObject.AccentColour.GetBoundCopy(); + lightingColour.BindValueChanged(colour => lighting.Colour = Result.Type == HitResult.Miss ? Color4.Transparent : colour.NewValue, true); + } + else + { + lighting.Colour = Color4.White; } } @@ -55,13 +66,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { if (lighting != null) { - JudgementBody.Delay(FadeInDuration).FadeOut(400); + JudgementBody.FadeIn().Delay(FadeInDuration).FadeOut(400); lighting.ScaleTo(0.8f).ScaleTo(1.2f, 600, Easing.Out); lighting.FadeIn(200).Then().Delay(200).FadeOut(1000); } - JudgementText?.TransformSpacingTo(new Vector2(14, 0), 1800, Easing.OutQuint); + JudgementText?.TransformSpacingTo(Vector2.Zero).Then().TransformSpacingTo(new Vector2(14, 0), 1800, Easing.OutQuint); base.ApplyHitAnimations(); } } diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 4b1a2ce43c..f9002a29ca 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -4,6 +4,8 @@ using osuTK; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Pooling; +using osu.Framework.Logging; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; @@ -26,10 +28,13 @@ namespace osu.Game.Rulesets.Osu.UI protected override GameplayCursorContainer CreateCursor() => new OsuCursorContainer(); + private readonly DrawablePool judgementPool; + public OsuPlayfield() { InternalChildren = new Drawable[] { + judgementPool = new DrawablePool(20), followPoints = new FollowPointRenderer { RelativeSizeAxes = Axes.Both, @@ -91,12 +96,17 @@ namespace osu.Game.Rulesets.Osu.UI if (!judgedObject.DisplayResult || !DisplayJudgements.Value) return; - DrawableOsuJudgement explosion = new DrawableOsuJudgement(result, judgedObject) + DrawableOsuJudgement explosion = judgementPool.Get(doj => { - Origin = Anchor.Centre, - Position = ((OsuHitObject)judgedObject.HitObject).StackedEndPosition, - Scale = new Vector2(((OsuHitObject)judgedObject.HitObject).Scale) - }; + if (doj.Result != null) + Logger.Log("reused!"); + doj.Result = result; + doj.JudgedObject = judgedObject; + + doj.Origin = Anchor.Centre; + doj.Position = ((OsuHitObject)judgedObject.HitObject).StackedEndPosition; + doj.Scale = new Vector2(((OsuHitObject)judgedObject.HitObject).Scale); + }); judgementLayer.Add(explosion); } diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs index 7113acbbfb..86dd02f2bb 100644 --- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs @@ -1,11 +1,14 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Diagnostics; using osuTK; using osu.Framework.Allocation; +using osu.Framework.Caching; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; @@ -18,16 +21,31 @@ namespace osu.Game.Rulesets.Judgements /// /// A drawable object which visualises the hit result of a . /// - public class DrawableJudgement : CompositeDrawable + public class DrawableJudgement : PoolableDrawable { private const float judgement_size = 128; [Resolved] private OsuColour colours { get; set; } - protected readonly JudgementResult Result; + private readonly Cached drawableCache = new Cached(); - public readonly DrawableHitObject JudgedObject; + private JudgementResult result; + + public JudgementResult Result + { + get => result; + set + { + if (result?.Type == value.Type) + return; + + result = value; + drawableCache.Invalidate(); + } + } + + public DrawableHitObject JudgedObject; protected Container JudgementBody; protected SpriteText JudgementText; @@ -48,29 +66,15 @@ namespace osu.Game.Rulesets.Judgements /// The judgement to visualise. /// The object which was judged. public DrawableJudgement(JudgementResult result, DrawableHitObject judgedObject) + : this() { Result = result; JudgedObject = judgedObject; - - Size = new Vector2(judgement_size); } - [BackgroundDependencyLoader] - private void load() + public DrawableJudgement() { - InternalChild = JudgementBody = new Container - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Child = new SkinnableDrawable(new GameplaySkinComponent(Result.Type), _ => JudgementText = new OsuSpriteText - { - Text = Result.Type.GetDescription().ToUpperInvariant(), - Font = OsuFont.Numeric.With(size: 20), - Colour = colours.ForHitResult(Result.Type), - Scale = new Vector2(0.85f, 1), - }, confineMode: ConfineMode.NoScaling) - }; + Size = new Vector2(judgement_size); } protected virtual void ApplyHitAnimations() @@ -81,11 +85,25 @@ namespace osu.Game.Rulesets.Judgements this.Delay(FadeOutDelay).FadeOut(400); } - protected override void LoadComplete() + [BackgroundDependencyLoader] + private void load() { - base.LoadComplete(); + prepareDrawables(); + } + + protected override void PrepareForUse() + { + base.PrepareForUse(); + + Debug.Assert(Result != null); + + if (!drawableCache.IsValid) + prepareDrawables(); this.FadeInFromZero(FadeInDuration, Easing.OutQuint); + JudgementBody.ScaleTo(1); + JudgementBody.RotateTo(0); + JudgementBody.MoveTo(Vector2.Zero); switch (Result.Type) { @@ -109,5 +127,26 @@ namespace osu.Game.Rulesets.Judgements Expire(true); } + + private void prepareDrawables() + { + var type = Result?.Type ?? HitResult.Perfect; //TODO: better default type from ruleset + + InternalChild = JudgementBody = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Child = new SkinnableDrawable(new GameplaySkinComponent(type), _ => JudgementText = new OsuSpriteText + { + Text = type.GetDescription().ToUpperInvariant(), + Font = OsuFont.Numeric.With(size: 20), + Colour = colours.ForHitResult(type), + Scale = new Vector2(0.85f, 1), + }, confineMode: ConfineMode.NoScaling) + }; + + drawableCache.Validate(); + } } } From 02871c960ba37153075a2781f94de943e49b5ac0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 4 Jul 2020 23:25:06 +0900 Subject: [PATCH 2284/2376] 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 a2c97ead2f..ff86ac6574 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 3ef53a2a53..afe2348c6e 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -24,7 +24,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 492bf89fab..80c37ab8f9 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From cd6bdcdb88035e5d0715d90520f5e083c47ea6ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 5 Jul 2020 00:25:01 +0200 Subject: [PATCH 2285/2376] Replace further spinner transforms with manual lerp --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 3c8ab0f5ab..bb1b6fdd26 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -14,6 +14,7 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; +using osu.Framework.Utils; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Ranking; @@ -193,9 +194,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables SpmCounter.SetRotation(Disc.RotationAbsolute); float relativeCircleScale = Spinner.Scale * circle.DrawHeight / mainContainer.DrawHeight; - Disc.ScaleTo(relativeCircleScale + (1 - relativeCircleScale) * Progress, 200, Easing.OutQuint); + float targetScale = relativeCircleScale + (1 - relativeCircleScale) * Progress; + Disc.Scale = new Vector2((float)Interpolation.Lerp(Disc.Scale.X, targetScale, Math.Clamp(Math.Abs(Time.Elapsed) / 100, 0, 1))); - symbol.RotateTo(Disc.Rotation / 2, 500, Easing.OutQuint); + symbol.Rotation = (float)Interpolation.Lerp(symbol.Rotation, Disc.Rotation / 2, Math.Clamp(Math.Abs(Time.Elapsed) / 40, 0, 1)); } protected override void UpdateInitialTransforms() From d229993e5c727ff9c10328a1360ad776606053b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 5 Jul 2020 02:12:26 +0200 Subject: [PATCH 2286/2376] Use RotationAbsolute instead --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index bb1b6fdd26..4d37622be5 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -197,7 +197,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables float targetScale = relativeCircleScale + (1 - relativeCircleScale) * Progress; Disc.Scale = new Vector2((float)Interpolation.Lerp(Disc.Scale.X, targetScale, Math.Clamp(Math.Abs(Time.Elapsed) / 100, 0, 1))); - symbol.Rotation = (float)Interpolation.Lerp(symbol.Rotation, Disc.Rotation / 2, Math.Clamp(Math.Abs(Time.Elapsed) / 40, 0, 1)); + symbol.Rotation = (float)Interpolation.Lerp(symbol.Rotation, Disc.RotationAbsolute / 2, Math.Clamp(Math.Abs(Time.Elapsed) / 40, 0, 1)); } protected override void UpdateInitialTransforms() From ec689ce824c0ff9930b6d8f4a38322f40e5a61af Mon Sep 17 00:00:00 2001 From: mcendu Date: Sun, 5 Jul 2020 12:31:16 +0800 Subject: [PATCH 2287/2376] add support for custom mania skin paths for stage decorations --- osu.Game/Skinning/LegacyManiaSkinDecoder.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs index 0806676fde..aebc229f7c 100644 --- a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs +++ b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs @@ -115,6 +115,7 @@ namespace osu.Game.Skinning case string _ when pair.Key.StartsWith("NoteImage"): case string _ when pair.Key.StartsWith("KeyImage"): case string _ when pair.Key.StartsWith("Hit"): + case string _ when pair.Key.StartsWith("Stage"): currentConfig.ImageLookups[pair.Key] = pair.Value; break; } From 5c2959eeb6844552a82ef8f2085448a1dc9a6b1d Mon Sep 17 00:00:00 2001 From: mcendu Date: Sun, 5 Jul 2020 13:02:50 +0800 Subject: [PATCH 2288/2376] allow lookup of stage decoration paths and add test images --- .../{mania-stage-left.png => mania/stage-left.png} | Bin .../stage-right.png} | Bin .../Resources/special-skin/skin.ini | 4 +++- osu.Game/Skinning/LegacySkin.cs | 9 +++++++++ 4 files changed, 12 insertions(+), 1 deletion(-) rename osu.Game.Rulesets.Mania.Tests/Resources/special-skin/{mania-stage-left.png => mania/stage-left.png} (100%) rename osu.Game.Rulesets.Mania.Tests/Resources/special-skin/{mania-stage-right.png => mania/stage-right.png} (100%) diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania-stage-left.png b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/stage-left.png similarity index 100% rename from osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania-stage-left.png rename to osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/stage-left.png diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania-stage-right.png b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/stage-right.png similarity index 100% rename from osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania-stage-right.png rename to osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/stage-right.png diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/skin.ini b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/skin.ini index 941abac1da..36765d61bf 100644 --- a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/skin.ini +++ b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/skin.ini @@ -9,4 +9,6 @@ Hit50: mania/hit50 Hit100: mania/hit100 Hit200: mania/hit200 Hit300: mania/hit300 -Hit300g: mania/hit300g \ No newline at end of file +Hit300g: mania/hit300g +StageLeft: mania/stage-left +StageRight: mania/stage-right \ No newline at end of file diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 0b2b723440..4b70ccc6ad 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -250,6 +250,15 @@ namespace osu.Game.Skinning case LegacyManiaSkinConfigurationLookups.RightStageImage: return SkinUtils.As(getManiaImage(existing, "StageRight")); + case LegacyManiaSkinConfigurationLookups.BottomStageImage: + return SkinUtils.As(getManiaImage(existing, "StageBottom")); + + case LegacyManiaSkinConfigurationLookups.LightImage: + return SkinUtils.As(getManiaImage(existing, "StageLight")); + + case LegacyManiaSkinConfigurationLookups.HitTargetImage: + return SkinUtils.As(getManiaImage(existing, "StageHint")); + case LegacyManiaSkinConfigurationLookups.LeftLineWidth: Debug.Assert(maniaLookup.TargetColumn != null); return SkinUtils.As(new Bindable(existing.ColumnLineWidth[maniaLookup.TargetColumn.Value])); From c18ca19c9d60c1e5715d7b2ea05e00566f827fbd Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 6 Jul 2020 05:31:34 +0300 Subject: [PATCH 2289/2376] Add NewsPost api response --- .../Online/API/Requests/Responses/NewsPost.cs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 osu.Game/Online/API/Requests/Responses/NewsPost.cs diff --git a/osu.Game/Online/API/Requests/Responses/NewsPost.cs b/osu.Game/Online/API/Requests/Responses/NewsPost.cs new file mode 100644 index 0000000000..f3ee0f9c35 --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/NewsPost.cs @@ -0,0 +1,38 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Newtonsoft.Json; +using System; + +namespace osu.Game.Online.API.Requests.Responses +{ + public class NewsPost + { + [JsonProperty(@"id")] + public long Id { get; set; } + + [JsonProperty(@"author")] + public string Author { get; set; } + + [JsonProperty(@"edit_url")] + public string EditUrl { get; set; } + + [JsonProperty(@"first_image")] + public string FirstImage { get; set; } + + [JsonProperty(@"published_at")] + public DateTimeOffset PublishedAt { get; set; } + + [JsonProperty(@"updated_at")] + public DateTimeOffset UpdatedAt { get; set; } + + [JsonProperty(@"slug")] + public string Slug { get; set; } + + [JsonProperty(@"title")] + public string Title { get; set; } + + [JsonProperty(@"preview")] + public string Preview { get; set; } + } +} From 51050ec4eff4a6d5dc81c0f95757f2194cbb9ee4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Jul 2020 12:54:39 +0900 Subject: [PATCH 2290/2376] Add per-result type pooling --- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 51 +++++++++++++++---- .../Rulesets/Judgements/DrawableJudgement.cs | 1 + 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index f9002a29ca..2eff99bd3e 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -1,18 +1,23 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Collections.Generic; +using System.Linq; using osuTK; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; -using osu.Framework.Logging; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables.Connections; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Osu.UI.Cursor; +using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; namespace osu.Game.Rulesets.Osu.UI @@ -28,13 +33,12 @@ namespace osu.Game.Rulesets.Osu.UI protected override GameplayCursorContainer CreateCursor() => new OsuCursorContainer(); - private readonly DrawablePool judgementPool; + private readonly IDictionary> poolDictionary = new Dictionary>(); public OsuPlayfield() { InternalChildren = new Drawable[] { - judgementPool = new DrawablePool(20), followPoints = new FollowPointRenderer { RelativeSizeAxes = Axes.Both, @@ -59,6 +63,13 @@ namespace osu.Game.Rulesets.Osu.UI }; hitPolicy = new OrderedHitPolicy(HitObjectContainer); + + var hitWindows = new OsuHitWindows(); + + foreach (var result in Enum.GetValues(typeof(HitResult)).OfType().Where(r => r > HitResult.None && hitWindows.IsHitResultAllowed(r))) + poolDictionary.Add(result, new DrawableJudgementPool(result)); + + AddRangeInternal(poolDictionary.Values); } public override void Add(DrawableHitObject h) @@ -96,16 +107,15 @@ namespace osu.Game.Rulesets.Osu.UI if (!judgedObject.DisplayResult || !DisplayJudgements.Value) return; - DrawableOsuJudgement explosion = judgementPool.Get(doj => + var osuObject = (OsuHitObject)judgedObject.HitObject; + + DrawableOsuJudgement explosion = poolDictionary[result.Type].Get(doj => { - if (doj.Result != null) - Logger.Log("reused!"); - doj.Result = result; doj.JudgedObject = judgedObject; - doj.Origin = Anchor.Centre; - doj.Position = ((OsuHitObject)judgedObject.HitObject).StackedEndPosition; - doj.Scale = new Vector2(((OsuHitObject)judgedObject.HitObject).Scale); + // todo: move to JudgedObject property? + doj.Position = osuObject.StackedEndPosition; + doj.Scale = new Vector2(osuObject.Scale); }); judgementLayer.Add(explosion); @@ -117,5 +127,26 @@ namespace osu.Game.Rulesets.Osu.UI { public void Add(Drawable approachCircleProxy) => AddInternal(approachCircleProxy); } + + private class DrawableJudgementPool : DrawablePool + { + private readonly HitResult result; + + public DrawableJudgementPool(HitResult result) + : base(10) + { + this.result = result; + } + + protected override DrawableOsuJudgement CreateNewDrawable() + { + var judgement = base.CreateNewDrawable(); + + // just a placeholder to initialise the correct drawable hierarchy for this pool. + judgement.Result = new JudgementResult(new HitObject(), new Judgement()) { Type = result }; + + return judgement; + } + } } } diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs index 86dd02f2bb..3ec5326299 100644 --- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs @@ -75,6 +75,7 @@ namespace osu.Game.Rulesets.Judgements public DrawableJudgement() { Size = new Vector2(judgement_size); + Origin = Anchor.Centre; } protected virtual void ApplyHitAnimations() From 7550097eb66f149c71338d2e12da3216300a9b9e Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 6 Jul 2020 07:27:53 +0300 Subject: [PATCH 2291/2376] Implement NewsCard --- .../Visual/Online/TestSceneNewsCard.cs | 52 +++++ .../Online/API/Requests/Responses/NewsPost.cs | 4 +- osu.Game/Overlays/News/NewsCard.cs | 196 ++++++++++++++++++ 3 files changed, 250 insertions(+), 2 deletions(-) create mode 100644 osu.Game.Tests/Visual/Online/TestSceneNewsCard.cs create mode 100644 osu.Game/Overlays/News/NewsCard.cs diff --git a/osu.Game.Tests/Visual/Online/TestSceneNewsCard.cs b/osu.Game.Tests/Visual/Online/TestSceneNewsCard.cs new file mode 100644 index 0000000000..17e3d3eb7f --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneNewsCard.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.Graphics.Containers; +using osu.Framework.Graphics; +using osu.Game.Overlays.News; +using osu.Game.Online.API.Requests.Responses; +using osu.Framework.Allocation; +using osu.Game.Overlays; +using osuTK; +using System; + +namespace osu.Game.Tests.Visual.Online +{ + public class TestSceneNewsCard : OsuTestScene + { + [Cached] + private readonly OverlayColourProvider overlayColour = new OverlayColourProvider(OverlayColourScheme.Purple); + + public TestSceneNewsCard() + { + Add(new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Direction = FillDirection.Vertical, + Width = 500, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0, 20), + Children = new[] + { + new NewsCard(new NewsPost + { + Title = "This post has an image which starts with \"/\" and has many authors!", + Preview = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + Author = "someone, someone1, someone2, someone3, someone4", + FirstImage = "/help/wiki/shared/news/banners/monthly-beatmapping-contest.png", + PublishedAt = DateTime.Now + }), + new NewsCard(new NewsPost + { + Title = "This post has a full-url image!", + Preview = "boom", + Author = "user", + FirstImage = "https://assets.ppy.sh/artists/88/header.jpg", + PublishedAt = DateTime.Now + }) + } + }); + } + } +} diff --git a/osu.Game/Online/API/Requests/Responses/NewsPost.cs b/osu.Game/Online/API/Requests/Responses/NewsPost.cs index f3ee0f9c35..fa10d7aa5c 100644 --- a/osu.Game/Online/API/Requests/Responses/NewsPost.cs +++ b/osu.Game/Online/API/Requests/Responses/NewsPost.cs @@ -21,10 +21,10 @@ namespace osu.Game.Online.API.Requests.Responses public string FirstImage { get; set; } [JsonProperty(@"published_at")] - public DateTimeOffset PublishedAt { get; set; } + public DateTime PublishedAt { get; set; } [JsonProperty(@"updated_at")] - public DateTimeOffset UpdatedAt { get; set; } + public DateTime UpdatedAt { get; set; } [JsonProperty(@"slug")] public string Slug { get; set; } diff --git a/osu.Game/Overlays/News/NewsCard.cs b/osu.Game/Overlays/News/NewsCard.cs new file mode 100644 index 0000000000..052f8edf52 --- /dev/null +++ b/osu.Game/Overlays/News/NewsCard.cs @@ -0,0 +1,196 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Framework.Input.Events; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Overlays.News +{ + public class NewsCard : CompositeDrawable + { + [Resolved] + private OverlayColourProvider colourProvider { get; set; } + + private readonly NewsPost post; + + private Box background; + private TextFlowContainer main; + + public NewsCard(NewsPost post) + { + this.post = post; + } + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Masking = true; + CornerRadius = 6; + + NewsBackground bg; + + InternalChildren = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background4 + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.X, + Height = 160, + Masking = true, + CornerRadius = 6, + Children = new Drawable[] + { + new DelayedLoadWrapper(bg = new NewsBackground(post.FirstImage) + { + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fill, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Alpha = 0 + }) + { + RelativeSizeAxes = Axes.Both + }, + new DateContainer(post.PublishedAt) + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Margin = new MarginPadding + { + Top = 10, + Right = 15 + } + } + } + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding + { + Horizontal = 15, + Vertical = 10 + }, + Child = main = new TextFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + } + } + } + }, + new HoverClickSounds() + }; + + bg.OnLoadComplete += d => d.FadeIn(250, Easing.In); + + main.AddParagraph(post.Title, t => t.Font = OsuFont.GetFont(size: 20, weight: FontWeight.SemiBold)); + main.AddParagraph(post.Preview, t => t.Font = OsuFont.GetFont(size: 12)); // Should use sans-serif font + main.AddParagraph("by ", t => t.Font = OsuFont.GetFont(size: 12)); + main.AddText(post.Author, t => t.Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold)); + } + + protected override bool OnHover(HoverEvent e) + { + background.FadeColour(colourProvider.Background3, 200, Easing.OutQuint); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + background.FadeColour(colourProvider.Background4, 200, Easing.OutQuint); + base.OnHoverLost(e); + } + + [LongRunningLoad] + private class NewsBackground : Sprite + { + private readonly string sourceUrl; + + public NewsBackground(string sourceUrl) + { + this.sourceUrl = sourceUrl; + } + + [BackgroundDependencyLoader] + private void load(LargeTextureStore store) + { + Texture = store.Get(createUrl(sourceUrl)); + } + + private string createUrl(string source) + { + if (string.IsNullOrEmpty(source)) + return "Headers/news"; + + if (source.StartsWith('/')) + return "https://osu.ppy.sh" + source; + + return source; + } + } + + private class DateContainer : CircularContainer, IHasTooltip + { + public string TooltipText => date.ToString("d MMMM yyyy hh:mm:ss UTCz"); + + private readonly DateTime date; + + public DateContainer(DateTime date) + { + this.date = date; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + AutoSizeAxes = Axes.Both; + Masking = true; + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background6.Opacity(0.5f) + }, + new OsuSpriteText + { + Text = date.ToString("d MMM yyyy").ToUpper(), + Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold), + Margin = new MarginPadding + { + Horizontal = 20, + Vertical = 5 + } + } + }; + } + } + } +} From fdb7727e956a1de4f94b261b78abd5b6974d67bc Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 6 Jul 2020 07:28:44 +0300 Subject: [PATCH 2292/2376] Rename NewsPost to APINewsPost --- osu.Game.Tests/Visual/Online/TestSceneNewsCard.cs | 4 ++-- .../API/Requests/Responses/{NewsPost.cs => APINewsPost.cs} | 2 +- osu.Game/Overlays/News/NewsCard.cs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) rename osu.Game/Online/API/Requests/Responses/{NewsPost.cs => APINewsPost.cs} (97%) diff --git a/osu.Game.Tests/Visual/Online/TestSceneNewsCard.cs b/osu.Game.Tests/Visual/Online/TestSceneNewsCard.cs index 17e3d3eb7f..73218794a9 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneNewsCard.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneNewsCard.cs @@ -29,7 +29,7 @@ namespace osu.Game.Tests.Visual.Online Spacing = new Vector2(0, 20), Children = new[] { - new NewsCard(new NewsPost + new NewsCard(new APINewsPost { Title = "This post has an image which starts with \"/\" and has many authors!", Preview = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", @@ -37,7 +37,7 @@ namespace osu.Game.Tests.Visual.Online FirstImage = "/help/wiki/shared/news/banners/monthly-beatmapping-contest.png", PublishedAt = DateTime.Now }), - new NewsCard(new NewsPost + new NewsCard(new APINewsPost { Title = "This post has a full-url image!", Preview = "boom", diff --git a/osu.Game/Online/API/Requests/Responses/NewsPost.cs b/osu.Game/Online/API/Requests/Responses/APINewsPost.cs similarity index 97% rename from osu.Game/Online/API/Requests/Responses/NewsPost.cs rename to osu.Game/Online/API/Requests/Responses/APINewsPost.cs index fa10d7aa5c..e25ad32594 100644 --- a/osu.Game/Online/API/Requests/Responses/NewsPost.cs +++ b/osu.Game/Online/API/Requests/Responses/APINewsPost.cs @@ -6,7 +6,7 @@ using System; namespace osu.Game.Online.API.Requests.Responses { - public class NewsPost + public class APINewsPost { [JsonProperty(@"id")] public long Id { get; set; } diff --git a/osu.Game/Overlays/News/NewsCard.cs b/osu.Game/Overlays/News/NewsCard.cs index 052f8edf52..994b3c8fd1 100644 --- a/osu.Game/Overlays/News/NewsCard.cs +++ b/osu.Game/Overlays/News/NewsCard.cs @@ -23,12 +23,12 @@ namespace osu.Game.Overlays.News [Resolved] private OverlayColourProvider colourProvider { get; set; } - private readonly NewsPost post; + private readonly APINewsPost post; private Box background; private TextFlowContainer main; - public NewsCard(NewsPost post) + public NewsCard(APINewsPost post) { this.post = post; } From dbbee481f60b21911b5c67fa1d575272ac7a3a25 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 6 Jul 2020 22:01:45 +0900 Subject: [PATCH 2293/2376] Expose dialog body text getter --- osu.Game/Overlays/Dialog/PopupDialog.cs | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/Dialog/PopupDialog.cs b/osu.Game/Overlays/Dialog/PopupDialog.cs index 02ef900dc5..1bcbe4dd2f 100644 --- a/osu.Game/Overlays/Dialog/PopupDialog.cs +++ b/osu.Game/Overlays/Dialog/PopupDialog.cs @@ -42,25 +42,34 @@ namespace osu.Game.Overlays.Dialog set => icon.Icon = value; } - private string text; + private string headerText; public string HeaderText { - get => text; + get => headerText; set { - if (text == value) + if (headerText == value) return; - text = value; - + headerText = value; header.Text = value; } } + private string bodyText; + public string BodyText { - set => body.Text = value; + get => bodyText; + set + { + if (bodyText == value) + return; + + bodyText = value; + body.Text = value; + } } public IEnumerable Buttons From 1effe71ec2279ea53aafe07e230160312612b5e4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 6 Jul 2020 22:03:09 +0900 Subject: [PATCH 2294/2376] Add dialog for storage options --- osu.Game/IO/OsuStorage.cs | 95 ++++++++++++++++++++++++++----- osu.Game/Screens/Menu/MainMenu.cs | 79 +++++++++++++++++++++++++ 2 files changed, 161 insertions(+), 13 deletions(-) diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index 8bcc0941c1..3d6903c56f 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.cs @@ -2,9 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Diagnostics; using System.IO; using System.Linq; using System.Threading; +using JetBrains.Annotations; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Configuration; @@ -13,12 +15,30 @@ namespace osu.Game.IO { public class OsuStorage : WrappedStorage { + /// + /// Indicates the error (if any) that occurred when initialising the custom storage during initial startup. + /// + public readonly OsuStorageError Error; + + /// + /// The custom storage path as selected by the user. + /// + [CanBeNull] + public string CustomStoragePath => storageConfig.Get(StorageConfig.FullPath); + + /// + /// The default storage path to be used if a custom storage path hasn't been selected or is not accessible. + /// + [NotNull] + public string DefaultStoragePath => defaultStorage.GetFullPath("."); + private readonly GameHost host; private readonly StorageConfigManager storageConfig; + private readonly Storage defaultStorage; - internal static readonly string[] IGNORE_DIRECTORIES = { "cache" }; + public static readonly string[] IGNORE_DIRECTORIES = { "cache" }; - internal static readonly string[] IGNORE_FILES = + public static readonly string[] IGNORE_FILES = { "framework.ini", "storage.ini" @@ -28,23 +48,53 @@ namespace osu.Game.IO : base(defaultStorage, string.Empty) { this.host = host; + this.defaultStorage = defaultStorage; storageConfig = new StorageConfigManager(defaultStorage); - var customStoragePath = storageConfig.Get(StorageConfig.FullPath); + if (!string.IsNullOrEmpty(CustomStoragePath)) + TryChangeToCustomStorage(out Error); + } - if (!string.IsNullOrEmpty(customStoragePath)) + /// + /// Resets the custom storage path, changing the target storage to the default location. + /// + public void ResetCustomStoragePath() + { + storageConfig.Set(StorageConfig.FullPath, string.Empty); + storageConfig.Save(); + + ChangeTargetStorage(defaultStorage); + } + + /// + /// Attempts to change to the user's custom storage path. + /// + /// The error that occurred. + /// Whether the custom storage path was used successfully. If not, will be populated with the reason. + public bool TryChangeToCustomStorage(out OsuStorageError error) + { + Debug.Assert(!string.IsNullOrEmpty(CustomStoragePath)); + + error = OsuStorageError.None; + Storage lastStorage = UnderlyingStorage; + + try { - try - { - ChangeTargetStorage(host.GetStorage(customStoragePath)); - } - catch (Exception ex) - { - Logger.Log($"Couldn't use custom storage path ({customStoragePath}): {ex}. Using default path.", LoggingTarget.Runtime, LogLevel.Error); - ChangeTargetStorage(defaultStorage); - } + Storage userStorage = host.GetStorage(CustomStoragePath); + + if (!userStorage.GetFiles(".").Any()) + error = OsuStorageError.AccessibleButEmpty; + + ChangeTargetStorage(userStorage); } + catch + { + error = OsuStorageError.NotAccessible; + ChangeTargetStorage(lastStorage); + } + + return error == OsuStorageError.None; } protected override void ChangeTargetStorage(Storage newStorage) @@ -155,4 +205,23 @@ namespace osu.Game.IO } } } + + public enum OsuStorageError + { + /// + /// No error. + /// + None, + + /// + /// Occurs when the target storage directory is accessible but does not already contain game files. + /// Only happens when the user changes the storage directory and then moves the files manually or mounts a different device to the same path. + /// + AccessibleButEmpty, + + /// + /// Occurs when the target storage directory cannot be accessed at all. + /// + NotAccessible, + } } diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 9245df2a7d..c391742c45 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.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 osuTK; using osuTK.Graphics; @@ -15,6 +16,7 @@ using osu.Framework.Screens; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; +using osu.Game.IO; using osu.Game.Online.API; using osu.Game.Overlays; using osu.Game.Overlays.Dialog; @@ -171,6 +173,9 @@ namespace osu.Game.Screens.Menu return s; } + [Resolved] + private Storage storage { get; set; } + public override void OnEntering(IScreen last) { base.OnEntering(last); @@ -187,6 +192,9 @@ namespace osu.Game.Screens.Menu Track.Start(); } } + + if (storage is OsuStorage osuStorage && osuStorage.Error != OsuStorageError.None) + dialogOverlay?.Push(new StorageErrorDialog(osuStorage, osuStorage.Error)); } private bool exitConfirmed; @@ -308,5 +316,76 @@ namespace osu.Game.Screens.Menu }; } } + + private class StorageErrorDialog : PopupDialog + { + [Resolved] + private DialogOverlay dialogOverlay { get; set; } + + [Resolved] + private OsuGameBase osuGame { get; set; } + + public StorageErrorDialog(OsuStorage storage, OsuStorageError error) + { + HeaderText = "osu! storage error"; + Icon = FontAwesome.Solid.ExclamationTriangle; + + var buttons = new List(); + + BodyText = $"osu! encountered an error when trying to use the custom storage path ('{storage.CustomStoragePath}').\n\n"; + + switch (error) + { + case OsuStorageError.NotAccessible: + BodyText += $"The default storage path ('{storage.DefaultStoragePath}') is currently being used because the custom storage path is not accessible.\n\n" + + "Is it on a removable device that is not currently connected?"; + + buttons.AddRange(new PopupDialogButton[] + { + new PopupDialogOkButton + { + Text = "Try again", + Action = () => + { + if (!storage.TryChangeToCustomStorage(out var nextError)) + dialogOverlay.Push(new StorageErrorDialog(storage, nextError)); + } + }, + new PopupDialogOkButton + { + Text = "Use the default path from now on", + Action = storage.ResetCustomStoragePath + }, + new PopupDialogCancelButton + { + Text = "Only use the default path for this session", + }, + }); + break; + + case OsuStorageError.AccessibleButEmpty: + BodyText += "The custom storage path is currently being used but is empty.\n\n" + + "Have you moved the files elsewhere?"; + + // Todo: Provide the option to search for the files similar to migration. + buttons.AddRange(new PopupDialogButton[] + { + new PopupDialogOkButton + { + Text = "Reset to default", + Action = storage.ResetCustomStoragePath + }, + new PopupDialogCancelButton + { + Text = "Keep using the custom path" + } + }); + + break; + } + + Buttons = buttons; + } + } } } From 8f792603ee6b03d19bba1ffc7d74203904205a35 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 6 Jul 2020 22:40:45 +0900 Subject: [PATCH 2295/2376] Apply suggestions from code review Co-authored-by: Dean Herbert --- osu.Game/Screens/Menu/MainMenu.cs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index c391742c45..d64d9b69fe 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -332,13 +332,11 @@ namespace osu.Game.Screens.Menu var buttons = new List(); - BodyText = $"osu! encountered an error when trying to use the custom storage path ('{storage.CustomStoragePath}').\n\n"; switch (error) { case OsuStorageError.NotAccessible: - BodyText += $"The default storage path ('{storage.DefaultStoragePath}') is currently being used because the custom storage path is not accessible.\n\n" - + "Is it on a removable device that is not currently connected?"; + BodyText = $"The specified osu! data location (\"{storage.CustomStoragePath}\") is not accessible. If it is on external storage, please reconnect the device and try again."; buttons.AddRange(new PopupDialogButton[] { @@ -353,31 +351,30 @@ namespace osu.Game.Screens.Menu }, new PopupDialogOkButton { - Text = "Use the default path from now on", + Text = "Reset to default location", Action = storage.ResetCustomStoragePath }, new PopupDialogCancelButton { - Text = "Only use the default path for this session", + Text = "Use default location for this session", }, }); break; case OsuStorageError.AccessibleButEmpty: - BodyText += "The custom storage path is currently being used but is empty.\n\n" - + "Have you moved the files elsewhere?"; + BodyText = $"The specified osu! data location (\"{storage.CustomStoragePath}\") is empty. If you have moved the files, please close osu! and move them back."; // Todo: Provide the option to search for the files similar to migration. buttons.AddRange(new PopupDialogButton[] { new PopupDialogOkButton { - Text = "Reset to default", + Text = "Reset to default location", Action = storage.ResetCustomStoragePath }, new PopupDialogCancelButton { - Text = "Keep using the custom path" + Text = "Start fresh at specified location" } }); From ddac511c8c5c3b4ef641c1a80e4e9dbbe6359ce4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 6 Jul 2020 22:41:51 +0900 Subject: [PATCH 2296/2376] Move start fresh button above --- osu.Game/Screens/Menu/MainMenu.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index d64d9b69fe..dcb141cce5 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -332,7 +332,6 @@ namespace osu.Game.Screens.Menu var buttons = new List(); - switch (error) { case OsuStorageError.NotAccessible: @@ -367,15 +366,15 @@ namespace osu.Game.Screens.Menu // Todo: Provide the option to search for the files similar to migration. buttons.AddRange(new PopupDialogButton[] { + new PopupDialogCancelButton + { + Text = "Start fresh at specified location" + }, new PopupDialogOkButton { Text = "Reset to default location", Action = storage.ResetCustomStoragePath }, - new PopupDialogCancelButton - { - Text = "Start fresh at specified location" - } }); break; From 00a2fbce06ac67d5bc077f10e43c302c98af629e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 6 Jul 2020 22:41:58 +0900 Subject: [PATCH 2297/2376] Fix test failures --- osu.Game/IO/OsuStorage.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index 3d6903c56f..1d15294666 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.cs @@ -83,7 +83,7 @@ namespace osu.Game.IO { Storage userStorage = host.GetStorage(CustomStoragePath); - if (!userStorage.GetFiles(".").Any()) + if (!userStorage.ExistsDirectory(".") || !userStorage.GetFiles(".").Any()) error = OsuStorageError.AccessibleButEmpty; ChangeTargetStorage(userStorage); From a650a5ec83ca93d17709c9c34fb5922857e23d90 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Jul 2020 23:44:26 +0900 Subject: [PATCH 2298/2376] Move dialog classes to own file --- osu.Game/Screens/Menu/ConfirmExitDialog.cs | 34 ++++++++ osu.Game/Screens/Menu/MainMenu.cs | 96 --------------------- osu.Game/Screens/Menu/StorageErrorDialog.cs | 79 +++++++++++++++++ 3 files changed, 113 insertions(+), 96 deletions(-) create mode 100644 osu.Game/Screens/Menu/ConfirmExitDialog.cs create mode 100644 osu.Game/Screens/Menu/StorageErrorDialog.cs diff --git a/osu.Game/Screens/Menu/ConfirmExitDialog.cs b/osu.Game/Screens/Menu/ConfirmExitDialog.cs new file mode 100644 index 0000000000..d120eb21a8 --- /dev/null +++ b/osu.Game/Screens/Menu/ConfirmExitDialog.cs @@ -0,0 +1,34 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Graphics.Sprites; +using osu.Game.Overlays.Dialog; + +namespace osu.Game.Screens.Menu +{ + public class ConfirmExitDialog : PopupDialog + { + public ConfirmExitDialog(Action confirm, Action cancel) + { + HeaderText = "Are you sure you want to exit?"; + BodyText = "Last chance to back out."; + + Icon = FontAwesome.Solid.ExclamationTriangle; + + Buttons = new PopupDialogButton[] + { + new PopupDialogOkButton + { + Text = @"Goodbye", + Action = confirm + }, + new PopupDialogCancelButton + { + Text = @"Just a little more", + Action = cancel + }, + }; + } + } +} diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index dcb141cce5..76950982e6 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using System.Linq; using osuTK; using osuTK.Graphics; @@ -10,7 +8,6 @@ using osu.Framework.Allocation; using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Sprites; using osu.Framework.Platform; using osu.Framework.Screens; using osu.Game.Configuration; @@ -19,7 +16,6 @@ using osu.Game.Graphics.Containers; using osu.Game.IO; using osu.Game.Online.API; using osu.Game.Overlays; -using osu.Game.Overlays.Dialog; using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Edit; using osu.Game.Screens.Multi; @@ -291,97 +287,5 @@ namespace osu.Game.Screens.Menu this.FadeOut(3000); return base.OnExiting(next); } - - private class ConfirmExitDialog : PopupDialog - { - public ConfirmExitDialog(Action confirm, Action cancel) - { - HeaderText = "Are you sure you want to exit?"; - BodyText = "Last chance to back out."; - - Icon = FontAwesome.Solid.ExclamationTriangle; - - Buttons = new PopupDialogButton[] - { - new PopupDialogOkButton - { - Text = @"Goodbye", - Action = confirm - }, - new PopupDialogCancelButton - { - Text = @"Just a little more", - Action = cancel - }, - }; - } - } - - private class StorageErrorDialog : PopupDialog - { - [Resolved] - private DialogOverlay dialogOverlay { get; set; } - - [Resolved] - private OsuGameBase osuGame { get; set; } - - public StorageErrorDialog(OsuStorage storage, OsuStorageError error) - { - HeaderText = "osu! storage error"; - Icon = FontAwesome.Solid.ExclamationTriangle; - - var buttons = new List(); - - switch (error) - { - case OsuStorageError.NotAccessible: - BodyText = $"The specified osu! data location (\"{storage.CustomStoragePath}\") is not accessible. If it is on external storage, please reconnect the device and try again."; - - buttons.AddRange(new PopupDialogButton[] - { - new PopupDialogOkButton - { - Text = "Try again", - Action = () => - { - if (!storage.TryChangeToCustomStorage(out var nextError)) - dialogOverlay.Push(new StorageErrorDialog(storage, nextError)); - } - }, - new PopupDialogOkButton - { - Text = "Reset to default location", - Action = storage.ResetCustomStoragePath - }, - new PopupDialogCancelButton - { - Text = "Use default location for this session", - }, - }); - break; - - case OsuStorageError.AccessibleButEmpty: - BodyText = $"The specified osu! data location (\"{storage.CustomStoragePath}\") is empty. If you have moved the files, please close osu! and move them back."; - - // Todo: Provide the option to search for the files similar to migration. - buttons.AddRange(new PopupDialogButton[] - { - new PopupDialogCancelButton - { - Text = "Start fresh at specified location" - }, - new PopupDialogOkButton - { - Text = "Reset to default location", - Action = storage.ResetCustomStoragePath - }, - }); - - break; - } - - Buttons = buttons; - } - } } } diff --git a/osu.Game/Screens/Menu/StorageErrorDialog.cs b/osu.Game/Screens/Menu/StorageErrorDialog.cs new file mode 100644 index 0000000000..38a6c07ce7 --- /dev/null +++ b/osu.Game/Screens/Menu/StorageErrorDialog.cs @@ -0,0 +1,79 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Sprites; +using osu.Game.IO; +using osu.Game.Overlays; +using osu.Game.Overlays.Dialog; + +namespace osu.Game.Screens.Menu +{ + public class StorageErrorDialog : PopupDialog + { + [Resolved] + private DialogOverlay dialogOverlay { get; set; } + + [Resolved] + private OsuGameBase osuGame { get; set; } + + public StorageErrorDialog(OsuStorage storage, OsuStorageError error) + { + HeaderText = "osu! storage error"; + Icon = FontAwesome.Solid.ExclamationTriangle; + + var buttons = new List(); + + switch (error) + { + case OsuStorageError.NotAccessible: + BodyText = $"The specified osu! data location (\"{storage.CustomStoragePath}\") is not accessible. If it is on external storage, please reconnect the device and try again."; + + buttons.AddRange(new PopupDialogButton[] + { + new PopupDialogOkButton + { + Text = "Try again", + Action = () => + { + if (!storage.TryChangeToCustomStorage(out var nextError)) + dialogOverlay.Push(new StorageErrorDialog(storage, nextError)); + } + }, + new PopupDialogOkButton + { + Text = "Reset to default location", + Action = storage.ResetCustomStoragePath + }, + new PopupDialogCancelButton + { + Text = "Use default location for this session", + }, + }); + break; + + case OsuStorageError.AccessibleButEmpty: + BodyText = $"The specified osu! data location (\"{storage.CustomStoragePath}\") is empty. If you have moved the files, please close osu! and move them back."; + + // Todo: Provide the option to search for the files similar to migration. + buttons.AddRange(new PopupDialogButton[] + { + new PopupDialogCancelButton + { + Text = "Start fresh at specified location" + }, + new PopupDialogOkButton + { + Text = "Reset to default location", + Action = storage.ResetCustomStoragePath + }, + }); + + break; + } + + Buttons = buttons; + } + } +} From 3f3bfb1ffbe001ed4016b6750ff830c5f983279b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Jul 2020 23:51:16 +0900 Subject: [PATCH 2299/2376] Minor reshuffling / recolouring --- osu.Game/Screens/Menu/StorageErrorDialog.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Menu/StorageErrorDialog.cs b/osu.Game/Screens/Menu/StorageErrorDialog.cs index 38a6c07ce7..dcaad4013a 100644 --- a/osu.Game/Screens/Menu/StorageErrorDialog.cs +++ b/osu.Game/Screens/Menu/StorageErrorDialog.cs @@ -32,7 +32,7 @@ namespace osu.Game.Screens.Menu buttons.AddRange(new PopupDialogButton[] { - new PopupDialogOkButton + new PopupDialogCancelButton { Text = "Try again", Action = () => @@ -41,15 +41,15 @@ namespace osu.Game.Screens.Menu dialogOverlay.Push(new StorageErrorDialog(storage, nextError)); } }, + new PopupDialogCancelButton + { + Text = "Use default location until restart", + }, new PopupDialogOkButton { Text = "Reset to default location", Action = storage.ResetCustomStoragePath }, - new PopupDialogCancelButton - { - Text = "Use default location for this session", - }, }); break; From ebbc8298917db15105130ea2b12e1dab67173a88 Mon Sep 17 00:00:00 2001 From: Rsplwe Date: Tue, 7 Jul 2020 00:15:27 +0800 Subject: [PATCH 2300/2376] disable HardwareAccelerated --- osu.Android/OsuGameActivity.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index 2e5fa59d20..9839d16030 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -9,7 +9,7 @@ using osu.Framework.Android; namespace osu.Android { - [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullSensor, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = true)] + [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullSensor, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false)] public class OsuGameActivity : AndroidGameActivity { protected override Framework.Game CreateGame() => new OsuGameAndroid(); From 9dde101f12201e66b92005a31773125e44629bd1 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 6 Jul 2020 23:53:27 +0300 Subject: [PATCH 2301/2376] Remove string prefixes --- .../API/Requests/Responses/APINewsPost.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game/Online/API/Requests/Responses/APINewsPost.cs b/osu.Game/Online/API/Requests/Responses/APINewsPost.cs index e25ad32594..5cd94efdd2 100644 --- a/osu.Game/Online/API/Requests/Responses/APINewsPost.cs +++ b/osu.Game/Online/API/Requests/Responses/APINewsPost.cs @@ -8,31 +8,31 @@ namespace osu.Game.Online.API.Requests.Responses { public class APINewsPost { - [JsonProperty(@"id")] + [JsonProperty("id")] public long Id { get; set; } - [JsonProperty(@"author")] + [JsonProperty("author")] public string Author { get; set; } - [JsonProperty(@"edit_url")] + [JsonProperty("edit_url")] public string EditUrl { get; set; } - [JsonProperty(@"first_image")] + [JsonProperty("first_image")] public string FirstImage { get; set; } - [JsonProperty(@"published_at")] + [JsonProperty("published_at")] public DateTime PublishedAt { get; set; } - [JsonProperty(@"updated_at")] + [JsonProperty("updated_at")] public DateTime UpdatedAt { get; set; } - [JsonProperty(@"slug")] + [JsonProperty("slug")] public string Slug { get; set; } - [JsonProperty(@"title")] + [JsonProperty("title")] public string Title { get; set; } - [JsonProperty(@"preview")] + [JsonProperty("preview")] public string Preview { get; set; } } } From 68d9f9de4629da8b41bc4389b878cf826bb76bb8 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 6 Jul 2020 23:55:20 +0300 Subject: [PATCH 2302/2376] Use DateTimeOffset --- osu.Game.Tests/Visual/Online/TestSceneNewsCard.cs | 4 ++-- osu.Game/Online/API/Requests/Responses/APINewsPost.cs | 4 ++-- osu.Game/Overlays/News/NewsCard.cs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneNewsCard.cs b/osu.Game.Tests/Visual/Online/TestSceneNewsCard.cs index 73218794a9..82f603df6a 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneNewsCard.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneNewsCard.cs @@ -35,7 +35,7 @@ namespace osu.Game.Tests.Visual.Online Preview = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", Author = "someone, someone1, someone2, someone3, someone4", FirstImage = "/help/wiki/shared/news/banners/monthly-beatmapping-contest.png", - PublishedAt = DateTime.Now + PublishedAt = DateTimeOffset.Now }), new NewsCard(new APINewsPost { @@ -43,7 +43,7 @@ namespace osu.Game.Tests.Visual.Online Preview = "boom", Author = "user", FirstImage = "https://assets.ppy.sh/artists/88/header.jpg", - PublishedAt = DateTime.Now + PublishedAt = DateTimeOffset.Now }) } }); diff --git a/osu.Game/Online/API/Requests/Responses/APINewsPost.cs b/osu.Game/Online/API/Requests/Responses/APINewsPost.cs index 5cd94efdd2..7cc6907949 100644 --- a/osu.Game/Online/API/Requests/Responses/APINewsPost.cs +++ b/osu.Game/Online/API/Requests/Responses/APINewsPost.cs @@ -21,10 +21,10 @@ namespace osu.Game.Online.API.Requests.Responses public string FirstImage { get; set; } [JsonProperty("published_at")] - public DateTime PublishedAt { get; set; } + public DateTimeOffset PublishedAt { get; set; } [JsonProperty("updated_at")] - public DateTime UpdatedAt { get; set; } + public DateTimeOffset UpdatedAt { get; set; } [JsonProperty("slug")] public string Slug { get; set; } diff --git a/osu.Game/Overlays/News/NewsCard.cs b/osu.Game/Overlays/News/NewsCard.cs index 994b3c8fd1..08a9fccc4e 100644 --- a/osu.Game/Overlays/News/NewsCard.cs +++ b/osu.Game/Overlays/News/NewsCard.cs @@ -160,9 +160,9 @@ namespace osu.Game.Overlays.News { public string TooltipText => date.ToString("d MMMM yyyy hh:mm:ss UTCz"); - private readonly DateTime date; + private readonly DateTimeOffset date; - public DateContainer(DateTime date) + public DateContainer(DateTimeOffset date) { this.date = date; } From c86bb2e755d9c43bfb7130d22883b19f40c8e3d2 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 7 Jul 2020 00:01:06 +0300 Subject: [PATCH 2303/2376] Use DrawableDate tooltip for DateContainer --- osu.Game/Graphics/DrawableDate.cs | 2 +- osu.Game/Overlays/News/NewsCard.cs | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Graphics/DrawableDate.cs b/osu.Game/Graphics/DrawableDate.cs index 8b6df4a834..953b7541e1 100644 --- a/osu.Game/Graphics/DrawableDate.cs +++ b/osu.Game/Graphics/DrawableDate.cs @@ -82,7 +82,7 @@ namespace osu.Game.Graphics public object TooltipContent => Date; - private class DateTooltip : VisibilityContainer, ITooltip + public class DateTooltip : VisibilityContainer, ITooltip { private readonly OsuSpriteText dateText, timeText; private readonly Box background; diff --git a/osu.Game/Overlays/News/NewsCard.cs b/osu.Game/Overlays/News/NewsCard.cs index 08a9fccc4e..c22a3268bf 100644 --- a/osu.Game/Overlays/News/NewsCard.cs +++ b/osu.Game/Overlays/News/NewsCard.cs @@ -156,9 +156,11 @@ namespace osu.Game.Overlays.News } } - private class DateContainer : CircularContainer, IHasTooltip + private class DateContainer : CircularContainer, IHasCustomTooltip { - public string TooltipText => date.ToString("d MMMM yyyy hh:mm:ss UTCz"); + public ITooltip GetCustomTooltip() => new DrawableDate.DateTooltip(); + + public object TooltipContent => date; private readonly DateTimeOffset date; From 857a027a7366952209e7548c0fe40ea371bf75f8 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 7 Jul 2020 00:11:35 +0300 Subject: [PATCH 2304/2376] Parse HTML entities during APINewsPost deserialisation --- .../Visual/Online/TestSceneNewsCard.cs | 6 ++--- .../API/Requests/Responses/APINewsPost.cs | 25 ++++++++++++++++--- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneNewsCard.cs b/osu.Game.Tests/Visual/Online/TestSceneNewsCard.cs index 82f603df6a..0446cadac9 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneNewsCard.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneNewsCard.cs @@ -39,9 +39,9 @@ namespace osu.Game.Tests.Visual.Online }), new NewsCard(new APINewsPost { - Title = "This post has a full-url image!", - Preview = "boom", - Author = "user", + Title = "This post has a full-url image! (HTML entity: &)", + Preview = "boom (HTML entity: &)", + Author = "user (HTML entity: &)", FirstImage = "https://assets.ppy.sh/artists/88/header.jpg", PublishedAt = DateTimeOffset.Now }) diff --git a/osu.Game/Online/API/Requests/Responses/APINewsPost.cs b/osu.Game/Online/API/Requests/Responses/APINewsPost.cs index 7cc6907949..ced08f0bf2 100644 --- a/osu.Game/Online/API/Requests/Responses/APINewsPost.cs +++ b/osu.Game/Online/API/Requests/Responses/APINewsPost.cs @@ -3,6 +3,7 @@ using Newtonsoft.Json; using System; +using System.Net; namespace osu.Game.Online.API.Requests.Responses { @@ -11,8 +12,14 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty("id")] public long Id { get; set; } + private string author; + [JsonProperty("author")] - public string Author { get; set; } + public string Author + { + get => author; + set => author = WebUtility.HtmlDecode(value); + } [JsonProperty("edit_url")] public string EditUrl { get; set; } @@ -29,10 +36,22 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty("slug")] public string Slug { get; set; } + private string title; + [JsonProperty("title")] - public string Title { get; set; } + public string Title + { + get => title; + set => title = WebUtility.HtmlDecode(value); + } + + private string preview; [JsonProperty("preview")] - public string Preview { get; set; } + public string Preview + { + get => preview; + set => preview = WebUtility.HtmlDecode(value); + } } } From 88b2a12c0942e6296f453456a42e8a7958a92488 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 7 Jul 2020 17:38:42 +0900 Subject: [PATCH 2305/2376] Reduce footer height to match back button --- osu.Game/Screens/Multi/Match/Components/Footer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Multi/Match/Components/Footer.cs b/osu.Game/Screens/Multi/Match/Components/Footer.cs index 94d7df6194..be4ee873fa 100644 --- a/osu.Game/Screens/Multi/Match/Components/Footer.cs +++ b/osu.Game/Screens/Multi/Match/Components/Footer.cs @@ -16,7 +16,7 @@ namespace osu.Game.Screens.Multi.Match.Components { public class Footer : CompositeDrawable { - public const float HEIGHT = 100; + public const float HEIGHT = 50; public Action OnStart; public readonly Bindable SelectedItem = new Bindable(); From c74bfd5c88e2a55c35a996b9902e127f1da35df7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 7 Jul 2020 17:42:20 +0900 Subject: [PATCH 2306/2376] Revert unintentional changes --- osu.Game/Tests/Visual/ModTestScene.cs | 13 +++++++++++++ osu.Game/Tests/Visual/ScreenTestScene.cs | 4 ++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/osu.Game/Tests/Visual/ModTestScene.cs b/osu.Game/Tests/Visual/ModTestScene.cs index add851ebf3..23b5ad0bd8 100644 --- a/osu.Game/Tests/Visual/ModTestScene.cs +++ b/osu.Game/Tests/Visual/ModTestScene.cs @@ -21,6 +21,19 @@ namespace osu.Game.Tests.Visual AddStep("set test data", () => currentTestData = testData); }); + public override void TearDownSteps() + { + AddUntilStep("test passed", () => + { + if (currentTestData == null) + return true; + + return currentTestData.PassCondition?.Invoke() ?? false; + }); + + base.TearDownSteps(); + } + protected sealed override IBeatmap CreateBeatmap(RulesetInfo ruleset) => currentTestData?.Beatmap ?? base.CreateBeatmap(ruleset); protected sealed override TestPlayer CreatePlayer(Ruleset ruleset) diff --git a/osu.Game/Tests/Visual/ScreenTestScene.cs b/osu.Game/Tests/Visual/ScreenTestScene.cs index 067d8faf54..33cc00e748 100644 --- a/osu.Game/Tests/Visual/ScreenTestScene.cs +++ b/osu.Game/Tests/Visual/ScreenTestScene.cs @@ -33,8 +33,8 @@ namespace osu.Game.Tests.Visual [SetUpSteps] public virtual void SetUpSteps() => addExitAllScreensStep(); - // [TearDownSteps] - // public virtual void TearDownSteps() => addExitAllScreensStep(); + [TearDownSteps] + public virtual void TearDownSteps() => addExitAllScreensStep(); private void addExitAllScreensStep() { From 4a1bea48745e925ef46c300213e9da762afd2992 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 7 Jul 2020 18:28:43 +0900 Subject: [PATCH 2307/2376] Adjust layout to be two columns (and more friendly to vertical screens) --- .../Multi/Components/OverlinedDisplay.cs | 5 ++- .../Screens/Multi/Match/MatchSubScreen.cs | 37 ++++++++++++++----- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Multi/Components/OverlinedDisplay.cs b/osu.Game/Screens/Multi/Components/OverlinedDisplay.cs index d2bb3c4876..2b589256fa 100644 --- a/osu.Game/Screens/Multi/Components/OverlinedDisplay.cs +++ b/osu.Game/Screens/Multi/Components/OverlinedDisplay.cs @@ -86,7 +86,10 @@ namespace osu.Game.Screens.Multi.Components Text = title, Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold) }, - details = new OsuSpriteText { Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold) }, + details = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold) + }, } }, }, diff --git a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs index 1b2fdffa5e..a93caed09c 100644 --- a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs +++ b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs @@ -115,6 +115,7 @@ namespace osu.Game.Screens.Multi.Match { new Drawable[] { + null, new Container { RelativeSizeAxes = Axes.Both, @@ -151,19 +152,37 @@ namespace osu.Game.Screens.Multi.Match } } }, - new Container + null, + new GridContainer { RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Horizontal = 5 }, - Child = leaderboard = new OverlinedLeaderboard { RelativeSizeAxes = Axes.Both }, + Content = new[] + { + new Drawable[] + { + leaderboard = new OverlinedLeaderboard { RelativeSizeAxes = Axes.Both }, + }, + new Drawable[] + { + new OverlinedChatDisplay { RelativeSizeAxes = Axes.Both } + } + }, + RowDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.Relative, size: 0.4f, minSize: 300), + } }, - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Left = 5 }, - Child = new OverlinedChatDisplay { RelativeSizeAxes = Axes.Both } - } + null }, + }, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.Relative, size: 0.5f, maxSize: 400), + new Dimension(), + new Dimension(GridSizeMode.Relative, size: 0.5f, maxSize: 600), + new Dimension(), } } } From 4b4fcd39e396a95b82d4d1676ade91a693fbf8f2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 7 Jul 2020 18:40:21 +0900 Subject: [PATCH 2308/2376] Further layout adjustments based on fedback --- osu.Game/Screens/Multi/Match/MatchSubScreen.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs index a93caed09c..694315a3b3 100644 --- a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs +++ b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs @@ -115,7 +115,6 @@ namespace osu.Game.Screens.Multi.Match { new Drawable[] { - null, new Container { RelativeSizeAxes = Axes.Both, @@ -170,7 +169,7 @@ namespace osu.Game.Screens.Multi.Match RowDimensions = new[] { new Dimension(), - new Dimension(GridSizeMode.Relative, size: 0.4f, minSize: 300), + new Dimension(GridSizeMode.Relative, size: 0.4f, minSize: 240), } }, null @@ -178,7 +177,6 @@ namespace osu.Game.Screens.Multi.Match }, ColumnDimensions = new[] { - new Dimension(), new Dimension(GridSizeMode.Relative, size: 0.5f, maxSize: 400), new Dimension(), new Dimension(GridSizeMode.Relative, size: 0.5f, maxSize: 600), From 8909bf628ca6d19843be0506b484d4ca7b609d55 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 7 Jul 2020 21:08:13 +0900 Subject: [PATCH 2309/2376] 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 a2c97ead2f..0563e5319d 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 3ef53a2a53..4e6de77e86 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -24,7 +24,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 492bf89fab..c31e28638f 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From 8152e0791dee15945908a565668e783d789a9514 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 7 Jul 2020 21:47:44 +0900 Subject: [PATCH 2310/2376] Fix potential nullref --- osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs index 64b3afcae1..45ef793deb 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs @@ -91,7 +91,8 @@ namespace osu.Game.Overlays.BeatmapListing [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) { - ((FilterDropdown)Dropdown).AccentColour = colourProvider.Light2; + if (Dropdown is FilterDropdown fd) + fd.AccentColour = colourProvider.Light2; } protected override Dropdown CreateDropdown() => new FilterDropdown(); From bdec13d4a48001b2b21c43b0e7c750ee4494bb88 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 7 Jul 2020 16:46:17 +0300 Subject: [PATCH 2311/2376] Move DateTooltip to it's on file --- osu.Game/Graphics/DateTooltip.cs | 78 ++++++++++++++++++++++++++++++ osu.Game/Graphics/DrawableDate.cs | 67 ------------------------- osu.Game/Overlays/News/NewsCard.cs | 2 +- 3 files changed, 79 insertions(+), 68 deletions(-) create mode 100644 osu.Game/Graphics/DateTooltip.cs diff --git a/osu.Game/Graphics/DateTooltip.cs b/osu.Game/Graphics/DateTooltip.cs new file mode 100644 index 0000000000..67fcab43f7 --- /dev/null +++ b/osu.Game/Graphics/DateTooltip.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 osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics.Sprites; +using osuTK; + +namespace osu.Game.Graphics +{ + public class DateTooltip : VisibilityContainer, ITooltip + { + private readonly OsuSpriteText dateText, timeText; + private readonly Box background; + + public DateTooltip() + { + AutoSizeAxes = Axes.Both; + Masking = true; + CornerRadius = 5; + + Children = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Padding = new MarginPadding(10), + Children = new Drawable[] + { + dateText = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold), + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + }, + timeText = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular), + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + } + } + }, + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + background.Colour = colours.GreySeafoamDarker; + timeText.Colour = colours.BlueLighter; + } + + protected override void PopIn() => this.FadeIn(200, Easing.OutQuint); + protected override void PopOut() => this.FadeOut(200, Easing.OutQuint); + + public bool SetContent(object content) + { + if (!(content is DateTimeOffset date)) + return false; + + dateText.Text = $"{date:d MMMM yyyy} "; + timeText.Text = $"{date:HH:mm:ss \"UTC\"z}"; + return true; + } + + public void Move(Vector2 pos) => Position = pos; + } +} diff --git a/osu.Game/Graphics/DrawableDate.cs b/osu.Game/Graphics/DrawableDate.cs index 953b7541e1..259d9c8d6e 100644 --- a/osu.Game/Graphics/DrawableDate.cs +++ b/osu.Game/Graphics/DrawableDate.cs @@ -4,12 +4,9 @@ using System; using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; -using osu.Framework.Graphics.Shapes; using osu.Game.Graphics.Sprites; using osu.Game.Utils; -using osuTK; namespace osu.Game.Graphics { @@ -81,69 +78,5 @@ namespace osu.Game.Graphics public ITooltip GetCustomTooltip() => new DateTooltip(); public object TooltipContent => Date; - - public class DateTooltip : VisibilityContainer, ITooltip - { - private readonly OsuSpriteText dateText, timeText; - private readonly Box background; - - public DateTooltip() - { - AutoSizeAxes = Axes.Both; - Masking = true; - CornerRadius = 5; - - Children = new Drawable[] - { - background = new Box - { - RelativeSizeAxes = Axes.Both - }, - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Padding = new MarginPadding(10), - Children = new Drawable[] - { - dateText = new OsuSpriteText - { - Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold), - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - }, - timeText = new OsuSpriteText - { - Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular), - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - } - } - }, - }; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - background.Colour = colours.GreySeafoamDarker; - timeText.Colour = colours.BlueLighter; - } - - protected override void PopIn() => this.FadeIn(200, Easing.OutQuint); - protected override void PopOut() => this.FadeOut(200, Easing.OutQuint); - - public bool SetContent(object content) - { - if (!(content is DateTimeOffset date)) - return false; - - dateText.Text = $"{date:d MMMM yyyy} "; - timeText.Text = $"{date:HH:mm:ss \"UTC\"z}"; - return true; - } - - public void Move(Vector2 pos) => Position = pos; - } } } diff --git a/osu.Game/Overlays/News/NewsCard.cs b/osu.Game/Overlays/News/NewsCard.cs index c22a3268bf..f9d7378279 100644 --- a/osu.Game/Overlays/News/NewsCard.cs +++ b/osu.Game/Overlays/News/NewsCard.cs @@ -158,7 +158,7 @@ namespace osu.Game.Overlays.News private class DateContainer : CircularContainer, IHasCustomTooltip { - public ITooltip GetCustomTooltip() => new DrawableDate.DateTooltip(); + public ITooltip GetCustomTooltip() => new DateTooltip(); public object TooltipContent => date; From c88a802b05b1ad2f13ad2c559e79af377444f50b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 7 Jul 2020 23:04:39 +0200 Subject: [PATCH 2312/2376] Adjust font size to match web design --- osu.Game/Overlays/News/NewsCard.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/News/NewsCard.cs b/osu.Game/Overlays/News/NewsCard.cs index f9d7378279..9c478a7c1d 100644 --- a/osu.Game/Overlays/News/NewsCard.cs +++ b/osu.Game/Overlays/News/NewsCard.cs @@ -184,7 +184,7 @@ namespace osu.Game.Overlays.News new OsuSpriteText { Text = date.ToString("d MMM yyyy").ToUpper(), - Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold), + Font = OsuFont.GetFont(size: 10, weight: FontWeight.SemiBold), Margin = new MarginPadding { Horizontal = 20, From d98a64dfbc67b0689a9ca8a044b3d9954d232dcb Mon Sep 17 00:00:00 2001 From: Shivam Date: Wed, 8 Jul 2020 03:26:36 +0200 Subject: [PATCH 2313/2376] Make seeding # bg black and white text color Makes it consistent with TournamentSpriteTextWithBackground --- osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs b/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs index d48e396b89..eed3cac9f0 100644 --- a/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs +++ b/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs @@ -203,13 +203,14 @@ namespace osu.Game.Tournament.Screens.TeamIntro new Box { RelativeSizeAxes = Axes.Both, - Colour = TournamentGame.TEXT_COLOUR, + Colour = TournamentGame.ELEMENT_BACKGROUND_COLOUR, }, new TournamentSpriteText { Anchor = Anchor.Centre, Origin = Anchor.Centre, Text = seeding.ToString("#,0"), + Colour = TournamentGame.ELEMENT_FOREGROUND_COLOUR }, } }, From 0684ac90c6180f5debbf5b5aeb6dc9383aaf0166 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 8 Jul 2020 13:36:32 +0900 Subject: [PATCH 2314/2376] Make toolbar opaque This is the general direction we're going with future designs. Just applying this now because it makes a lot of screens feel much better (multiplayer lobby / match, song select etc. where there are elements adjacent to the bar which cause the transparency to feel a bit awkward). --- osu.Game/Overlays/Toolbar/Toolbar.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/osu.Game/Overlays/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs index 1b748cb672..ba6e52ec1d 100644 --- a/osu.Game/Overlays/Toolbar/Toolbar.cs +++ b/osu.Game/Overlays/Toolbar/Toolbar.cs @@ -28,9 +28,6 @@ namespace osu.Game.Overlays.Toolbar private const double transition_time = 500; - private const float alpha_hovering = 0.8f; - private const float alpha_normal = 0.6f; - private readonly Bindable overlayActivationMode = new Bindable(OverlayActivation.All); // Toolbar components like RulesetSelector should receive keyboard input events even when the toolbar is hidden. @@ -103,7 +100,6 @@ namespace osu.Game.Overlays.Toolbar public class ToolbarBackground : Container { - private readonly Box solidBackground; private readonly Box gradientBackground; public ToolbarBackground() @@ -111,11 +107,10 @@ namespace osu.Game.Overlays.Toolbar RelativeSizeAxes = Axes.Both; Children = new Drawable[] { - solidBackground = new Box + new Box { RelativeSizeAxes = Axes.Both, Colour = OsuColour.Gray(0.1f), - Alpha = alpha_normal, }, gradientBackground = new Box { @@ -131,14 +126,12 @@ namespace osu.Game.Overlays.Toolbar protected override bool OnHover(HoverEvent e) { - solidBackground.FadeTo(alpha_hovering, transition_time, Easing.OutQuint); gradientBackground.FadeIn(transition_time, Easing.OutQuint); return true; } protected override void OnHoverLost(HoverLostEvent e) { - solidBackground.FadeTo(alpha_normal, transition_time, Easing.OutQuint); gradientBackground.FadeOut(transition_time, Easing.OutQuint); } } From 35d329220028ed8e1c80760b29e4dfd325dbab4b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 7 Jul 2020 19:23:11 +0900 Subject: [PATCH 2315/2376] Remove nesting of components inside overlined component I think this makes things a bit more readable. The only weird case is the transfer of details from the component to the `OverlinedHeader`, but bindables make it not too bad. --- .../TestSceneOverlinedParticipants.cs | 5 +- .../Multiplayer/TestSceneOverlinedPlaylist.cs | 4 +- .../Multi/Components/OverlinedDisplay.cs | 131 ------------------ .../Multi/Components/OverlinedHeader.cs | 89 ++++++++++++ .../Multi/Components/OverlinedPlaylist.cs | 33 ----- ...Participants.cs => ParticipantsDisplay.cs} | 31 +++-- .../Screens/Multi/DrawableRoomPlaylist.cs | 2 - .../Multi/Lounge/Components/RoomInspector.cs | 19 ++- .../Match/Components/OverlinedChatDisplay.cs | 20 --- .../Match/Components/OverlinedLeaderboard.cs | 24 ---- .../Screens/Multi/Match/MatchSubScreen.cs | 48 ++++--- 11 files changed, 151 insertions(+), 255 deletions(-) delete mode 100644 osu.Game/Screens/Multi/Components/OverlinedDisplay.cs create mode 100644 osu.Game/Screens/Multi/Components/OverlinedHeader.cs delete mode 100644 osu.Game/Screens/Multi/Components/OverlinedPlaylist.cs rename osu.Game/Screens/Multi/Components/{OverlinedParticipants.cs => ParticipantsDisplay.cs} (63%) delete mode 100644 osu.Game/Screens/Multi/Match/Components/OverlinedChatDisplay.cs delete mode 100644 osu.Game/Screens/Multi/Match/Components/OverlinedLeaderboard.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedParticipants.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedParticipants.cs index 7ea3bba23f..a13fcdaef8 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedParticipants.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedParticipants.cs @@ -22,12 +22,11 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("create component", () => { - Child = new OverlinedParticipants(Direction.Horizontal) + Child = new ParticipantsDisplay(Direction.Horizontal) { Anchor = Anchor.Centre, Origin = Anchor.Centre, Width = 500, - AutoSizeAxes = Axes.Y, }; }); } @@ -37,7 +36,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("create component", () => { - Child = new OverlinedParticipants(Direction.Vertical) + Child = new ParticipantsDisplay(Direction.Vertical) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedPlaylist.cs index 14b7934dc7..d3ffb9649e 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedPlaylist.cs @@ -4,7 +4,7 @@ using osu.Framework.Graphics; using osu.Game.Online.Multiplayer; using osu.Game.Rulesets.Osu; -using osu.Game.Screens.Multi.Components; +using osu.Game.Screens.Multi; using osu.Game.Tests.Beatmaps; using osuTK; @@ -26,7 +26,7 @@ namespace osu.Game.Tests.Visual.Multiplayer }); } - Add(new OverlinedPlaylist(false) + Add(new DrawableRoomPlaylist(false, false) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Screens/Multi/Components/OverlinedDisplay.cs b/osu.Game/Screens/Multi/Components/OverlinedDisplay.cs deleted file mode 100644 index 2b589256fa..0000000000 --- a/osu.Game/Screens/Multi/Components/OverlinedDisplay.cs +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osuTK; - -namespace osu.Game.Screens.Multi.Components -{ - public abstract class OverlinedDisplay : MultiplayerComposite - { - protected readonly Container Content; - - public override Axes RelativeSizeAxes - { - get => base.RelativeSizeAxes; - set - { - base.RelativeSizeAxes = value; - updateDimensions(); - } - } - - public override Axes AutoSizeAxes - { - get => base.AutoSizeAxes; - protected set - { - base.AutoSizeAxes = value; - updateDimensions(); - } - } - - private bool showLine = true; - - public bool ShowLine - { - get => showLine; - set - { - showLine = value; - line.Alpha = value ? 1 : 0; - } - } - - protected string Details - { - set => details.Text = value; - } - - private readonly Circle line; - private readonly OsuSpriteText details; - private readonly GridContainer grid; - - protected OverlinedDisplay(string title) - { - InternalChild = grid = new GridContainer - { - Content = new[] - { - new Drawable[] - { - line = new Circle - { - RelativeSizeAxes = Axes.X, - Height = 2, - Margin = new MarginPadding { Bottom = 2 } - }, - }, - new Drawable[] - { - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Margin = new MarginPadding { Top = 5 }, - Spacing = new Vector2(10, 0), - Children = new Drawable[] - { - new OsuSpriteText - { - Text = title, - Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold) - }, - details = new OsuSpriteText - { - Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold) - }, - } - }, - }, - new Drawable[] - { - Content = new Container { Padding = new MarginPadding { Top = 5 } } - } - } - }; - - updateDimensions(); - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - line.Colour = colours.Yellow; - details.Colour = colours.Yellow; - } - - private void updateDimensions() - { - grid.RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.AutoSize), - new Dimension(AutoSizeAxes.HasFlag(Axes.Y) ? GridSizeMode.AutoSize : GridSizeMode.Distributed), - }; - - // Assigning to none is done so that setting auto and relative size modes doesn't cause exceptions to be thrown - grid.AutoSizeAxes = Content.AutoSizeAxes = Axes.None; - grid.RelativeSizeAxes = Content.RelativeSizeAxes = Axes.None; - - // Auto-size when required, otherwise eagerly relative-size - grid.AutoSizeAxes = Content.AutoSizeAxes = AutoSizeAxes; - grid.RelativeSizeAxes = Content.RelativeSizeAxes = ~AutoSizeAxes; - } - } -} diff --git a/osu.Game/Screens/Multi/Components/OverlinedHeader.cs b/osu.Game/Screens/Multi/Components/OverlinedHeader.cs new file mode 100644 index 0000000000..7ec20c8cae --- /dev/null +++ b/osu.Game/Screens/Multi/Components/OverlinedHeader.cs @@ -0,0 +1,89 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osuTK; + +namespace osu.Game.Screens.Multi.Components +{ + /// + /// A header used in the multiplayer interface which shows text / details beneath a line. + /// + public class OverlinedHeader : MultiplayerComposite + { + private bool showLine = true; + + public bool ShowLine + { + get => showLine; + set + { + showLine = value; + line.Alpha = value ? 1 : 0; + } + } + + public Bindable Details = new Bindable(); + + private readonly Circle line; + private readonly OsuSpriteText details; + + public OverlinedHeader(string title) + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + Margin = new MarginPadding { Bottom = 5 }; + + InternalChild = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + line = new Circle + { + RelativeSizeAxes = Axes.X, + Height = 2, + Margin = new MarginPadding { Bottom = 2 } + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Margin = new MarginPadding { Top = 5 }, + Spacing = new Vector2(10, 0), + Children = new Drawable[] + { + new OsuSpriteText + { + Text = title, + Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold) + }, + details = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold) + }, + } + }, + } + }; + + Details.BindValueChanged(val => details.Text = val.NewValue); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + line.Colour = colours.Yellow; + details.Colour = colours.Yellow; + } + } +} diff --git a/osu.Game/Screens/Multi/Components/OverlinedPlaylist.cs b/osu.Game/Screens/Multi/Components/OverlinedPlaylist.cs deleted file mode 100644 index 4fe79b40a0..0000000000 --- a/osu.Game/Screens/Multi/Components/OverlinedPlaylist.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Game.Online.Multiplayer; - -namespace osu.Game.Screens.Multi.Components -{ - public class OverlinedPlaylist : OverlinedDisplay - { - public readonly Bindable SelectedItem = new Bindable(); - - private readonly DrawableRoomPlaylist playlist; - - public OverlinedPlaylist(bool allowSelection) - : base("Playlist") - { - Content.Add(playlist = new DrawableRoomPlaylist(false, allowSelection) - { - RelativeSizeAxes = Axes.Both, - SelectedItem = { BindTarget = SelectedItem } - }); - } - - [BackgroundDependencyLoader] - private void load() - { - playlist.Items.BindTo(Playlist); - } - } -} diff --git a/osu.Game/Screens/Multi/Components/OverlinedParticipants.cs b/osu.Game/Screens/Multi/Components/ParticipantsDisplay.cs similarity index 63% rename from osu.Game/Screens/Multi/Components/OverlinedParticipants.cs rename to osu.Game/Screens/Multi/Components/ParticipantsDisplay.cs index eb1782d147..6ea4283379 100644 --- a/osu.Game/Screens/Multi/Components/OverlinedParticipants.cs +++ b/osu.Game/Screens/Multi/Components/ParticipantsDisplay.cs @@ -2,26 +2,22 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Graphics.Containers; namespace osu.Game.Screens.Multi.Components { - public class OverlinedParticipants : OverlinedDisplay + public class ParticipantsDisplay : MultiplayerComposite { - public new Axes AutoSizeAxes - { - get => base.AutoSizeAxes; - set => base.AutoSizeAxes = value; - } + public Bindable Details = new Bindable(); - public OverlinedParticipants(Direction direction) - : base("Recent participants") + public ParticipantsDisplay(Direction direction) { OsuScrollContainer scroll; ParticipantsList list; - Content.Add(scroll = new OsuScrollContainer(direction) + AddInternal(scroll = new OsuScrollContainer(direction) { Child = list = new ParticipantsList() }); @@ -29,13 +25,21 @@ namespace osu.Game.Screens.Multi.Components switch (direction) { case Direction.Horizontal: + AutoSizeAxes = Axes.Y; + RelativeSizeAxes = Axes.X; + scroll.RelativeSizeAxes = Axes.X; scroll.Height = ParticipantsList.TILE_SIZE + OsuScrollContainer.SCROLL_BAR_HEIGHT + OsuScrollContainer.SCROLL_BAR_PADDING * 2; - list.AutoSizeAxes = Axes.Both; + + list.RelativeSizeAxes = Axes.Y; + list.AutoSizeAxes = Axes.X; break; case Direction.Vertical: + RelativeSizeAxes = Axes.Both; + scroll.RelativeSizeAxes = Axes.Both; + list.RelativeSizeAxes = Axes.X; list.AutoSizeAxes = Axes.Y; break; @@ -46,11 +50,10 @@ namespace osu.Game.Screens.Multi.Components private void load() { ParticipantCount.BindValueChanged(_ => setParticipantCount()); - MaxParticipants.BindValueChanged(_ => setParticipantCount()); - - setParticipantCount(); + MaxParticipants.BindValueChanged(_ => setParticipantCount(), true); } - private void setParticipantCount() => Details = MaxParticipants.Value != null ? $"{ParticipantCount.Value}/{MaxParticipants.Value}" : ParticipantCount.Value.ToString(); + private void setParticipantCount() => + Details.Value = MaxParticipants.Value != null ? $"{ParticipantCount.Value}/{MaxParticipants.Value}" : ParticipantCount.Value.ToString(); } } diff --git a/osu.Game/Screens/Multi/DrawableRoomPlaylist.cs b/osu.Game/Screens/Multi/DrawableRoomPlaylist.cs index 9a3fcb1cdc..89c335183b 100644 --- a/osu.Game/Screens/Multi/DrawableRoomPlaylist.cs +++ b/osu.Game/Screens/Multi/DrawableRoomPlaylist.cs @@ -60,8 +60,6 @@ namespace osu.Game.Screens.Multi RequestDeletion = requestDeletion }; - private void requestSelection(PlaylistItem item) => SelectedItem.Value = item; - private void requestDeletion(PlaylistItem item) { if (SelectedItem.Value == item) diff --git a/osu.Game/Screens/Multi/Lounge/Components/RoomInspector.cs b/osu.Game/Screens/Multi/Lounge/Components/RoomInspector.cs index 891853dee5..77fbd606f4 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/RoomInspector.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/RoomInspector.cs @@ -24,6 +24,8 @@ namespace osu.Game.Screens.Multi.Lounge.Components [BackgroundDependencyLoader] private void load(OsuColour colours) { + OverlinedHeader participantsHeader; + InternalChildren = new Drawable[] { new Box @@ -55,22 +57,31 @@ namespace osu.Game.Screens.Multi.Lounge.Components RelativeSizeAxes = Axes.X, Margin = new MarginPadding { Vertical = 60 }, }, - new OverlinedParticipants(Direction.Horizontal) + participantsHeader = new OverlinedHeader("Recent Participants"), + new ParticipantsDisplay(Direction.Vertical) { RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y - }, + Height = ParticipantsList.TILE_SIZE * 3, + Details = { BindTarget = participantsHeader.Details } + } } } }, + new Drawable[] { new OverlinedHeader("Playlist"), }, new Drawable[] { - new OverlinedPlaylist(false) { RelativeSizeAxes = Axes.Both }, + new DrawableRoomPlaylist(false, false) + { + RelativeSizeAxes = Axes.Both, + Items = { BindTarget = Playlist } + }, }, }, RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), + new Dimension(), } } } diff --git a/osu.Game/Screens/Multi/Match/Components/OverlinedChatDisplay.cs b/osu.Game/Screens/Multi/Match/Components/OverlinedChatDisplay.cs deleted file mode 100644 index a8d898385a..0000000000 --- a/osu.Game/Screens/Multi/Match/Components/OverlinedChatDisplay.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Graphics; -using osu.Game.Screens.Multi.Components; - -namespace osu.Game.Screens.Multi.Match.Components -{ - public class OverlinedChatDisplay : OverlinedDisplay - { - public OverlinedChatDisplay() - : base("Chat") - { - Content.Add(new MatchChatDisplay - { - RelativeSizeAxes = Axes.Both - }); - } - } -} diff --git a/osu.Game/Screens/Multi/Match/Components/OverlinedLeaderboard.cs b/osu.Game/Screens/Multi/Match/Components/OverlinedLeaderboard.cs deleted file mode 100644 index bda2cd70d7..0000000000 --- a/osu.Game/Screens/Multi/Match/Components/OverlinedLeaderboard.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Graphics; -using osu.Game.Screens.Multi.Components; - -namespace osu.Game.Screens.Multi.Match.Components -{ - public class OverlinedLeaderboard : OverlinedDisplay - { - private readonly MatchLeaderboard leaderboard; - - public OverlinedLeaderboard() - : base("Leaderboard") - { - Content.Add(leaderboard = new MatchLeaderboard - { - RelativeSizeAxes = Axes.Both - }); - } - - public void RefreshScores() => leaderboard.RefreshScores(); - } -} diff --git a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs index 694315a3b3..dffd6a0331 100644 --- a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs +++ b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs @@ -53,9 +53,10 @@ namespace osu.Game.Screens.Multi.Match protected readonly Bindable SelectedItem = new Bindable(); private MatchSettingsOverlay settingsOverlay; - private OverlinedLeaderboard leaderboard; + private MatchLeaderboard leaderboard; private IBindable> managerUpdated; + private OverlinedHeader participantsHeader; public MatchSubScreen(Room room) { @@ -85,11 +86,22 @@ namespace osu.Game.Screens.Multi.Match Child = new GridContainer { RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), + new Dimension(), + }, Content = new[] { + new Drawable[] { new Components.Header() }, new Drawable[] { - new Components.Header() + participantsHeader = new OverlinedHeader("Participants") + { + ShowLine = false + } }, new Drawable[] { @@ -97,12 +109,10 @@ namespace osu.Game.Screens.Multi.Match { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Margin = new MarginPadding { Top = 10 }, - Child = new OverlinedParticipants(Direction.Horizontal) + Margin = new MarginPadding { Top = 5 }, + Child = new ParticipantsDisplay(Direction.Horizontal) { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - ShowLine = false + Details = { BindTarget = participantsHeader.Details } } } }, @@ -126,9 +136,10 @@ namespace osu.Game.Screens.Multi.Match { new Drawable[] { - new OverlinedPlaylist(true) // Temporarily always allow selection + new DrawableRoomPlaylist(false, true) // Temporarily always allow selection { RelativeSizeAxes = Axes.Both, + Items = { BindTarget = playlist }, SelectedItem = { BindTarget = SelectedItem } } }, @@ -157,18 +168,16 @@ namespace osu.Game.Screens.Multi.Match RelativeSizeAxes = Axes.Both, Content = new[] { - new Drawable[] - { - leaderboard = new OverlinedLeaderboard { RelativeSizeAxes = Axes.Both }, - }, - new Drawable[] - { - new OverlinedChatDisplay { RelativeSizeAxes = Axes.Both } - } + new Drawable[] { new OverlinedHeader("Leaderboard"), }, + new Drawable[] { leaderboard = new MatchLeaderboard { RelativeSizeAxes = Axes.Both }, }, + new Drawable[] { new OverlinedHeader("Chat"), }, + new Drawable[] { new MatchChatDisplay { RelativeSizeAxes = Axes.Both } } }, RowDimensions = new[] { + new Dimension(GridSizeMode.AutoSize), new Dimension(), + new Dimension(GridSizeMode.AutoSize), new Dimension(GridSizeMode.Relative, size: 0.4f, minSize: 240), } }, @@ -185,12 +194,6 @@ namespace osu.Game.Screens.Multi.Match } } }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.AutoSize), - new Dimension(), - } } } }, @@ -219,6 +222,7 @@ namespace osu.Game.Screens.Multi.Match } [Resolved] + private IAPIProvider api { get; set; } protected override void LoadComplete() From 12e3a3c38a70095ab0a4ee50bf669374f9941186 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 8 Jul 2020 15:06:40 +0900 Subject: [PATCH 2316/2376] Adjust toolbar fade in/out on toggle --- osu.Game/Overlays/Toolbar/Toolbar.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs index ba6e52ec1d..de08b79f57 100644 --- a/osu.Game/Overlays/Toolbar/Toolbar.cs +++ b/osu.Game/Overlays/Toolbar/Toolbar.cs @@ -139,7 +139,7 @@ namespace osu.Game.Overlays.Toolbar protected override void PopIn() { this.MoveToY(0, transition_time, Easing.OutQuint); - this.FadeIn(transition_time / 2, Easing.OutQuint); + this.FadeIn(transition_time / 4, Easing.OutQuint); } protected override void PopOut() @@ -147,7 +147,7 @@ namespace osu.Game.Overlays.Toolbar userButton.StateContainer?.Hide(); this.MoveToY(-DrawSize.Y, transition_time, Easing.OutQuint); - this.FadeOut(transition_time); + this.FadeOut(transition_time, Easing.InQuint); } } } From 6c8b6f05f838ffd7a6139b2eeb93d91aabaa2ad8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 8 Jul 2020 15:24:26 +0900 Subject: [PATCH 2317/2376] Fix key bindings switching order at random on consecutive "reset to defaults" --- osu.Game/Input/KeyBindingStore.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Input/KeyBindingStore.cs b/osu.Game/Input/KeyBindingStore.cs index 74b3134964..198ab6883d 100644 --- a/osu.Game/Input/KeyBindingStore.cs +++ b/osu.Game/Input/KeyBindingStore.cs @@ -55,6 +55,9 @@ namespace osu.Game.Input RulesetID = rulesetId, Variant = variant }); + + // required to ensure stable insert order (https://github.com/dotnet/efcore/issues/11686) + usage.Context.SaveChanges(); } } } From e6ec883084899f368847d3367f7000f409844b68 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 8 Jul 2020 20:20:50 +0900 Subject: [PATCH 2318/2376] Remove slider tail circle judgement requirements --- osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs index c11e20c9e7..1e54b576f1 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs @@ -4,6 +4,7 @@ using osu.Framework.Bindables; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects @@ -24,6 +25,13 @@ namespace osu.Game.Rulesets.Osu.Objects protected override HitWindows CreateHitWindows() => HitWindows.Empty; - public override Judgement CreateJudgement() => new SliderRepeat.SliderRepeatJudgement(); + public override Judgement CreateJudgement() => new SliderTailJudgement(); + + public class SliderTailJudgement : OsuJudgement + { + protected override int NumericResultFor(HitResult result) => 0; + + public override bool AffectsCombo => false; + } } } From 37ecab3f2f3cbea1a818941bf4153c58ec087158 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 8 Jul 2020 20:44:27 +0200 Subject: [PATCH 2319/2376] Add assertions to make spinner tests fail --- .../TestSceneSpinnerRotation.cs | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index ea006ec607..579c47f585 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -14,6 +14,7 @@ using osu.Game.Rulesets.Osu.Objects.Drawables; using osuTK; using System.Collections.Generic; using System.Linq; +using osu.Framework.Graphics.Sprites; using osu.Game.Storyboards; using static osu.Game.Tests.Visual.OsuTestScene.ClockBackedTestWorkingBeatmap; @@ -36,6 +37,7 @@ namespace osu.Game.Rulesets.Osu.Tests } private DrawableSpinner drawableSpinner; + private SpriteIcon spinnerSymbol => drawableSpinner.ChildrenOfType().Single(); [SetUpSteps] public override void SetUpSteps() @@ -50,23 +52,38 @@ namespace osu.Game.Rulesets.Osu.Tests public void TestSpinnerRewindingRotation() { addSeekStep(5000); - AddAssert("is rotation absolute not almost 0", () => !Precision.AlmostEquals(drawableSpinner.Disc.RotationAbsolute, 0, 100)); + AddAssert("is disc rotation not almost 0", () => !Precision.AlmostEquals(drawableSpinner.Disc.Rotation, 0, 100)); + AddAssert("is disc rotation absolute not almost 0", () => !Precision.AlmostEquals(drawableSpinner.Disc.RotationAbsolute, 0, 100)); addSeekStep(0); - AddAssert("is rotation absolute almost 0", () => Precision.AlmostEquals(drawableSpinner.Disc.RotationAbsolute, 0, 100)); + AddAssert("is disc rotation almost 0", () => Precision.AlmostEquals(drawableSpinner.Disc.Rotation, 0, 100)); + AddAssert("is disc rotation absolute almost 0", () => Precision.AlmostEquals(drawableSpinner.Disc.RotationAbsolute, 0, 100)); } [Test] public void TestSpinnerMiddleRewindingRotation() { - double estimatedRotation = 0; + double finalAbsoluteDiscRotation = 0, finalRelativeDiscRotation = 0, finalSpinnerSymbolRotation = 0; addSeekStep(5000); - AddStep("retrieve rotation", () => estimatedRotation = drawableSpinner.Disc.RotationAbsolute); + AddStep("retrieve disc relative rotation", () => finalRelativeDiscRotation = drawableSpinner.Disc.Rotation); + AddStep("retrieve disc absolute rotation", () => finalAbsoluteDiscRotation = drawableSpinner.Disc.RotationAbsolute); + AddStep("retrieve spinner symbol rotation", () => finalSpinnerSymbolRotation = spinnerSymbol.Rotation); addSeekStep(2500); + AddUntilStep("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. + () => Precision.AlmostEquals(drawableSpinner.Disc.Rotation, finalRelativeDiscRotation / 2, 100)); + AddUntilStep("symbol rotation rewound", + () => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation / 2, 100)); + addSeekStep(5000); - AddAssert("is rotation absolute almost same", () => Precision.AlmostEquals(drawableSpinner.Disc.RotationAbsolute, estimatedRotation, 100)); + AddAssert("is disc rotation almost same", + () => Precision.AlmostEquals(drawableSpinner.Disc.Rotation, finalRelativeDiscRotation, 100)); + AddAssert("is symbol rotation almost same", + () => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation, 100)); + AddAssert("is disc rotation absolute almost same", + () => Precision.AlmostEquals(drawableSpinner.Disc.RotationAbsolute, finalAbsoluteDiscRotation, 100)); } [Test] From 31a1f8b9a75b944c6e52e8089f26feb671c061cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 8 Jul 2020 22:37:45 +0200 Subject: [PATCH 2320/2376] Add coverage for spinning in both directions --- .../TestSceneSpinnerRotation.cs | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index 579c47f585..de06570d3c 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -15,6 +15,11 @@ using osuTK; using System.Collections.Generic; using System.Linq; using osu.Framework.Graphics.Sprites; +using osu.Game.Replays; +using osu.Game.Rulesets.Osu.Replays; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Rulesets.Replays; +using osu.Game.Scoring; using osu.Game.Storyboards; using static osu.Game.Tests.Visual.OsuTestScene.ClockBackedTestWorkingBeatmap; @@ -86,6 +91,44 @@ namespace osu.Game.Rulesets.Osu.Tests () => Precision.AlmostEquals(drawableSpinner.Disc.RotationAbsolute, finalAbsoluteDiscRotation, 100)); } + [Test] + public void TestRotationDirection([Values(true, false)] bool clockwise) + { + if (clockwise) + { + AddStep("flip replay", () => + { + var drawableRuleset = this.ChildrenOfType().Single(); + var score = drawableRuleset.ReplayScore; + var scoreWithFlippedReplay = new Score + { + ScoreInfo = score.ScoreInfo, + Replay = flipReplay(score.Replay) + }; + drawableRuleset.SetReplayScore(scoreWithFlippedReplay); + }); + } + + addSeekStep(5000); + + AddAssert("disc spin direction correct", () => clockwise ? drawableSpinner.Disc.Rotation > 0 : drawableSpinner.Disc.Rotation < 0); + AddAssert("spinner symbol direction correct", () => clockwise ? spinnerSymbol.Rotation > 0 : spinnerSymbol.Rotation < 0); + } + + private Replay flipReplay(Replay scoreReplay) => new Replay + { + Frames = scoreReplay + .Frames + .Cast() + .Select(replayFrame => + { + var flippedPosition = new Vector2(OsuPlayfield.BASE_SIZE.X - replayFrame.Position.X, replayFrame.Position.Y); + return new OsuReplayFrame(replayFrame.Time, flippedPosition, replayFrame.Actions.ToArray()); + }) + .Cast() + .ToList() + }; + [Test] public void TestSpinPerMinuteOnRewind() { From 213dfac344f67f776874217bca80f7eaa2479bf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 8 Jul 2020 20:56:47 +0200 Subject: [PATCH 2321/2376] Fix broken spinner rotation logic --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 4d37622be5..12034ad333 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -197,7 +197,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables float targetScale = relativeCircleScale + (1 - relativeCircleScale) * Progress; Disc.Scale = new Vector2((float)Interpolation.Lerp(Disc.Scale.X, targetScale, Math.Clamp(Math.Abs(Time.Elapsed) / 100, 0, 1))); - symbol.Rotation = (float)Interpolation.Lerp(symbol.Rotation, Disc.RotationAbsolute / 2, Math.Clamp(Math.Abs(Time.Elapsed) / 40, 0, 1)); + symbol.Rotation = (float)Interpolation.Lerp(symbol.Rotation, Disc.Rotation / 2, Math.Clamp(Math.Abs(Time.Elapsed) / 40, 0, 1)); } protected override void UpdateInitialTransforms() @@ -207,9 +207,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables circleContainer.ScaleTo(Spinner.Scale * 0.3f); circleContainer.ScaleTo(Spinner.Scale, HitObject.TimePreempt / 1.4f, Easing.OutQuint); - Disc.RotateTo(-720); - symbol.RotateTo(-720); - mainContainer .ScaleTo(0) .ScaleTo(Spinner.Scale * circle.DrawHeight / DrawHeight * 1.4f, HitObject.TimePreempt - 150, Easing.OutQuint) From 4cd874280cd853722d5cae76c5a2af16c99b58f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 8 Jul 2020 21:05:41 +0200 Subject: [PATCH 2322/2376] Add clarifying xmldoc for RotationAbsolute --- .../Objects/Drawables/Pieces/SpinnerDisc.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs index d4ef039b79..408aba54d7 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs @@ -73,6 +73,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces } } + /// + /// The total rotation performed on the spinner disc, disregarding the spin direction. + /// + /// + /// This value is always non-negative and is monotonically increasing with time + /// (i.e. will only increase if time is passing forward, but can decrease during rewind). + /// + /// + /// If the spinner is spun 360 degrees clockwise and then 360 degrees counter-clockwise, + /// this property will return the value of 720 (as opposed to 0 for ). + /// + public float RotationAbsolute; + /// /// Whether currently in the correct time range to allow spinning. /// @@ -88,7 +101,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces private float lastAngle; private float currentRotation; - public float RotationAbsolute; private int completeTick; private bool updateCompleteTick() => completeTick != (completeTick = (int)(RotationAbsolute / 360)); From c10cf2ef496544f9dfcd9c3a0533ae29c81176fa Mon Sep 17 00:00:00 2001 From: Joehu Date: Wed, 8 Jul 2020 19:01:12 -0700 Subject: [PATCH 2323/2376] Fix multi header title not aligning correctly when changing screens --- osu.Game/Screens/Multi/Header.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Multi/Header.cs b/osu.Game/Screens/Multi/Header.cs index e27fa154af..653cb3791a 100644 --- a/osu.Game/Screens/Multi/Header.cs +++ b/osu.Game/Screens/Multi/Header.cs @@ -95,22 +95,22 @@ namespace osu.Game.Screens.Multi { new OsuSpriteText { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, Font = OsuFont.GetFont(size: 24), Text = "Multiplayer" }, dot = new OsuSpriteText { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, Font = OsuFont.GetFont(size: 24), Text = "·" }, pageTitle = new OsuSpriteText { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, Font = OsuFont.GetFont(size: 24), Text = "Lounge" } From efb2c2f4aee0df8952d1efeac6817a49a6d1b391 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 9 Jul 2020 12:01:00 +0900 Subject: [PATCH 2324/2376] Rename variable to be more clear on purpose --- osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs | 2 +- osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs | 8 ++++---- .../Objects/Drawables/DrawableSpinner.cs | 4 ++-- .../Objects/Drawables/Pieces/SpinnerDisc.cs | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs index 65bed071cd..8cb7f3f4b6 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs @@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.Tests if (auto && !userTriggered && Time.Current > Spinner.StartTime + Spinner.Duration / 2 && Progress < 1) { // force completion only once to not break human interaction - Disc.RotationAbsolute = Spinner.SpinsRequired * 360; + Disc.CumulativeRotation = Spinner.SpinsRequired * 360; auto = false; } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index de06570d3c..6b1394d799 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -58,11 +58,11 @@ namespace osu.Game.Rulesets.Osu.Tests { addSeekStep(5000); AddAssert("is disc rotation not almost 0", () => !Precision.AlmostEquals(drawableSpinner.Disc.Rotation, 0, 100)); - AddAssert("is disc rotation absolute not almost 0", () => !Precision.AlmostEquals(drawableSpinner.Disc.RotationAbsolute, 0, 100)); + AddAssert("is disc rotation absolute not almost 0", () => !Precision.AlmostEquals(drawableSpinner.Disc.CumulativeRotation, 0, 100)); addSeekStep(0); AddAssert("is disc rotation almost 0", () => Precision.AlmostEquals(drawableSpinner.Disc.Rotation, 0, 100)); - AddAssert("is disc rotation absolute almost 0", () => Precision.AlmostEquals(drawableSpinner.Disc.RotationAbsolute, 0, 100)); + AddAssert("is disc rotation absolute almost 0", () => Precision.AlmostEquals(drawableSpinner.Disc.CumulativeRotation, 0, 100)); } [Test] @@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Osu.Tests addSeekStep(5000); AddStep("retrieve disc relative rotation", () => finalRelativeDiscRotation = drawableSpinner.Disc.Rotation); - AddStep("retrieve disc absolute rotation", () => finalAbsoluteDiscRotation = drawableSpinner.Disc.RotationAbsolute); + AddStep("retrieve disc absolute rotation", () => finalAbsoluteDiscRotation = drawableSpinner.Disc.CumulativeRotation); AddStep("retrieve spinner symbol rotation", () => finalSpinnerSymbolRotation = spinnerSymbol.Rotation); addSeekStep(2500); @@ -88,7 +88,7 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("is symbol rotation almost same", () => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation, 100)); AddAssert("is disc rotation absolute almost same", - () => Precision.AlmostEquals(drawableSpinner.Disc.RotationAbsolute, finalAbsoluteDiscRotation, 100)); + () => Precision.AlmostEquals(drawableSpinner.Disc.CumulativeRotation, finalAbsoluteDiscRotation, 100)); } [Test] diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 12034ad333..be6766509c 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -138,7 +138,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables positionBindable.BindTo(HitObject.PositionBindable); } - public float Progress => Math.Clamp(Disc.RotationAbsolute / 360 / Spinner.SpinsRequired, 0, 1); + public float Progress => Math.Clamp(Disc.CumulativeRotation / 360 / Spinner.SpinsRequired, 0, 1); protected override void CheckForResult(bool userTriggered, double timeOffset) { @@ -191,7 +191,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables circle.Rotation = Disc.Rotation; Ticks.Rotation = Disc.Rotation; - SpmCounter.SetRotation(Disc.RotationAbsolute); + SpmCounter.SetRotation(Disc.CumulativeRotation); float relativeCircleScale = Spinner.Scale * circle.DrawHeight / mainContainer.DrawHeight; float targetScale = relativeCircleScale + (1 - relativeCircleScale) * Progress; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs index 408aba54d7..35819cd05e 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs @@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces /// If the spinner is spun 360 degrees clockwise and then 360 degrees counter-clockwise, /// this property will return the value of 720 (as opposed to 0 for ). /// - public float RotationAbsolute; + public float CumulativeRotation; /// /// Whether currently in the correct time range to allow spinning. @@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces private float currentRotation; private int completeTick; - private bool updateCompleteTick() => completeTick != (completeTick = (int)(RotationAbsolute / 360)); + private bool updateCompleteTick() => completeTick != (completeTick = (int)(CumulativeRotation / 360)); private bool rotationTransferred; @@ -161,7 +161,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces } currentRotation += angle; - RotationAbsolute += Math.Abs(angle) * Math.Sign(Clock.ElapsedFrameTime); + CumulativeRotation += Math.Abs(angle) * Math.Sign(Clock.ElapsedFrameTime); } } } From efdf179906dc810e04d444cbc028ce1d58591d17 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 9 Jul 2020 12:31:20 +0900 Subject: [PATCH 2325/2376] Replace poo icon at disclaimer screen --- osu.Game/Screens/Menu/Disclaimer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/Disclaimer.cs b/osu.Game/Screens/Menu/Disclaimer.cs index 35091028ae..986de1edf0 100644 --- a/osu.Game/Screens/Menu/Disclaimer.cs +++ b/osu.Game/Screens/Menu/Disclaimer.cs @@ -51,7 +51,7 @@ namespace osu.Game.Screens.Menu { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Icon = FontAwesome.Solid.Poo, + Icon = FontAwesome.Solid.Flask, Size = new Vector2(icon_size), Y = icon_y, }, From bbbe8d6f685215fcce28912f65f81077c128ce70 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 9 Jul 2020 13:47:11 +0900 Subject: [PATCH 2326/2376] Remove group selector for now, tidy up code somewhat --- .../Graphics/UserInterface/OsuTabControl.cs | 4 +- osu.Game/Screens/Select/FilterControl.cs | 116 ++++++++---------- osu.Game/Screens/Select/SongSelect.cs | 1 - 3 files changed, 51 insertions(+), 70 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuTabControl.cs b/osu.Game/Graphics/UserInterface/OsuTabControl.cs index c2feca171b..61501b0cd8 100644 --- a/osu.Game/Graphics/UserInterface/OsuTabControl.cs +++ b/osu.Game/Graphics/UserInterface/OsuTabControl.cs @@ -23,6 +23,8 @@ namespace osu.Game.Graphics.UserInterface { private Color4 accentColour; + public const float HORIZONTAL_SPACING = 10; + public virtual Color4 AccentColour { get => accentColour; @@ -54,7 +56,7 @@ namespace osu.Game.Graphics.UserInterface public OsuTabControl() { - TabContainer.Spacing = new Vector2(10f, 0f); + TabContainer.Spacing = new Vector2(HORIZONTAL_SPACING, 0f); AddInternal(strip = new Box { diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index d613ce649a..a26664325e 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -2,21 +2,20 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osuTK; -using osuTK.Graphics; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.UserInterface; -using osu.Game.Graphics; -using osu.Game.Graphics.UserInterface; -using osu.Game.Screens.Select.Filter; -using Container = osu.Framework.Graphics.Containers.Container; using osu.Framework.Graphics.Shapes; -using osu.Game.Configuration; -using osu.Game.Rulesets; using osu.Framework.Input.Events; +using osu.Game.Configuration; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets; +using osu.Game.Screens.Select.Filter; +using osuTK; +using osuTK.Graphics; namespace osu.Game.Screens.Select { @@ -26,9 +25,7 @@ namespace osu.Game.Screens.Select public Action FilterChanged; - private readonly OsuTabControl sortTabs; - - private readonly TabControl groupTabs; + private OsuTabControl sortTabs; private Bindable sortMode; @@ -56,19 +53,39 @@ namespace osu.Game.Screens.Select return criteria; } - private readonly SeekLimitedSearchTextBox searchTextBox; + private SeekLimitedSearchTextBox searchTextBox; public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => - base.ReceivePositionalInputAt(screenSpacePos) || groupTabs.ReceivePositionalInputAt(screenSpacePos) || sortTabs.ReceivePositionalInputAt(screenSpacePos); + base.ReceivePositionalInputAt(screenSpacePos) || sortTabs.ReceivePositionalInputAt(screenSpacePos); - public FilterControl() + [BackgroundDependencyLoader(permitNulls: true)] + private void load(OsuColour colours, IBindable parentRuleset, OsuConfigManager config) { + config.BindWith(OsuSetting.ShowConvertedBeatmaps, showConverted); + showConverted.ValueChanged += _ => updateCriteria(); + + config.BindWith(OsuSetting.DisplayStarsMinimum, minimumStars); + minimumStars.ValueChanged += _ => updateCriteria(); + + config.BindWith(OsuSetting.DisplayStarsMaximum, maximumStars); + maximumStars.ValueChanged += _ => updateCriteria(); + + ruleset.BindTo(parentRuleset); + ruleset.BindValueChanged(_ => updateCriteria()); + + sortMode = config.GetBindable(OsuSetting.SongSelectSortingMode); + groupMode = config.GetBindable(OsuSetting.SongSelectGroupingMode); + + groupMode.BindValueChanged(_ => updateCriteria()); + sortMode.BindValueChanged(_ => updateCriteria()); + Children = new Drawable[] { - Background = new Box + new Box { Colour = Color4.Black, Alpha = 0.8f, + Width = 2, RelativeSizeAxes = Axes.Both, }, new Container @@ -96,33 +113,28 @@ namespace osu.Game.Screens.Select Direction = FillDirection.Horizontal, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, + Spacing = new Vector2(OsuTabControl.HORIZONTAL_SPACING, 0), Children = new Drawable[] { - groupTabs = new OsuTabControl - { - RelativeSizeAxes = Axes.X, - Height = 24, - Width = 0.5f, - AutoSort = true, - }, - //spriteText = new OsuSpriteText - //{ - // Font = @"Exo2.0-Bold", - // Text = "Sort results by", - // Size = 14, - // Margin = new MarginPadding - // { - // Top = 5, - // Bottom = 5 - // }, - //}, sortTabs = new OsuTabControl { RelativeSizeAxes = Axes.X, Width = 0.5f, Height = 24, AutoSort = true, - } + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + AccentColour = colours.GreenLight, + Current = { BindTarget = sortMode } + }, + new OsuSpriteText + { + Text = "Sort by", + Font = OsuFont.GetFont(size: 14), + Margin = new MarginPadding(5), + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + }, } }, } @@ -131,8 +143,7 @@ namespace osu.Game.Screens.Select searchTextBox.Current.ValueChanged += _ => FilterChanged?.Invoke(CreateCriteria()); - groupTabs.PinItem(GroupMode.All); - groupTabs.PinItem(GroupMode.RecentlyPlayed); + updateCriteria(); } public void Deactivate() @@ -156,37 +167,6 @@ namespace osu.Game.Screens.Select private readonly Bindable minimumStars = new BindableDouble(); private readonly Bindable maximumStars = new BindableDouble(); - public readonly Box Background; - - [BackgroundDependencyLoader(permitNulls: true)] - private void load(OsuColour colours, IBindable parentRuleset, OsuConfigManager config) - { - sortTabs.AccentColour = colours.GreenLight; - - config.BindWith(OsuSetting.ShowConvertedBeatmaps, showConverted); - showConverted.ValueChanged += _ => updateCriteria(); - - config.BindWith(OsuSetting.DisplayStarsMinimum, minimumStars); - minimumStars.ValueChanged += _ => updateCriteria(); - - config.BindWith(OsuSetting.DisplayStarsMaximum, maximumStars); - maximumStars.ValueChanged += _ => updateCriteria(); - - ruleset.BindTo(parentRuleset); - ruleset.BindValueChanged(_ => updateCriteria()); - - sortMode = config.GetBindable(OsuSetting.SongSelectSortingMode); - groupMode = config.GetBindable(OsuSetting.SongSelectGroupingMode); - - sortTabs.Current.BindTo(sortMode); - groupTabs.Current.BindTo(groupMode); - - groupMode.BindValueChanged(_ => updateCriteria()); - sortMode.BindValueChanged(_ => updateCriteria()); - - updateCriteria(); - } - private void updateCriteria() => FilterChanged?.Invoke(CreateCriteria()); protected override bool OnClick(ClickEvent e) => true; diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index d613b0ae8d..e3705b15fa 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -173,7 +173,6 @@ namespace osu.Game.Screens.Select RelativeSizeAxes = Axes.X, Height = FilterControl.HEIGHT, FilterChanged = ApplyFilterToCarousel, - Background = { Width = 2 }, }, new GridContainer // used for max width implementation { From f231b5925f142d305c62482912502a93668401cf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 9 Jul 2020 13:47:23 +0900 Subject: [PATCH 2327/2376] Add "show converted" checkbox to song select for convenience --- osu.Game/Screens/Select/FilterControl.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index a26664325e..e111ec4b15 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -116,6 +116,13 @@ namespace osu.Game.Screens.Select Spacing = new Vector2(OsuTabControl.HORIZONTAL_SPACING, 0), Children = new Drawable[] { + new OsuTabControlCheckbox + { + Text = "Show converted", + Current = config.GetBindable(OsuSetting.ShowConvertedBeatmaps), + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + }, sortTabs = new OsuTabControl { RelativeSizeAxes = Axes.X, From 04ce436f6aad199ba7f07a440aaa740b85b64a17 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 9 Jul 2020 14:46:58 +0900 Subject: [PATCH 2328/2376] Dispose beatmap lookup task scheduler --- osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs b/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs index d47d37806e..3106d1143e 100644 --- a/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs +++ b/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs @@ -183,6 +183,7 @@ namespace osu.Game.Beatmaps public void Dispose() { cacheDownloadRequest?.Dispose(); + updateScheduler?.Dispose(); } [Serializable] From 3a5784c4102a221440686bc30badcefb4eb3a2d9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 9 Jul 2020 15:08:03 +0900 Subject: [PATCH 2329/2376] Ensure directories are deleted before migration tests run --- .../NonVisual/CustomDataDirectoryTest.cs | 106 +++++++++++------- 1 file changed, 66 insertions(+), 40 deletions(-) diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs index 8ea0e34214..199e69a19d 100644 --- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs @@ -19,24 +19,18 @@ namespace osu.Game.Tests.NonVisual [TestFixture] public class CustomDataDirectoryTest { - [SetUp] - public void SetUp() - { - if (Directory.Exists(customPath)) - Directory.Delete(customPath, true); - } - [Test] public void TestDefaultDirectory() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestDefaultDirectory))) + using (HeadlessGameHost host = new CustomTestHeadlessGameHost(nameof(TestDefaultDirectory))) { try { + string defaultStorageLocation = getDefaultLocationFor(nameof(TestDefaultDirectory)); + var osu = loadOsu(host); var storage = osu.Dependencies.Get(); - string defaultStorageLocation = Path.Combine(RuntimeInfo.StartupDirectory, "headless", nameof(TestDefaultDirectory)); Assert.That(storage.GetFullPath("."), Is.EqualTo(defaultStorageLocation)); } finally @@ -46,21 +40,14 @@ namespace osu.Game.Tests.NonVisual } } - private string customPath => Path.Combine(RuntimeInfo.StartupDirectory, "custom-path"); - [Test] public void TestCustomDirectory() { - using (var host = new HeadlessGameHost(nameof(TestCustomDirectory))) + string customPath = prepareCustomPath(); + + using (var host = new CustomTestHeadlessGameHost(nameof(TestCustomDirectory))) { - string defaultStorageLocation = Path.Combine(RuntimeInfo.StartupDirectory, "headless", nameof(TestCustomDirectory)); - - // need access before the game has constructed its own storage yet. - Storage storage = new DesktopStorage(defaultStorageLocation, host); - // manual cleaning so we can prepare a config file. - storage.DeleteDirectory(string.Empty); - - using (var storageConfig = new StorageConfigManager(storage)) + using (var storageConfig = new StorageConfigManager(host.InitialStorage)) storageConfig.Set(StorageConfig.FullPath, customPath); try @@ -68,7 +55,7 @@ namespace osu.Game.Tests.NonVisual var osu = loadOsu(host); // switch to DI'd storage - storage = osu.Dependencies.Get(); + var storage = osu.Dependencies.Get(); Assert.That(storage.GetFullPath("."), Is.EqualTo(customPath)); } @@ -82,16 +69,11 @@ namespace osu.Game.Tests.NonVisual [Test] public void TestSubDirectoryLookup() { - using (var host = new HeadlessGameHost(nameof(TestSubDirectoryLookup))) + string customPath = prepareCustomPath(); + + using (var host = new CustomTestHeadlessGameHost(nameof(TestSubDirectoryLookup))) { - string defaultStorageLocation = Path.Combine(RuntimeInfo.StartupDirectory, "headless", nameof(TestSubDirectoryLookup)); - - // need access before the game has constructed its own storage yet. - Storage storage = new DesktopStorage(defaultStorageLocation, host); - // manual cleaning so we can prepare a config file. - storage.DeleteDirectory(string.Empty); - - using (var storageConfig = new StorageConfigManager(storage)) + using (var storageConfig = new StorageConfigManager(host.InitialStorage)) storageConfig.Set(StorageConfig.FullPath, customPath); try @@ -99,7 +81,7 @@ namespace osu.Game.Tests.NonVisual var osu = loadOsu(host); // switch to DI'd storage - storage = osu.Dependencies.Get(); + var storage = osu.Dependencies.Get(); string actualTestFile = Path.Combine(customPath, "rulesets", "test"); @@ -120,10 +102,14 @@ namespace osu.Game.Tests.NonVisual [Test] public void TestMigration() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestMigration))) + string customPath = prepareCustomPath(); + + using (HeadlessGameHost host = new CustomTestHeadlessGameHost(nameof(TestMigration))) { try { + string defaultStorageLocation = getDefaultLocationFor(nameof(TestMigration)); + var osu = loadOsu(host); var storage = osu.Dependencies.Get(); @@ -139,8 +125,6 @@ namespace osu.Game.Tests.NonVisual // for testing nested files are not ignored (only top level) host.Storage.GetStorageForDirectory("test-nested").GetStorageForDirectory("cache"); - string defaultStorageLocation = Path.Combine(RuntimeInfo.StartupDirectory, "headless", nameof(TestMigration)); - Assert.That(storage.GetFullPath("."), Is.EqualTo(defaultStorageLocation)); osu.Migrate(customPath); @@ -178,14 +162,15 @@ namespace osu.Game.Tests.NonVisual [Test] public void TestMigrationBetweenTwoTargets() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestMigrationBetweenTwoTargets))) + string customPath = prepareCustomPath(); + string customPath2 = prepareCustomPath("-2"); + + using (HeadlessGameHost host = new CustomTestHeadlessGameHost(nameof(TestMigrationBetweenTwoTargets))) { try { var osu = loadOsu(host); - string customPath2 = $"{customPath}-2"; - const string database_filename = "client.db"; Assert.DoesNotThrow(() => osu.Migrate(customPath)); @@ -207,7 +192,9 @@ namespace osu.Game.Tests.NonVisual [Test] public void TestMigrationToSameTargetFails() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestMigrationToSameTargetFails))) + string customPath = prepareCustomPath(); + + using (HeadlessGameHost host = new CustomTestHeadlessGameHost(nameof(TestMigrationToSameTargetFails))) { try { @@ -226,7 +213,9 @@ namespace osu.Game.Tests.NonVisual [Test] public void TestMigrationToNestedTargetFails() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestMigrationToNestedTargetFails))) + string customPath = prepareCustomPath(); + + using (HeadlessGameHost host = new CustomTestHeadlessGameHost(nameof(TestMigrationToNestedTargetFails))) { try { @@ -253,7 +242,9 @@ namespace osu.Game.Tests.NonVisual [Test] public void TestMigrationToSeeminglyNestedTarget() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestMigrationToSeeminglyNestedTarget))) + string customPath = prepareCustomPath(); + + using (HeadlessGameHost host = new CustomTestHeadlessGameHost(nameof(TestMigrationToSeeminglyNestedTarget))) { try { @@ -282,6 +273,7 @@ namespace osu.Game.Tests.NonVisual var osu = new OsuGameBase(); Task.Run(() => host.Run(osu)); waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time"); + return osu; } @@ -294,5 +286,39 @@ namespace osu.Game.Tests.NonVisual Assert.IsTrue(task.Wait(timeout), failureMessage); } + + private static string getDefaultLocationFor(string testTypeName) + { + string path = Path.Combine(RuntimeInfo.StartupDirectory, "headless", testTypeName); + + if (Directory.Exists(path)) + Directory.Delete(path, true); + + return path; + } + + private string prepareCustomPath(string suffix = "") + { + string path = Path.Combine(RuntimeInfo.StartupDirectory, $"custom-path{suffix}"); + + if (Directory.Exists(path)) + Directory.Delete(path, true); + + return path; + } + + public class CustomTestHeadlessGameHost : HeadlessGameHost + { + public Storage InitialStorage { get; } + + public CustomTestHeadlessGameHost(string name) + : base(name) + { + string defaultStorageLocation = getDefaultLocationFor(name); + + InitialStorage = new DesktopStorage(defaultStorageLocation, this); + InitialStorage.DeleteDirectory(string.Empty); + } + } } } From 7d59825851258e972bf26a53a70551371b812483 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 9 Jul 2020 15:16:40 +0900 Subject: [PATCH 2330/2376] 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 0563e5319d..ff04c7f120 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 4e6de77e86..e4753e7ee9 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -24,7 +24,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index c31e28638f..91fa003604 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From 69062a3ed1100844be3c69cca4091475465700c7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 9 Jul 2020 17:43:26 +0900 Subject: [PATCH 2331/2376] Remove unused search container in lounge --- osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs index d4b6a3b79f..9c2ed26b52 100644 --- a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs @@ -34,7 +34,7 @@ namespace osu.Game.Screens.Multi.Lounge public LoungeSubScreen() { - SearchContainer searchContainer; + RoomsContainer roomsContainer; InternalChildren = new Drawable[] { @@ -55,14 +55,9 @@ namespace osu.Game.Screens.Multi.Lounge RelativeSizeAxes = Axes.Both, ScrollbarOverlapsContent = false, Padding = new MarginPadding(10), - Child = searchContainer = new SearchContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Child = new RoomsContainer { JoinRequested = joinRequested } - }, + Child = roomsContainer = new RoomsContainer { JoinRequested = joinRequested } }, - loadingLayer = new LoadingLayer(searchContainer), + loadingLayer = new LoadingLayer(roomsContainer), } }, new RoomInspector From 80f6f87e0169b678ab22ff6ac16e4609820cd5f9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 9 Jul 2020 17:28:22 +0900 Subject: [PATCH 2332/2376] Scroll selected room into view on selection --- .../Screens/Multi/Lounge/LoungeSubScreen.cs | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs index 9c2ed26b52..f512b864a6 100644 --- a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -20,21 +21,23 @@ namespace osu.Game.Screens.Multi.Lounge { public override string Title => "Lounge"; - protected readonly FilterControl Filter; + protected FilterControl Filter; private readonly Bindable initialRoomsReceived = new Bindable(); - private readonly Container content; - private readonly LoadingLayer loadingLayer; + private Container content; + private LoadingLayer loadingLayer; [Resolved] private Bindable selectedRoom { get; set; } private bool joiningRoom; - public LoungeSubScreen() + [BackgroundDependencyLoader] + private void load() { RoomsContainer roomsContainer; + OsuScrollContainer scrollContainer; InternalChildren = new Drawable[] { @@ -50,7 +53,7 @@ namespace osu.Game.Screens.Multi.Lounge Width = 0.55f, Children = new Drawable[] { - new OsuScrollContainer + scrollContainer = new OsuScrollContainer { RelativeSizeAxes = Axes.Both, ScrollbarOverlapsContent = false, @@ -70,6 +73,14 @@ namespace osu.Game.Screens.Multi.Lounge }, }, }; + + // scroll selected room into view on selection. + selectedRoom.BindValueChanged(val => + { + var drawable = roomsContainer.Rooms.FirstOrDefault(r => r.Room == val.NewValue); + if (drawable != null) + scrollContainer.ScrollIntoView(drawable); + }); } protected override void LoadComplete() From 1ded94e5be049a9bd5eaba13bc02dc75131b83d8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 9 Jul 2020 18:07:34 +0900 Subject: [PATCH 2333/2376] Add test coverage --- .../Multiplayer/RoomManagerTestScene.cs | 60 ++++++++++++ .../Visual/Multiplayer/TestRoomManager.cs | 35 +++++++ .../TestSceneLoungeRoomsContainer.cs | 91 ++----------------- .../Multiplayer/TestSceneLoungeSubScreen.cs | 57 ++++++++++++ 4 files changed, 160 insertions(+), 83 deletions(-) create mode 100644 osu.Game.Tests/Visual/Multiplayer/RoomManagerTestScene.cs create mode 100644 osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs create mode 100644 osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeSubScreen.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/RoomManagerTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/RoomManagerTestScene.cs new file mode 100644 index 0000000000..ef9bdd5f27 --- /dev/null +++ b/osu.Game.Tests/Visual/Multiplayer/RoomManagerTestScene.cs @@ -0,0 +1,60 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Game.Beatmaps; +using osu.Game.Online.Multiplayer; +using osu.Game.Rulesets; +using osu.Game.Screens.Multi; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + public class RoomManagerTestScene : MultiplayerTestScene + { + [Cached(Type = typeof(IRoomManager))] + protected TestRoomManager RoomManager { get; } = new TestRoomManager(); + + public override void SetUpSteps() + { + base.SetUpSteps(); + + AddStep("clear rooms", () => RoomManager.Rooms.Clear()); + } + + protected void AddRooms(int count, RulesetInfo ruleset = null) + { + AddStep("add rooms", () => + { + for (int i = 0; i < count; i++) + { + var room = new Room + { + RoomID = { Value = i }, + Name = { Value = $"Room {i}" }, + Host = { Value = new User { Username = "Host" } }, + EndDate = { Value = DateTimeOffset.Now + TimeSpan.FromSeconds(10) } + }; + + if (ruleset != null) + { + room.Playlist.Add(new PlaylistItem + { + Ruleset = { Value = ruleset }, + Beatmap = + { + Value = new BeatmapInfo + { + Metadata = new BeatmapMetadata() + } + } + }); + } + + RoomManager.Rooms.Add(room); + } + }); + } + } +} diff --git a/osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs b/osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs new file mode 100644 index 0000000000..67a53307fc --- /dev/null +++ b/osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs @@ -0,0 +1,35 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Bindables; +using osu.Game.Online.Multiplayer; +using osu.Game.Screens.Multi; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + public class TestRoomManager : IRoomManager + { + public event Action RoomsUpdated + { + add { } + remove { } + } + + public readonly BindableList Rooms = new BindableList(); + + public Bindable InitialRoomsReceived { get; } = new Bindable(true); + + IBindableList IRoomManager.Rooms => Rooms; + + public void CreateRoom(Room room, Action onSuccess = null, Action onError = null) => Rooms.Add(room); + + public void JoinRoom(Room room, Action onSuccess = null, Action onError = null) + { + } + + public void PartRoom() + { + } + } +} diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs index 83f2297bd2..5cf3a9d320 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs @@ -1,30 +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.Linq; using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Online.Multiplayer; -using osu.Game.Rulesets; using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Osu; -using osu.Game.Screens.Multi; using osu.Game.Screens.Multi.Lounge.Components; -using osu.Game.Users; using osuTK.Graphics; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneLoungeRoomsContainer : MultiplayerTestScene + public class TestSceneLoungeRoomsContainer : RoomManagerTestScene { - [Cached(Type = typeof(IRoomManager))] - private TestRoomManager roomManager = new TestRoomManager(); - private RoomsContainer container; [BackgroundDependencyLoader] @@ -39,34 +30,27 @@ namespace osu.Game.Tests.Visual.Multiplayer }; } - public override void SetUpSteps() - { - base.SetUpSteps(); - - AddStep("clear rooms", () => roomManager.Rooms.Clear()); - } - [Test] public void TestBasicListChanges() { - addRooms(3); + AddRooms(3); AddAssert("has 3 rooms", () => container.Rooms.Count == 3); - AddStep("remove first room", () => roomManager.Rooms.Remove(roomManager.Rooms.FirstOrDefault())); + AddStep("remove first room", () => RoomManager.Rooms.Remove(RoomManager.Rooms.FirstOrDefault())); AddAssert("has 2 rooms", () => container.Rooms.Count == 2); AddAssert("first room removed", () => container.Rooms.All(r => r.Room.RoomID.Value != 0)); AddStep("select first room", () => container.Rooms.First().Action?.Invoke()); - AddAssert("first room selected", () => Room == roomManager.Rooms.First()); + AddAssert("first room selected", () => Room == RoomManager.Rooms.First()); AddStep("join first room", () => container.Rooms.First().Action?.Invoke()); - AddAssert("first room joined", () => roomManager.Rooms.First().Status.Value is JoinedRoomStatus); + AddAssert("first room joined", () => RoomManager.Rooms.First().Status.Value is JoinedRoomStatus); } [Test] public void TestStringFiltering() { - addRooms(4); + AddRooms(4); AddUntilStep("4 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 4); @@ -82,8 +66,8 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestRulesetFiltering() { - addRooms(2, new OsuRuleset().RulesetInfo); - addRooms(3, new CatchRuleset().RulesetInfo); + AddRooms(2, new OsuRuleset().RulesetInfo); + AddRooms(3, new CatchRuleset().RulesetInfo); AddUntilStep("5 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 5); @@ -96,67 +80,8 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("3 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 3); } - private void addRooms(int count, RulesetInfo ruleset = null) - { - AddStep("add rooms", () => - { - for (int i = 0; i < count; i++) - { - var room = new Room - { - RoomID = { Value = i }, - Name = { Value = $"Room {i}" }, - Host = { Value = new User { Username = "Host" } }, - EndDate = { Value = DateTimeOffset.Now + TimeSpan.FromSeconds(10) } - }; - - if (ruleset != null) - { - room.Playlist.Add(new PlaylistItem - { - Ruleset = { Value = ruleset }, - Beatmap = - { - Value = new BeatmapInfo - { - Metadata = new BeatmapMetadata() - } - } - }); - } - - roomManager.Rooms.Add(room); - } - }); - } - private void joinRequested(Room room) => room.Status.Value = new JoinedRoomStatus(); - private class TestRoomManager : IRoomManager - { - public event Action RoomsUpdated - { - add { } - remove { } - } - - public readonly BindableList Rooms = new BindableList(); - - public Bindable InitialRoomsReceived { get; } = new Bindable(true); - - IBindableList IRoomManager.Rooms => Rooms; - - public void CreateRoom(Room room, Action onSuccess = null, Action onError = null) => Rooms.Add(room); - - public void JoinRoom(Room room, Action onSuccess = null, Action onError = null) - { - } - - public void PartRoom() - { - } - } - private class JoinedRoomStatus : RoomStatus { public override string Message => "Joined"; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeSubScreen.cs new file mode 100644 index 0000000000..475c39c9dc --- /dev/null +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeSubScreen.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.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Screens; +using osu.Framework.Testing; +using osu.Game.Graphics.Containers; +using osu.Game.Screens.Multi.Lounge; +using osu.Game.Screens.Multi.Lounge.Components; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + public class TestSceneLoungeSubScreen : RoomManagerTestScene + { + private LoungeSubScreen loungeScreen; + + [BackgroundDependencyLoader] + private void load() + { + Child = new ScreenStack(loungeScreen = new LoungeSubScreen + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 0.5f, + }); + } + + public override void SetUpSteps() + { + base.SetUpSteps(); + + AddStep("clear rooms", () => RoomManager.Rooms.Clear()); + } + + private RoomsContainer roomsContainer => loungeScreen.ChildrenOfType().First(); + + [Test] + public void TestScrollSelectedIntoView() + { + AddRooms(30); + + AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.Rooms.First())); + + AddStep("select last room", () => roomsContainer.Rooms.Last().Action?.Invoke()); + + AddUntilStep("first room is masked", () => !checkRoomVisible(roomsContainer.Rooms.First())); + AddUntilStep("last room is not masked", () => checkRoomVisible(roomsContainer.Rooms.Last())); + } + + private bool checkRoomVisible(DrawableRoom room) => + loungeScreen.ChildrenOfType().First().ScreenSpaceDrawQuad + .Contains(room.ScreenSpaceDrawQuad.Centre); + } +} From 95096cbf5ea87d5f8c70a4b8d247abafd803037a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 9 Jul 2020 18:25:07 +0900 Subject: [PATCH 2334/2376] Use better screen load logic --- .../Visual/Multiplayer/TestSceneLoungeSubScreen.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeSubScreen.cs index 475c39c9dc..c4ec74859b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeSubScreen.cs @@ -20,12 +20,6 @@ namespace osu.Game.Tests.Visual.Multiplayer [BackgroundDependencyLoader] private void load() { - Child = new ScreenStack(loungeScreen = new LoungeSubScreen - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Width = 0.5f, - }); } public override void SetUpSteps() @@ -33,6 +27,14 @@ namespace osu.Game.Tests.Visual.Multiplayer base.SetUpSteps(); AddStep("clear rooms", () => RoomManager.Rooms.Clear()); + AddStep("push screen", () => LoadScreen(loungeScreen = new LoungeSubScreen + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 0.5f, + })); + + AddUntilStep("wait for present", () => loungeScreen.IsCurrentScreen()); } private RoomsContainer roomsContainer => loungeScreen.ChildrenOfType().First(); From 601101147eed5802151d7fd9c23aac3b040feec8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 9 Jul 2020 17:15:16 +0900 Subject: [PATCH 2335/2376] Allow keyboard selection of rooms at the multiplayer lounge --- .../Multi/Lounge/Components/RoomsContainer.cs | 111 ++++++++++++++++-- 1 file changed, 101 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs index f14aa5fd8c..e440c2225c 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs @@ -9,13 +9,17 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Bindings; +using osu.Framework.Threading; +using osu.Game.Extensions; using osu.Game.Graphics.UserInterface; +using osu.Game.Input.Bindings; using osu.Game.Online.Multiplayer; using osuTK; namespace osu.Game.Screens.Multi.Lounge.Components { - public class RoomsContainer : CompositeDrawable + public class RoomsContainer : CompositeDrawable, IKeyBindingHandler { public Action JoinRequested; @@ -88,8 +92,22 @@ namespace osu.Game.Screens.Multi.Lounge.Components private void addRooms(IEnumerable rooms) { - foreach (var r in rooms) - roomFlow.Add(new DrawableRoom(r) { Action = () => selectRoom(r) }); + foreach (var room in rooms) + { + roomFlow.Add(new DrawableRoom(room) + { + Action = () => + { + if (room == selectedRoom.Value) + { + JoinRequested?.Invoke(room); + return; + } + + selectRoom(room); + } + }); + } Filter(filter?.Value); } @@ -115,16 +133,89 @@ namespace osu.Game.Screens.Multi.Lounge.Components private void selectRoom(Room room) { - var drawable = roomFlow.FirstOrDefault(r => r.Room == room); - - if (drawable != null && drawable.State == SelectionState.Selected) - JoinRequested?.Invoke(room); - else - roomFlow.Children.ForEach(r => r.State = r.Room == room ? SelectionState.Selected : SelectionState.NotSelected); - + roomFlow.Children.ForEach(r => r.State = r.Room == room ? SelectionState.Selected : SelectionState.NotSelected); selectedRoom.Value = room; } + #region Key selection logic + + public bool OnPressed(GlobalAction action) + { + switch (action) + { + case GlobalAction.SelectNext: + beginRepeatSelection(() => selectNext(1), action); + return true; + + case GlobalAction.SelectPrevious: + beginRepeatSelection(() => selectNext(-1), action); + return true; + } + + return false; + } + + public void OnReleased(GlobalAction action) + { + switch (action) + { + case GlobalAction.SelectNext: + case GlobalAction.SelectPrevious: + endRepeatSelection(action); + break; + } + } + + private ScheduledDelegate repeatDelegate; + private object lastRepeatSource; + + /// + /// Begin repeating the specified selection action. + /// + /// The action to perform. + /// The source of the action. Used in conjunction with to only cancel the correct action (most recently pressed key). + private void beginRepeatSelection(Action action, object source) + { + endRepeatSelection(); + + lastRepeatSource = source; + repeatDelegate = this.BeginKeyRepeat(Scheduler, action); + } + + private void endRepeatSelection(object source = null) + { + // only the most recent source should be able to cancel the current action. + if (source != null && !EqualityComparer.Default.Equals(lastRepeatSource, source)) + return; + + repeatDelegate?.Cancel(); + repeatDelegate = null; + lastRepeatSource = null; + } + + private void selectNext(int direction) + { + var visibleRooms = Rooms.AsEnumerable().Where(r => r.IsPresent); + + Room room; + + if (selectedRoom.Value == null) + room = visibleRooms.FirstOrDefault()?.Room; + else + { + if (direction < 0) + visibleRooms = visibleRooms.Reverse(); + + room = visibleRooms.SkipWhile(r => r.Room != selectedRoom.Value).Skip(1).FirstOrDefault()?.Room; + } + + // we already have a valid selection only change selection if we still have a room to switch to. + if (room != null) + selectRoom(room); + } + + #endregion + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); From 115bb408166587431ea98a936298ea1f6e9df5ac Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 9 Jul 2020 17:33:02 +0900 Subject: [PATCH 2336/2376] Select via select action --- .../SearchableList/SearchableListFilterControl.cs | 2 -- .../Multi/Lounge/Components/RoomsContainer.cs | 15 +++++++++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/SearchableList/SearchableListFilterControl.cs b/osu.Game/Overlays/SearchableList/SearchableListFilterControl.cs index d31470e685..de5e558943 100644 --- a/osu.Game/Overlays/SearchableList/SearchableListFilterControl.cs +++ b/osu.Game/Overlays/SearchableList/SearchableListFilterControl.cs @@ -136,8 +136,6 @@ namespace osu.Game.Overlays.SearchableList private class FilterSearchTextBox : SearchTextBox { - protected override bool AllowCommit => true; - [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs index e440c2225c..bf153b77df 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs @@ -100,7 +100,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components { if (room == selectedRoom.Value) { - JoinRequested?.Invoke(room); + joinSelected(); return; } @@ -137,12 +137,23 @@ namespace osu.Game.Screens.Multi.Lounge.Components selectedRoom.Value = room; } - #region Key selection logic + private void joinSelected() + { + if (selectedRoom.Value == null) return; + + JoinRequested?.Invoke(selectedRoom.Value); + } + + #region Key selection logic (shared with BeatmapCarousel) public bool OnPressed(GlobalAction action) { switch (action) { + case GlobalAction.Select: + joinSelected(); + return true; + case GlobalAction.SelectNext: beginRepeatSelection(() => selectNext(1), action); return true; From 25ddc5784ddd79df9ac9abe2379006b64aa07424 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 9 Jul 2020 18:55:10 +0900 Subject: [PATCH 2337/2376] Change multiplayer tests to have null room by default --- .../Visual/Multiplayer/TestSceneLoungeRoomInfo.cs | 2 +- .../Multiplayer/TestSceneMatchBeatmapDetailArea.cs | 2 +- .../Visual/Multiplayer/TestSceneMatchHeader.cs | 1 + .../Visual/Multiplayer/TestSceneMatchLeaderboard.cs | 3 ++- .../Visual/Multiplayer/TestSceneMatchSongSelect.cs | 3 ++- .../Visual/Multiplayer/TestSceneMatchSubScreen.cs | 2 +- .../Multiplayer/TestSceneOverlinedParticipants.cs | 8 +++++--- .../Visual/Multiplayer/TestSceneOverlinedPlaylist.cs | 2 ++ .../Visual/Multiplayer/TestSceneParticipantsList.cs | 10 ++++++++-- osu.Game/Tests/Visual/MultiplayerTestScene.cs | 2 +- 10 files changed, 24 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs index 8b74eb5f27..cdad37a9ad 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs @@ -16,7 +16,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [SetUp] public void Setup() => Schedule(() => { - Room.CopyFrom(new Room()); + Room = new Room(); Child = new RoomInfo { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs index 24d9f5ab12..01cd26fbe5 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs @@ -26,7 +26,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [SetUp] public void Setup() => Schedule(() => { - Room.Playlist.Clear(); + Room = new Room(); Child = new MatchBeatmapDetailArea { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs index 38eb3181bf..e5943105b7 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs @@ -14,6 +14,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { public TestSceneMatchHeader() { + Room = new Room(); Room.Playlist.Add(new PlaylistItem { Beatmap = diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs index 7ba1782a28..c24c6c4ba3 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs @@ -6,6 +6,7 @@ using Newtonsoft.Json; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Online.API; +using osu.Game.Online.Multiplayer; using osu.Game.Screens.Multi.Match.Components; using osu.Game.Users; using osuTK; @@ -18,7 +19,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public TestSceneMatchLeaderboard() { - Room.RoomID.Value = 3; + Room = new Room { RoomID = { Value = 3 } }; Add(new MatchLeaderboard { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs index 5cff2d7d05..c62479faa0 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs @@ -14,6 +14,7 @@ using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Online.Multiplayer; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Screens.Multi.Components; @@ -95,7 +96,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [SetUp] public void Setup() => Schedule(() => { - Room.Playlist.Clear(); + Room = new Room(); }); [Test] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs index 66091f5679..2e22317539 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs @@ -48,7 +48,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [SetUp] public void Setup() => Schedule(() => { - Room.CopyFrom(new Room()); + Room = new Room(); }); [SetUpSteps] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedParticipants.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedParticipants.cs index 7ea3bba23f..2b4cac06bd 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedParticipants.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedParticipants.cs @@ -3,6 +3,7 @@ using NUnit.Framework; using osu.Framework.Graphics; +using osu.Game.Online.Multiplayer; using osu.Game.Screens.Multi.Components; using osuTK; @@ -12,10 +13,11 @@ namespace osu.Game.Tests.Visual.Multiplayer { protected override bool UseOnlineAPI => true; - public TestSceneOverlinedParticipants() + [SetUp] + public void Setup() => Schedule(() => { - Room.RoomID.Value = 7; - } + Room = new Room { RoomID = { Value = 7 } }; + }); [Test] public void TestHorizontalLayout() diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedPlaylist.cs index 14b7934dc7..88b2a6a4bc 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedPlaylist.cs @@ -16,6 +16,8 @@ namespace osu.Game.Tests.Visual.Multiplayer public TestSceneOverlinedPlaylist() { + Room = new Room { RoomID = { Value = 7 } }; + for (int i = 0; i < 10; i++) { Room.Playlist.Add(new PlaylistItem diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneParticipantsList.cs index 9c4c45f94a..f71c5fc5d2 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneParticipantsList.cs @@ -1,7 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using NUnit.Framework; using osu.Framework.Graphics; +using osu.Game.Online.Multiplayer; using osu.Game.Screens.Multi.Components; namespace osu.Game.Tests.Visual.Multiplayer @@ -10,10 +12,14 @@ namespace osu.Game.Tests.Visual.Multiplayer { protected override bool UseOnlineAPI => true; + [SetUp] + public void Setup() => Schedule(() => + { + Room = new Room { RoomID = { Value = 7 } }; + }); + public TestSceneParticipantsList() { - Room.RoomID.Value = 7; - Add(new ParticipantsList { RelativeSizeAxes = Axes.Both }); } } diff --git a/osu.Game/Tests/Visual/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/MultiplayerTestScene.cs index ffb431b4d3..4d073f16f4 100644 --- a/osu.Game/Tests/Visual/MultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/MultiplayerTestScene.cs @@ -10,7 +10,7 @@ namespace osu.Game.Tests.Visual public abstract class MultiplayerTestScene : ScreenTestScene { [Cached] - private readonly Bindable currentRoom = new Bindable(new Room()); + private readonly Bindable currentRoom = new Bindable(); protected Room Room { From 0bc54528018961421a0dc4611791bc9629199ee3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 9 Jul 2020 18:55:18 +0900 Subject: [PATCH 2338/2376] Add test coverage --- .../TestSceneLoungeRoomsContainer.cs | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs index 5cf3a9d320..b1f6ee3e3a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs @@ -11,6 +11,7 @@ using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Osu; using osu.Game.Screens.Multi.Lounge.Components; using osuTK.Graphics; +using osuTK.Input; namespace osu.Game.Tests.Visual.Multiplayer { @@ -41,12 +42,42 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("first room removed", () => container.Rooms.All(r => r.Room.RoomID.Value != 0)); AddStep("select first room", () => container.Rooms.First().Action?.Invoke()); - AddAssert("first room selected", () => Room == RoomManager.Rooms.First()); + AddAssert("first room selected", () => checkRoomSelected(RoomManager.Rooms.First())); AddStep("join first room", () => container.Rooms.First().Action?.Invoke()); AddAssert("first room joined", () => RoomManager.Rooms.First().Status.Value is JoinedRoomStatus); } + [Test] + public void TestKeyboardNavigation() + { + AddRooms(3); + + AddAssert("no selection", () => checkRoomSelected(null)); + + press(Key.Down); + AddAssert("first room selected", () => checkRoomSelected(RoomManager.Rooms.First())); + + press(Key.Up); + AddAssert("first room selected", () => checkRoomSelected(RoomManager.Rooms.First())); + + press(Key.Down); + press(Key.Down); + AddAssert("last room selected", () => checkRoomSelected(RoomManager.Rooms.Last())); + + press(Key.Enter); + AddAssert("last room joined", () => RoomManager.Rooms.Last().Status.Value is JoinedRoomStatus); + } + + private void press(Key down) + { + AddStep($"press {down}", () => + { + InputManager.PressKey(down); + InputManager.ReleaseKey(down); + }); + } + [Test] public void TestStringFiltering() { @@ -80,6 +111,8 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("3 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 3); } + private bool checkRoomSelected(Room room) => Room == room; + private void joinRequested(Room room) => room.Status.Value = new JoinedRoomStatus(); private class JoinedRoomStatus : RoomStatus From 43624381bf59cb1afcd96149ee626939b9b594d2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 9 Jul 2020 18:55:10 +0900 Subject: [PATCH 2339/2376] Change multiplayer tests to have null room by default --- .../Visual/Multiplayer/TestSceneLoungeRoomInfo.cs | 2 +- .../Multiplayer/TestSceneMatchBeatmapDetailArea.cs | 2 +- .../Visual/Multiplayer/TestSceneMatchHeader.cs | 1 + .../Visual/Multiplayer/TestSceneMatchLeaderboard.cs | 3 ++- .../Visual/Multiplayer/TestSceneMatchSongSelect.cs | 3 ++- .../Visual/Multiplayer/TestSceneMatchSubScreen.cs | 2 +- .../Multiplayer/TestSceneOverlinedParticipants.cs | 8 +++++--- .../Visual/Multiplayer/TestSceneOverlinedPlaylist.cs | 2 ++ .../Visual/Multiplayer/TestSceneParticipantsList.cs | 10 ++++++++-- osu.Game/Tests/Visual/MultiplayerTestScene.cs | 2 +- 10 files changed, 24 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs index 8b74eb5f27..cdad37a9ad 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs @@ -16,7 +16,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [SetUp] public void Setup() => Schedule(() => { - Room.CopyFrom(new Room()); + Room = new Room(); Child = new RoomInfo { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs index 24d9f5ab12..01cd26fbe5 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs @@ -26,7 +26,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [SetUp] public void Setup() => Schedule(() => { - Room.Playlist.Clear(); + Room = new Room(); Child = new MatchBeatmapDetailArea { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs index 38eb3181bf..e5943105b7 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs @@ -14,6 +14,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { public TestSceneMatchHeader() { + Room = new Room(); Room.Playlist.Add(new PlaylistItem { Beatmap = diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs index 7ba1782a28..c24c6c4ba3 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs @@ -6,6 +6,7 @@ using Newtonsoft.Json; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Online.API; +using osu.Game.Online.Multiplayer; using osu.Game.Screens.Multi.Match.Components; using osu.Game.Users; using osuTK; @@ -18,7 +19,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public TestSceneMatchLeaderboard() { - Room.RoomID.Value = 3; + Room = new Room { RoomID = { Value = 3 } }; Add(new MatchLeaderboard { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs index 5cff2d7d05..c62479faa0 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs @@ -14,6 +14,7 @@ using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Online.Multiplayer; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Screens.Multi.Components; @@ -95,7 +96,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [SetUp] public void Setup() => Schedule(() => { - Room.Playlist.Clear(); + Room = new Room(); }); [Test] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs index 66091f5679..2e22317539 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs @@ -48,7 +48,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [SetUp] public void Setup() => Schedule(() => { - Room.CopyFrom(new Room()); + Room = new Room(); }); [SetUpSteps] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedParticipants.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedParticipants.cs index 7ea3bba23f..2b4cac06bd 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedParticipants.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedParticipants.cs @@ -3,6 +3,7 @@ using NUnit.Framework; using osu.Framework.Graphics; +using osu.Game.Online.Multiplayer; using osu.Game.Screens.Multi.Components; using osuTK; @@ -12,10 +13,11 @@ namespace osu.Game.Tests.Visual.Multiplayer { protected override bool UseOnlineAPI => true; - public TestSceneOverlinedParticipants() + [SetUp] + public void Setup() => Schedule(() => { - Room.RoomID.Value = 7; - } + Room = new Room { RoomID = { Value = 7 } }; + }); [Test] public void TestHorizontalLayout() diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedPlaylist.cs index 14b7934dc7..88b2a6a4bc 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedPlaylist.cs @@ -16,6 +16,8 @@ namespace osu.Game.Tests.Visual.Multiplayer public TestSceneOverlinedPlaylist() { + Room = new Room { RoomID = { Value = 7 } }; + for (int i = 0; i < 10; i++) { Room.Playlist.Add(new PlaylistItem diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneParticipantsList.cs index 9c4c45f94a..f71c5fc5d2 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneParticipantsList.cs @@ -1,7 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using NUnit.Framework; using osu.Framework.Graphics; +using osu.Game.Online.Multiplayer; using osu.Game.Screens.Multi.Components; namespace osu.Game.Tests.Visual.Multiplayer @@ -10,10 +12,14 @@ namespace osu.Game.Tests.Visual.Multiplayer { protected override bool UseOnlineAPI => true; + [SetUp] + public void Setup() => Schedule(() => + { + Room = new Room { RoomID = { Value = 7 } }; + }); + public TestSceneParticipantsList() { - Room.RoomID.Value = 7; - Add(new ParticipantsList { RelativeSizeAxes = Axes.Both }); } } diff --git a/osu.Game/Tests/Visual/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/MultiplayerTestScene.cs index ffb431b4d3..4d073f16f4 100644 --- a/osu.Game/Tests/Visual/MultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/MultiplayerTestScene.cs @@ -10,7 +10,7 @@ namespace osu.Game.Tests.Visual public abstract class MultiplayerTestScene : ScreenTestScene { [Cached] - private readonly Bindable currentRoom = new Bindable(new Room()); + private readonly Bindable currentRoom = new Bindable(); protected Room Room { From 4c24388fc0a6ad7d14ffb50142ea5124c76fad4f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 10 Jul 2020 10:16:44 +0900 Subject: [PATCH 2340/2376] Apply review fixes --- osu.Game.Tests/Visual/Multiplayer/RoomManagerTestScene.cs | 2 +- osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeSubScreen.cs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/RoomManagerTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/RoomManagerTestScene.cs index ef9bdd5f27..46bc279d5c 100644 --- a/osu.Game.Tests/Visual/Multiplayer/RoomManagerTestScene.cs +++ b/osu.Game.Tests/Visual/Multiplayer/RoomManagerTestScene.cs @@ -11,7 +11,7 @@ using osu.Game.Users; namespace osu.Game.Tests.Visual.Multiplayer { - public class RoomManagerTestScene : MultiplayerTestScene + public abstract class RoomManagerTestScene : MultiplayerTestScene { [Cached(Type = typeof(IRoomManager))] protected TestRoomManager RoomManager { get; } = new TestRoomManager(); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeSubScreen.cs index c4ec74859b..68987127d2 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeSubScreen.cs @@ -26,7 +26,6 @@ namespace osu.Game.Tests.Visual.Multiplayer { base.SetUpSteps(); - AddStep("clear rooms", () => RoomManager.Rooms.Clear()); AddStep("push screen", () => LoadScreen(loungeScreen = new LoungeSubScreen { Anchor = Anchor.Centre, From 1bcd673a55437e0c4945ad663b13c5ee1a6dd3d4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 10 Jul 2020 12:07:17 +0900 Subject: [PATCH 2341/2376] Fix crash when switching rooms quickly --- osu.Game/Online/Multiplayer/Room.cs | 32 ++++++++++++++--------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/osu.Game/Online/Multiplayer/Room.cs b/osu.Game/Online/Multiplayer/Room.cs index d074ac9775..66d5d8b3e0 100644 --- a/osu.Game/Online/Multiplayer/Room.cs +++ b/osu.Game/Online/Multiplayer/Room.cs @@ -16,54 +16,54 @@ namespace osu.Game.Online.Multiplayer { [Cached] [JsonProperty("id")] - public Bindable RoomID { get; private set; } = new Bindable(); + public readonly Bindable RoomID = new Bindable(); [Cached] [JsonProperty("name")] - public Bindable Name { get; private set; } = new Bindable(); + public readonly Bindable Name = new Bindable(); [Cached] [JsonProperty("host")] - public Bindable Host { get; private set; } = new Bindable(); + public readonly Bindable Host = new Bindable(); [Cached] [JsonProperty("playlist")] - public BindableList Playlist { get; private set; } = new BindableList(); + public readonly BindableList Playlist = new BindableList(); [Cached] [JsonProperty("channel_id")] - public Bindable ChannelId { get; private set; } = new Bindable(); + public readonly Bindable ChannelId = new Bindable(); [Cached] [JsonIgnore] - public Bindable Duration { get; private set; } = new Bindable(TimeSpan.FromMinutes(30)); + public readonly Bindable Duration = new Bindable(TimeSpan.FromMinutes(30)); [Cached] [JsonIgnore] - public Bindable MaxAttempts { get; private set; } = new Bindable(); + public readonly Bindable MaxAttempts = new Bindable(); [Cached] [JsonIgnore] - public Bindable Status { get; private set; } = new Bindable(new RoomStatusOpen()); + public readonly Bindable Status = new Bindable(new RoomStatusOpen()); [Cached] [JsonIgnore] - public Bindable Availability { get; private set; } = new Bindable(); + public readonly Bindable Availability = new Bindable(); [Cached] [JsonIgnore] - public Bindable Type { get; private set; } = new Bindable(new GameTypeTimeshift()); + public readonly Bindable Type = new Bindable(new GameTypeTimeshift()); [Cached] [JsonIgnore] - public Bindable MaxParticipants { get; private set; } = new Bindable(); + public readonly Bindable MaxParticipants = new Bindable(); [Cached] [JsonProperty("recent_participants")] - public BindableList RecentParticipants { get; private set; } = new BindableList(); + public readonly BindableList RecentParticipants = new BindableList(); [Cached] - public Bindable ParticipantCount { get; private set; } = new Bindable(); + public readonly Bindable ParticipantCount = new Bindable(); // todo: TEMPORARY [JsonProperty("participant_count")] @@ -83,7 +83,7 @@ namespace osu.Game.Online.Multiplayer // Only supports retrieval for now [Cached] [JsonProperty("ends_at")] - public Bindable EndDate { get; private set; } = new Bindable(); + public readonly Bindable EndDate = new Bindable(); // Todo: Find a better way to do this (https://github.com/ppy/osu-framework/issues/1930) [JsonProperty("max_attempts", DefaultValueHandling = DefaultValueHandling.Ignore)] @@ -97,7 +97,7 @@ namespace osu.Game.Online.Multiplayer /// The position of this in the list. This is not read from or written to the API. /// [JsonIgnore] - public Bindable Position { get; private set; } = new Bindable(-1); + public readonly Bindable Position = new Bindable(-1); public void CopyFrom(Room other) { @@ -130,7 +130,7 @@ namespace osu.Game.Online.Multiplayer RecentParticipants.AddRange(other.RecentParticipants); } - Position = other.Position; + Position.Value = other.Position.Value; } public bool ShouldSerializeRoomID() => false; From e211ba5e7dc3c4c5c332ca9ec4105d77391dcd7b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Jul 2020 14:43:30 +0900 Subject: [PATCH 2342/2376] Fix cursor scale potentially not being updated if set too early --- osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs | 10 ++++++---- osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs index 28600ef55b..5812e8cf75 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs @@ -30,7 +30,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor private readonly Drawable cursorTrail; - public Bindable CursorScale = new BindableFloat(1); + public IBindable CursorScale => cursorScale; + + private readonly Bindable cursorScale = new BindableFloat(1); private Bindable userCursorScale; private Bindable autoCursorScale; @@ -68,13 +70,13 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor autoCursorScale = config.GetBindable(OsuSetting.AutoCursorSize); autoCursorScale.ValueChanged += _ => calculateScale(); - CursorScale.ValueChanged += e => + CursorScale.BindValueChanged(e => { var newScale = new Vector2(e.NewValue); ActiveCursor.Scale = newScale; cursorTrail.Scale = newScale; - }; + }, true); calculateScale(); } @@ -95,7 +97,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor scale *= GetScaleForCircleSize(beatmap.BeatmapInfo.BaseDifficulty.CircleSize); } - CursorScale.Value = scale; + cursorScale.Value = scale; var newScale = new Vector2(scale); diff --git a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs index abba444c73..ec7751d2b4 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.UI private OsuClickToResumeCursor clickToResumeCursor; private OsuCursorContainer localCursorContainer; - private Bindable localCursorScale; + private IBindable localCursorScale; public override CursorContainer LocalCursor => State.Value == Visibility.Visible ? localCursorContainer : null; From a21c2422c5ec70285ec8f2235a275de1449109f0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Jul 2020 14:44:20 +0900 Subject: [PATCH 2343/2376] Make cursor centre portion non-expanding and more visible with outline --- osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs | 34 +++++++++----------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs index 4f3d07f208..ef05514146 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs @@ -115,24 +115,22 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor }, }, }, - new CircularContainer - { - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Scale = new Vector2(0.1f), - Masking = true, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.White, - }, - }, - }, - } - } + }, + }, + new Circle + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Scale = new Vector2(0.14f), + Colour = new Color4(34, 93, 204, 255), + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Radius = 8, + Colour = Color4.White, + }, + }, }; } } From c562435267a6fcd2a650380b1a54f92356fe9015 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Jul 2020 14:44:30 +0900 Subject: [PATCH 2344/2376] Adjust cursor transforms for better feel --- osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs index ef05514146..eea45c6c80 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs @@ -59,10 +59,10 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor { if (!cursorExpand) return; - expandTarget.ScaleTo(released_scale).ScaleTo(pressed_scale, 100, Easing.OutQuad); + expandTarget.ScaleTo(released_scale).ScaleTo(pressed_scale, 400, Easing.OutElasticHalf); } - public void Contract() => expandTarget.ScaleTo(released_scale, 100, Easing.OutQuad); + public void Contract() => expandTarget.ScaleTo(released_scale, 400, Easing.OutQuad); private class DefaultCursor : OsuCursorSprite { From 13618915b7ff9ab4eb52bc3f0efd203a2450722f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Jul 2020 14:46:49 +0900 Subject: [PATCH 2345/2376] Don't show cursor guide in gameplay cursor test --- osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs index 38c2bb9b95..16eedad465 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs @@ -87,6 +87,7 @@ namespace osu.Game.Rulesets.Osu.Tests public MovingCursorInputManager() { UseParentInput = false; + ShowVisualCursorGuide = false; } protected override void Update() From fee19753e12c27e5ea429c276f015159d5f1fe6d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Jul 2020 14:47:11 +0900 Subject: [PATCH 2346/2376] Fix animations not playing correctly in test scene due to too many calls to OnPressed --- .../TestSceneGameplayCursor.cs | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs index 16eedad465..dcac3367db 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs @@ -69,16 +69,27 @@ namespace osu.Game.Rulesets.Osu.Tests private class ClickingCursorContainer : OsuCursorContainer { + private bool pressed; + + public bool Pressed + { + set + { + if (value == pressed) + return; + + pressed = value; + if (value) + OnPressed(OsuAction.LeftButton); + else + OnReleased(OsuAction.LeftButton); + } + } + protected override void Update() { base.Update(); - - double currentTime = Time.Current; - - if (((int)(currentTime / 1000)) % 2 == 0) - OnPressed(OsuAction.LeftButton); - else - OnReleased(OsuAction.LeftButton); + Pressed = ((int)(Time.Current / 1000)) % 2 == 0; } } From b68a2d885c4a5ad5b7f21c886b8b146c0cceecae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Jul 2020 14:47:26 +0900 Subject: [PATCH 2347/2376] Add testability against different background colours / with user input --- .../TestSceneGameplayCursor.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs index dcac3367db..461779b185 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs @@ -5,7 +5,9 @@ using System; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; using osu.Framework.Testing.Input; +using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Screens.Play; @@ -24,9 +26,34 @@ namespace osu.Game.Rulesets.Osu.Tests [Resolved] private OsuConfigManager config { get; set; } + private Drawable background; + public TestSceneGameplayCursor() { gameplayBeatmap = new GameplayBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)); + + AddStep("change background colour", () => + { + background?.Expire(); + + Add(background = new Box + { + RelativeSizeAxes = Axes.Both, + Depth = float.MaxValue, + Colour = new Colour4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1) + }); + }); + + AddSliderStep("circle size", 0f, 10f, 0f, val => + { + config.Set(OsuSetting.AutoCursorSize, true); + gameplayBeatmap.BeatmapInfo.BaseDifficulty.CircleSize = val; + Scheduler.AddOnce(recreate); + }); + + AddStep("test cursor container", recreate); + + void recreate() => SetContents(() => new OsuInputManager(new OsuRuleset().RulesetInfo) { Child = new OsuCursorContainer() }); } [TestCase(1, 1)] From bd5957bc0a9eabd0843b2e1d201c126ca44e1d3f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Jul 2020 14:49:44 +0900 Subject: [PATCH 2348/2376] Add dynamic compilation exclusion rules for ruleset types --- osu.Game.Rulesets.Catch/CatchRuleset.cs | 2 ++ osu.Game.Rulesets.Mania/ManiaRuleset.cs | 2 ++ osu.Game.Rulesets.Osu/OsuRuleset.cs | 2 ++ osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 2 ++ 4 files changed, 8 insertions(+) diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index ca75a816f1..9437023c70 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -21,11 +21,13 @@ using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using System; +using osu.Framework.Testing; using osu.Game.Rulesets.Catch.Skinning; using osu.Game.Skinning; namespace osu.Game.Rulesets.Catch { + [ExcludeFromDynamicCompile] public class CatchRuleset : Ruleset, ILegacyRuleset { public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => new DrawableCatchRuleset(this, beatmap, mods); diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index a27485dd06..68dce8b139 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -12,6 +12,7 @@ using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; +using osu.Framework.Testing; using osu.Game.Graphics; using osu.Game.Rulesets.Mania.Replays; using osu.Game.Rulesets.Replays.Types; @@ -34,6 +35,7 @@ using osu.Game.Screens.Ranking.Statistics; namespace osu.Game.Rulesets.Mania { + [ExcludeFromDynamicCompile] public class ManiaRuleset : Ruleset, ILegacyRuleset { /// diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index e488ba65c8..eaa5d8937a 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -30,12 +30,14 @@ using osu.Game.Scoring; using osu.Game.Skinning; using System; using System.Linq; +using osu.Framework.Testing; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Statistics; using osu.Game.Screens.Ranking.Statistics; namespace osu.Game.Rulesets.Osu { + [ExcludeFromDynamicCompile] public class OsuRuleset : Ruleset, ILegacyRuleset { public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => new DrawableOsuRuleset(this, beatmap, mods); diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 156905fa9c..2011842591 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -22,6 +22,7 @@ using osu.Game.Rulesets.Taiko.Scoring; using osu.Game.Scoring; using System; using System.Linq; +using osu.Framework.Testing; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Taiko.Edit; using osu.Game.Rulesets.Taiko.Objects; @@ -31,6 +32,7 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko { + [ExcludeFromDynamicCompile] public class TaikoRuleset : Ruleset, ILegacyRuleset { public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => new DrawableTaikoRuleset(this, beatmap, mods); From a9faa11dcbfcd885abedf03a33ec621d7dc435b4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 10 Jul 2020 15:37:08 +0900 Subject: [PATCH 2349/2376] Add back playlist header --- osu.Game/Screens/Multi/Match/MatchSubScreen.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs index dffd6a0331..1233581575 100644 --- a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs +++ b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs @@ -134,6 +134,7 @@ namespace osu.Game.Screens.Multi.Match RelativeSizeAxes = Axes.Both, Content = new[] { + new Drawable[] { new OverlinedHeader("Playlist"), }, new Drawable[] { new DrawableRoomPlaylist(false, true) // Temporarily always allow selection @@ -156,6 +157,7 @@ namespace osu.Game.Screens.Multi.Match }, RowDimensions = new[] { + new Dimension(GridSizeMode.AutoSize), new Dimension(), new Dimension(GridSizeMode.Absolute, 5), new Dimension(GridSizeMode.AutoSize) From 2ed8d42d222b45f53f029ac0c4d93d06a0a71916 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 10 Jul 2020 15:37:13 +0900 Subject: [PATCH 2350/2376] Remove whitespace --- osu.Game/Screens/Multi/Match/MatchSubScreen.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs index 1233581575..40a8427701 100644 --- a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs +++ b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs @@ -224,7 +224,6 @@ namespace osu.Game.Screens.Multi.Match } [Resolved] - private IAPIProvider api { get; set; } protected override void LoadComplete() From bc6f2199f3deb2c50fd0732aadd59e7111617e09 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Jul 2020 16:49:11 +0900 Subject: [PATCH 2351/2376] 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 ff04c7f120..0881861bdc 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index e4753e7ee9..cba2d62bf5 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -24,7 +24,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 91fa003604..45e0da36c1 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From 632f333ce2361af1e9b9cdb3c41ebfbe381b2656 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Jul 2020 16:33:20 +0900 Subject: [PATCH 2352/2376] Add ability to return protected beatmaps in GetAllUsable call --- osu.Game/Beatmaps/BeatmapManager.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 637833fb5d..b4b341634c 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -283,14 +283,16 @@ namespace osu.Game.Beatmaps /// Returns a list of all usable s. /// /// A list of available . - public List GetAllUsableBeatmapSets(IncludedDetails includes = IncludedDetails.All) => GetAllUsableBeatmapSetsEnumerable(includes).ToList(); + 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) + public IEnumerable GetAllUsableBeatmapSetsEnumerable(IncludedDetails includes, bool includeProtected = false) { IQueryable queryable; @@ -312,7 +314,7 @@ namespace osu.Game.Beatmaps // 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 && !s.Protected); + return queryable.AsEnumerable().Where(s => !s.DeletePending && (includeProtected || !s.Protected)); } /// From 49b88971d1ef7e05887296003566a86216b0a901 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Jul 2020 16:33:31 +0900 Subject: [PATCH 2353/2376] Display all usable beatmaps in playlist, including protected --- osu.Game/Overlays/MusicController.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 92cf490be2..63e828a782 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Input.Bindings; @@ -71,7 +72,7 @@ namespace osu.Game.Overlays managerRemoved = beatmaps.ItemRemoved.GetBoundCopy(); managerRemoved.BindValueChanged(beatmapRemoved); - beatmapSets.AddRange(beatmaps.GetAllUsableBeatmapSets(IncludedDetails.Minimal).OrderBy(_ => RNG.Next())); + beatmapSets.AddRange(beatmaps.GetAllUsableBeatmapSets(IncludedDetails.Minimal, true).OrderBy(_ => RNG.Next())); } protected override void LoadComplete() @@ -135,6 +136,7 @@ namespace osu.Game.Overlays /// /// Start playing the current track (if not already playing). + /// Will select the next valid track if the current track is null or . /// /// Whether the operation was successful. public bool Play(bool restart = false) @@ -143,12 +145,12 @@ namespace osu.Game.Overlays IsUserPaused = false; - if (track == null) + if (track == null || track is TrackVirtual) { if (beatmap.Disabled) return false; - next(true); + next(); return true; } @@ -228,10 +230,9 @@ namespace osu.Game.Overlays /// public void NextTrack() => Schedule(() => next()); - private bool next(bool instant = false) + private bool next() { - if (!instant) - queuedDirection = TrackChangeDirection.Next; + queuedDirection = TrackChangeDirection.Next; var playable = BeatmapSets.SkipWhile(i => i.ID != current.BeatmapSetInfo.ID).ElementAtOrDefault(1) ?? BeatmapSets.FirstOrDefault(); From 44fdb5b82e040e1a129257f177f51e35ee8ff1a3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Jul 2020 16:33:45 +0900 Subject: [PATCH 2354/2376] Ensure music starts when returning to lounge or main menu --- osu.Game/Screens/Menu/MainMenu.cs | 4 ++-- osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 76950982e6..41e2564141 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -260,8 +260,8 @@ namespace osu.Game.Screens.Menu // we may have consumed our preloaded instance, so let's make another. preloadSongSelect(); - if (Beatmap.Value.Track != null && music?.IsUserPaused != true) - Beatmap.Value.Track.Start(); + if (music?.IsUserPaused == false) + music.Play(); } public override bool OnExiting(IScreen next) diff --git a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs index f512b864a6..e2ffb21153 100644 --- a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs @@ -11,6 +11,7 @@ using osu.Framework.Screens; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; +using osu.Game.Overlays; using osu.Game.Overlays.SearchableList; using osu.Game.Screens.Multi.Lounge.Components; using osu.Game.Screens.Multi.Match; @@ -31,6 +32,9 @@ namespace osu.Game.Screens.Multi.Lounge [Resolved] private Bindable selectedRoom { get; set; } + [Resolved(canBeNull: true)] + private MusicController music { get; set; } + private bool joiningRoom; [BackgroundDependencyLoader] @@ -122,6 +126,9 @@ namespace osu.Game.Screens.Multi.Lounge if (selectedRoom.Value?.RoomID.Value == null) selectedRoom.Value = new Room(); + if (music?.IsUserPaused == false) + music.Play(); + onReturning(); } From d0c2a1b9d30a1f3b37b59d59f7a02d9caf1e3693 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 10 Jul 2020 17:25:07 +0900 Subject: [PATCH 2355/2376] Move dropdown out of display style selector --- .../SearchableList/DisplayStyleControl.cs | 38 ++++--------------- .../SearchableListFilterControl.cs | 32 ++++++++++++---- osu.Game/Overlays/SocialOverlay.cs | 4 +- 3 files changed, 35 insertions(+), 39 deletions(-) diff --git a/osu.Game/Overlays/SearchableList/DisplayStyleControl.cs b/osu.Game/Overlays/SearchableList/DisplayStyleControl.cs index 5ecb477a2f..ffbc1c9586 100644 --- a/osu.Game/Overlays/SearchableList/DisplayStyleControl.cs +++ b/osu.Game/Overlays/SearchableList/DisplayStyleControl.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 osuTK; using osu.Framework.Graphics; @@ -11,44 +10,23 @@ using osu.Game.Graphics.Containers; namespace osu.Game.Overlays.SearchableList { - public class DisplayStyleControl : Container - where T : struct, Enum + public class DisplayStyleControl : CompositeDrawable { - public readonly SlimEnumDropdown Dropdown; public readonly Bindable DisplayStyle = new Bindable(); public DisplayStyleControl() { AutoSizeAxes = Axes.Both; - Children = new[] + InternalChild = new FillFlowContainer { - new FillFlowContainer + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(5f, 0f), + Direction = FillDirection.Horizontal, + Children = new[] { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Spacing = new Vector2(10f, 0f), - Direction = FillDirection.Horizontal, - Children = new Drawable[] - { - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Spacing = new Vector2(5f, 0f), - Direction = FillDirection.Horizontal, - Children = new[] - { - new DisplayStyleToggleButton(FontAwesome.Solid.ThLarge, PanelDisplayStyle.Grid, DisplayStyle), - new DisplayStyleToggleButton(FontAwesome.Solid.ListUl, PanelDisplayStyle.List, DisplayStyle), - }, - }, - Dropdown = new SlimEnumDropdown - { - RelativeSizeAxes = Axes.None, - Width = 160f, - }, - }, + new DisplayStyleToggleButton(FontAwesome.Solid.ThLarge, PanelDisplayStyle.Grid, DisplayStyle), + new DisplayStyleToggleButton(FontAwesome.Solid.ListUl, PanelDisplayStyle.List, DisplayStyle), }, }; diff --git a/osu.Game/Overlays/SearchableList/SearchableListFilterControl.cs b/osu.Game/Overlays/SearchableList/SearchableListFilterControl.cs index de5e558943..3d0ff373b7 100644 --- a/osu.Game/Overlays/SearchableList/SearchableListFilterControl.cs +++ b/osu.Game/Overlays/SearchableList/SearchableListFilterControl.cs @@ -19,12 +19,14 @@ namespace osu.Game.Overlays.SearchableList { private const float padding = 10; - private readonly Container filterContainer; + private readonly Drawable filterContainer; + private readonly Drawable rightFilterContainer; private readonly Box tabStrip; public readonly SearchTextBox Search; public readonly PageTabControl Tabs; - public readonly DisplayStyleControl DisplayStyleControl; + public readonly SlimEnumDropdown Dropdown; + public readonly DisplayStyleControl DisplayStyleControl; protected abstract Color4 BackgroundColour { get; } protected abstract TTab DefaultTab { get; } @@ -42,7 +44,7 @@ namespace osu.Game.Overlays.SearchableList var controls = CreateSupplementaryControls(); Container controlsContainer; - Children = new Drawable[] + Children = new[] { filterContainer = new Container { @@ -104,11 +106,27 @@ namespace osu.Game.Overlays.SearchableList }, }, }, - DisplayStyleControl = new DisplayStyleControl + rightFilterContainer = new FillFlowContainer { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, - }, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + Dropdown = new SlimEnumDropdown + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + RelativeSizeAxes = Axes.None, + Width = 160f, + }, + DisplayStyleControl = new DisplayStyleControl + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + }, + } + } }; if (controls != null) controlsContainer.Children = new[] { controls }; @@ -116,8 +134,8 @@ namespace osu.Game.Overlays.SearchableList Tabs.Current.Value = DefaultTab; Tabs.Current.TriggerChange(); - DisplayStyleControl.Dropdown.Current.Value = DefaultCategory; - DisplayStyleControl.Dropdown.Current.TriggerChange(); + Dropdown.Current.Value = DefaultCategory; + Dropdown.Current.TriggerChange(); } [BackgroundDependencyLoader] diff --git a/osu.Game/Overlays/SocialOverlay.cs b/osu.Game/Overlays/SocialOverlay.cs index 9548573b4f..1b05142192 100644 --- a/osu.Game/Overlays/SocialOverlay.cs +++ b/osu.Game/Overlays/SocialOverlay.cs @@ -72,7 +72,7 @@ namespace osu.Game.Overlays Filter.Tabs.Current.ValueChanged += _ => onFilterUpdate(); Filter.DisplayStyleControl.DisplayStyle.ValueChanged += _ => recreatePanels(); - Filter.DisplayStyleControl.Dropdown.Current.ValueChanged += _ => recreatePanels(); + Filter.Dropdown.Current.ValueChanged += _ => recreatePanels(); currentQuery.BindTo(Filter.Search.Current); currentQuery.ValueChanged += query => @@ -155,7 +155,7 @@ namespace osu.Game.Overlays break; } - if (Filter.DisplayStyleControl.Dropdown.Current.Value == SortDirection.Descending) + if (Filter.Dropdown.Current.Value == SortDirection.Descending) sortedUsers = sortedUsers.Reverse(); var newPanels = new FillFlowContainer From ed926de77ffe739c2ea3fa07edd39ab1daba5ad3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 10 Jul 2020 17:25:28 +0900 Subject: [PATCH 2356/2376] Fix up/improve dropdown styling/positioning --- osu.Game/Overlays/SearchableList/SearchableListFilterControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/SearchableList/SearchableListFilterControl.cs b/osu.Game/Overlays/SearchableList/SearchableListFilterControl.cs index 3d0ff373b7..e0163b5b0c 100644 --- a/osu.Game/Overlays/SearchableList/SearchableListFilterControl.cs +++ b/osu.Game/Overlays/SearchableList/SearchableListFilterControl.cs @@ -149,7 +149,7 @@ namespace osu.Game.Overlays.SearchableList base.Update(); Height = filterContainer.Height; - DisplayStyleControl.Margin = new MarginPadding { Top = filterContainer.Height - 35, Right = SearchableListOverlay.WIDTH_PADDING }; + rightFilterContainer.Margin = new MarginPadding { Top = filterContainer.Height - 30, Right = ContentHorizontalPadding }; } private class FilterSearchTextBox : SearchTextBox From 926279e39be2d81a6906d13fbeca6f45dfabdb6b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 10 Jul 2020 17:26:42 +0900 Subject: [PATCH 2357/2376] Implement category dropdown for multiplayer --- .../TestSceneLoungeFilterControl.cs | 20 +++++++ .../Online/API/Requests/GetRoomsRequest.cs | 54 ++++++++++--------- .../Multi/Lounge/Components/FilterControl.cs | 19 +++---- .../Multi/Lounge/Components/FilterCriteria.cs | 4 +- .../Multi/Lounge/Components/RoomsContainer.cs | 8 --- osu.Game/Screens/Multi/RoomManager.cs | 2 +- 6 files changed, 63 insertions(+), 44 deletions(-) create mode 100644 osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeFilterControl.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeFilterControl.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeFilterControl.cs new file mode 100644 index 0000000000..7c0c2797f5 --- /dev/null +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeFilterControl.cs @@ -0,0 +1,20 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Game.Screens.Multi.Lounge.Components; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + public class TestSceneLoungeFilterControl : OsuTestScene + { + public TestSceneLoungeFilterControl() + { + Child = new FilterControl + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }; + } + } +} diff --git a/osu.Game/Online/API/Requests/GetRoomsRequest.cs b/osu.Game/Online/API/Requests/GetRoomsRequest.cs index 8f1497ef33..4b90b04b51 100644 --- a/osu.Game/Online/API/Requests/GetRoomsRequest.cs +++ b/osu.Game/Online/API/Requests/GetRoomsRequest.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using osu.Framework.IO.Network; using osu.Game.Online.Multiplayer; using osu.Game.Screens.Multi.Lounge.Components; @@ -9,39 +10,44 @@ namespace osu.Game.Online.API.Requests { public class GetRoomsRequest : APIRequest> { - private readonly PrimaryFilter primaryFilter; + private readonly RoomStatusFilter statusFilter; + private readonly RoomCategoryFilter categoryFilter; - public GetRoomsRequest(PrimaryFilter primaryFilter) + public GetRoomsRequest(RoomStatusFilter statusFilter, RoomCategoryFilter categoryFilter) { - this.primaryFilter = primaryFilter; + this.statusFilter = statusFilter; + this.categoryFilter = categoryFilter; } - protected override string Target + protected override WebRequest CreateWebRequest() { - get + var req = base.CreateWebRequest(); + + switch (statusFilter) { - string target = "rooms"; + case RoomStatusFilter.Owned: + req.AddParameter("mode", "owned"); + break; - switch (primaryFilter) - { - case PrimaryFilter.Open: - break; + case RoomStatusFilter.Participated: + req.AddParameter("mode", "participated"); + break; - case PrimaryFilter.Owned: - target += "/owned"; - break; - - case PrimaryFilter.Participated: - target += "/participated"; - break; - - case PrimaryFilter.RecentlyEnded: - target += "/ended"; - break; - } - - return target; + case RoomStatusFilter.RecentlyEnded: + req.AddParameter("mode", "ended"); + break; } + + switch (categoryFilter) + { + case RoomCategoryFilter.Spotlight: + req.AddParameter("category", "spotlight"); + break; + } + + return req; } + + protected override string Target => "rooms"; } } diff --git a/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs b/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs index 2742ef3404..3a4ead44b7 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs @@ -12,11 +12,11 @@ using osuTK.Graphics; namespace osu.Game.Screens.Multi.Lounge.Components { - public class FilterControl : SearchableListFilterControl + public class FilterControl : SearchableListFilterControl { protected override Color4 BackgroundColour => Color4.Black.Opacity(0.5f); - protected override PrimaryFilter DefaultTab => PrimaryFilter.Open; - protected override SecondaryFilter DefaultCategory => SecondaryFilter.Public; + protected override RoomStatusFilter DefaultTab => RoomStatusFilter.Open; + protected override RoomCategoryFilter DefaultCategory => RoomCategoryFilter.Normal; protected override float ContentHorizontalPadding => base.ContentHorizontalPadding + OsuScreen.HORIZONTAL_OVERFLOW_PADDING; @@ -43,6 +43,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components ruleset.BindValueChanged(_ => updateFilter()); Search.Current.BindValueChanged(_ => scheduleUpdateFilter()); + Dropdown.Current.BindValueChanged(_ => updateFilter()); Tabs.Current.BindValueChanged(_ => updateFilter(), true); } @@ -61,14 +62,14 @@ namespace osu.Game.Screens.Multi.Lounge.Components filter.Value = new FilterCriteria { SearchString = Search.Current.Value ?? string.Empty, - PrimaryFilter = Tabs.Current.Value, - SecondaryFilter = DisplayStyleControl.Dropdown.Current.Value, + StatusFilter = Tabs.Current.Value, + RoomCategoryFilter = Dropdown.Current.Value, Ruleset = ruleset.Value }; } } - public enum PrimaryFilter + public enum RoomStatusFilter { Open, @@ -78,9 +79,9 @@ namespace osu.Game.Screens.Multi.Lounge.Components Owned, } - public enum SecondaryFilter + public enum RoomCategoryFilter { - Public, - //Private, + Normal, + Spotlight } } diff --git a/osu.Game/Screens/Multi/Lounge/Components/FilterCriteria.cs b/osu.Game/Screens/Multi/Lounge/Components/FilterCriteria.cs index 26d445e151..6d70225eec 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/FilterCriteria.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/FilterCriteria.cs @@ -8,8 +8,8 @@ namespace osu.Game.Screens.Multi.Lounge.Components public class FilterCriteria { public string SearchString; - public PrimaryFilter PrimaryFilter; - public SecondaryFilter SecondaryFilter; + public RoomStatusFilter StatusFilter; + public RoomCategoryFilter RoomCategoryFilter; public RulesetInfo Ruleset; } } diff --git a/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs index bf153b77df..447c99039a 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs @@ -77,14 +77,6 @@ namespace osu.Game.Screens.Multi.Lounge.Components if (!string.IsNullOrEmpty(criteria.SearchString)) matchingFilter &= r.FilterTerms.Any(term => term.IndexOf(criteria.SearchString, StringComparison.InvariantCultureIgnoreCase) >= 0); - switch (criteria.SecondaryFilter) - { - default: - case SecondaryFilter.Public: - matchingFilter &= r.Room.Availability.Value == RoomAvailability.Public; - break; - } - r.MatchingFilter = matchingFilter; } }); diff --git a/osu.Game/Screens/Multi/RoomManager.cs b/osu.Game/Screens/Multi/RoomManager.cs index 642378d8d5..ac1f74b6a6 100644 --- a/osu.Game/Screens/Multi/RoomManager.cs +++ b/osu.Game/Screens/Multi/RoomManager.cs @@ -318,7 +318,7 @@ namespace osu.Game.Screens.Multi var tcs = new TaskCompletionSource(); pollReq?.Cancel(); - pollReq = new GetRoomsRequest(currentFilter.Value.PrimaryFilter); + pollReq = new GetRoomsRequest(currentFilter.Value.StatusFilter, currentFilter.Value.RoomCategoryFilter); pollReq.Success += result => { From 1760cc242730fb1998135a79bd74586f63fc79f1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Jul 2020 18:03:56 +0900 Subject: [PATCH 2358/2376] Fix behavioural regression by splitting methods out --- osu.Game/Overlays/MusicController.cs | 34 ++++++++++++++----- osu.Game/Screens/Menu/MainMenu.cs | 3 +- .../Screens/Multi/Lounge/LoungeSubScreen.cs | 3 +- 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 63e828a782..09f2a66b47 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -134,9 +134,31 @@ namespace osu.Game.Overlays }); } + /// + /// Ensures music is playing, no matter what, unless the user has explicitly paused. + /// This means that if the current beatmap has a virtual track (see ) a new beatmap will be selected. + /// + public void EnsurePlayingSomething() + { + if (IsUserPaused) return; + + var track = current?.Track; + + if (track == null || track is TrackVirtual) + { + if (beatmap.Disabled) + return; + + next(); + } + else if (!IsPlaying) + { + Play(); + } + } + /// /// Start playing the current track (if not already playing). - /// Will select the next valid track if the current track is null or . /// /// Whether the operation was successful. public bool Play(bool restart = false) @@ -145,14 +167,8 @@ namespace osu.Game.Overlays IsUserPaused = false; - if (track == null || track is TrackVirtual) - { - if (beatmap.Disabled) - return false; - - next(); - return true; - } + if (track == null) + return false; if (restart) track.Restart(); diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 41e2564141..57252d557e 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -260,8 +260,7 @@ namespace osu.Game.Screens.Menu // we may have consumed our preloaded instance, so let's make another. preloadSongSelect(); - if (music?.IsUserPaused == false) - music.Play(); + music.EnsurePlayingSomething(); } public override bool OnExiting(IScreen next) diff --git a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs index e2ffb21153..ff7d56a95b 100644 --- a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs @@ -126,8 +126,7 @@ namespace osu.Game.Screens.Multi.Lounge if (selectedRoom.Value?.RoomID.Value == null) selectedRoom.Value = new Room(); - if (music?.IsUserPaused == false) - music.Play(); + music.EnsurePlayingSomething(); onReturning(); } From cb56b8e031a6bbb431963d46d28349ca318fbf59 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Jul 2020 18:13:58 +0900 Subject: [PATCH 2359/2376] Add test for menu playing music on return --- .../Navigation/TestSceneScreenNavigation.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 9d603ac471..8ccaca8630 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -6,6 +6,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio.Track; using osu.Framework.Graphics.Containers; +using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Overlays; using osu.Game.Overlays.Mods; @@ -70,6 +71,23 @@ namespace osu.Game.Tests.Visual.Navigation AddAssert("Ensure time wasn't reset to preview point", () => track().CurrentTime < beatmap().Metadata.PreviewTime); } + [Test] + public void TestMenuMakesMusic() + { + WorkingBeatmap beatmap() => Game.Beatmap.Value; + Track track() => beatmap().Track; + + TestSongSelect songSelect = null; + + PushAndConfirm(() => songSelect = new TestSongSelect()); + + AddUntilStep("wait for no track", () => track() is TrackVirtual); + + AddStep("return to menu", () => songSelect.Exit()); + + AddUntilStep("wait for track", () => !(track() is TrackVirtual) && track().IsRunning); + } + [Test] public void TestExitSongSelectWithClick() { From f699a34c77298ec0324903659d63cd9d4ab48808 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Jul 2020 18:19:18 +0900 Subject: [PATCH 2360/2376] Rename variable for potential future expansion --- osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs | 2 +- osu.Game/Graphics/UserInterface/OsuTextBox.cs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs b/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs index 11867cf103..ac6f5ceb1b 100644 --- a/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs @@ -24,7 +24,7 @@ namespace osu.Game.Graphics.UserInterface Child = new PasswordMaskChar(CalculatedTextSize), }; - protected override bool AllowUpperCaseSamples => false; + protected override bool AllowUniqueCharacterSamples => false; protected override bool AllowClipboardExport => false; diff --git a/osu.Game/Graphics/UserInterface/OsuTextBox.cs b/osu.Game/Graphics/UserInterface/OsuTextBox.cs index 753efcd16d..0d173e2d3e 100644 --- a/osu.Game/Graphics/UserInterface/OsuTextBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuTextBox.cs @@ -30,10 +30,10 @@ namespace osu.Game.Graphics.UserInterface private SampleChannel caretMovedSample; /// - /// Whether to allow playing a different sample when inserting upper case text. - /// If set to false, same sample will be played for both letter cases. + /// Whether to allow playing a different samples based on the type of character. + /// If set to false, the same sample will be used for all characters. /// - protected virtual bool AllowUpperCaseSamples => true; + protected virtual bool AllowUniqueCharacterSamples => true; protected override float LeftRightPadding => 10; @@ -78,7 +78,7 @@ namespace osu.Game.Graphics.UserInterface { base.OnTextAdded(added); - if (added.Any(char.IsUpper) && AllowUpperCaseSamples) + if (added.Any(char.IsUpper) && AllowUniqueCharacterSamples) capsTextAddedSample?.Play(); else textAddedSamples[RNG.Next(0, 3)]?.Play(); From 8aff828dfe51a56be3c6eb9f7c5001830f351612 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Jul 2020 18:34:31 +0900 Subject: [PATCH 2361/2376] Move application of judgements to Apply method --- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 4 +- .../Rulesets/Judgements/DrawableJudgement.cs | 47 ++++++++----------- 2 files changed, 22 insertions(+), 29 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 2eff99bd3e..291a572fdf 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.Osu.UI DrawableOsuJudgement explosion = poolDictionary[result.Type].Get(doj => { - doj.JudgedObject = judgedObject; + doj.Apply(result, judgedObject); // todo: move to JudgedObject property? doj.Position = osuObject.StackedEndPosition; @@ -143,7 +143,7 @@ namespace osu.Game.Rulesets.Osu.UI var judgement = base.CreateNewDrawable(); // just a placeholder to initialise the correct drawable hierarchy for this pool. - judgement.Result = new JudgementResult(new HitObject(), new Judgement()) { Type = result }; + judgement.Apply(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null); return judgement; } diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs index 3ec5326299..1671e97db1 100644 --- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs @@ -2,9 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System.Diagnostics; +using JetBrains.Annotations; using osuTK; using osu.Framework.Allocation; -using osu.Framework.Caching; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -28,24 +28,8 @@ namespace osu.Game.Rulesets.Judgements [Resolved] private OsuColour colours { get; set; } - private readonly Cached drawableCache = new Cached(); - - private JudgementResult result; - - public JudgementResult Result - { - get => result; - set - { - if (result?.Type == value.Type) - return; - - result = value; - drawableCache.Invalidate(); - } - } - - public DrawableHitObject JudgedObject; + public JudgementResult Result { get; private set; } + public DrawableHitObject JudgedObject { get; private set; } protected Container JudgementBody; protected SpriteText JudgementText; @@ -68,8 +52,7 @@ namespace osu.Game.Rulesets.Judgements public DrawableJudgement(JudgementResult result, DrawableHitObject judgedObject) : this() { - Result = result; - JudgedObject = judgedObject; + Apply(result, judgedObject); } public DrawableJudgement() @@ -78,6 +61,12 @@ namespace osu.Game.Rulesets.Judgements Origin = Anchor.Centre; } + [BackgroundDependencyLoader] + private void load() + { + prepareDrawables(); + } + protected virtual void ApplyHitAnimations() { JudgementBody.ScaleTo(0.9f); @@ -86,10 +75,10 @@ namespace osu.Game.Rulesets.Judgements this.Delay(FadeOutDelay).FadeOut(400); } - [BackgroundDependencyLoader] - private void load() + public void Apply([NotNull] JudgementResult result, [CanBeNull] DrawableHitObject judgedObject) { - prepareDrawables(); + Result = result; + JudgedObject = judgedObject; } protected override void PrepareForUse() @@ -98,8 +87,7 @@ namespace osu.Game.Rulesets.Judgements Debug.Assert(Result != null); - if (!drawableCache.IsValid) - prepareDrawables(); + prepareDrawables(); this.FadeInFromZero(FadeInDuration, Easing.OutQuint); JudgementBody.ScaleTo(1); @@ -129,10 +117,15 @@ namespace osu.Game.Rulesets.Judgements Expire(true); } + private HitResult? currentDrawableType; + private void prepareDrawables() { var type = Result?.Type ?? HitResult.Perfect; //TODO: better default type from ruleset + if (type == currentDrawableType) + return; + InternalChild = JudgementBody = new Container { Anchor = Anchor.Centre, @@ -147,7 +140,7 @@ namespace osu.Game.Rulesets.Judgements }, confineMode: ConfineMode.NoScaling) }; - drawableCache.Validate(); + currentDrawableType = type; } } } From f872343babd8ecc5285212d430abdef00be5664e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Jul 2020 18:35:20 +0900 Subject: [PATCH 2362/2376] Make Apply virtual to further simplify application process --- .../Objects/Drawables/DrawableOsuJudgement.cs | 11 +++++++++++ osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 11 +---------- osu.Game/Rulesets/Judgements/DrawableJudgement.cs | 2 +- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs index 9d0c406295..fa980c7581 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs @@ -43,6 +43,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } } + public override void Apply(JudgementResult result, DrawableHitObject judgedObject) + { + base.Apply(result, judgedObject); + + if (judgedObject?.HitObject is OsuHitObject osuObject) + { + Position = osuObject.StackedPosition; + Scale = new Vector2(osuObject.Scale); + } + } + protected override void PrepareForUse() { base.PrepareForUse(); diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 291a572fdf..474e7c0f93 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -107,16 +107,7 @@ namespace osu.Game.Rulesets.Osu.UI if (!judgedObject.DisplayResult || !DisplayJudgements.Value) return; - var osuObject = (OsuHitObject)judgedObject.HitObject; - - DrawableOsuJudgement explosion = poolDictionary[result.Type].Get(doj => - { - doj.Apply(result, judgedObject); - - // todo: move to JudgedObject property? - doj.Position = osuObject.StackedEndPosition; - doj.Scale = new Vector2(osuObject.Scale); - }); + DrawableOsuJudgement explosion = poolDictionary[result.Type].Get(doj => doj.Apply(result, judgedObject)); judgementLayer.Add(explosion); } diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs index 1671e97db1..4e7f0018ef 100644 --- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs @@ -75,7 +75,7 @@ namespace osu.Game.Rulesets.Judgements this.Delay(FadeOutDelay).FadeOut(400); } - public void Apply([NotNull] JudgementResult result, [CanBeNull] DrawableHitObject judgedObject) + public virtual void Apply([NotNull] JudgementResult result, [CanBeNull] DrawableHitObject judgedObject) { Result = result; JudgedObject = judgedObject; From 024fb52726d1d2b694caa6cc9fc5886dc76ae02e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Jul 2020 19:05:31 +0900 Subject: [PATCH 2363/2376] Fix unnecessary using --- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 474e7c0f93..600efefca3 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -4,21 +4,20 @@ using System; using System.Collections.Generic; using System.Linq; -using osuTK; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; -using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.Objects.Drawables; -using osu.Game.Rulesets.Osu.Objects.Drawables.Connections; -using osu.Game.Rulesets.UI; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables.Connections; using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI; using osu.Game.Skinning; +using osuTK; namespace osu.Game.Rulesets.Osu.UI { From cf3251a950a2f3318ce33b92f9b5b9a5c2655493 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Jul 2020 19:36:54 +0900 Subject: [PATCH 2364/2376] Default to showing all rooms --- osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs b/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs index 3a4ead44b7..f43763486c 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs @@ -16,7 +16,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components { protected override Color4 BackgroundColour => Color4.Black.Opacity(0.5f); protected override RoomStatusFilter DefaultTab => RoomStatusFilter.Open; - protected override RoomCategoryFilter DefaultCategory => RoomCategoryFilter.Normal; + protected override RoomCategoryFilter DefaultCategory => RoomCategoryFilter.Any; protected override float ContentHorizontalPadding => base.ContentHorizontalPadding + OsuScreen.HORIZONTAL_OVERFLOW_PADDING; @@ -81,6 +81,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components public enum RoomCategoryFilter { + Any, Normal, Spotlight } From 64e8dce1ada23b4101ca49336cb382be1a76aff4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Jul 2020 19:37:27 +0900 Subject: [PATCH 2365/2376] Highlight spotlight rooms with a different colour --- .../Visual/Multiplayer/RoomManagerTestScene.cs | 3 ++- osu.Game/Online/Multiplayer/Room.cs | 4 ++++ osu.Game/Online/Multiplayer/RoomCategory.cs | 11 +++++++++++ .../Multi/Components/StatusColouredContainer.cs | 9 ++++++++- .../Screens/Multi/Lounge/Components/DrawableRoom.cs | 8 +++++--- 5 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 osu.Game/Online/Multiplayer/RoomCategory.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/RoomManagerTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/RoomManagerTestScene.cs index 46bc279d5c..8b7e0fd9da 100644 --- a/osu.Game.Tests/Visual/Multiplayer/RoomManagerTestScene.cs +++ b/osu.Game.Tests/Visual/Multiplayer/RoomManagerTestScene.cs @@ -34,7 +34,8 @@ namespace osu.Game.Tests.Visual.Multiplayer RoomID = { Value = i }, Name = { Value = $"Room {i}" }, Host = { Value = new User { Username = "Host" } }, - EndDate = { Value = DateTimeOffset.Now + TimeSpan.FromSeconds(10) } + EndDate = { Value = DateTimeOffset.Now + TimeSpan.FromSeconds(10) }, + Category = { Value = i % 2 == 0 ? RoomCategory.Spotlight : RoomCategory.Normal } }; if (ruleset != null) diff --git a/osu.Game/Online/Multiplayer/Room.cs b/osu.Game/Online/Multiplayer/Room.cs index 66d5d8b3e0..34cf158442 100644 --- a/osu.Game/Online/Multiplayer/Room.cs +++ b/osu.Game/Online/Multiplayer/Room.cs @@ -34,6 +34,10 @@ namespace osu.Game.Online.Multiplayer [JsonProperty("channel_id")] public readonly Bindable ChannelId = new Bindable(); + [Cached] + [JsonProperty("category")] + public readonly Bindable Category = new Bindable(); + [Cached] [JsonIgnore] public readonly Bindable Duration = new Bindable(TimeSpan.FromMinutes(30)); diff --git a/osu.Game/Online/Multiplayer/RoomCategory.cs b/osu.Game/Online/Multiplayer/RoomCategory.cs new file mode 100644 index 0000000000..636a73a3e9 --- /dev/null +++ b/osu.Game/Online/Multiplayer/RoomCategory.cs @@ -0,0 +1,11 @@ +// 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.Online.Multiplayer +{ + public enum RoomCategory + { + Normal, + Spotlight + } +} diff --git a/osu.Game/Screens/Multi/Components/StatusColouredContainer.cs b/osu.Game/Screens/Multi/Components/StatusColouredContainer.cs index 97af6674bf..a115f06e7b 100644 --- a/osu.Game/Screens/Multi/Components/StatusColouredContainer.cs +++ b/osu.Game/Screens/Multi/Components/StatusColouredContainer.cs @@ -17,6 +17,9 @@ namespace osu.Game.Screens.Multi.Components [Resolved(typeof(Room), nameof(Room.Status))] private Bindable status { get; set; } + [Resolved(typeof(Room), nameof(Room.Category))] + private Bindable category { get; set; } + public StatusColouredContainer(double transitionDuration = 100) { this.transitionDuration = transitionDuration; @@ -25,7 +28,11 @@ namespace osu.Game.Screens.Multi.Components [BackgroundDependencyLoader] private void load(OsuColour colours) { - status.BindValueChanged(s => this.FadeColour(s.NewValue.GetAppropriateColour(colours), transitionDuration), true); + status.BindValueChanged(s => + { + this.FadeColour(category.Value == RoomCategory.Spotlight ? colours.Pink : s.NewValue.GetAppropriateColour(colours) + , transitionDuration); + }, true); } } } diff --git a/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs index de02d779e1..3f5a2eb1d3 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs @@ -107,6 +107,8 @@ namespace osu.Game.Screens.Multi.Lounge.Components [BackgroundDependencyLoader] private void load(OsuColour colours) { + float stripWidth = side_strip_width * (Room.Category.Value == RoomCategory.Spotlight ? 2 : 1); + Children = new Drawable[] { new StatusColouredContainer(transition_duration) @@ -139,7 +141,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components new StatusColouredContainer(transition_duration) { RelativeSizeAxes = Axes.Y, - Width = side_strip_width, + Width = stripWidth, Child = new Box { RelativeSizeAxes = Axes.Both } }, new Container @@ -147,7 +149,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components RelativeSizeAxes = Axes.Y, Width = cover_width, Masking = true, - Margin = new MarginPadding { Left = side_strip_width }, + Margin = new MarginPadding { Left = stripWidth }, Child = new MultiplayerBackgroundSprite(BeatmapSetCoverType.List) { RelativeSizeAxes = Axes.Both } }, new Container @@ -156,7 +158,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components Padding = new MarginPadding { Vertical = content_padding, - Left = side_strip_width + cover_width + content_padding, + Left = stripWidth + cover_width + content_padding, Right = content_padding, }, Children = new Drawable[] From fe585611e7169ebc615c2ad03932bcfef2504a7b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 10 Jul 2020 19:54:09 +0900 Subject: [PATCH 2366/2376] Fix + simplify web request --- .../Online/API/Requests/GetRoomsRequest.cs | 25 ++++--------------- .../Multi/Lounge/Components/FilterControl.cs | 2 +- 2 files changed, 6 insertions(+), 21 deletions(-) diff --git a/osu.Game/Online/API/Requests/GetRoomsRequest.cs b/osu.Game/Online/API/Requests/GetRoomsRequest.cs index 4b90b04b51..c47ed20909 100644 --- a/osu.Game/Online/API/Requests/GetRoomsRequest.cs +++ b/osu.Game/Online/API/Requests/GetRoomsRequest.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using Humanizer; using osu.Framework.IO.Network; using osu.Game.Online.Multiplayer; using osu.Game.Screens.Multi.Lounge.Components; @@ -23,27 +24,11 @@ namespace osu.Game.Online.API.Requests { var req = base.CreateWebRequest(); - switch (statusFilter) - { - case RoomStatusFilter.Owned: - req.AddParameter("mode", "owned"); - break; + if (statusFilter != RoomStatusFilter.Open) + req.AddParameter("mode", statusFilter.ToString().Underscore().ToLowerInvariant()); - case RoomStatusFilter.Participated: - req.AddParameter("mode", "participated"); - break; - - case RoomStatusFilter.RecentlyEnded: - req.AddParameter("mode", "ended"); - break; - } - - switch (categoryFilter) - { - case RoomCategoryFilter.Spotlight: - req.AddParameter("category", "spotlight"); - break; - } + if (categoryFilter != RoomCategoryFilter.Any) + req.AddParameter("category", categoryFilter.ToString().Underscore().ToLowerInvariant()); return req; } diff --git a/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs b/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs index f43763486c..be1083ce8d 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs @@ -74,7 +74,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components Open, [Description("Recently Ended")] - RecentlyEnded, + Ended, Participated, Owned, } From 9556166c1b963e60a82690e60e6cd0ef568d7c46 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Jul 2020 19:37:27 +0900 Subject: [PATCH 2367/2376] Replace large "show results" button with embedded button each playlist item --- .../Screens/Multi/DrawableRoomPlaylistItem.cs | 49 ++++++++------ .../Multi/DrawableRoomPlaylistWithResults.cs | 66 +++++++++++++++++++ .../Screens/Multi/Match/MatchSubScreen.cs | 28 ++------ 3 files changed, 103 insertions(+), 40 deletions(-) create mode 100644 osu.Game/Screens/Multi/DrawableRoomPlaylistWithResults.cs diff --git a/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs index 414c1f5748..8086449401 100644 --- a/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -48,7 +49,8 @@ namespace osu.Game.Screens.Multi private readonly Bindable ruleset = new Bindable(); private readonly BindableList requiredMods = new BindableList(); - private readonly PlaylistItem item; + public readonly PlaylistItem Item; + private readonly bool allowEdit; private readonly bool allowSelection; @@ -57,8 +59,11 @@ namespace osu.Game.Screens.Multi public DrawableRoomPlaylistItem(PlaylistItem item, bool allowEdit, bool allowSelection) : base(item) { - this.item = item; + Item = item; + + // TODO: edit support should be moved out into a derived class this.allowEdit = allowEdit; + this.allowSelection = allowSelection; beatmap.BindTo(item.Beatmap); @@ -91,6 +96,8 @@ namespace osu.Game.Screens.Multi private ScheduledDelegate scheduledRefresh; + public FillFlowContainer ButtonsContainer { get; private set; } + private void scheduleRefresh() { scheduledRefresh?.Cancel(); @@ -102,14 +109,14 @@ namespace osu.Game.Screens.Multi difficultyIconContainer.Child = new DifficultyIcon(beatmap.Value, ruleset.Value) { Size = new Vector2(32) }; beatmapText.Clear(); - beatmapText.AddLink(item.Beatmap.ToString(), LinkAction.OpenBeatmap, item.Beatmap.Value.OnlineBeatmapID.ToString()); + beatmapText.AddLink(Item.Beatmap.ToString(), LinkAction.OpenBeatmap, Item.Beatmap.Value.OnlineBeatmapID.ToString()); authorText.Clear(); - if (item.Beatmap?.Value?.Metadata?.Author != null) + if (Item.Beatmap?.Value?.Metadata?.Author != null) { authorText.AddText("mapped by "); - authorText.AddUserLink(item.Beatmap.Value?.Metadata.Author); + authorText.AddUserLink(Item.Beatmap.Value?.Metadata.Author); } modDisplay.Current.Value = requiredMods.ToArray(); @@ -180,29 +187,33 @@ namespace osu.Game.Screens.Multi } } }, - new Container + ButtonsContainer = new FillFlowContainer { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, + Direction = FillDirection.Horizontal, AutoSizeAxes = Axes.Both, X = -18, - Children = new Drawable[] - { - new PlaylistDownloadButton(item) - { - Size = new Vector2(50, 30) - }, - new IconButton - { - Icon = FontAwesome.Solid.MinusSquare, - Alpha = allowEdit ? 1 : 0, - Action = () => RequestDeletion?.Invoke(Model), - }, - } + ChildrenEnumerable = CreateButtons() } } }; + protected virtual IEnumerable CreateButtons() => + new Drawable[] + { + new PlaylistDownloadButton(Item) + { + Size = new Vector2(50, 30) + }, + new IconButton + { + Icon = FontAwesome.Solid.MinusSquare, + Alpha = allowEdit ? 1 : 0, + Action = () => RequestDeletion?.Invoke(Model), + }, + }; + protected override bool OnClick(ClickEvent e) { if (allowSelection) diff --git a/osu.Game/Screens/Multi/DrawableRoomPlaylistWithResults.cs b/osu.Game/Screens/Multi/DrawableRoomPlaylistWithResults.cs new file mode 100644 index 0000000000..439aaaa275 --- /dev/null +++ b/osu.Game/Screens/Multi/DrawableRoomPlaylistWithResults.cs @@ -0,0 +1,66 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Multiplayer; + +namespace osu.Game.Screens.Multi +{ + public class DrawableRoomPlaylistWithResults : DrawableRoomPlaylist + { + public Action RequestShowResults; + + public DrawableRoomPlaylistWithResults() + : base(false, true) + { + } + + protected override OsuRearrangeableListItem CreateOsuDrawable(PlaylistItem item) => + new DrawableRoomPlaylistItemWithResults(item, false, true) + { + RequestShowResults = () => RequestShowResults(item), + SelectedItem = { BindTarget = SelectedItem }, + }; + + private class DrawableRoomPlaylistItemWithResults : DrawableRoomPlaylistItem + { + public Action RequestShowResults; + + public DrawableRoomPlaylistItemWithResults(PlaylistItem item, bool allowEdit, bool allowSelection) + : base(item, allowEdit, allowSelection) + { + } + + protected override IEnumerable CreateButtons() => + base.CreateButtons().Prepend(new FilledIconButton + { + Icon = FontAwesome.Solid.ChartPie, + Action = () => RequestShowResults?.Invoke(), + TooltipText = "View results" + }); + + private class FilledIconButton : IconButton + { + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Add(new Box + { + RelativeSizeAxes = Axes.Both, + Depth = float.MaxValue, + Colour = colours.Gray4, + }); + } + } + } + } +} diff --git a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs index 40a8427701..7c2d5cf85d 100644 --- a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs +++ b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs @@ -11,7 +11,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Game.Audio; using osu.Game.Beatmaps; -using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.GameTypes; @@ -137,30 +136,23 @@ namespace osu.Game.Screens.Multi.Match new Drawable[] { new OverlinedHeader("Playlist"), }, new Drawable[] { - new DrawableRoomPlaylist(false, true) // Temporarily always allow selection + new DrawableRoomPlaylistWithResults { RelativeSizeAxes = Axes.Both, Items = { BindTarget = playlist }, - SelectedItem = { BindTarget = SelectedItem } + SelectedItem = { BindTarget = SelectedItem }, + RequestShowResults = item => + { + Debug.Assert(roomId.Value != null); + multiplayer?.Push(new TimeshiftResultsScreen(null, roomId.Value.Value, item, false)); + } } }, - null, - new Drawable[] - { - new TriangleButton - { - RelativeSizeAxes = Axes.X, - Text = "Show beatmap results", - Action = showBeatmapResults - } - } }, RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize), new Dimension(), - new Dimension(GridSizeMode.Absolute, 5), - new Dimension(GridSizeMode.AutoSize) } } }, @@ -296,11 +288,5 @@ namespace osu.Game.Screens.Multi.Match break; } } - - private void showBeatmapResults() - { - Debug.Assert(roomId.Value != null); - multiplayer?.Push(new TimeshiftResultsScreen(null, roomId.Value.Value, SelectedItem.Value, false)); - } } } From c7b5c5aef42689f2d0dfe6ab817189dc15a4448d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Jul 2020 20:22:51 +0900 Subject: [PATCH 2368/2376] Add tooltips to beatmap download button --- osu.Game/Graphics/UserInterface/DownloadButton.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Graphics/UserInterface/DownloadButton.cs b/osu.Game/Graphics/UserInterface/DownloadButton.cs index 86a5cb9aa6..bec5a45556 100644 --- a/osu.Game/Graphics/UserInterface/DownloadButton.cs +++ b/osu.Game/Graphics/UserInterface/DownloadButton.cs @@ -63,22 +63,26 @@ namespace osu.Game.Graphics.UserInterface background.FadeColour(colours.Gray4, 500, Easing.InOutExpo); icon.MoveToX(0, 500, Easing.InOutExpo); checkmark.ScaleTo(Vector2.Zero, 500, Easing.InOutExpo); + TooltipText = "Download"; break; case DownloadState.Downloading: background.FadeColour(colours.Blue, 500, Easing.InOutExpo); icon.MoveToX(0, 500, Easing.InOutExpo); checkmark.ScaleTo(Vector2.Zero, 500, Easing.InOutExpo); + TooltipText = "Downloading..."; break; case DownloadState.Downloaded: background.FadeColour(colours.Yellow, 500, Easing.InOutExpo); + TooltipText = "Importing"; break; case DownloadState.LocallyAvailable: background.FadeColour(colours.Green, 500, Easing.InOutExpo); icon.MoveToX(-8, 500, Easing.InOutExpo); checkmark.ScaleTo(new Vector2(13), 500, Easing.InOutExpo); + TooltipText = "Go to beatmap"; break; } } From 840380e0de4d068b5fe16af44a01b22ef948f7a3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Jul 2020 20:25:52 +0900 Subject: [PATCH 2369/2376] Fix LocallyAvailable state case getting cleared --- osu.Game/Graphics/UserInterface/DownloadButton.cs | 1 - .../BeatmapListing/Panels/BeatmapPanelDownloadButton.cs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/DownloadButton.cs b/osu.Game/Graphics/UserInterface/DownloadButton.cs index bec5a45556..da6c95299e 100644 --- a/osu.Game/Graphics/UserInterface/DownloadButton.cs +++ b/osu.Game/Graphics/UserInterface/DownloadButton.cs @@ -82,7 +82,6 @@ namespace osu.Game.Graphics.UserInterface background.FadeColour(colours.Green, 500, Easing.InOutExpo); icon.MoveToX(-8, 500, Easing.InOutExpo); checkmark.ScaleTo(new Vector2(13), 500, Easing.InOutExpo); - TooltipText = "Go to beatmap"; break; } } diff --git a/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelDownloadButton.cs b/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelDownloadButton.cs index 67782dfe3f..001ca801d9 100644 --- a/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelDownloadButton.cs +++ b/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelDownloadButton.cs @@ -81,7 +81,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels { case DownloadState.LocallyAvailable: button.Enabled.Value = true; - button.TooltipText = string.Empty; + button.TooltipText = "Go to beatmap"; break; default: From d3367bb0f14597f959c7032beaecfd07383c6498 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Jul 2020 20:32:47 +0900 Subject: [PATCH 2370/2376] Remove unused public property --- osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs index 8086449401..c0892235f2 100644 --- a/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs @@ -96,8 +96,6 @@ namespace osu.Game.Screens.Multi private ScheduledDelegate scheduledRefresh; - public FillFlowContainer ButtonsContainer { get; private set; } - private void scheduleRefresh() { scheduledRefresh?.Cancel(); @@ -187,7 +185,7 @@ namespace osu.Game.Screens.Multi } } }, - ButtonsContainer = new FillFlowContainer + new FillFlowContainer { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, From 13205319f3c910a12e8854da1607244d27da65f8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Jul 2020 22:37:29 +0900 Subject: [PATCH 2371/2376] Fix null reference if hit lighting is disabled --- .../Objects/Drawables/DrawableOsuJudgement.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs index fa980c7581..f32ce2c4cd 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs @@ -60,10 +60,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables lightingColour?.UnbindAll(); - if (JudgedObject != null) + if (JudgedObject != null && lighting != null) { lightingColour = JudgedObject.AccentColour.GetBoundCopy(); - lightingColour.BindValueChanged(colour => lighting.Colour = Result.Type == HitResult.Miss ? Color4.Transparent : colour.NewValue, true); + lightingColour.BindValueChanged(colour => lighting.Colour = Result?.Type == HitResult.Miss ? Color4.Transparent : colour.NewValue, true); } else { From 0a61f80c8b1ab7cf1d3b1060c28831c2547027d4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Jul 2020 22:39:35 +0900 Subject: [PATCH 2372/2376] Remove result nullable check --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs index f32ce2c4cd..33ad674679 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs @@ -63,7 +63,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (JudgedObject != null && lighting != null) { lightingColour = JudgedObject.AccentColour.GetBoundCopy(); - lightingColour.BindValueChanged(colour => lighting.Colour = Result?.Type == HitResult.Miss ? Color4.Transparent : colour.NewValue, true); + lightingColour.BindValueChanged(colour => lighting.Colour = Result.Type == HitResult.Miss ? Color4.Transparent : colour.NewValue, true); } else { From dd025262d07a7e6e94fc448c094ae7dc5179bd78 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Jul 2020 22:48:34 +0900 Subject: [PATCH 2373/2376] Fix one more nullref --- .../Objects/Drawables/DrawableOsuJudgement.cs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs index 33ad674679..cfe969d1cc 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs @@ -60,14 +60,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables lightingColour?.UnbindAll(); - if (JudgedObject != null && lighting != null) + if (lighting != null) { - lightingColour = JudgedObject.AccentColour.GetBoundCopy(); - lightingColour.BindValueChanged(colour => lighting.Colour = Result.Type == HitResult.Miss ? Color4.Transparent : colour.NewValue, true); - } - else - { - lighting.Colour = Color4.White; + if (JudgedObject != null) + { + lightingColour = JudgedObject.AccentColour.GetBoundCopy(); + lightingColour.BindValueChanged(colour => lighting.Colour = Result.Type == HitResult.Miss ? Color4.Transparent : colour.NewValue, true); + } + else + { + lighting.Colour = Color4.White; + } } } From acfb6eecc65b5a3b66af8f44d155178f7b0cbcf7 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 11 Jul 2020 20:17:40 +0900 Subject: [PATCH 2374/2376] Fix bonus judgements being required toward HP --- .../TestSceneDrainingHealthProcessor.cs | 37 ++++++++++++++++++- .../Scoring/DrainingHealthProcessor.cs | 4 +- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs index e50b2231bf..460ad1b898 100644 --- a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs +++ b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs @@ -157,6 +157,24 @@ namespace osu.Game.Tests.Gameplay assertHealthNotEqualTo(1); } + [Test] + public void TestBonusObjectsExcludedFromDrain() + { + var beatmap = new Beatmap + { + BeatmapInfo = { BaseDifficulty = { DrainRate = 10 } }, + }; + + beatmap.HitObjects.Add(new JudgeableHitObject { StartTime = 0 }); + for (double time = 0; time < 5000; time += 100) + beatmap.HitObjects.Add(new JudgeableHitObject(false) { StartTime = time }); + beatmap.HitObjects.Add(new JudgeableHitObject { StartTime = 5000 }); + + createProcessor(beatmap); + setTime(4900); // Get close to the second combo-affecting object + assertHealthNotEqualTo(0); + } + private Beatmap createBeatmap(double startTime, double endTime, params BreakPeriod[] breaks) { var beatmap = new Beatmap @@ -197,8 +215,25 @@ namespace osu.Game.Tests.Gameplay private class JudgeableHitObject : HitObject { - public override Judgement CreateJudgement() => new Judgement(); + private readonly bool affectsCombo; + + public JudgeableHitObject(bool affectsCombo = true) + { + this.affectsCombo = affectsCombo; + } + + public override Judgement CreateJudgement() => new TestJudgement(affectsCombo); protected override HitWindows CreateHitWindows() => new HitWindows(); + + private class TestJudgement : Judgement + { + public override bool AffectsCombo { get; } + + public TestJudgement(bool affectsCombo) + { + AffectsCombo = affectsCombo; + } + } } } } diff --git a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs index ef341575fa..130907b242 100644 --- a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs @@ -114,7 +114,9 @@ namespace osu.Game.Rulesets.Scoring protected override void ApplyResultInternal(JudgementResult result) { base.ApplyResultInternal(result); - healthIncreases.Add((result.HitObject.GetEndTime() + result.TimeOffset, GetHealthIncreaseFor(result))); + + if (!result.Judgement.IsBonus) + healthIncreases.Add((result.HitObject.GetEndTime() + result.TimeOffset, GetHealthIncreaseFor(result))); } protected override void Reset(bool storeResults) From ede4235884d852989275ba305411991dc7a3806e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 11 Jul 2020 20:33:42 +0900 Subject: [PATCH 2375/2376] Increase HP gain of bananas --- osu.Game.Rulesets.Catch/Judgements/CatchBananaJudgement.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Judgements/CatchBananaJudgement.cs b/osu.Game.Rulesets.Catch/Judgements/CatchBananaJudgement.cs index fc030877f1..a7449ba4e1 100644 --- a/osu.Game.Rulesets.Catch/Judgements/CatchBananaJudgement.cs +++ b/osu.Game.Rulesets.Catch/Judgements/CatchBananaJudgement.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Judgements return 0; case HitResult.Perfect: - return 0.01; + return DEFAULT_MAX_HEALTH_INCREASE * 0.75; } } From 2bb0283a6855c10599e58f97eea8ca6484553a8e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 12 Jul 2020 00:52:55 +0900 Subject: [PATCH 2376/2376] Clean up HitEvents after use to avoid near-permanent memory retention --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 9c1bc35169..eb49638d59 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -252,6 +252,12 @@ namespace osu.Game.Rulesets.Scoring HighestCombo.Value = 0; } + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + hitEvents.Clear(); + } + /// /// Retrieve a score populated with data for the current play this processor is responsible for. /// @@ -269,7 +275,7 @@ namespace osu.Game.Rulesets.Scoring foreach (var result in Enum.GetValues(typeof(HitResult)).OfType().Where(r => r > HitResult.None && hitWindows.IsHitResultAllowed(r))) score.Statistics[result] = GetStatistic(result); - score.HitEvents = new List(hitEvents); + score.HitEvents = hitEvents; } ///
    /// Sets a replay to be used, overriding local input. diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 375976ea6c..acc8dc9c7c 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -184,7 +184,7 @@ namespace osu.Game.Screens.Play addGameplayComponents(GameplayClockContainer, Beatmap.Value, playableBeatmap); addOverlayComponents(GameplayClockContainer, Beatmap.Value); - if (!DrawableRuleset.DisplayHud) + if (!DrawableRuleset.AllowGameplayOverlays) { HUDOverlay.ShowHud.Value = false; HUDOverlay.ShowHud.Disabled = true; From a59db976d66fab80cb3035dd779c59db6070d68c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 8 May 2020 18:05:06 +0900 Subject: [PATCH 1478/2376] Fix loading a ruleset with a new version specification causing a crash --- osu.Game/Rulesets/RulesetInfo.cs | 16 +++++++++++++++- osu.Game/Rulesets/RulesetStore.cs | 12 ++++-------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/osu.Game/Rulesets/RulesetInfo.cs b/osu.Game/Rulesets/RulesetInfo.cs index afd499cb9e..2e32b96084 100644 --- a/osu.Game/Rulesets/RulesetInfo.cs +++ b/osu.Game/Rulesets/RulesetInfo.cs @@ -3,6 +3,7 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Linq; using Newtonsoft.Json; namespace osu.Game.Rulesets @@ -15,7 +16,20 @@ namespace osu.Game.Rulesets public string ShortName { get; set; } - public string InstantiationInfo { get; set; } + private string instantiationInfo; + + public string InstantiationInfo + { + get => instantiationInfo; + set => instantiationInfo = abbreviateInstantiationInfo(value); + } + + private string abbreviateInstantiationInfo(string value) + { + // exclude version onwards, matching only on namespace and type. + // this is mainly to allow for new versions of already loaded rulesets to "upgrade" from old. + return string.Join(',', value.Split(',').Take(2)); + } [JsonIgnore] public bool Available { get; set; } diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index f302f8700f..b8f2abd766 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -93,7 +93,9 @@ namespace osu.Game.Rulesets // add any other modes foreach (var r in instances.Where(r => !(r is ILegacyRuleset))) { - if (context.RulesetInfo.FirstOrDefault(ri => ri.InstantiationInfo == r.RulesetInfo.InstantiationInfo) == null) + // todo: StartsWith can be changed to Equals on 2020-11-08 + // This is to give users enough time to have their database use new abbreviated info). + if (context.RulesetInfo.FirstOrDefault(ri => r.RulesetInfo.InstantiationInfo.StartsWith(ri.InstantiationInfo)) == null) context.RulesetInfo.Add(r.RulesetInfo); } @@ -104,13 +106,7 @@ namespace osu.Game.Rulesets { try { - var instanceInfo = ((Ruleset)Activator.CreateInstance(Type.GetType(r.InstantiationInfo, asm => - { - // for the time being, let's ignore the version being loaded. - // this allows for debug builds to successfully load rulesets (even though debug rulesets have a 0.0.0 version). - asm.Version = null; - return Assembly.Load(asm); - }, null))).RulesetInfo; + var instanceInfo = ((Ruleset)Activator.CreateInstance(Type.GetType(r.InstantiationInfo))).RulesetInfo; r.Name = instanceInfo.Name; r.ShortName = instanceInfo.ShortName; From dcfef6b44383c48246ab2a648b13855d13e8834c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 8 May 2020 18:46:37 +0900 Subject: [PATCH 1479/2376] Add clear method to EditorBeatmap --- osu.Game/Screens/Edit/EditorBeatmap.cs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 2e8e03bc73..23c8c9f605 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -4,6 +4,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -201,6 +202,25 @@ namespace osu.Game.Screens.Edit updateHitObject(null, true); } + /// + /// Clears all from this . + /// + public void Clear() + { + var removed = HitObjects.ToList(); + + mutableHitObjects.Clear(); + + foreach (var b in startTimeBindables) + b.Value.UnbindAll(); + startTimeBindables.Clear(); + + foreach (var h in removed) + HitObjectRemoved?.Invoke(h); + + updateHitObject(null, true); + } + private void trackStartTime(HitObject hitObject) { startTimeBindables[hitObject] = hitObject.StartTimeBindable.GetBoundCopy(); From efff2bf15df6d588bcb6335c6bcac54c572fe23a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 8 May 2020 18:49:19 +0900 Subject: [PATCH 1480/2376] Add HitObject to DefaultsApplied event --- osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs | 2 +- .../Objects/Drawables/Connections/FollowPointConnection.cs | 2 +- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 7 +++++-- osu.Game/Rulesets/Objects/HitObject.cs | 4 ++-- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs index a6c3be7e5a..c3b4d2625e 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs @@ -399,7 +399,7 @@ namespace osu.Game.Rulesets.Osu.Tests { public TestSlider() { - DefaultsApplied += () => + DefaultsApplied += _ => { HeadCircle.HitWindows = new TestHitWindows(); TailCircle.HitWindows = new TestHitWindows(); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs index 6f09bbcd57..8a0ef22c4a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs @@ -78,7 +78,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections private void bindEvents(DrawableOsuHitObject drawableObject) { drawableObject.HitObject.PositionBindable.BindValueChanged(_ => scheduleRefresh()); - drawableObject.HitObject.DefaultsApplied += scheduleRefresh; + drawableObject.HitObject.DefaultsApplied += _ => scheduleRefresh(); } private void scheduleRefresh() diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 0047142cbd..3838e52f9b 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -148,7 +148,7 @@ namespace osu.Game.Rulesets.Objects.Drawables samplesBindable.CollectionChanged += (_, __) => loadSamples(); updateState(ArmedState.Idle, true); - onDefaultsApplied(); + apply(HitObject); } private void loadSamples() @@ -175,7 +175,10 @@ namespace osu.Game.Rulesets.Objects.Drawables AddInternal(Samples); } - private void onDefaultsApplied() => apply(HitObject); + private void onDefaultsApplied(HitObject hitObject) + { + apply(hitObject); + } private void apply(HitObject hitObject) { diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs index 9a8efdde84..cffbdbae08 100644 --- a/osu.Game/Rulesets/Objects/HitObject.cs +++ b/osu.Game/Rulesets/Objects/HitObject.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Objects /// /// Invoked after has completed on this . /// - public event Action DefaultsApplied; + public event Action DefaultsApplied; public readonly Bindable StartTimeBindable = new BindableDouble(); @@ -124,7 +124,7 @@ namespace osu.Game.Rulesets.Objects foreach (var h in nestedHitObjects) h.ApplyDefaults(controlPointInfo, difficulty); - DefaultsApplied?.Invoke(); + DefaultsApplied?.Invoke(this); } protected virtual void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) From 22dda3fe0231dc01c66591724097aef72e3c7f1a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 8 May 2020 18:49:58 +0900 Subject: [PATCH 1481/2376] Make ScrollingHitObjectContainer respond to defaults applied events --- .../Objects/Drawables/DrawableHitObject.cs | 3 + .../Scrolling/ScrollingHitObjectContainer.cs | 69 +++++++++++++------ 2 files changed, 51 insertions(+), 21 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 3838e52f9b..ba6571fe1a 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -25,6 +25,8 @@ namespace osu.Game.Rulesets.Objects.Drawables [Cached(typeof(DrawableHitObject))] public abstract class DrawableHitObject : SkinReloadableDrawable { + public event Action DefaultsApplied; + public readonly HitObject HitObject; /// @@ -178,6 +180,7 @@ namespace osu.Game.Rulesets.Objects.Drawables private void onDefaultsApplied(HitObject hitObject) { apply(hitObject); + DefaultsApplied?.Invoke(this); } private void apply(HitObject hitObject) diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index 57f58be55a..3e01bb1d31 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -16,17 +16,23 @@ namespace osu.Game.Rulesets.UI.Scrolling { private readonly IBindable timeRange = new BindableDouble(); private readonly IBindable direction = new Bindable(); + private readonly Dictionary hitObjectInitialStateCache = new Dictionary(); [Resolved] private IScrollingInfo scrollingInfo { get; set; } - private readonly LayoutValue initialStateCache = new LayoutValue(Invalidation.RequiredParentSizeToFit | Invalidation.DrawInfo); + // Responds to changes in the layout. When the layout is changes, all hit object states must be recomputed. + private readonly LayoutValue layoutCache = new LayoutValue(Invalidation.RequiredParentSizeToFit | Invalidation.DrawInfo); + + // A combined cache across all hit object states to reduce per-update iterations. + // When invalidated, one or more (but not necessarily all) hitobject states must be re-validated. + private readonly Cached combinedObjCache = new Cached(); public ScrollingHitObjectContainer() { RelativeSizeAxes = Axes.Both; - AddLayout(initialStateCache); + AddLayout(layoutCache); } [BackgroundDependencyLoader] @@ -35,13 +41,14 @@ namespace osu.Game.Rulesets.UI.Scrolling direction.BindTo(scrollingInfo.Direction); timeRange.BindTo(scrollingInfo.TimeRange); - direction.ValueChanged += _ => initialStateCache.Invalidate(); - timeRange.ValueChanged += _ => initialStateCache.Invalidate(); + direction.ValueChanged += _ => layoutCache.Invalidate(); + timeRange.ValueChanged += _ => layoutCache.Invalidate(); } public override void Add(DrawableHitObject hitObject) { - initialStateCache.Invalidate(); + combinedObjCache.Invalidate(); + hitObject.DefaultsApplied += onDefaultsApplied; base.Add(hitObject); } @@ -51,8 +58,10 @@ namespace osu.Game.Rulesets.UI.Scrolling if (result) { - initialStateCache.Invalidate(); + combinedObjCache.Invalidate(); hitObjectInitialStateCache.Remove(hitObject); + + hitObject.DefaultsApplied -= onDefaultsApplied; } return result; @@ -60,23 +69,45 @@ namespace osu.Game.Rulesets.UI.Scrolling public override void Clear(bool disposeChildren = true) { + foreach (var h in Objects) + h.DefaultsApplied -= onDefaultsApplied; + base.Clear(disposeChildren); - initialStateCache.Invalidate(); + combinedObjCache.Invalidate(); hitObjectInitialStateCache.Clear(); } + private void onDefaultsApplied(DrawableHitObject drawableObject) + { + // The cache may not exist if the hitobject state hasn't been computed yet (e.g. if the hitobject was added + defaults applied in the same frame). + // In such a case, combinedObjCache will take care of updating the hitobject. + if (hitObjectInitialStateCache.TryGetValue(drawableObject, out var objCache)) + { + combinedObjCache.Invalidate(); + objCache.Invalidate(); + } + } + private float scrollLength; protected override void Update() { base.Update(); - if (!initialStateCache.IsValid) + if (!layoutCache.IsValid) { foreach (var cached in hitObjectInitialStateCache.Values) cached.Invalidate(); + combinedObjCache.Invalidate(); + scrollingInfo.Algorithm.Reset(); + + layoutCache.Validate(); + } + + if (!combinedObjCache.IsValid) + { switch (direction.Value) { case ScrollingDirection.Up: @@ -89,15 +120,21 @@ namespace osu.Game.Rulesets.UI.Scrolling break; } - scrollingInfo.Algorithm.Reset(); - foreach (var obj in Objects) { + if (!hitObjectInitialStateCache.TryGetValue(obj, out var objCache)) + objCache = hitObjectInitialStateCache[obj] = new Cached(); + + if (objCache.IsValid) + return; + computeLifetimeStartRecursive(obj); computeInitialStateRecursive(obj); + + objCache.Validate(); } - initialStateCache.Validate(); + combinedObjCache.Validate(); } } @@ -109,8 +146,6 @@ namespace osu.Game.Rulesets.UI.Scrolling computeLifetimeStartRecursive(obj); } - private readonly Dictionary hitObjectInitialStateCache = new Dictionary(); - private double computeOriginAdjustedLifetimeStart(DrawableHitObject hitObject) { float originAdjustment = 0.0f; @@ -142,12 +177,6 @@ namespace osu.Game.Rulesets.UI.Scrolling // Cant use AddOnce() since the delegate is re-constructed every invocation private void computeInitialStateRecursive(DrawableHitObject hitObject) => hitObject.Schedule(() => { - if (!hitObjectInitialStateCache.TryGetValue(hitObject, out var cached)) - cached = hitObjectInitialStateCache[hitObject] = new Cached(); - - if (cached.IsValid) - return; - if (hitObject.HitObject is IHasEndTime e) { switch (direction.Value) @@ -171,8 +200,6 @@ namespace osu.Game.Rulesets.UI.Scrolling // Nested hitobjects don't need to scroll, but they do need accurate positions updatePosition(obj, hitObject.HitObject.StartTime); } - - cached.Validate(); }); protected override void UpdateAfterChildrenLife() From d67facf8e4c28443840af1519ec7be669ef552d3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 8 May 2020 18:50:06 +0900 Subject: [PATCH 1482/2376] Add test scene --- .../TestSceneManiaHitObjectComposer.cs | 49 ++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs index 286e3f6e50..1554a956a2 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs @@ -10,10 +10,13 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; +using osu.Framework.Utils; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Edit; +using osu.Game.Rulesets.Mania.Edit.Blueprints; using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI.Scrolling; @@ -48,6 +51,8 @@ namespace osu.Game.Rulesets.Mania.Tests DrawableHitObject lastObject = null; Vector2 originalPosition = Vector2.Zero; + setScrollStep(ScrollingDirection.Up); + AddStep("seek to last object", () => { lastObject = this.ChildrenOfType().Single(d => d.HitObject == composer.EditorBeatmap.HitObjects.Last()); @@ -81,7 +86,7 @@ namespace osu.Game.Rulesets.Mania.Tests DrawableHitObject lastObject = null; Vector2 originalPosition = Vector2.Zero; - AddStep("set down scroll", () => ((Bindable)composer.Composer.ScrollingInfo.Direction).Value = ScrollingDirection.Down); + setScrollStep(ScrollingDirection.Down); AddStep("seek to last object", () => { @@ -116,6 +121,8 @@ namespace osu.Game.Rulesets.Mania.Tests DrawableHitObject lastObject = null; Vector2 originalPosition = Vector2.Zero; + setScrollStep(ScrollingDirection.Down); + AddStep("seek to last object", () => { lastObject = this.ChildrenOfType().Single(d => d.HitObject == composer.EditorBeatmap.HitObjects.Last()); @@ -147,6 +154,46 @@ namespace osu.Game.Rulesets.Mania.Tests AddAssert("hitobjects not moved vertically", () => lastObject.DrawPosition.Y - originalPosition.Y <= DefaultNotePiece.NOTE_HEIGHT); } + [Test] + public void TestDragHoldNoteSelectionVertically() + { + setScrollStep(ScrollingDirection.Down); + + AddStep("setup beatmap", () => + { + composer.EditorBeatmap.Clear(); + composer.EditorBeatmap.Add(new HoldNote + { + Column = 1, + EndTime = 200 + }); + }); + + DrawableHoldNote holdNote = null; + + AddStep("grab hold note", () => + { + holdNote = this.ChildrenOfType().FirstOrDefault(); + InputManager.MoveMouseTo(holdNote); + InputManager.PressButton(MouseButton.Left); + }); + + AddStep("move drag upwards", () => + { + InputManager.MoveMouseTo(holdNote, new Vector2(0, -100)); + InputManager.ReleaseButton(MouseButton.Left); + }); + + AddAssert("head note positioned correctly", () => Precision.AlmostEquals(holdNote.ScreenSpaceDrawQuad.BottomLeft, holdNote.Head.ScreenSpaceDrawQuad.BottomLeft)); + AddAssert("tail note positioned correctly", () => Precision.AlmostEquals(holdNote.ScreenSpaceDrawQuad.TopLeft, holdNote.Tail.ScreenSpaceDrawQuad.BottomLeft)); + + AddAssert("head blueprint positioned correctly", () => this.ChildrenOfType().ElementAt(0).DrawPosition == holdNote.Head.DrawPosition); + AddAssert("tail blueprint positioned correctly", () => this.ChildrenOfType().ElementAt(1).DrawPosition == holdNote.Tail.DrawPosition); + } + + private void setScrollStep(ScrollingDirection direction) + => AddStep($"set scroll direction = {direction}", () => ((Bindable)composer.Composer.ScrollingInfo.Direction).Value = direction); + private class TestComposer : CompositeDrawable { [Cached(typeof(EditorBeatmap))] From abd1115c6d1dcf068e7eccd48a987dc27fe54839 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 8 May 2020 19:08:43 +0900 Subject: [PATCH 1483/2376] Fix test failures --- osu.Game/OsuGameBase.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index d9f9e2de42..cf39c03f9d 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -302,7 +302,8 @@ namespace osu.Game { base.SetHost(host); - Storage = new OsuStorage(host); + if (Storage == null) // may be non-null for certain tests + Storage = new OsuStorage(host); if (LocalConfig == null) LocalConfig = new OsuConfigManager(Storage); From 1a31e1f10fe91f646e18b285c6312a4d8ebf86be Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 8 May 2020 19:13:41 +0900 Subject: [PATCH 1484/2376] Also check for AffectsCombo to avoid too many passing switches --- osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs index 027fe1f302..8cf8b9e05d 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs @@ -35,8 +35,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning LastResult.BindValueChanged(result => { + var r = result.NewValue; + + bool passing = r == null || (r.Judgement.AffectsCombo && r.Type > HitResult.Miss); + foreach (var sprite in InternalChildren.OfType()) - sprite.Passing = result.NewValue == null || result.NewValue.Type > HitResult.Miss; + sprite.Passing = passing; }, true); } From 115cbf25ae21c9f1a1d950f0783c875ff3bfd082 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 8 May 2020 19:15:19 +0900 Subject: [PATCH 1485/2376] Fix new sprites not getting spawned with correct passing state --- osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs index 8cf8b9e05d..9ddfe75b40 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs @@ -29,6 +29,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning ((IBindable)LastResult).BindTo(gameplayBeatmap.LastJudgementResult); } + private bool passing; + protected override void LoadComplete() { base.LoadComplete(); @@ -37,7 +39,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning { var r = result.NewValue; - bool passing = r == null || (r.Judgement.AffectsCombo && r.Type > HitResult.Miss); + passing = r == null || (r.Judgement.AffectsCombo && r.Type > HitResult.Miss); foreach (var sprite in InternalChildren.OfType()) sprite.Passing = passing; @@ -71,7 +73,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning if (last != null && last.ScreenSpaceDrawQuad.TopRight.X >= ScreenSpaceDrawQuad.TopRight.X) break; - AddInternal(new ScrollerSprite()); + AddInternal(new ScrollerSprite + { + Passing = passing + }); } } From be3b77cf25708ceb77d14d830a473f09d0403bfc Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 8 May 2020 20:09:59 +0900 Subject: [PATCH 1486/2376] Fix potentially skipping hitobject updates --- osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index 3e01bb1d31..ea72061216 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.UI.Scrolling objCache = hitObjectInitialStateCache[obj] = new Cached(); if (objCache.IsValid) - return; + continue; computeLifetimeStartRecursive(obj); computeInitialStateRecursive(obj); From 5c2778d5f08288211a1cd57b14beb85e456a513b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 8 May 2020 20:33:02 +0900 Subject: [PATCH 1487/2376] Change comparison direction --- osu.Game/Rulesets/RulesetStore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index b8f2abd766..b3026bf2b7 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -95,7 +95,7 @@ namespace osu.Game.Rulesets { // todo: StartsWith can be changed to Equals on 2020-11-08 // This is to give users enough time to have their database use new abbreviated info). - if (context.RulesetInfo.FirstOrDefault(ri => r.RulesetInfo.InstantiationInfo.StartsWith(ri.InstantiationInfo)) == null) + if (context.RulesetInfo.FirstOrDefault(ri => ri.InstantiationInfo.StartsWith(r.RulesetInfo.InstantiationInfo)) == null) context.RulesetInfo.Add(r.RulesetInfo); } From d1976b194d9bd2314775eb56dcbb1125738d0075 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sat, 9 May 2020 10:42:56 +0300 Subject: [PATCH 1488/2376] Check local availability before disabling buttons --- .../BeatmapListing/Panels/BeatmapPanelDownloadButton.cs | 2 +- osu.Game/Overlays/BeatmapSet/Header.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelDownloadButton.cs b/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelDownloadButton.cs index 589f2d5072..6d5862ee2b 100644 --- a/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelDownloadButton.cs +++ b/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelDownloadButton.cs @@ -50,7 +50,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels [BackgroundDependencyLoader(true)] private void load(OsuGame game, BeatmapManager beatmaps, OsuConfigManager osuConfig) { - if (BeatmapSet.Value?.OnlineInfo?.Availability?.DownloadDisabled ?? false) + if ((BeatmapSet.Value?.OnlineInfo?.Availability?.DownloadDisabled ?? false) && State.Value != DownloadState.LocallyAvailable) { button.Enabled.Value = false; button.TooltipText = "this beatmap is currently not available for download."; diff --git a/osu.Game/Overlays/BeatmapSet/Header.cs b/osu.Game/Overlays/BeatmapSet/Header.cs index 1ff08aab2c..06e31277dd 100644 --- a/osu.Game/Overlays/BeatmapSet/Header.cs +++ b/osu.Game/Overlays/BeatmapSet/Header.cs @@ -264,7 +264,7 @@ namespace osu.Game.Overlays.BeatmapSet { if (BeatmapSet.Value == null) return; - if (BeatmapSet.Value.OnlineInfo.Availability?.DownloadDisabled ?? false) + if ((BeatmapSet.Value.OnlineInfo.Availability?.DownloadDisabled ?? false) && State.Value != DownloadState.LocallyAvailable) { downloadButtonsContainer.Clear(); return; From 74cbe9306c6403c4bb7e99303306b1be194666c3 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Sat, 9 May 2020 16:39:46 +0800 Subject: [PATCH 1489/2376] Use strongly-typed JsonConerter. --- .../Serialization/Converters/TypedListConverter.cs | 12 ++++-------- .../Serialization/Converters/Vector2Converter.cs | 14 +++++--------- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/osu.Game/IO/Serialization/Converters/TypedListConverter.cs b/osu.Game/IO/Serialization/Converters/TypedListConverter.cs index 64f1ebeb1a..25fe335047 100644 --- a/osu.Game/IO/Serialization/Converters/TypedListConverter.cs +++ b/osu.Game/IO/Serialization/Converters/TypedListConverter.cs @@ -14,7 +14,7 @@ namespace osu.Game.IO.Serialization.Converters /// reconstruct the objects with their original types. /// /// The type of objects contained in the this attribute is attached to. - public class TypedListConverter : JsonConverter + public class TypedListConverter : JsonConverter> { private readonly bool requiresTypeVersion; @@ -36,9 +36,7 @@ namespace osu.Game.IO.Serialization.Converters this.requiresTypeVersion = requiresTypeVersion; } - public override bool CanConvert(Type objectType) => objectType == typeof(List); - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + public override List ReadJson(JsonReader reader, Type objectType, List existingValue, bool hasExistingValue, JsonSerializer serializer) { var list = new List(); @@ -59,14 +57,12 @@ namespace osu.Game.IO.Serialization.Converters return list; } - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + public override void WriteJson(JsonWriter writer, List value, JsonSerializer serializer) { - var list = (IEnumerable)value; - var lookupTable = new List(); var objects = new List(); - foreach (var item in list) + foreach (var item in value) { var type = item.GetType(); var assemblyName = type.Assembly.GetName(); diff --git a/osu.Game/IO/Serialization/Converters/Vector2Converter.cs b/osu.Game/IO/Serialization/Converters/Vector2Converter.cs index bf5edeef94..46447b607b 100644 --- a/osu.Game/IO/Serialization/Converters/Vector2Converter.cs +++ b/osu.Game/IO/Serialization/Converters/Vector2Converter.cs @@ -11,26 +11,22 @@ namespace osu.Game.IO.Serialization.Converters /// /// A type of that serializes only the X and Y coordinates of a . /// - public class Vector2Converter : JsonConverter + public class Vector2Converter : JsonConverter { - public override bool CanConvert(Type objectType) => objectType == typeof(Vector2); - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + public override Vector2 ReadJson(JsonReader reader, Type objectType, Vector2 existingValue, bool hasExistingValue, JsonSerializer serializer) { var obj = JObject.Load(reader); return new Vector2((float)obj["x"], (float)obj["y"]); } - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + public override void WriteJson(JsonWriter writer, Vector2 value, JsonSerializer serializer) { - var vector = (Vector2)value; - writer.WriteStartObject(); writer.WritePropertyName("x"); - writer.WriteValue(vector.X); + writer.WriteValue(value.X); writer.WritePropertyName("y"); - writer.WriteValue(vector.Y); + writer.WriteValue(value.Y); writer.WriteEndObject(); } From 55e0d91f37591061869917968a0897ff10ef726d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 9 May 2020 18:09:17 +0900 Subject: [PATCH 1490/2376] Fix download button being disabled after importing a download disabled beatmap --- .../Panels/BeatmapPanelDownloadButton.cs | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelDownloadButton.cs b/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelDownloadButton.cs index 6d5862ee2b..67782dfe3f 100644 --- a/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelDownloadButton.cs +++ b/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelDownloadButton.cs @@ -50,13 +50,6 @@ namespace osu.Game.Overlays.BeatmapListing.Panels [BackgroundDependencyLoader(true)] private void load(OsuGame game, BeatmapManager beatmaps, OsuConfigManager osuConfig) { - if ((BeatmapSet.Value?.OnlineInfo?.Availability?.DownloadDisabled ?? false) && State.Value != DownloadState.LocallyAvailable) - { - button.Enabled.Value = false; - button.TooltipText = "this beatmap is currently not available for download."; - return; - } - noVideoSetting = osuConfig.GetBindable(OsuSetting.PreferNoVideo); button.Action = () => @@ -81,6 +74,26 @@ namespace osu.Game.Overlays.BeatmapListing.Panels break; } }; + + State.BindValueChanged(state => + { + switch (state.NewValue) + { + case DownloadState.LocallyAvailable: + button.Enabled.Value = true; + button.TooltipText = string.Empty; + break; + + default: + if (BeatmapSet.Value?.OnlineInfo?.Availability?.DownloadDisabled ?? false) + { + button.Enabled.Value = false; + button.TooltipText = "this beatmap is currently not available for download."; + } + + break; + } + }, true); } } } From 80a193a61608397d3a0b01e6e115445d2e26783b Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Sat, 9 May 2020 17:34:40 +0800 Subject: [PATCH 1491/2376] Use IEnumerable for TypedListConverter. --- .../IO/Serialization/Converters/TypedListConverter.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/IO/Serialization/Converters/TypedListConverter.cs b/osu.Game/IO/Serialization/Converters/TypedListConverter.cs index 25fe335047..ec0036dae2 100644 --- a/osu.Game/IO/Serialization/Converters/TypedListConverter.cs +++ b/osu.Game/IO/Serialization/Converters/TypedListConverter.cs @@ -9,12 +9,12 @@ using Newtonsoft.Json.Linq; namespace osu.Game.IO.Serialization.Converters { /// - /// A type of that serializes a alongside + /// A type of that serializes an alongside /// a lookup table for the types contained. The lookup table is used in deserialization to /// reconstruct the objects with their original types. /// - /// The type of objects contained in the this attribute is attached to. - public class TypedListConverter : JsonConverter> + /// The type of objects contained in the this attribute is attached to. + public class TypedListConverter : JsonConverter> { private readonly bool requiresTypeVersion; @@ -36,7 +36,7 @@ namespace osu.Game.IO.Serialization.Converters this.requiresTypeVersion = requiresTypeVersion; } - public override List ReadJson(JsonReader reader, Type objectType, List existingValue, bool hasExistingValue, JsonSerializer serializer) + public override IEnumerable ReadJson(JsonReader reader, Type objectType, IEnumerable existingValue, bool hasExistingValue, JsonSerializer serializer) { var list = new List(); @@ -57,7 +57,7 @@ namespace osu.Game.IO.Serialization.Converters return list; } - public override void WriteJson(JsonWriter writer, List value, JsonSerializer serializer) + public override void WriteJson(JsonWriter writer, IEnumerable value, JsonSerializer serializer) { var lookupTable = new List(); var objects = new List(); From fa711a6456a4ca25eb74a3fda8da3977e13a8a63 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 9 May 2020 19:11:51 +0900 Subject: [PATCH 1492/2376] Fix null reference causing hard freeze if game is forcefully closed during disclaimer --- osu.Game/OsuGame.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index fdc8d94352..b86be9858f 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -914,7 +914,7 @@ namespace osu.Game if (ScreenStack.CurrentScreen is Loader) return false; - if (introScreen.DidLoadMenu && !(ScreenStack.CurrentScreen is IntroScreen)) + if (introScreen?.DidLoadMenu == true && !(ScreenStack.CurrentScreen is IntroScreen)) { Scheduler.Add(introScreen.MakeCurrent); return true; From 3f78ddf482b7b74c399aa4adf0791156813860e2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 9 May 2020 19:13:18 +0900 Subject: [PATCH 1493/2376] Add CanBeNull hinting --- osu.Game/OsuGame.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index b86be9858f..3caffb6db5 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -18,6 +18,7 @@ using osu.Game.Screens.Menu; using System.Linq; using System.Threading; using System.Threading.Tasks; +using JetBrains.Annotations; using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Framework.Development; @@ -97,6 +98,7 @@ namespace osu.Game private MainMenu menuScreen; + [CanBeNull] private IntroScreen introScreen; private Bindable configRuleset; From bbebd26efb46b2091a53a966c08faca896b20e15 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 9 May 2020 20:13:31 +0900 Subject: [PATCH 1494/2376] Use DirectoryInfo in more places --- osu.Game/IO/OsuStorage.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index 955aae7b68..e178cb0a02 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.cs @@ -41,20 +41,18 @@ namespace osu.Game.IO public void Migrate(string newLocation) { - string oldLocation = GetFullPath("."); + var source = new DirectoryInfo(GetFullPath(".")); + var destination = new DirectoryInfo(newLocation); // ensure the new location has no files present, else hard abort - if (Directory.Exists(newLocation)) + if (destination.Exists) { - if (Directory.GetFiles(newLocation).Length > 0) + if (destination.GetFiles().Length > 0) throw new InvalidOperationException("Migration destination already has files present"); - Directory.Delete(newLocation, true); + deleteRecursive(destination); } - var source = new DirectoryInfo(oldLocation); - var destination = new DirectoryInfo(newLocation); - copyRecursive(source, destination); ChangeTargetStorage(host.GetStorage(newLocation)); From 5dda94187e54d3980507872f69947330a5bfc62d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 9 May 2020 20:13:37 +0900 Subject: [PATCH 1495/2376] Add more edge case testing --- .../NonVisual/CustomDataDirectoryTest.cs | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs index 7f08fad5be..8688ecd078 100644 --- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs @@ -123,6 +123,56 @@ namespace osu.Game.Tests.NonVisual } } + [Test] + public void TestMigrationBetweenTwoTargets() + { + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestMigrationBetweenTwoTargets))) + { + try + { + var osu = loadOsu(host); + var storage = osu.Dependencies.Get(); + + string customPath2 = $"{customPath}-2"; + + const string database_filename = "client.db"; + + Assert.DoesNotThrow(() => (storage as OsuStorage)?.Migrate(customPath)); + Assert.That(File.Exists(Path.Combine(customPath, database_filename))); + + Assert.DoesNotThrow(() => (storage as OsuStorage)?.Migrate(customPath2)); + Assert.That(File.Exists(Path.Combine(customPath2, database_filename))); + + Assert.DoesNotThrow(() => (storage as OsuStorage)?.Migrate(customPath)); + Assert.That(File.Exists(Path.Combine(customPath, database_filename))); + } + finally + { + host.Exit(); + } + } + } + + [Test] + public void TestMigrationToSameTargetFails() + { + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestMigrationToSameTargetFails))) + { + try + { + var osu = loadOsu(host); + var storage = osu.Dependencies.Get(); + + Assert.DoesNotThrow(() => (storage as OsuStorage)?.Migrate(customPath)); + Assert.Throws(() => (storage as OsuStorage)?.Migrate(customPath)); + } + finally + { + host.Exit(); + } + } + } + private OsuGameBase loadOsu(GameHost host) { var osu = new OsuGameBase(); From 3565fe1cb27602557d3b6e8522feb2e792f08a67 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 10 May 2020 07:51:39 +0900 Subject: [PATCH 1496/2376] Fix incorrect passing logic --- osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs index 9ddfe75b40..8f2c25e9b2 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs @@ -39,7 +39,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning { var r = result.NewValue; - passing = r == null || (r.Judgement.AffectsCombo && r.Type > HitResult.Miss); + // always ignore hitobjects that don't affect combo (drumroll ticks etc.) + if (r?.Judgement.AffectsCombo == false) + return; + + passing = r == null || r.Type > HitResult.Miss; foreach (var sprite in InternalChildren.OfType()) sprite.Passing = passing; From 5902cd81a40ed2fbf21a92f6bb041cc1d8b3bcd4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 10 May 2020 07:52:54 +0900 Subject: [PATCH 1497/2376] Move passing transforms to post-load for safety --- .../Skinning/LegacyTaikoScroller.cs | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs index 8f2c25e9b2..1ecdb839fb 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs @@ -101,16 +101,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning passing = value; - if (passing) - { - passingSprite.Show(); - failingSprite.FadeOut(200); - } - else - { - failingSprite.FadeIn(200); - passingSprite.Delay(200).FadeOut(); - } + if (IsLoaded) + updatePassing(); } } @@ -127,6 +119,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning passingSprite = new Sprite { Texture = skin.GetTexture("taiko-slider") }, failingSprite = new Sprite { Texture = skin.GetTexture("taiko-slider-fail"), Alpha = 0 }, }; + + updatePassing(); } protected override void Update() @@ -136,6 +130,20 @@ namespace osu.Game.Rulesets.Taiko.Skinning foreach (var c in InternalChildren) c.Scale = new Vector2(DrawHeight / c.Height); } + + private void updatePassing() + { + if (passing) + { + passingSprite.Show(); + failingSprite.FadeOut(200); + } + else + { + failingSprite.FadeIn(200); + passingSprite.Delay(200).FadeOut(); + } + } } } } From 384862d48b5e614488ca4d19354c2481aca74a8d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 10 May 2020 13:17:37 +0900 Subject: [PATCH 1498/2376] Fix incorrect relative paths when using GetStorageForDirectory --- .../NonVisual/CustomDataDirectoryTest.cs | 45 ++++++++++++++++++- osu.Game/IO/WrappedStorage.cs | 12 ++++- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs index d741bc5de1..7c559ea6d2 100644 --- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs @@ -3,6 +3,7 @@ using System; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; using NUnit.Framework; @@ -36,6 +37,8 @@ namespace osu.Game.Tests.NonVisual } } + private string customPath => Path.Combine(Environment.CurrentDirectory, "custom-path"); + [Test] public void TestCustomDirectory() { @@ -49,7 +52,7 @@ namespace osu.Game.Tests.NonVisual storage.DeleteDirectory(string.Empty); using (var storageConfig = new StorageConfigManager(storage)) - storageConfig.Set(StorageConfig.FullPath, Path.Combine(Environment.CurrentDirectory, "custom-path")); + storageConfig.Set(StorageConfig.FullPath, customPath); try { @@ -58,7 +61,45 @@ namespace osu.Game.Tests.NonVisual // switch to DI'd storage storage = osu.Dependencies.Get(); - Assert.That(storage.GetFullPath("."), Is.EqualTo(Path.Combine(Environment.CurrentDirectory, "custom-path"))); + Assert.That(storage.GetFullPath("."), Is.EqualTo(customPath)); + } + finally + { + host.Exit(); + } + } + } + + [Test] + public void TestSubDirectoryLookup() + { + using (var host = new HeadlessGameHost(nameof(TestSubDirectoryLookup))) + { + string headlessPrefix = Path.Combine("headless", nameof(TestSubDirectoryLookup)); + + // need access before the game has constructed its own storage yet. + Storage storage = new DesktopStorage(headlessPrefix, host); + // manual cleaning so we can prepare a config file. + storage.DeleteDirectory(string.Empty); + + using (var storageConfig = new StorageConfigManager(storage)) + storageConfig.Set(StorageConfig.FullPath, customPath); + + try + { + var osu = loadOsu(host); + + // switch to DI'd storage + storage = osu.Dependencies.Get(); + + string actualTestFile = Path.Combine(customPath, "rulesets", "test"); + + File.WriteAllText(actualTestFile, "test"); + + var rulesetStorage = storage.GetStorageForDirectory("rulesets"); + var lookupPath = rulesetStorage.GetFiles(".").Single(); + + Assert.That(lookupPath, Is.EqualTo("test")); } finally { diff --git a/osu.Game/IO/WrappedStorage.cs b/osu.Game/IO/WrappedStorage.cs index 705bbf6840..cc59e2cc28 100644 --- a/osu.Game/IO/WrappedStorage.cs +++ b/osu.Game/IO/WrappedStorage.cs @@ -48,10 +48,18 @@ namespace osu.Game.IO UnderlyingStorage.Delete(MutatePath(path)); public override IEnumerable GetDirectories(string path) => - UnderlyingStorage.GetDirectories(MutatePath(path)); + ToLocalRelative(UnderlyingStorage.GetDirectories(MutatePath(path))); + + public IEnumerable ToLocalRelative(IEnumerable paths) + { + string localRoot = GetFullPath(string.Empty); + + foreach (var path in paths) + yield return Path.GetRelativePath(localRoot, UnderlyingStorage.GetFullPath(path)); + } public override IEnumerable GetFiles(string path, string pattern = "*") => - UnderlyingStorage.GetFiles(MutatePath(path), pattern); + ToLocalRelative(UnderlyingStorage.GetFiles(MutatePath(path), pattern)); public override Stream GetStream(string path, FileAccess access = FileAccess.Read, FileMode mode = FileMode.OpenOrCreate) => UnderlyingStorage.GetStream(MutatePath(path), access, mode); From 738c6da594f8a0838e33c7c0d8a73d3f79ef510a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 10 May 2020 13:39:20 +0900 Subject: [PATCH 1499/2376] Implement midi keybindings --- osu.Game/Overlays/KeyBinding/KeyBindingRow.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs index 58ca2143f9..01d5991d3e 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs @@ -250,6 +250,28 @@ namespace osu.Game.Overlays.KeyBinding finalise(); } + protected override bool OnMidiDown(MidiDownEvent e) + { + if (!HasFocus) + return false; + + bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState)); + finalise(); + + return true; + } + + protected override void OnMidiUp(MidiUpEvent e) + { + if (!HasFocus) + { + base.OnMidiUp(e); + return; + } + + finalise(); + } + private void clear() { bindTarget.UpdateKeyCombination(InputKey.None); From 2f12c4126a10676982cc301c58e98ab376d2f493 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sun, 10 May 2020 13:49:08 +0900 Subject: [PATCH 1500/2376] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- .../TestSceneManiaHitObjectComposer.cs | 2 +- osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs index 1554a956a2..48159c817d 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs @@ -173,7 +173,7 @@ namespace osu.Game.Rulesets.Mania.Tests AddStep("grab hold note", () => { - holdNote = this.ChildrenOfType().FirstOrDefault(); + holdNote = this.ChildrenOfType().Single(); InputManager.MoveMouseTo(holdNote); InputManager.PressButton(MouseButton.Left); }); diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index ea72061216..15e625872d 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.UI.Scrolling [Resolved] private IScrollingInfo scrollingInfo { get; set; } - // Responds to changes in the layout. When the layout is changes, all hit object states must be recomputed. + // Responds to changes in the layout. When the layout changes, all hit object states must be recomputed. private readonly LayoutValue layoutCache = new LayoutValue(Invalidation.RequiredParentSizeToFit | Invalidation.DrawInfo); // A combined cache across all hit object states to reduce per-update iterations. From 44fdf1e6b05b033977287647e126e4ed99b898e4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 10 May 2020 20:09:41 +0900 Subject: [PATCH 1501/2376] Reword xmldoc --- osu.Game/Rulesets/UI/DrawableRuleset.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 5f0a4b0975..9a10b7d1b2 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -488,12 +488,8 @@ namespace osu.Game.Rulesets.UI protected virtual ResumeOverlay CreateResumeOverlay() => null; /// - /// Whether to display gameplay overlays with this ruleset. - /// Override to false to completely disable the display of gameplay overlays. + /// Whether to display gameplay overlays, such as and . /// - /// - /// Gameplay overlays refer here to in player as well as . - /// public virtual bool AllowGameplayOverlays => true; /// From 5bab53b04ce53325aab9991f2b811034580ef957 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 10 May 2020 17:05:30 +0200 Subject: [PATCH 1502/2376] Centralise trail visibility state management --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 558555af96..9cce46d730 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -74,7 +74,7 @@ namespace osu.Game.Rulesets.Catch.UI dashing = value; - trails.DisplayTrail = value || HyperDashing; + updateTrailVisibility(); } } @@ -255,10 +255,7 @@ namespace osu.Game.Rulesets.Catch.UI hyperDashDirection = 0; if (wasHyperDashing) - { - updateCatcherColour(false); - trails.DisplayTrail &= Dashing; - } + runHyperDashStateTransition(false); } else { @@ -268,16 +265,18 @@ namespace osu.Game.Rulesets.Catch.UI if (!wasHyperDashing) { - updateCatcherColour(true); - - trails.DisplayTrail = true; trails.DisplayEndGlow(); + runHyperDashStateTransition(true); } } } - private void updateCatcherColour(bool hyperDashing) + private void runHyperDashStateTransition(bool hyperDashing) { + trails.HyperDashTrailsColour = hyperDashColour; + trails.EndGlowSpritesColour = hyperDashEndGlowColour; + updateTrailVisibility(); + if (hyperDashing) { this.FadeColour(hyperDashColour, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint); @@ -288,11 +287,10 @@ namespace osu.Game.Rulesets.Catch.UI this.FadeColour(Color4.White, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint); this.FadeTo(1f, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint); } - - trails.HyperDashTrailsColour = hyperDashColour; - trails.EndGlowSpritesColour = hyperDashEndGlowColour; } + private void updateTrailVisibility() => trails.DisplayTrail = Dashing || HyperDashing; + public bool OnPressed(CatchAction action) { switch (action) @@ -393,7 +391,7 @@ namespace osu.Game.Rulesets.Catch.UI skin.GetConfig(CatchSkinColour.HyperDashAfterImage)?.Value ?? hyperDashColour; - updateCatcherColour(HyperDashing); + runHyperDashStateTransition(HyperDashing); } protected override void Update() From 1d999bb634de168c5acb1f3abbc5fd12d5596e31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 10 May 2020 18:32:38 +0200 Subject: [PATCH 1503/2376] Integrate PeriodTracker changes --- .../Scoring/DrainingHealthProcessor.cs | 56 +++++-------------- 1 file changed, 15 insertions(+), 41 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs index b36e42326c..1958efdd6f 100644 --- a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs @@ -5,9 +5,9 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; -using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; +using osu.Game.Utils; namespace osu.Game.Rulesets.Scoring { @@ -49,33 +49,7 @@ namespace osu.Game.Rulesets.Scoring private double targetMinimumHealth; private double drainRate = 1; - private readonly List<(double startTime, double endTime)> nonDrainSections = new List<(double, double)>(); - private int currentNonDrainSection; - - private bool isInNonDrainSection - { - get - { - if (nonDrainSections.Count == 0) - return false; - - var time = Time.Current; - - if (time > nonDrainSections[currentNonDrainSection].endTime) - { - while (time > nonDrainSections[currentNonDrainSection].endTime && currentNonDrainSection < nonDrainSections.Count - 1) - currentNonDrainSection++; - } - else - { - while (time < nonDrainSections[currentNonDrainSection].startTime && currentNonDrainSection > 0) - currentNonDrainSection--; - } - - var closestSection = nonDrainSections[currentNonDrainSection]; - return time >= closestSection.startTime && time <= closestSection.endTime; - } - } + private PeriodTracker noDrainPeriodTracker; /// /// Creates a new . @@ -90,7 +64,7 @@ namespace osu.Game.Rulesets.Scoring { base.Update(); - if (isInNonDrainSection) + if (noDrainPeriodTracker?.IsInAny(Time.Current) == true) return; // When jumping in and out of gameplay time within a single frame, health should only be drained for the period within the gameplay time @@ -102,23 +76,23 @@ namespace osu.Game.Rulesets.Scoring public override void ApplyBeatmap(IBeatmap beatmap) { - nonDrainSections.Clear(); - this.beatmap = beatmap; if (beatmap.HitObjects.Count > 0) gameplayEndTime = beatmap.HitObjects[^1].GetEndTime(); - // Ranges between the end of last hit object before a break - // and the start of first hit object after a break should - // not allow HP draining. (with break periods in) - foreach (BreakPeriod b in beatmap.Breaks) - { - var startTime = beatmap.HitObjects.LastOrDefault(h => h.GetEndTime() < b.StartTime)?.GetEndTime() ?? double.MinValue; - var endTime = beatmap.HitObjects.FirstOrDefault(h => h.StartTime > b.EndTime)?.StartTime ?? double.MaxValue; - - nonDrainSections.Add((startTime, endTime)); - } + noDrainPeriodTracker = new PeriodTracker(beatmap.Breaks.Select(breakPeriod => new Period( + beatmap.HitObjects + .Select(hitObject => hitObject.GetEndTime()) + .Where(endTime => endTime < breakPeriod.StartTime) + .DefaultIfEmpty(double.MinValue) + .Last(), + beatmap.HitObjects + .Select(hitObject => hitObject.StartTime) + .Where(startTime => startTime > breakPeriod.EndTime) + .DefaultIfEmpty(double.MaxValue) + .First() + ))); targetMinimumHealth = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, min_health_target, mid_health_target, max_health_target); From 916d8245e6c951a5b9d31d61a7aec3226f2c4922 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 May 2020 12:50:47 +0900 Subject: [PATCH 1504/2376] Don't timeout on long beatmap load when debugging --- osu.Game/Beatmaps/WorkingBeatmap.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index d2804bdc05..bf2b9944a4 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -84,7 +85,7 @@ namespace osu.Game.Beatmaps public IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList mods = null, TimeSpan? timeout = null) { - using (var cancellationSource = new CancellationTokenSource(timeout ?? TimeSpan.FromSeconds(10))) + using (var cancellationSource = createCancellationTokenSource(timeout)) { mods ??= Array.Empty(); @@ -181,6 +182,15 @@ namespace osu.Game.Beatmaps beatmapLoadTask = null; } + private CancellationTokenSource createCancellationTokenSource(TimeSpan? timeout) + { + if (Debugger.IsAttached) + // ignore timeout when debugger is attached (may be breakpointing / debugging). + return new CancellationTokenSource(); + + return new CancellationTokenSource(timeout ?? TimeSpan.FromSeconds(10)); + } + private Task loadBeatmapAsync() => beatmapLoadTask ??= Task.Factory.StartNew(() => { // Todo: Handle cancellation during beatmap parsing From b4d790c076904a1bd24bd086fae7d159ccd06614 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 May 2020 12:53:54 +0900 Subject: [PATCH 1505/2376] Fix taiko sample mapping for strong hits --- .../Beatmaps/TaikoBeatmapConverter.cs | 30 +++++++++++++++++-- .../Objects/Drawables/DrawableHit.cs | 8 +++++ .../Drawables/DrawableTaikoHitObject.cs | 4 +-- 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index caf645d5a2..822931396a 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs @@ -167,13 +167,39 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps default: { - bool isRim = samples.Any(s => s.Name == HitSampleInfo.HIT_CLAP || s.Name == HitSampleInfo.HIT_WHISTLE); + bool isRimDefinition(HitSampleInfo s) => s.Name == HitSampleInfo.HIT_CLAP || s.Name == HitSampleInfo.HIT_WHISTLE; + + bool isRim = samples.Any(isRimDefinition); + + if (isRim) + { + // consume then remove the rim definition sample types. + var updatedSamples = samples.Where(s => !isRimDefinition(s)).ToList(); + + // strong + rim always maps to whistle. + if (strong) + { + for (var i = 0; i < updatedSamples.Count; i++) + { + var s = samples[i]; + + if (s.Name != HitSampleInfo.HIT_FINISH) + continue; + + var sClone = s.Clone(); + sClone.Name = HitSampleInfo.HIT_WHISTLE; + updatedSamples[i] = sClone; + } + } + + samples = updatedSamples; + } yield return new Hit { StartTime = obj.StartTime, Type = isRim ? HitType.Rim : HitType.Centre, - Samples = obj.Samples, + Samples = samples, IsStrong = strong }; diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index d2671eadda..d4dc3316e7 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -2,9 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; using osu.Framework.Graphics; +using osu.Game.Audio; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; @@ -47,6 +49,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables ? new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.CentreHit), _ => new CentreHitCirclePiece(), confineMode: ConfineMode.ScaleToFit) : new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.RimHit), _ => new RimHitCirclePiece(), confineMode: ConfineMode.ScaleToFit); + protected override IEnumerable GetSamples() + { + // normal and claps are always handled by the drum (see DrumSampleMapping). + return HitObject.Samples.Where(s => s.Name != HitSampleInfo.HIT_NORMAL && s.Name != HitSampleInfo.HIT_CLAP); + } + protected override void CheckForResult(bool userTriggered, double timeOffset) { Debug.Assert(HitObject.HitWindows != null); diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs index 1be04f1760..90daf3950c 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs @@ -165,8 +165,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables return base.CreateNestedHitObject(hitObject); } - // Normal and clap samples are handled by the drum - protected override IEnumerable GetSamples() => HitObject.Samples.Where(s => s.Name != HitSampleInfo.HIT_NORMAL && s.Name != HitSampleInfo.HIT_CLAP); + // Most osu!taiko hitsounds are managed by the drum (see DrumSampleMapping). + protected override IEnumerable GetSamples() => Enumerable.Empty(); protected abstract SkinnableDrawable CreateMainPiece(); From f1959338446a0c98345330e61d6beb8ca8deed6c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 11 May 2020 13:03:59 +0900 Subject: [PATCH 1506/2376] Fix hardrock potentially taking a long time to load --- osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs index cf6677a55d..e0577dd464 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs @@ -28,8 +28,11 @@ namespace osu.Game.Rulesets.Osu.Mods slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y)); slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y)); - foreach (var point in slider.Path.ControlPoints) + var controlPoints = slider.Path.ControlPoints.Select(p => new PathControlPoint(p.Position.Value, p.Type.Value)).ToArray(); + foreach (var point in controlPoints) point.Position.Value = new Vector2(point.Position.Value.X, -point.Position.Value.Y); + + slider.Path = new SliderPath(controlPoints, slider.Path.ExpectedDistance.Value); } } } From 43342c57b885f3baabf0fc73212d1adf679677da Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Mon, 11 May 2020 07:13:06 +0200 Subject: [PATCH 1507/2376] Fix switch case ... caused by a poor merge --- osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs index f0d0ce05b5..1096b8db00 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs @@ -91,6 +91,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning if (GetTexture("taiko-slider") != null) return new LegacyTaikoScroller(); + return null; + case TaikoSkinComponents.TaikoDon: if (GetTexture("pippidonclear0") != null) return new DrawableTaikoMascot(); From 77041bdbb571a738da0c22f10948059642502bb2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 May 2020 16:19:47 +0900 Subject: [PATCH 1508/2376] Move implementation to DrawableHit to avoid "breaking" legacy encoding --- .../Beatmaps/TaikoBeatmapConverter.cs | 24 ----------------- .../Objects/Drawables/DrawableHit.cs | 27 ++++++++++++++++++- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index 822931396a..d324441285 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs @@ -171,30 +171,6 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps bool isRim = samples.Any(isRimDefinition); - if (isRim) - { - // consume then remove the rim definition sample types. - var updatedSamples = samples.Where(s => !isRimDefinition(s)).ToList(); - - // strong + rim always maps to whistle. - if (strong) - { - for (var i = 0; i < updatedSamples.Count; i++) - { - var s = samples[i]; - - if (s.Name != HitSampleInfo.HIT_FINISH) - continue; - - var sClone = s.Clone(); - sClone.Name = HitSampleInfo.HIT_WHISTLE; - updatedSamples[i] = sClone; - } - } - - samples = updatedSamples; - } - yield return new Hit { StartTime = obj.StartTime, diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index d4dc3316e7..d332f90cd4 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -52,7 +52,32 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected override IEnumerable GetSamples() { // normal and claps are always handled by the drum (see DrumSampleMapping). - return HitObject.Samples.Where(s => s.Name != HitSampleInfo.HIT_NORMAL && s.Name != HitSampleInfo.HIT_CLAP); + var samples = HitObject.Samples.Where(s => s.Name != HitSampleInfo.HIT_NORMAL && s.Name != HitSampleInfo.HIT_CLAP); + + if (HitObject.Type == HitType.Rim && HitObject.IsStrong) + { + // strong + rim always maps to whistle. + // TODO: this should really be in the legacy decoder, but can't be because legacy encoding parity would be broken. + // when we add a taiko editor, this is probably not going to play nice. + + var corrected = samples.ToList(); + + for (var i = 0; i < corrected.Count; i++) + { + var s = corrected[i]; + + if (s.Name != HitSampleInfo.HIT_FINISH) + continue; + + var sClone = s.Clone(); + sClone.Name = HitSampleInfo.HIT_WHISTLE; + corrected[i] = sClone; + } + + return corrected; + } + + return samples; } protected override void CheckForResult(bool userTriggered, double timeOffset) From 93440874db8d92e300d0e62508e5cf44e343f4bb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 May 2020 16:30:54 +0900 Subject: [PATCH 1509/2376] Refactor beatmap encoder test to be a bit easier to understand --- .../Formats/LegacyBeatmapEncoderTest.cs | 36 +++++++++---------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs index bcc873b0b7..64efe08929 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs @@ -28,14 +28,15 @@ namespace osu.Game.Tests.Beatmaps.Formats private static IEnumerable allBeatmaps => TestResources.GetStore().GetAvailableResources().Where(res => res.EndsWith(".osu")); [TestCaseSource(nameof(allBeatmaps))] - public void TestBeatmap(string name) + public void TestEncodeDecodeStability(string name) { - var decoded = decode(name, out var encoded); + var decoded = decodeFromLegacy(TestResources.GetStore().GetStream(name)); + var decodedAfterEncode = decodeFromLegacy(encodeToLegacy(decoded)); sort(decoded); - sort(encoded); + sort(decodedAfterEncode); - Assert.That(encoded.Serialize(), Is.EqualTo(decoded.Serialize())); + Assert.That(decodedAfterEncode.Serialize(), Is.EqualTo(decoded.Serialize())); } private void sort(IBeatmap beatmap) @@ -48,27 +49,22 @@ namespace osu.Game.Tests.Beatmaps.Formats } } - private IBeatmap decode(string filename, out IBeatmap encoded) + private IBeatmap decodeFromLegacy(Stream stream) { - using (var stream = TestResources.GetStore().GetStream(filename)) - using (var sr = new LineBufferedReader(stream)) - { - var legacyDecoded = convert(new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(sr)); + using (var reader = new LineBufferedReader(stream)) + return convert(new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(reader)); + } - using (var ms = new MemoryStream()) - using (var sw = new StreamWriter(ms)) - using (var sr2 = new LineBufferedReader(ms, true)) - { - new LegacyBeatmapEncoder(legacyDecoded).Encode(sw); + private Stream encodeToLegacy(IBeatmap beatmap) + { + var stream = new MemoryStream(); - sw.Flush(); - ms.Position = 0; + using (var writer = new StreamWriter(stream, leaveOpen: true)) + new LegacyBeatmapEncoder(beatmap).Encode(writer); - encoded = convert(new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(sr2)); + stream.Position = 0; - return legacyDecoded; - } - } + return stream; } private IBeatmap convert(IBeatmap beatmap) From 7f7d5e6617a127de2729e01b53b7c7b34608dfc1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 May 2020 16:37:08 +0900 Subject: [PATCH 1510/2376] Fix apparently required argument --- osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs index 64efe08929..b1782f7f08 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs @@ -6,6 +6,7 @@ using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text; using NUnit.Framework; using osu.Framework.Audio.Track; using osu.Framework.Graphics.Textures; @@ -59,7 +60,7 @@ namespace osu.Game.Tests.Beatmaps.Formats { var stream = new MemoryStream(); - using (var writer = new StreamWriter(stream, leaveOpen: true)) + using (var writer = new StreamWriter(stream, Encoding.UTF8, leaveOpen: true)) new LegacyBeatmapEncoder(beatmap).Encode(writer); stream.Position = 0; From b9e5009dd5066fb3976725592b04c63812a622c1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 May 2020 17:09:34 +0900 Subject: [PATCH 1511/2376] 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 a406cdf08a..69f897128c 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 5ccfaaac9e..c6dba8da13 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -24,7 +24,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index dc83d937f7..f78fd2e4ff 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From ca6e6f7496f5468df67644e47c6b1c01bd0b82c6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 11 May 2020 17:26:11 +0900 Subject: [PATCH 1512/2376] Add required parameter for android build --- osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs index b1782f7f08..30331e98d2 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs @@ -60,7 +60,7 @@ namespace osu.Game.Tests.Beatmaps.Formats { var stream = new MemoryStream(); - using (var writer = new StreamWriter(stream, Encoding.UTF8, leaveOpen: true)) + using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true)) new LegacyBeatmapEncoder(beatmap).Encode(writer); stream.Position = 0; From 6c350db0973c74f0294cc975c2c2b3aa26bf17a1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 May 2020 21:37:07 +0900 Subject: [PATCH 1513/2376] Add connection flushing support --- .../NonVisual/CustomDataDirectoryTest.cs | 14 ++++++-------- osu.Game/Database/DatabaseContextFactory.cs | 8 ++++++++ osu.Game/OsuGameBase.cs | 8 ++++++++ 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs index 4bce5056ce..ed83ff358c 100644 --- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs @@ -137,7 +137,7 @@ namespace osu.Game.Tests.NonVisual Assert.That(storage.GetFullPath("."), Is.EqualTo(defaultStorageLocation)); - (storage as OsuStorage)?.Migrate(customPath); + osu.Migrate(customPath); Assert.That(storage.GetFullPath("."), Is.EqualTo(customPath)); @@ -170,19 +170,18 @@ namespace osu.Game.Tests.NonVisual try { var osu = loadOsu(host); - var storage = osu.Dependencies.Get(); string customPath2 = $"{customPath}-2"; const string database_filename = "client.db"; - Assert.DoesNotThrow(() => (storage as OsuStorage)?.Migrate(customPath)); + Assert.DoesNotThrow(() => osu.Migrate(customPath)); Assert.That(File.Exists(Path.Combine(customPath, database_filename))); - Assert.DoesNotThrow(() => (storage as OsuStorage)?.Migrate(customPath2)); + Assert.DoesNotThrow(() => osu.Migrate(customPath2)); Assert.That(File.Exists(Path.Combine(customPath2, database_filename))); - Assert.DoesNotThrow(() => (storage as OsuStorage)?.Migrate(customPath)); + Assert.DoesNotThrow(() => osu.Migrate(customPath)); Assert.That(File.Exists(Path.Combine(customPath, database_filename))); } finally @@ -200,10 +199,9 @@ namespace osu.Game.Tests.NonVisual try { var osu = loadOsu(host); - var storage = osu.Dependencies.Get(); - Assert.DoesNotThrow(() => (storage as OsuStorage)?.Migrate(customPath)); - Assert.Throws(() => (storage as OsuStorage)?.Migrate(customPath)); + Assert.DoesNotThrow(() => osu.Migrate(customPath)); + Assert.Throws(() => osu.Migrate(customPath)); } finally { diff --git a/osu.Game/Database/DatabaseContextFactory.cs b/osu.Game/Database/DatabaseContextFactory.cs index 1ed5fb3268..1cceb59b11 100644 --- a/osu.Game/Database/DatabaseContextFactory.cs +++ b/osu.Game/Database/DatabaseContextFactory.cs @@ -160,5 +160,13 @@ namespace osu.Game.Database } } } + + public void FlushConnections() + { + foreach (var context in threadContexts.Values) + context.Dispose(); + + recycleThreadContexts(); + } } } diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 8fbde67afe..6282f5cb8b 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -328,6 +328,8 @@ namespace osu.Game { base.Dispose(isDisposing); RulesetStore?.Dispose(); + + ContextFactory.FlushConnections(); } private class OsuUserInputManager : UserInputManager @@ -355,5 +357,11 @@ namespace osu.Game public override bool ChangeFocusOnClick => false; } } + + public void Migrate(string path) + { + ContextFactory.FlushConnections(); + (Storage as OsuStorage)?.Migrate(path); + } } } From a11be07bb122b3e1c186cfc418d31a48a1f8869d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 May 2020 21:38:27 +0900 Subject: [PATCH 1514/2376] Move log storage location after migration complete --- osu.Game/IO/OsuStorage.cs | 9 ++++++--- osu.Game/IO/WrappedStorage.cs | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index e178cb0a02..7e1c676324 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.cs @@ -33,10 +33,13 @@ namespace osu.Game.IO var customStoragePath = storageConfig.Get(StorageConfig.FullPath); if (!string.IsNullOrEmpty(customStoragePath)) - { ChangeTargetStorage(host.GetStorage(customStoragePath)); - Logger.Storage = UnderlyingStorage.GetStorageForDirectory("logs"); - } + } + + protected override void ChangeTargetStorage(Storage newStorage) + { + base.ChangeTargetStorage(newStorage); + Logger.Storage = UnderlyingStorage.GetStorageForDirectory("logs"); } public void Migrate(string newLocation) diff --git a/osu.Game/IO/WrappedStorage.cs b/osu.Game/IO/WrappedStorage.cs index cc59e2cc28..646faba9eb 100644 --- a/osu.Game/IO/WrappedStorage.cs +++ b/osu.Game/IO/WrappedStorage.cs @@ -27,7 +27,7 @@ namespace osu.Game.IO protected virtual string MutatePath(string path) => !string.IsNullOrEmpty(subPath) ? Path.Combine(subPath, path) : path; - protected void ChangeTargetStorage(Storage newStorage) + protected virtual void ChangeTargetStorage(Storage newStorage) { UnderlyingStorage = newStorage; } From 984f27c107ed817ee677a4642e995c594b8805db Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 May 2020 21:38:41 +0900 Subject: [PATCH 1515/2376] Add simple retry logic on file copy failure (may be in use) --- osu.Game/IO/OsuStorage.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index 7e1c676324..7c0b90e208 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.cs @@ -4,6 +4,7 @@ using System; using System.IO; using System.Linq; +using System.Threading; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Configuration; @@ -95,7 +96,20 @@ namespace osu.Game.IO if (IGNORE_FILES.Contains(fi.Name)) continue; - fi.CopyTo(Path.Combine(destination.FullName, fi.Name), true); + int tries = 5; + + while (tries-- > 0) + { + try + { + fi.CopyTo(Path.Combine(destination.FullName, fi.Name), true); + break; + } + catch (Exception) + { + Thread.Sleep(50); + } + } } foreach (DirectoryInfo dir in source.GetDirectories()) From e650b10b5e1adfb00123064e043d5768eb0e409f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 11 May 2020 19:03:13 +0200 Subject: [PATCH 1516/2376] Add test case for maximal break --- .../TestSceneDrainingHealthProcessor.cs | 75 +++++++++---------- 1 file changed, 37 insertions(+), 38 deletions(-) diff --git a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs index 2f83ea4832..e50b2231bf 100644 --- a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs +++ b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.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.Collections.Generic; -using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Utils; @@ -43,52 +41,61 @@ namespace osu.Game.Tests.Gameplay } [Test] - public void TestHealthNotDrainedBeforeBreak() + public void TestHealthDrainBetweenBreakAndObjects() { - createProcessor(createBeatmap(0, 2000, - new BreakPeriod(400, 600), new BreakPeriod(1200, 1400))); + createProcessor(createBeatmap(0, 2000, new BreakPeriod(325, 375))); - setTime(300); + // 275 300 325 350 375 400 425 + // hitobjects o o + // break [-------------] + // no drain [---------------------------] + + setTime(285); setHealth(1); - setTime(400); - assertHealthEqualTo(1); + setTime(295); + assertHealthNotEqualTo(1); - setTime(1100); + setTime(305); setHealth(1); - setTime(1200); + setTime(315); assertHealthEqualTo(1); + + setTime(365); + assertHealthEqualTo(1); + + setTime(395); + assertHealthEqualTo(1); + + setTime(425); + assertHealthNotEqualTo(1); } [Test] - public void TestHealthNotDrainedDuringBreak() + public void TestHealthDrainDuringMaximalBreak() { - createProcessor(createBeatmap(0, 2000, new BreakPeriod(0, 1200))); + createProcessor(createBeatmap(0, 2000, new BreakPeriod(300, 400))); - setTime(700); - assertHealthEqualTo(1); - setTime(900); - assertHealthEqualTo(1); - } + // 275 300 325 350 375 400 425 + // hitobjects o o + // break [---------------------------] + // no drain [---------------------------] - [Test] - public void TestHealthNotDrainedAfterBreak() - { - createProcessor(createBeatmap(0, 2000, - new BreakPeriod(400, 600), new BreakPeriod(1200, 1400))); - - setTime(600); + setTime(285); setHealth(1); - setTime(700); - assertHealthEqualTo(1); + setTime(295); + assertHealthNotEqualTo(1); - setTime(1400); + setTime(305); setHealth(1); - setTime(1500); + setTime(395); assertHealthEqualTo(1); + + setTime(425); + assertHealthNotEqualTo(1); } [Test] @@ -154,24 +161,16 @@ namespace osu.Game.Tests.Gameplay { var beatmap = new Beatmap { - BeatmapInfo = { BaseDifficulty = { DrainRate = 5 } }, + BeatmapInfo = { BaseDifficulty = { DrainRate = 10 } }, }; - double time = startTime; - - while (time <= endTime) + for (double time = startTime; time <= endTime; time += 100) { beatmap.HitObjects.Add(new JudgeableHitObject { StartTime = time }); - - // leave a 100ms gap between the start and end of a break period. - time += (getCurrentBreak(breaks, time)?.Duration ?? 0) + 100; } beatmap.Breaks.AddRange(breaks); - static BreakPeriod getCurrentBreak(IEnumerable breaks, double time) => - breaks?.FirstOrDefault(b => time >= b.StartTime && time <= b.EndTime); - return beatmap; } From 848a3fb6d74ec3b443029cc048934c1df4ea728f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 11 May 2020 19:06:36 +0200 Subject: [PATCH 1517/2376] Take hitobject start/end times into account in drain --- osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs index 1958efdd6f..982f527517 100644 --- a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs @@ -84,12 +84,12 @@ namespace osu.Game.Rulesets.Scoring noDrainPeriodTracker = new PeriodTracker(beatmap.Breaks.Select(breakPeriod => new Period( beatmap.HitObjects .Select(hitObject => hitObject.GetEndTime()) - .Where(endTime => endTime < breakPeriod.StartTime) + .Where(endTime => endTime <= breakPeriod.StartTime) .DefaultIfEmpty(double.MinValue) .Last(), beatmap.HitObjects .Select(hitObject => hitObject.StartTime) - .Where(startTime => startTime > breakPeriod.EndTime) + .Where(startTime => startTime >= breakPeriod.EndTime) .DefaultIfEmpty(double.MaxValue) .First() ))); From 35e7cee458f66391d6bb7e705a34fdfeb2bb65a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=80=8C=E7=A9=BA=E7=99=BD=E3=80=8D?= <「空白」> Date: Tue, 12 May 2020 03:18:47 +0900 Subject: [PATCH 1518/2376] Squash commits from private fork Temporary comments left to-remove later --- .../Online/API/Requests/ResponseWithCursor.cs | 9 ++ .../API/Requests/SearchBeatmapSetsRequest.cs | 27 +++++- .../API/Requests/SearchBeatmapSetsResponse.cs | 2 +- .../BeatmapListingFilterControl.cs | 57 +++++++---- .../BeatmapListing/BeatmapListingPager.cs | 92 ++++++++++++++++++ osu.Game/Overlays/BeatmapListingOverlay.cs | 94 ++++++++++++++----- 6 files changed, 236 insertions(+), 45 deletions(-) create mode 100644 osu.Game/Overlays/BeatmapListing/BeatmapListingPager.cs diff --git a/osu.Game/Online/API/Requests/ResponseWithCursor.cs b/osu.Game/Online/API/Requests/ResponseWithCursor.cs index e38e73dd01..51e88ca52b 100644 --- a/osu.Game/Online/API/Requests/ResponseWithCursor.cs +++ b/osu.Game/Online/API/Requests/ResponseWithCursor.cs @@ -13,4 +13,13 @@ namespace osu.Game.Online.API.Requests [JsonProperty("cursor")] public dynamic CursorJson; } + + public abstract class ResponseWithCursor : ResponseWithCursor where T : class + { + /// + /// Cursor deserialized into T class type (cannot implicitly convert type to object using raw Cursor) + /// + [JsonProperty("cursor")] + public T Cursor; + } } diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs index 047496b473..fb2cc66dd8 100644 --- a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs +++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs @@ -5,11 +5,21 @@ using osu.Framework.IO.Network; using osu.Game.Overlays; using osu.Game.Overlays.BeatmapListing; using osu.Game.Rulesets; +using Newtonsoft.Json; namespace osu.Game.Online.API.Requests { public class SearchBeatmapSetsRequest : APIRequest { + public class Cursor + { + [JsonProperty("approved_date")] + public string ApprovedDate; + + [JsonProperty("_id")] + public string Id; + } + public SearchCategory SearchCategory { get; set; } public SortCriteria SortCriteria { get; set; } @@ -22,17 +32,20 @@ namespace osu.Game.Online.API.Requests private readonly string query; private readonly RulesetInfo ruleset; + private readonly Cursor cursor; private string directionString => SortDirection == SortDirection.Descending ? @"desc" : @"asc"; - public SearchBeatmapSetsRequest(string query, RulesetInfo ruleset) + public SearchBeatmapSetsRequest(string query, RulesetInfo ruleset, Cursor cursor = null, + SearchCategory searchCategory = SearchCategory.Any, SortCriteria sortCriteria = SortCriteria.Ranked, SortDirection sortDirection = SortDirection.Descending) { this.query = string.IsNullOrEmpty(query) ? string.Empty : System.Uri.EscapeDataString(query); this.ruleset = ruleset; + this.cursor = cursor; - SearchCategory = SearchCategory.Any; - SortCriteria = SortCriteria.Ranked; - SortDirection = SortDirection.Descending; + SearchCategory = searchCategory; + SortCriteria = sortCriteria; + SortDirection = sortDirection; Genre = SearchGenre.Any; Language = SearchLanguage.Any; } @@ -55,6 +68,12 @@ namespace osu.Game.Online.API.Requests req.AddParameter("sort", $"{SortCriteria.ToString().ToLowerInvariant()}_{directionString}"); + if (cursor != null) + { + req.AddParameter("cursor[_id]", cursor.Id); + req.AddParameter("cursor[approved_date]", cursor.ApprovedDate); + } + return req; } diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsResponse.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsResponse.cs index 3c4fb11ed1..2adf7004e8 100644 --- a/osu.Game/Online/API/Requests/SearchBeatmapSetsResponse.cs +++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsResponse.cs @@ -7,7 +7,7 @@ using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Online.API.Requests { - public class SearchBeatmapSetsResponse : ResponseWithCursor + public class SearchBeatmapSetsResponse : ResponseWithCursor { [JsonProperty("beatmapsets")] public IEnumerable BeatmapSets; diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs index 4dd60c7113..c3e8505ddc 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -13,7 +12,6 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Online.API; -using osu.Game.Online.API.Requests; using osu.Game.Rulesets; using osuTK; using osuTK.Graphics; @@ -24,6 +22,8 @@ namespace osu.Game.Overlays.BeatmapListing { public Action> SearchFinished; public Action SearchStarted; + /// List of currently displayed beatmap entries + private List currentBeatmaps; [Resolved] private IAPIProvider api { get; set; } @@ -35,7 +35,7 @@ namespace osu.Game.Overlays.BeatmapListing private readonly BeatmapListingSortTabControl sortControl; private readonly Box sortControlBackground; - private SearchBeatmapSetsRequest getSetsRequest; + private BeatmapListingPager beatmapListingPager; public BeatmapListingFilterControl() { @@ -115,12 +115,13 @@ namespace osu.Game.Overlays.BeatmapListing } private ScheduledDelegate queryChangedDebounce; + private ScheduledDelegate queryPagingDebounce; private void queueUpdateSearch(bool queryTextChanged = false) { SearchStarted?.Invoke(); - getSetsRequest?.Cancel(); + beatmapListingPager?.Reset(); queryChangedDebounce?.Cancel(); queryChangedDebounce = Scheduler.AddDelayed(updateSearch, queryTextChanged ? 500 : 100); @@ -128,37 +129,55 @@ namespace osu.Game.Overlays.BeatmapListing private void updateSearch() { - getSetsRequest = new SearchBeatmapSetsRequest(searchControl.Query.Value, searchControl.Ruleset.Value) - { - SearchCategory = searchControl.Category.Value, - SortCriteria = sortControl.Current.Value, - SortDirection = sortControl.SortDirection.Value, - Genre = searchControl.Genre.Value, - Language = searchControl.Language.Value - }; + beatmapListingPager = new BeatmapListingPager( + api, + rulesets, + searchControl.Query.Value, + searchControl.Ruleset.Value, + searchControl.Category.Value, + sortControl.Current.Value, + sortControl.SortDirection.Value + ); - getSetsRequest.Success += response => Schedule(() => onSearchFinished(response)); + queryPagingDebounce?.Cancel(); + queryPagingDebounce = null; + beatmapListingPager.PageFetched += onSearchFinished; - api.Queue(getSetsRequest); + AddPageToResult(); } - private void onSearchFinished(SearchBeatmapSetsResponse response) + private void onSearchFinished(List beatmaps) { - var beatmaps = response.BeatmapSets.Select(r => r.ToBeatmapSet(rulesets)).ToList(); - - searchControl.BeatmapSet = response.Total == 0 ? null : beatmaps.First(); + queryPagingDebounce = Scheduler.AddDelayed(() => queryPagingDebounce = null, 1000); + if (currentBeatmaps == null || !beatmapListingPager.IsPastFirstPage) + currentBeatmaps = beatmaps; + else + currentBeatmaps.AddRange(beatmaps); + SearchFinished?.Invoke(beatmaps); } protected override void Dispose(bool isDisposing) { - getSetsRequest?.Cancel(); + beatmapListingPager?.Reset(); queryChangedDebounce?.Cancel(); + queryPagingDebounce?.Cancel(); base.Dispose(isDisposing); } public void TakeFocus() => searchControl.TakeFocus(); + + /// Request next 50 matches if available + public void AddPageToResult() + { + if (beatmapListingPager == null || !beatmapListingPager.CanFetchNextPage) + return; + if (queryPagingDebounce != null) + return; + + beatmapListingPager.FetchNextPage(); + } } } diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingPager.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingPager.cs new file mode 100644 index 0000000000..66faf8df7a --- /dev/null +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingPager.cs @@ -0,0 +1,92 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Game.Beatmaps; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Rulesets; + +namespace osu.Game.Overlays.BeatmapListing +{ + public class BeatmapListingPager + { + private readonly IAPIProvider api; + private readonly RulesetStore rulesets; + private readonly string query; + private readonly RulesetInfo ruleset; + private readonly SearchCategory searchCategory; + private readonly SortCriteria sortCriteria; + private readonly SortDirection sortDirection; + + public event PageFetchHandler PageFetched; + private SearchBeatmapSetsRequest getSetsRequest; + private SearchBeatmapSetsResponse lastResponse; + + /// Reports end of results + private bool isLastPageFetched = false; + /// Job in process lock flag + private bool isFetching => getSetsRequest != null; + /// Whether beatmaps should be appended or replaced + public bool IsPastFirstPage { get; private set; } = false; + /// call FetchNextPage() safe-check + public bool CanFetchNextPage => !isLastPageFetched && !isFetching; + + public BeatmapListingPager(IAPIProvider api, RulesetStore rulesets, string query, RulesetInfo ruleset, SearchCategory searchCategory = SearchCategory.Any, SortCriteria sortCriteria = SortCriteria.Ranked, SortDirection sortDirection = SortDirection.Descending) + { + this.api = api; + this.rulesets = rulesets; + this.query = query; + this.ruleset = ruleset; + this.searchCategory = searchCategory; + this.sortCriteria = sortCriteria; + this.sortDirection = sortDirection; + } + + public void FetchNextPage() + { + if (isFetching) + return; + + if (lastResponse != null) + IsPastFirstPage = true; + + getSetsRequest = new SearchBeatmapSetsRequest( + query, + ruleset, + lastResponse?.Cursor, + searchCategory, + sortCriteria, + sortDirection); + + getSetsRequest.Success += response => + { + var sets = response.BeatmapSets.Select(responseJson => responseJson.ToBeatmapSet(rulesets)).ToList(); + + if (sets.Count == 0) + isLastPageFetched = true; + + lastResponse = response; + getSetsRequest = null; + + PageFetched?.Invoke(sets); + }; + + api.Queue(getSetsRequest); + } + + public void Reset() + { + isLastPageFetched = false; + IsPastFirstPage = false; + + lastResponse = null; + + getSetsRequest?.Cancel(); + getSetsRequest = null; + } + + public delegate void PageFetchHandler(List sets); + } +} diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index f680f7c67b..c495c8d21b 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -30,13 +30,21 @@ namespace osu.Game.Overlays private Drawable currentContent; private LoadingLayer loadingLayer; private Container panelTarget; + private FillFlowContainer foundContent; + private NotFoundDrawable notFoundContent; + + private OverlayScrollContainer resultScrollContainer; + /// Scroll distance threshold from results tail, higher means sooner + private const int pagination_scroll_distance = 500; + /// This is paging event flag + private bool shouldAddNextPage => resultScrollContainer.ScrollableExtent > 0 && resultScrollContainer.IsScrolledToEnd(pagination_scroll_distance); public BeatmapListingOverlay() : base(OverlayColourScheme.Blue) { } - private BeatmapListingFilterControl filterControl; + private BeatmapListingFilterControl filterControl;//actual search settings [BackgroundDependencyLoader] private void load() @@ -48,7 +56,7 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.Both, Colour = ColourProvider.Background6 }, - new OverlayScrollContainer + resultScrollContainer = new OverlayScrollContainer { RelativeSizeAxes = Axes.Both, ScrollbarVisible = false, @@ -80,9 +88,14 @@ namespace osu.Game.Overlays { AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, - Padding = new MarginPadding { Horizontal = 20 } - }, - loadingLayer = new LoadingLayer(panelTarget) + Padding = new MarginPadding { Horizontal = 20 }, + Children = new Drawable[] + { + foundContent = new FillFlowContainer(), + notFoundContent = new NotFoundDrawable(), + loadingLayer = new LoadingLayer(panelTarget) + } + } } }, } @@ -112,27 +125,52 @@ namespace osu.Game.Overlays private void onSearchFinished(List beatmaps) { + //No matches case if (!beatmaps.Any()) { - LoadComponentAsync(new NotFoundDrawable(), addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token); + LoadComponentAsync(notFoundContent, addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token); return; } - var newPanels = new FillFlowContainer + //New query case + if (!shouldAddNextPage) { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Spacing = new Vector2(10), - Alpha = 0, - Margin = new MarginPadding { Vertical = 15 }, - ChildrenEnumerable = beatmaps.Select(b => new GridBeatmapPanel(b) + //Spawn new child + var newPanels = new FillFlowContainer { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - }) - }; + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(10), + Alpha = 0, + Margin = new MarginPadding { Vertical = 15 }, + ChildrenEnumerable = beatmaps.Select(b => new GridBeatmapPanel(b) + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }) + }; - LoadComponentAsync(newPanels, addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token); + foundContent = newPanels; + LoadComponentAsync(foundContent, addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token); + } + + //Pagination case + else + { + + beatmaps.ForEach(x => + { + LoadComponentAsync(new GridBeatmapPanel(x) + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, loaded => + { + foundContent.Add(loaded); + loaded.FadeIn(200, Easing.OutQuint); + }); + }); + } } private void addContentToPlaceholder(Drawable content) @@ -149,13 +187,18 @@ namespace osu.Game.Overlays // If the auto-size computation is delayed until fade out completes, the background remain high for too long making the resulting transition to the smaller height look weird. // At the same time, if the last content's height is bypassed immediately, there is a period where the new content is at Alpha = 0 when the auto-sized height will be 0. // To resolve both of these issues, the bypass is delayed until a point when the content transitions (fade-in and fade-out) overlap and it looks good to do so. - lastContent.Delay(25).Schedule(() => lastContent.BypassAutoSizeAxes = Axes.Y); + lastContent.Delay(25).Schedule(() => lastContent.BypassAutoSizeAxes = Axes.Y) + .Then().Schedule(() => panelTarget.Remove(lastContent)); } - panelTarget.Add(currentContent = content); - currentContent.FadeIn(200, Easing.OutQuint); + if (!content.IsAlive) + panelTarget.Add(content); + content.FadeIn(200, Easing.OutQuint); + + currentContent = content; } + protected override void Dispose(bool isDisposing) { cancellationToken?.Cancel(); @@ -203,5 +246,14 @@ namespace osu.Game.Overlays }); } } + + protected override void Update() + { + base.Update(); + + if (shouldAddNextPage) + filterControl.AddPageToResult(); + + } } } From e5821ff2b28df51ed81585d6f2825b688fce5d2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 11 May 2020 22:53:05 +0200 Subject: [PATCH 1519/2376] Integrate GameplayBeatmap changes --- .../Skinning/TestSceneDrawableTaikoMascot.cs | 23 ++++++------- .../UI/DrawableTaikoMascot.cs | 32 +++++++++++++------ osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 9 +----- 3 files changed, 32 insertions(+), 32 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs index 492f628482..bd3b360577 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs @@ -7,7 +7,6 @@ using System.Linq; using Humanizer; using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Testing; @@ -76,23 +75,14 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning { AddStep("set beatmap", () => setBeatmap()); - // the bindables need to be independent for each content cell to prevent interference, - // as if some of the skins don't implement the animation they'll immediately revert to the previous state from the clear state. - var states = new List>(); + AddStep("create mascot", () => SetContents(() => new DrawableTaikoMascot { RelativeSizeAxes = Axes.Both })); - AddStep("create mascot", () => SetContents(() => - { - var state = new Bindable(TaikoMascotAnimationState.Clear); - states.Add(state); - return new DrawableTaikoMascot { State = { BindTarget = state }, RelativeSizeAxes = Axes.Both }; - })); - - AddStep("set clear state", () => states.ForEach(state => state.Value = TaikoMascotAnimationState.Clear)); - AddStep("miss", () => mascots.ForEach(mascot => mascot.OnNewResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Miss }))); + AddStep("set clear state", () => mascots.ForEach(mascot => mascot.State.Value = TaikoMascotAnimationState.Clear)); + AddStep("miss", () => mascots.ForEach(mascot => mascot.LastResult.Value = new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Miss })); AddAssert("skins with animations remain in clear state", () => someMascotsIn(TaikoMascotAnimationState.Clear)); AddUntilStep("state reverts to fail", () => allMascotsIn(TaikoMascotAnimationState.Fail)); - AddStep("set clear state again", () => states.ForEach(state => state.Value = TaikoMascotAnimationState.Clear)); + AddStep("set clear state again", () => mascots.ForEach(mascot => mascot.State.Value = TaikoMascotAnimationState.Clear)); AddAssert("skins with animations change to clear", () => someMascotsIn(TaikoMascotAnimationState.Clear)); } @@ -220,6 +210,11 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning playfield.OnNewResult(hit, judgementResult); } + + foreach (var mascot in mascots) + { + mascot.LastResult.Value = judgementResult; + } } private bool allMascotsIn(TaikoMascotAnimationState state) => mascots.All(d => d.State.Value == state); diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs index 9328b607e6..105baa84cc 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs @@ -12,14 +12,15 @@ using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics.Containers; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Taiko.Judgements; +using osu.Game.Screens.Play; namespace osu.Game.Rulesets.Taiko.UI { public class DrawableTaikoMascot : BeatSyncedContainer { - public IBindable State => state; + public readonly Bindable State; + public readonly Bindable LastResult; - private readonly Bindable state; private readonly Dictionary animations; private TaikoMascotAnimation currentAnimation; @@ -30,12 +31,14 @@ namespace osu.Game.Rulesets.Taiko.UI { Origin = Anchor = Anchor.BottomLeft; - state = new Bindable(startingState); + State = new Bindable(startingState); + LastResult = new Bindable(); + animations = new Dictionary(); } - [BackgroundDependencyLoader] - private void load(TextureStore textures) + [BackgroundDependencyLoader(true)] + private void load(TextureStore textures, GameplayBeatmap gameplayBeatmap) { InternalChildren = new[] { @@ -44,6 +47,9 @@ namespace osu.Game.Rulesets.Taiko.UI animations[TaikoMascotAnimationState.Kiai] = new TaikoMascotAnimation(TaikoMascotAnimationState.Kiai), animations[TaikoMascotAnimationState.Fail] = new TaikoMascotAnimation(TaikoMascotAnimationState.Fail), }; + + if (gameplayBeatmap != null) + ((IBindable)LastResult).BindTo(gameplayBeatmap.LastJudgementResult); } protected override void LoadComplete() @@ -51,16 +57,22 @@ namespace osu.Game.Rulesets.Taiko.UI base.LoadComplete(); animations.Values.ForEach(animation => animation.Hide()); - state.BindValueChanged(mascotStateChanged, true); + + State.BindValueChanged(mascotStateChanged, true); + LastResult.BindValueChanged(onNewResult); } - public void OnNewResult(JudgementResult result) + private void onNewResult(ValueChangedEvent resultChangedEvent) { + var result = resultChangedEvent.NewValue; + if (result == null) + return; + // TODO: missing support for clear/fail state transition at end of beatmap gameplay if (triggerComboClear(result) || triggerSwellClear(result)) { - state.Value = TaikoMascotAnimationState.Clear; + State.Value = TaikoMascotAnimationState.Clear; // always consider a clear equivalent to a hit to avoid clear -> miss transitions lastObjectHit = true; } @@ -79,7 +91,7 @@ namespace osu.Game.Rulesets.Taiko.UI protected override void Update() { base.Update(); - state.Value = getNextState(); + State.Value = getNextState(); } private TaikoMascotAnimationState getNextState() @@ -87,7 +99,7 @@ namespace osu.Game.Rulesets.Taiko.UI // don't change state if current animation is playing // (used for clear state - others are manually animated on new beats) if (currentAnimation != null && !currentAnimation.Completed) - return state.Value; + return State.Value; if (!lastObjectHit) return TaikoMascotAnimationState.Fail; diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 0fe0d6165b..21676510ad 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -40,8 +40,6 @@ namespace osu.Game.Rulesets.Taiko.UI private Container hitTargetOffsetContent; - private SkinnableDrawable mascotDrawable; - public TaikoPlayfield(ControlPointInfo controlPoints) { this.controlPoints = controlPoints; @@ -127,7 +125,7 @@ namespace osu.Game.Rulesets.Taiko.UI }, } }, - mascotDrawable = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.TaikoDon), _ => Empty()) + new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.TaikoDon), _ => Empty()) { Origin = Anchor.BottomLeft, Anchor = Anchor.TopLeft, @@ -212,11 +210,6 @@ namespace osu.Game.Rulesets.Taiko.UI addExplosion(judgedObject, type); break; } - - if (mascotDrawable.Drawable is DrawableTaikoMascot mascot) - { - mascot.OnNewResult(result); - } } private void addDrumRollHit(DrawableDrumRollTick drawableTick) => From bf719f98d5c664a0282b4bd186070dfa0fed0ccc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 May 2020 11:08:30 +0900 Subject: [PATCH 1520/2376] Fix beatmap skins providing fallback version lookup, preceding user skins --- osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs | 6 +++--- osu.Game/Skinning/LegacyBeatmapSkin.cs | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs b/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs index 685decf097..8deed75a56 100644 --- a/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs +++ b/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs @@ -152,11 +152,12 @@ namespace osu.Game.Tests.Skins } [Test] - public void TestSetBeatmapVersionNoFallback() + public void TestSetBeatmapVersionFallsBackToUserSkin() { + // completely ignoring beatmap versions for simplicity. AddStep("Set user skin version 2.3", () => userSource.Configuration.LegacyVersion = 2.3m); AddStep("Set beatmap skin version null", () => beatmapSource.Configuration.LegacyVersion = 1.7m); - AddAssert("Check legacy version lookup", () => requester.GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value == 1.7m); + AddAssert("Check legacy version lookup", () => requester.GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value == 2.3m); } [Test] @@ -172,7 +173,6 @@ namespace osu.Game.Tests.Skins public void TestIniWithNoVersionFallsBackTo1() { AddStep("Parse skin with no version", () => userSource.Configuration = new LegacySkinDecoder().Decode(new LineBufferedReader(new MemoryStream()))); - AddStep("Set beatmap skin version null", () => beatmapSource.Configuration.LegacyVersion = null); AddAssert("Check legacy version lookup", () => requester.GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value == 1.0m); } diff --git a/osu.Game/Skinning/LegacyBeatmapSkin.cs b/osu.Game/Skinning/LegacyBeatmapSkin.cs index 21533e58cd..87bca856a3 100644 --- a/osu.Game/Skinning/LegacyBeatmapSkin.cs +++ b/osu.Game/Skinning/LegacyBeatmapSkin.cs @@ -27,9 +27,11 @@ namespace osu.Game.Skinning switch (lookup) { case LegacySkinConfiguration.LegacySetting s when s == LegacySkinConfiguration.LegacySetting.Version: - if (Configuration.LegacyVersion is decimal version) - return SkinUtils.As(new Bindable(version)); + // For lookup simplicity, ignore beatmap-level versioning completely. + // If it is decided that we need this due to beatmaps somehow using it, the default (1.0 specified in LegacySkinDecoder.CreateTemplateObject) + // needs to be removed else it will cause incorrect skin behaviours. This is due to the config lookup having no context of which skin + // it should be returning the version for. return null; } From a5c1b461f6985d4c369a8f57336de16f4e07c662 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 May 2020 11:14:51 +0900 Subject: [PATCH 1521/2376] Fix null reference in difficulty recommender --- osu.Game/Screens/Select/DifficultyRecommender.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/DifficultyRecommender.cs b/osu.Game/Screens/Select/DifficultyRecommender.cs index 20cdca858a..0dd3341a93 100644 --- a/osu.Game/Screens/Select/DifficultyRecommender.cs +++ b/osu.Game/Screens/Select/DifficultyRecommender.cs @@ -86,7 +86,7 @@ namespace osu.Game.Screens.Select { base.Dispose(isDisposing); - api.Unregister(this); + api?.Unregister(this); } } } From 3b1680583e2ff39976485b1991355daa7c9ec13e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 May 2020 11:31:08 +0900 Subject: [PATCH 1522/2376] Fix taiko scroller not following gameplay time --- osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs index 1ecdb839fb..4cf1af3b8f 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs @@ -17,6 +17,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning { public class LegacyTaikoScroller : CompositeDrawable { + [Resolved(canBeNull: true)] + private GameplayClock gameplayClock { get; set; } + public LegacyTaikoScroller() { RelativeSizeAxes = Axes.Both; @@ -63,7 +66,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning foreach (var sprite in InternalChildren) { // add the x coordinates and perform re-layout on all sprites as spacing may change with gameplay scale. - sprite.X = additiveX ??= sprite.X - (float)Time.Elapsed * 0.1f; + sprite.X = additiveX ??= sprite.X - (float)(gameplayClock ?? Clock).ElapsedFrameTime * 0.1f; additiveX += sprite.DrawWidth - 1; From e9804bf11be0c535540bde81c09909ba2ecdb132 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 May 2020 11:55:12 +0900 Subject: [PATCH 1523/2376] 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 69f897128c..650ebde54d 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 c6dba8da13..ee6206e166 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -25,7 +25,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index f78fd2e4ff..cbf8600c62 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + From e4a23b3e7d7eb065578f48c2d53e7981e37c6a59 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 May 2020 12:39:04 +0900 Subject: [PATCH 1524/2376] Fix ignored excluding more than top level --- osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs | 10 ++++++++++ osu.Game/IO/OsuStorage.cs | 8 ++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs index ed83ff358c..ef2b20de64 100644 --- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs @@ -133,6 +133,9 @@ namespace osu.Game.Tests.NonVisual // ensure we "use" cache host.Storage.GetStorageForDirectory("cache"); + // for testing nested files are not ignored (only top level) + host.Storage.GetStorageForDirectory("test-nested").GetStorageForDirectory("cache"); + string defaultStorageLocation = Path.Combine(Environment.CurrentDirectory, "headless", nameof(TestMigration)); Assert.That(storage.GetFullPath("."), Is.EqualTo(defaultStorageLocation)); @@ -141,6 +144,13 @@ namespace osu.Game.Tests.NonVisual Assert.That(storage.GetFullPath("."), Is.EqualTo(customPath)); + // ensure cache was not moved + Assert.That(host.Storage.ExistsDirectory("cache")); + + // ensure nested cache was moved + Assert.That(!host.Storage.ExistsDirectory(Path.Combine("test-nested", "cache"))); + Assert.That(storage.ExistsDirectory(Path.Combine("test-nested", "cache"))); + foreach (var file in OsuStorage.IGNORE_FILES) { Assert.That(host.Storage.Exists(file), Is.True); diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index 7c0b90e208..6dc25e871c 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.cs @@ -71,7 +71,7 @@ namespace osu.Game.IO { foreach (System.IO.FileInfo fi in target.GetFiles()) { - if (IGNORE_FILES.Contains(fi.Name)) + if (topLevelExcludes && IGNORE_FILES.Contains(fi.Name)) continue; fi.Delete(); @@ -79,7 +79,7 @@ namespace osu.Game.IO foreach (DirectoryInfo dir in target.GetDirectories()) { - if (IGNORE_DIRECTORIES.Contains(dir.Name)) + if (topLevelExcludes && IGNORE_DIRECTORIES.Contains(dir.Name)) continue; dir.Delete(true); @@ -93,7 +93,7 @@ namespace osu.Game.IO foreach (System.IO.FileInfo fi in source.GetFiles()) { - if (IGNORE_FILES.Contains(fi.Name)) + if (topLevelExcludes && IGNORE_FILES.Contains(fi.Name)) continue; int tries = 5; @@ -114,7 +114,7 @@ namespace osu.Game.IO foreach (DirectoryInfo dir in source.GetDirectories()) { - if (IGNORE_DIRECTORIES.Contains(dir.Name)) + if (topLevelExcludes && IGNORE_DIRECTORIES.Contains(dir.Name)) continue; copyRecursive(dir, destination.CreateSubdirectory(dir.Name), false); From 75a40578e85690ae62bf089ae77ad06519b4ef08 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 May 2020 12:39:52 +0900 Subject: [PATCH 1525/2376] Revert ContextFactory to private --- osu.Game/OsuGameBase.cs | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 6282f5cb8b..11a3834c71 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -121,7 +121,7 @@ namespace osu.Game protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - protected DatabaseContextFactory ContextFactory; + private DatabaseContextFactory contextFactory; protected override UserInputManager CreateUserInputManager() => new OsuUserInputManager(); @@ -130,7 +130,7 @@ namespace osu.Game { Resources.AddStore(new DllResourceStore(OsuResources.ResourceAssembly)); - dependencies.Cache(ContextFactory = new DatabaseContextFactory(Storage)); + dependencies.Cache(contextFactory = new DatabaseContextFactory(Storage)); dependencies.CacheAs(Storage); @@ -161,7 +161,7 @@ namespace osu.Game runMigrations(); - dependencies.Cache(SkinManager = new SkinManager(Storage, ContextFactory, Host, Audio, new NamespacedResourceStore(Resources, "Skins/Legacy"))); + dependencies.Cache(SkinManager = new SkinManager(Storage, contextFactory, Host, Audio, new NamespacedResourceStore(Resources, "Skins/Legacy"))); dependencies.CacheAs(SkinManager); if (API == null) API = new APIAccess(LocalConfig); @@ -170,12 +170,12 @@ namespace osu.Game var defaultBeatmap = new DummyWorkingBeatmap(Audio, Textures); - dependencies.Cache(RulesetStore = new RulesetStore(ContextFactory, Storage)); - dependencies.Cache(FileStore = new FileStore(ContextFactory, Storage)); + dependencies.Cache(RulesetStore = new RulesetStore(contextFactory, Storage)); + dependencies.Cache(FileStore = new FileStore(contextFactory, Storage)); // 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, Host)); - dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, ContextFactory, RulesetStore, API, Audio, Host, defaultBeatmap)); + dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, API, contextFactory, Host)); + dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, contextFactory, RulesetStore, API, Audio, Host, defaultBeatmap)); // this should likely be moved to ArchiveModelManager when another case appers where it is necessary // to have inter-dependent model managers. this could be obtained with an IHasForeign interface to @@ -189,8 +189,8 @@ namespace osu.Game BeatmapManager.ItemRemoved += i => ScoreManager.Delete(getBeatmapScores(i), true); BeatmapManager.ItemAdded += i => ScoreManager.Undelete(getBeatmapScores(i), true); - dependencies.Cache(KeyBindingStore = new KeyBindingStore(ContextFactory, RulesetStore)); - dependencies.Cache(SettingsStore = new SettingsStore(ContextFactory)); + dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore)); + dependencies.Cache(SettingsStore = new SettingsStore(contextFactory)); dependencies.Cache(RulesetConfigCache = new RulesetConfigCache(SettingsStore)); dependencies.Cache(new SessionStatics()); dependencies.Cache(new OsuColour()); @@ -279,7 +279,7 @@ namespace osu.Game { try { - using (var db = ContextFactory.GetForWrite(false)) + using (var db = contextFactory.GetForWrite(false)) db.Context.Migrate(); } catch (Exception e) @@ -288,12 +288,12 @@ namespace osu.Game // if we failed, let's delete the database and start fresh. // todo: we probably want a better (non-destructive) migrations/recovery process at a later point than this. - ContextFactory.ResetDatabase(); + contextFactory.ResetDatabase(); Logger.Log("Database purged successfully.", LoggingTarget.Database); // only run once more, then hard bail. - using (var db = ContextFactory.GetForWrite(false)) + using (var db = contextFactory.GetForWrite(false)) db.Context.Migrate(); } } @@ -329,7 +329,7 @@ namespace osu.Game base.Dispose(isDisposing); RulesetStore?.Dispose(); - ContextFactory.FlushConnections(); + contextFactory.FlushConnections(); } private class OsuUserInputManager : UserInputManager @@ -360,7 +360,7 @@ namespace osu.Game public void Migrate(string path) { - ContextFactory.FlushConnections(); + contextFactory.FlushConnections(); (Storage as OsuStorage)?.Migrate(path); } } From a86c7f478c8b251ceec760ce860fa21658505dee Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Tue, 12 May 2020 05:49:35 +0200 Subject: [PATCH 1526/2376] Initial commit --- osu.Game/OsuGame.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 3caffb6db5..6e202b6315 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -91,7 +91,7 @@ namespace osu.Game protected BackButton BackButton; - protected SettingsPanel Settings; + protected SettingsOverlay Settings; private VolumeOverlay volume; private OsuLogo osuLogo; @@ -767,11 +767,17 @@ namespace osu.Game private Task asyncLoadStream; + /// + /// Schedules loading the provided in a single file. + /// + /// The component to load. + /// The method to invoke for adding the component. + /// Whether to cache the component as type into the game dependencies before any scheduling. private T loadComponentSingleFile(T d, Action add, bool cache = false) where T : Drawable { if (cache) - dependencies.Cache(d); + dependencies.CacheAs(d); if (d is OverlayContainer overlay) overlays.Add(overlay); From 39c36998c99509104c60f23cdce61a70e64e3c59 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Tue, 12 May 2020 06:06:31 +0200 Subject: [PATCH 1527/2376] Revert changes that are to be resolved in #9002 --- osu.Game/OsuGame.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 899056e179..294180cb30 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -90,7 +90,7 @@ namespace osu.Game protected BackButton BackButton; - protected SettingsOverlay Settings; + protected SettingsPanel Settings; private VolumeOverlay volume; private OsuLogo osuLogo; @@ -767,17 +767,11 @@ namespace osu.Game private Task asyncLoadStream; - /// - /// Schedules loading the provided in a single file. - /// - /// The component to load. - /// The method to invoke for adding the component. - /// Whether to cache the component as type into the game dependencies before any scheduling. private T loadComponentSingleFile(T d, Action add, bool cache = false) where T : Drawable { if (cache) - dependencies.CacheAs(d); + dependencies.Cache(d); if (d is OverlayContainer overlay) overlays.Add(overlay); From 949e17cc0e8f4666d87b9be541b5c31b3e5686a4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 May 2020 15:23:47 +0900 Subject: [PATCH 1528/2376] Rework scroller to support backwards playback --- .../Skinning/TestSceneTaikoScroller.cs | 20 ++++++- .../Skinning/LegacyTaikoScroller.cs | 52 +++++++++++-------- .../UI/DrawableTaikoRuleset.cs | 2 +- 3 files changed, 50 insertions(+), 24 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs index e26f410b71..9a2ada7f72 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs @@ -3,6 +3,7 @@ using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Testing; +using osu.Framework.Timing; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Skinning; @@ -12,11 +13,28 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning { public class TestSceneTaikoScroller : TaikoSkinnableTestScene { + private readonly ManualClock clock = new ManualClock(); + + private bool reversed; + public TestSceneTaikoScroller() { - AddStep("Load scroller", () => SetContents(() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.TaikoScroller), _ => Empty()))); + AddStep("Load scroller", () => SetContents(() => + new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.TaikoScroller), _ => Empty()) + { + Clock = new FramedClock(clock) + })); AddToggleStep("Toggle passing", passing => this.ChildrenOfType().ForEach(s => s.LastResult.Value = new JudgementResult(null, new Judgement()) { Type = passing ? HitResult.Perfect : HitResult.Miss })); + + AddToggleStep("toggle playback direction", reversed => this.reversed = reversed); + } + + protected override void Update() + { + base.Update(); + + clock.CurrentTime += (reversed ? -1 : 1) * Clock.ElapsedFrameTime; } } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs index 4cf1af3b8f..b3a325ea68 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs @@ -17,9 +17,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning { public class LegacyTaikoScroller : CompositeDrawable { - [Resolved(canBeNull: true)] - private GameplayClock gameplayClock { get; set; } - public LegacyTaikoScroller() { RelativeSizeAxes = Axes.Both; @@ -59,31 +56,42 @@ namespace osu.Game.Rulesets.Taiko.Skinning { base.Update(); - while (true) + bool wideEnough() => + InternalChildren.Any() + && InternalChildren.First().ScreenSpaceDrawQuad.Width * InternalChildren.Count >= ScreenSpaceDrawQuad.Width * 2; + + // store X before checking wide enough so if we perform layout there is no positional discrepancy. + float currentX = (InternalChildren?.FirstOrDefault()?.X ?? 0) - (float)Clock.ElapsedFrameTime * 0.1f; + + // ensure we have enough sprites + if (!wideEnough()) { - float? additiveX = null; + ClearInternal(); - foreach (var sprite in InternalChildren) - { - // add the x coordinates and perform re-layout on all sprites as spacing may change with gameplay scale. - sprite.X = additiveX ??= sprite.X - (float)(gameplayClock ?? Clock).ElapsedFrameTime * 0.1f; + while (!wideEnough()) + AddInternal(new ScrollerSprite { Passing = passing }); + } - additiveX += sprite.DrawWidth - 1; + var first = InternalChildren.First(); + var last = InternalChildren.Last(); - if (sprite.X + sprite.DrawWidth < 0) - sprite.Expire(); - } + foreach (var sprite in InternalChildren) + { + // add the x coordinates and perform re-layout on all sprites as spacing may change with gameplay scale. + sprite.X = currentX; + currentX += sprite.DrawWidth - 1; + } - var last = InternalChildren.LastOrDefault(); + if (first.ScreenSpaceDrawQuad.TopLeft.X >= ScreenSpaceDrawQuad.TopLeft.X) + { + foreach (var internalChild in InternalChildren) + internalChild.X -= first.DrawWidth; + } - // only break from this loop once we have saturated horizontal space completely. - if (last != null && last.ScreenSpaceDrawQuad.TopRight.X >= ScreenSpaceDrawQuad.TopRight.X) - break; - - AddInternal(new ScrollerSprite - { - Passing = passing - }); + if (last.ScreenSpaceDrawQuad.TopRight.X <= ScreenSpaceDrawQuad.TopRight.X) + { + foreach (var internalChild in InternalChildren) + internalChild.X += first.DrawWidth; } } diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs index c0a6c4582c..21d595a97a 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Taiko.UI { new BarLineGenerator(Beatmap).BarLines.ForEach(bar => Playfield.Add(bar.Major ? new DrawableBarLineMajor(bar) : new DrawableBarLine(bar))); - AddInternal(scroller = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.TaikoScroller), _ => Empty()) + FrameStableComponents.Add(scroller = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.TaikoScroller), _ => Empty()) { RelativeSizeAxes = Axes.X, Depth = float.MaxValue From c7d8793c1d96bd817d36ad1ae918ab4c6048fa33 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 May 2020 15:44:14 +0900 Subject: [PATCH 1529/2376] Remove unnecessary overlap --- osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs index b3a325ea68..28114ac74f 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs @@ -79,7 +79,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning { // add the x coordinates and perform re-layout on all sprites as spacing may change with gameplay scale. sprite.X = currentX; - currentX += sprite.DrawWidth - 1; + currentX += sprite.DrawWidth; } if (first.ScreenSpaceDrawQuad.TopLeft.X >= ScreenSpaceDrawQuad.TopLeft.X) From c04f2b0840ded76704dc37210273285d2bf8200a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 May 2020 15:51:59 +0900 Subject: [PATCH 1530/2376] Reposition taiko playfield to be closer to the top of the screen --- .../UI/TaikoPlayfieldAdjustmentContainer.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs index 980f5ea340..1041456020 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs @@ -13,18 +13,16 @@ namespace osu.Game.Rulesets.Taiko.UI private const float default_relative_height = TaikoPlayfield.DEFAULT_HEIGHT / 768; private const float default_aspect = 16f / 9f; - public TaikoPlayfieldAdjustmentContainer() - { - Anchor = Anchor.CentreLeft; - Origin = Anchor.CentreLeft; - } - protected override void Update() { base.Update(); float aspectAdjust = Math.Clamp(Parent.ChildSize.X / Parent.ChildSize.Y, 0.4f, 4) / default_aspect; Size = new Vector2(1, default_relative_height * aspectAdjust); + + // Position the taiko playfield exactly one playfield from the top of the screen. + RelativePositionAxes = Axes.Y; + Y = Size.Y; } } } From e28e89213f07767c12b2eed7d0a5b3063a5ef82b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 May 2020 16:58:47 +0900 Subject: [PATCH 1531/2376] Fix incorrect spawning when scale adjustments are applied to child sprites --- .../Skinning/TestSceneTaikoScroller.cs | 3 ++- osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs | 9 ++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs index 9a2ada7f72..39e6bc2d6d 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs @@ -22,7 +22,8 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning AddStep("Load scroller", () => SetContents(() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.TaikoScroller), _ => Empty()) { - Clock = new FramedClock(clock) + Clock = new FramedClock(clock), + Height = 0.4f, })); AddToggleStep("Toggle passing", passing => this.ChildrenOfType().ForEach(s => s.LastResult.Value = new JudgementResult(null, new Judgement()) { Type = passing ? HitResult.Perfect : HitResult.Miss })); diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs index 28114ac74f..3ec6be8a6c 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs @@ -64,13 +64,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning float currentX = (InternalChildren?.FirstOrDefault()?.X ?? 0) - (float)Clock.ElapsedFrameTime * 0.1f; // ensure we have enough sprites - if (!wideEnough()) - { - ClearInternal(); - - while (!wideEnough()) - AddInternal(new ScrollerSprite { Passing = passing }); - } + while (!wideEnough()) + AddInternal(new ScrollerSprite { Passing = passing }); var first = InternalChildren.First(); var last = InternalChildren.Last(); From de50b725d59be235c0be5ccee68d20f648c0c045 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 May 2020 20:08:35 +0900 Subject: [PATCH 1532/2376] Fix mod failure checks executing actual game logic --- osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs | 3 ++- osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs | 3 ++- osu.Game/Rulesets/Mods/IApplicableFailOverride.cs | 5 +++-- osu.Game/Rulesets/Mods/ModAutoplay.cs | 3 ++- osu.Game/Rulesets/Mods/ModBlockFail.cs | 2 +- osu.Game/Rulesets/Mods/ModEasy.cs | 13 +++++-------- osu.Game/Rulesets/Mods/ModSuddenDeath.cs | 3 ++- osu.Game/Screens/Play/Player.cs | 4 ++-- osu.Game/Screens/Play/ReplayPlayer.cs | 2 +- osu.Game/Tests/Visual/ModPerfectTestScene.cs | 2 +- osu.Game/Tests/Visual/ModTestScene.cs | 6 ++++-- 11 files changed, 25 insertions(+), 21 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs index fe46876050..d75f4c70d7 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs @@ -24,7 +24,8 @@ namespace osu.Game.Rulesets.Osu.Mods public override double ScoreMultiplier => 1; public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModSuddenDeath), typeof(ModNoFail), typeof(ModAutoplay) }; - public bool AllowFail => false; + public bool PerformFail() => false; + public bool RestartOnFail => false; private OsuInputManager inputManager; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs index e82722e7a2..1908988739 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs @@ -33,7 +33,8 @@ namespace osu.Game.Tests.Visual.Gameplay { public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; public new HUDOverlay HUDOverlay => base.HUDOverlay; - public new bool AllowFail => base.AllowFail; + + public bool AllowFail => base.CheckModsAllowFailure(); protected override bool PauseOnFocusLost => false; diff --git a/osu.Game/Rulesets/Mods/IApplicableFailOverride.cs b/osu.Game/Rulesets/Mods/IApplicableFailOverride.cs index 120bfc9a23..8c99d739cb 100644 --- a/osu.Game/Rulesets/Mods/IApplicableFailOverride.cs +++ b/osu.Game/Rulesets/Mods/IApplicableFailOverride.cs @@ -11,10 +11,11 @@ namespace osu.Game.Rulesets.Mods /// /// Whether we should allow failing at the current point in time. /// - bool AllowFail { get; } + /// Whether the fail should be allowed to proceed. Return false to block. + bool PerformFail(); /// - /// Whether we want to restart on fail. Only used if is true. + /// Whether we want to restart on fail. Only used if returns true. /// bool RestartOnFail { get; } } diff --git a/osu.Game/Rulesets/Mods/ModAutoplay.cs b/osu.Game/Rulesets/Mods/ModAutoplay.cs index e51b8b6457..945dd444be 100644 --- a/osu.Game/Rulesets/Mods/ModAutoplay.cs +++ b/osu.Game/Rulesets/Mods/ModAutoplay.cs @@ -27,7 +27,8 @@ namespace osu.Game.Rulesets.Mods public override string Description => "Watch a perfect automated play through the song."; public override double ScoreMultiplier => 1; - public bool AllowFail => false; + public bool PerformFail() => false; + public bool RestartOnFail => false; public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModSuddenDeath), typeof(ModNoFail) }; diff --git a/osu.Game/Rulesets/Mods/ModBlockFail.cs b/osu.Game/Rulesets/Mods/ModBlockFail.cs index 7d7ecfa416..1fde5abad4 100644 --- a/osu.Game/Rulesets/Mods/ModBlockFail.cs +++ b/osu.Game/Rulesets/Mods/ModBlockFail.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Mods /// /// We never fail, 'yo. /// - public bool AllowFail => false; + public bool PerformFail() => false; public bool RestartOnFail => false; diff --git a/osu.Game/Rulesets/Mods/ModEasy.cs b/osu.Game/Rulesets/Mods/ModEasy.cs index c1c4124b98..7cf9656810 100644 --- a/osu.Game/Rulesets/Mods/ModEasy.cs +++ b/osu.Game/Rulesets/Mods/ModEasy.cs @@ -48,17 +48,14 @@ namespace osu.Game.Rulesets.Mods retries = Retries.Value; } - public bool AllowFail + public bool PerformFail() { - get - { - if (retries == 0) return true; + if (retries == 0) return true; - health.Value = health.MaxValue; - retries--; + health.Value = health.MaxValue; + retries--; - return false; - } + return false; } public bool RestartOnFail => false; diff --git a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs index 8799431f1d..df10262845 100644 --- a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs +++ b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs @@ -20,7 +20,8 @@ namespace osu.Game.Rulesets.Mods public override bool Ranked => true; public override Type[] IncompatibleMods => new[] { typeof(ModNoFail), typeof(ModRelax), typeof(ModAutoplay) }; - public bool AllowFail => true; + public bool PerformFail() => true; + public bool RestartOnFail => true; public void ApplyToHealthProcessor(HealthProcessor healthProcessor) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index a2735c8c55..1ec3a69b24 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -105,7 +105,7 @@ namespace osu.Game.Screens.Play /// Whether failing should be allowed. /// By default, this checks whether all selected mods allow failing. /// - protected virtual bool AllowFail => Mods.Value.OfType().All(m => m.AllowFail); + protected virtual bool CheckModsAllowFailure() => Mods.Value.OfType().All(m => m.PerformFail()); private readonly bool allowPause; private readonly bool showResults; @@ -485,7 +485,7 @@ namespace osu.Game.Screens.Play private bool onFail() { - if (!AllowFail) + if (!CheckModsAllowFailure()) return false; HasFailed = true; diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index 0d2ddb7b01..f0c76163f1 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -12,7 +12,7 @@ namespace osu.Game.Screens.Play private readonly Score score; // Disallow replays from failing. (see https://github.com/ppy/osu/issues/6108) - protected override bool AllowFail => false; + protected override bool CheckModsAllowFailure() => false; public ReplayPlayer(Score score, bool allowPause = true, bool showResults = true) : base(allowPause, showResults) diff --git a/osu.Game/Tests/Visual/ModPerfectTestScene.cs b/osu.Game/Tests/Visual/ModPerfectTestScene.cs index 5948283428..c16352bead 100644 --- a/osu.Game/Tests/Visual/ModPerfectTestScene.cs +++ b/osu.Game/Tests/Visual/ModPerfectTestScene.cs @@ -41,7 +41,7 @@ namespace osu.Game.Tests.Visual { } - protected override bool AllowFail => true; + protected override bool CheckModsAllowFailure() => false; public bool CheckFailed(bool failed) { diff --git a/osu.Game/Tests/Visual/ModTestScene.cs b/osu.Game/Tests/Visual/ModTestScene.cs index 8b41fb5075..b5b3084097 100644 --- a/osu.Game/Tests/Visual/ModTestScene.cs +++ b/osu.Game/Tests/Visual/ModTestScene.cs @@ -64,12 +64,14 @@ namespace osu.Game.Tests.Visual protected class ModTestPlayer : TestPlayer { - protected override bool AllowFail { get; } + private readonly bool allowFail; + + protected override bool CheckModsAllowFailure() => allowFail; public ModTestPlayer(bool allowFail) : base(false, false) { - AllowFail = allowFail; + this.allowFail = allowFail; } } From 44319c1b719c64468de0594e22c48c357c3c62ac Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 May 2020 20:26:34 +0900 Subject: [PATCH 1533/2376] Commit missed change --- osu.Game/Tests/Visual/ModPerfectTestScene.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Tests/Visual/ModPerfectTestScene.cs b/osu.Game/Tests/Visual/ModPerfectTestScene.cs index c16352bead..95a62bbf65 100644 --- a/osu.Game/Tests/Visual/ModPerfectTestScene.cs +++ b/osu.Game/Tests/Visual/ModPerfectTestScene.cs @@ -41,7 +41,7 @@ namespace osu.Game.Tests.Visual { } - protected override bool CheckModsAllowFailure() => false; + protected override bool CheckModsAllowFailure() => true; public bool CheckFailed(bool failed) { From f07d95ac59e77eb1fbfeaae58ece62b26828de46 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Tue, 12 May 2020 21:49:55 +0800 Subject: [PATCH 1534/2376] Use IReadOnlyList for TypedListConverter. --- .../IO/Serialization/Converters/TypedListConverter.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/IO/Serialization/Converters/TypedListConverter.cs b/osu.Game/IO/Serialization/Converters/TypedListConverter.cs index ec0036dae2..f98fa05821 100644 --- a/osu.Game/IO/Serialization/Converters/TypedListConverter.cs +++ b/osu.Game/IO/Serialization/Converters/TypedListConverter.cs @@ -9,12 +9,12 @@ using Newtonsoft.Json.Linq; namespace osu.Game.IO.Serialization.Converters { /// - /// A type of that serializes an alongside + /// A type of that serializes an alongside /// a lookup table for the types contained. The lookup table is used in deserialization to /// reconstruct the objects with their original types. /// - /// The type of objects contained in the this attribute is attached to. - public class TypedListConverter : JsonConverter> + /// The type of objects contained in the this attribute is attached to. + public class TypedListConverter : JsonConverter> { private readonly bool requiresTypeVersion; @@ -36,7 +36,7 @@ namespace osu.Game.IO.Serialization.Converters this.requiresTypeVersion = requiresTypeVersion; } - public override IEnumerable ReadJson(JsonReader reader, Type objectType, IEnumerable existingValue, bool hasExistingValue, JsonSerializer serializer) + public override IReadOnlyList ReadJson(JsonReader reader, Type objectType, IReadOnlyList existingValue, bool hasExistingValue, JsonSerializer serializer) { var list = new List(); @@ -57,7 +57,7 @@ namespace osu.Game.IO.Serialization.Converters return list; } - public override void WriteJson(JsonWriter writer, IEnumerable value, JsonSerializer serializer) + public override void WriteJson(JsonWriter writer, IReadOnlyList value, JsonSerializer serializer) { var lookupTable = new List(); var objects = new List(); From 0c60b10757b1d605cb1d15c20c510a3b9a63ddbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=80=8C=E7=A9=BA=E7=99=BD=E3=80=8D?= <「空白」> Date: Wed, 13 May 2020 01:14:11 +0900 Subject: [PATCH 1535/2376] Fix code factor issues > ran "dotnet format --check", shouldn't return whitespace errors anymore --- .../Overlays/BeatmapListing/BeatmapListingFilterControl.cs | 2 +- osu.Game/Overlays/BeatmapListingOverlay.cs | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs index c3e8505ddc..3f06bc20ed 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs @@ -154,7 +154,7 @@ namespace osu.Game.Overlays.BeatmapListing currentBeatmaps = beatmaps; else currentBeatmaps.AddRange(beatmaps); - + SearchFinished?.Invoke(beatmaps); } diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index c495c8d21b..115fe02999 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -44,7 +44,7 @@ namespace osu.Game.Overlays { } - private BeatmapListingFilterControl filterControl;//actual search settings + private BeatmapListingFilterControl filterControl; [BackgroundDependencyLoader] private void load() @@ -157,7 +157,6 @@ namespace osu.Game.Overlays //Pagination case else { - beatmaps.ForEach(x => { LoadComponentAsync(new GridBeatmapPanel(x) @@ -198,7 +197,6 @@ namespace osu.Game.Overlays currentContent = content; } - protected override void Dispose(bool isDisposing) { cancellationToken?.Cancel(); @@ -253,7 +251,6 @@ namespace osu.Game.Overlays if (shouldAddNextPage) filterControl.AddPageToResult(); - } } } From 82190a07b82f439c8e221c2420ca9b61e7d7e944 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=80=8C=E7=A9=BA=E7=99=BD=E3=80=8D?= <「空白」> Date: Wed, 13 May 2020 02:01:38 +0900 Subject: [PATCH 1536/2376] Remove temporary comments > Removes unnecessary xmldoc comments --- osu.Game/Online/API/Requests/ResponseWithCursor.cs | 3 --- .../Overlays/BeatmapListing/BeatmapListingFilterControl.cs | 2 -- osu.Game/Overlays/BeatmapListing/BeatmapListingPager.cs | 4 ---- osu.Game/Overlays/BeatmapListingOverlay.cs | 2 -- 4 files changed, 11 deletions(-) diff --git a/osu.Game/Online/API/Requests/ResponseWithCursor.cs b/osu.Game/Online/API/Requests/ResponseWithCursor.cs index 51e88ca52b..b0fe9eea28 100644 --- a/osu.Game/Online/API/Requests/ResponseWithCursor.cs +++ b/osu.Game/Online/API/Requests/ResponseWithCursor.cs @@ -16,9 +16,6 @@ namespace osu.Game.Online.API.Requests public abstract class ResponseWithCursor : ResponseWithCursor where T : class { - /// - /// Cursor deserialized into T class type (cannot implicitly convert type to object using raw Cursor) - /// [JsonProperty("cursor")] public T Cursor; } diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs index 3f06bc20ed..ac5ad96f7c 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs @@ -22,7 +22,6 @@ namespace osu.Game.Overlays.BeatmapListing { public Action> SearchFinished; public Action SearchStarted; - /// List of currently displayed beatmap entries private List currentBeatmaps; [Resolved] @@ -169,7 +168,6 @@ namespace osu.Game.Overlays.BeatmapListing public void TakeFocus() => searchControl.TakeFocus(); - /// Request next 50 matches if available public void AddPageToResult() { if (beatmapListingPager == null || !beatmapListingPager.CanFetchNextPage) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingPager.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingPager.cs index 66faf8df7a..4c8902d314 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingPager.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingPager.cs @@ -24,13 +24,9 @@ namespace osu.Game.Overlays.BeatmapListing private SearchBeatmapSetsRequest getSetsRequest; private SearchBeatmapSetsResponse lastResponse; - /// Reports end of results private bool isLastPageFetched = false; - /// Job in process lock flag private bool isFetching => getSetsRequest != null; - /// Whether beatmaps should be appended or replaced public bool IsPastFirstPage { get; private set; } = false; - /// call FetchNextPage() safe-check public bool CanFetchNextPage => !isLastPageFetched && !isFetching; public BeatmapListingPager(IAPIProvider api, RulesetStore rulesets, string query, RulesetInfo ruleset, SearchCategory searchCategory = SearchCategory.Any, SortCriteria sortCriteria = SortCriteria.Ranked, SortDirection sortDirection = SortDirection.Descending) diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index 115fe02999..ba92181ac5 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -34,9 +34,7 @@ namespace osu.Game.Overlays private NotFoundDrawable notFoundContent; private OverlayScrollContainer resultScrollContainer; - /// Scroll distance threshold from results tail, higher means sooner private const int pagination_scroll_distance = 500; - /// This is paging event flag private bool shouldAddNextPage => resultScrollContainer.ScrollableExtent > 0 && resultScrollContainer.IsScrolledToEnd(pagination_scroll_distance); public BeatmapListingOverlay() From e321494f15c182beec3e8c911341968cbfa73417 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=80=8C=E7=A9=BA=E7=99=BD=E3=80=8D?= <「空白」> Date: Wed, 13 May 2020 02:15:24 +0900 Subject: [PATCH 1537/2376] Fix sort-by handling > Add other cursor fields for paging different sortings > Sorted as they show in GUI code-wise for more readability for now --- .../API/Requests/SearchBeatmapSetsRequest.cs | 41 ++++++++++++++++++- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs index fb2cc66dd8..2987d554af 100644 --- a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs +++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs @@ -13,8 +13,29 @@ namespace osu.Game.Online.API.Requests { public class Cursor { + [JsonProperty("title.raw")] + public string Title; + + [JsonProperty("artist.raw")] + public string Artist; + + [JsonProperty("beatmaps.difficultyrating")] + public string Difficulty; + [JsonProperty("approved_date")] - public string ApprovedDate; + public string Ranked; + + [JsonProperty("rating")] + public string Rating; + + [JsonProperty("play_count")] + public string Plays; + + [JsonProperty("favourite_count")] + public string Favourites; + + [JsonProperty("_score")] + public string Relevance; [JsonProperty("_id")] public string Id; @@ -70,8 +91,24 @@ namespace osu.Game.Online.API.Requests if (cursor != null) { + if (cursor.Title != null) + req.AddParameter("cursor[title.raw]", cursor.Title); + if (cursor.Artist != null) + req.AddParameter("cursor[artist.raw]", cursor.Artist); + if (cursor.Difficulty != null) + req.AddParameter("cursor[beatmaps.difficultyrating]", cursor.Difficulty); + if (cursor.Ranked != null) + req.AddParameter("cursor[approved_date]", cursor.Ranked); + if (cursor.Rating != null) + req.AddParameter("cursor[rating]", cursor.Rating); + if (cursor.Plays != null) + req.AddParameter("cursor[play_count]", cursor.Plays); + if (cursor.Favourites != null) + req.AddParameter("cursor[favourite_count]", cursor.Favourites); + if (cursor.Relevance != null) + req.AddParameter("cursor[_score]", cursor.Relevance); + req.AddParameter("cursor[_id]", cursor.Id); - req.AddParameter("cursor[approved_date]", cursor.ApprovedDate); } return req; From 942cc48e99ac41a480c48faa9270534d31225e78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 12 May 2020 20:26:11 +0200 Subject: [PATCH 1538/2376] Improve mascot scaling --- .../UI/TaikoMascotAnimation.cs | 2 +- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs b/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs index 0bf6bc7d49..01cf88a87e 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Taiko.UI InternalChild = textureAnimation = createTextureAnimation(state).With(animation => { animation.Origin = animation.Anchor = Anchor.BottomLeft; - animation.Scale = new Vector2(0.6f); + animation.Scale = new Vector2(0.51f); // close enough to stable }); RelativeSizeAxes = Axes.Both; diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 21676510ad..5940ee8b69 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -5,6 +5,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Layout; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; @@ -15,6 +16,7 @@ using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.Taiko.Judgements; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Skinning; +using osuTK; namespace osu.Game.Rulesets.Taiko.UI { @@ -32,6 +34,7 @@ namespace osu.Game.Rulesets.Taiko.UI private JudgementContainer judgementContainer; private ScrollingHitObjectContainer drumRollHitContainer; internal Drawable HitTarget; + private SkinnableDrawable mascot; private ProxyContainer topLevelHitContainer; private ProxyContainer barlineContainer; @@ -40,9 +43,14 @@ namespace osu.Game.Rulesets.Taiko.UI private Container hitTargetOffsetContent; + private readonly LayoutValue playfieldScaleLayout = new LayoutValue(Invalidation.DrawSize); + private float playfieldScale => playfieldScaleLayout.IsValid ? playfieldScaleLayout.Value : playfieldScaleLayout.Value = DrawHeight / DEFAULT_HEIGHT; + public TaikoPlayfield(ControlPointInfo controlPoints) { this.controlPoints = controlPoints; + + AddLayout(playfieldScaleLayout); } [BackgroundDependencyLoader] @@ -125,14 +133,13 @@ namespace osu.Game.Rulesets.Taiko.UI }, } }, - new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.TaikoDon), _ => Empty()) + mascot = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.TaikoDon), _ => Empty()) { Origin = Anchor.BottomLeft, Anchor = Anchor.TopLeft, - RelativePositionAxes = Axes.None, + RelativePositionAxes = Axes.Y, RelativeSizeAxes = Axes.None, - X = 15, - Y = 45 + Y = 0.2f }, topLevelHitContainer = new ProxyContainer { @@ -151,6 +158,8 @@ namespace osu.Game.Rulesets.Taiko.UI // This is basically allowing for correct alignment as relative pieces move around them. rightArea.Padding = new MarginPadding { Left = leftArea.DrawWidth }; hitTargetOffsetContent.Padding = new MarginPadding { Left = HitTarget.DrawWidth / 2 }; + + mascot.Scale = new Vector2(playfieldScale); } public override void Add(DrawableHitObject h) From cabf3a89b1e79edcd452900477a684ad4f5b3bd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=80=8C=E7=A9=BA=E7=99=BD=E3=80=8D?= <「空白」> Date: Wed, 13 May 2020 03:44:57 +0900 Subject: [PATCH 1539/2376] More robust cursor parsing solution > Change cursor request to return last response's entire cursor structure --- .../API/Requests/SearchBeatmapSetsRequest.cs | 54 +++---------------- 1 file changed, 8 insertions(+), 46 deletions(-) diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs index 2987d554af..e1df7fe6d3 100644 --- a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs +++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs @@ -2,10 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.IO.Network; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Game.Overlays; using osu.Game.Overlays.BeatmapListing; using osu.Game.Rulesets; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System.Collections.Generic; namespace osu.Game.Online.API.Requests { @@ -13,32 +16,8 @@ namespace osu.Game.Online.API.Requests { public class Cursor { - [JsonProperty("title.raw")] - public string Title; - - [JsonProperty("artist.raw")] - public string Artist; - - [JsonProperty("beatmaps.difficultyrating")] - public string Difficulty; - - [JsonProperty("approved_date")] - public string Ranked; - - [JsonProperty("rating")] - public string Rating; - - [JsonProperty("play_count")] - public string Plays; - - [JsonProperty("favourite_count")] - public string Favourites; - - [JsonProperty("_score")] - public string Relevance; - - [JsonProperty("_id")] - public string Id; + [JsonExtensionData] + public IDictionary Properties; } public SearchCategory SearchCategory { get; set; } @@ -89,27 +68,10 @@ namespace osu.Game.Online.API.Requests req.AddParameter("sort", $"{SortCriteria.ToString().ToLowerInvariant()}_{directionString}"); - if (cursor != null) + cursor?.Properties.ForEach(x => { - if (cursor.Title != null) - req.AddParameter("cursor[title.raw]", cursor.Title); - if (cursor.Artist != null) - req.AddParameter("cursor[artist.raw]", cursor.Artist); - if (cursor.Difficulty != null) - req.AddParameter("cursor[beatmaps.difficultyrating]", cursor.Difficulty); - if (cursor.Ranked != null) - req.AddParameter("cursor[approved_date]", cursor.Ranked); - if (cursor.Rating != null) - req.AddParameter("cursor[rating]", cursor.Rating); - if (cursor.Plays != null) - req.AddParameter("cursor[play_count]", cursor.Plays); - if (cursor.Favourites != null) - req.AddParameter("cursor[favourite_count]", cursor.Favourites); - if (cursor.Relevance != null) - req.AddParameter("cursor[_score]", cursor.Relevance); - - req.AddParameter("cursor[_id]", cursor.Id); - } + req.AddParameter("cursor[" + x.Key + "]", x.Value?.ToString()); + }); return req; } From 5962dedd35947eab90642aef09a6dee8547e3ef5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=80=8C=E7=A9=BA=E7=99=BD=E3=80=8D?= <「空白」> Date: Wed, 13 May 2020 05:04:39 +0900 Subject: [PATCH 1540/2376] Reimplement cursor as part of WebRequest extensions > Added WebRequestExtensions > Moved Cursor request logic from SearchBeatmapSetsRequest --- osu.Game/Extensions/WebRequestExtensions.cs | 25 +++++++++++++++++++ .../API/Requests/SearchBeatmapSetsRequest.cs | 16 ++---------- .../API/Requests/SearchBeatmapSetsResponse.cs | 3 ++- 3 files changed, 29 insertions(+), 15 deletions(-) create mode 100644 osu.Game/Extensions/WebRequestExtensions.cs diff --git a/osu.Game/Extensions/WebRequestExtensions.cs b/osu.Game/Extensions/WebRequestExtensions.cs new file mode 100644 index 0000000000..c8e3c564a5 --- /dev/null +++ b/osu.Game/Extensions/WebRequestExtensions.cs @@ -0,0 +1,25 @@ +using osu.Framework.IO.Network; +using osu.Framework.Extensions.IEnumerableExtensions; +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace osu.Game.Extensions +{ + public class Cursor + { + [JsonExtensionData] + public IDictionary Properties; + } + + public static class WebRequestExtensions + { + public static void AddCursor(this WebRequest webRequest, Cursor cursor) + { + cursor?.Properties.ForEach(x => + { + webRequest.AddParameter("cursor[" + x.Key + "]", x.Value.ToString()); + }); + } + } +} diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs index e1df7fe6d3..a49cb70c37 100644 --- a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs +++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs @@ -2,24 +2,15 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.IO.Network; -using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Game.Extensions; using osu.Game.Overlays; using osu.Game.Overlays.BeatmapListing; using osu.Game.Rulesets; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using System.Collections.Generic; namespace osu.Game.Online.API.Requests { public class SearchBeatmapSetsRequest : APIRequest { - public class Cursor - { - [JsonExtensionData] - public IDictionary Properties; - } - public SearchCategory SearchCategory { get; set; } public SortCriteria SortCriteria { get; set; } @@ -68,10 +59,7 @@ namespace osu.Game.Online.API.Requests req.AddParameter("sort", $"{SortCriteria.ToString().ToLowerInvariant()}_{directionString}"); - cursor?.Properties.ForEach(x => - { - req.AddParameter("cursor[" + x.Key + "]", x.Value?.ToString()); - }); + req.AddCursor(cursor); return req; } diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsResponse.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsResponse.cs index 2adf7004e8..a4d2c0e871 100644 --- a/osu.Game/Online/API/Requests/SearchBeatmapSetsResponse.cs +++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsResponse.cs @@ -3,11 +3,12 @@ using System.Collections.Generic; using Newtonsoft.Json; +using osu.Game.Extensions; using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Online.API.Requests { - public class SearchBeatmapSetsResponse : ResponseWithCursor + public class SearchBeatmapSetsResponse : ResponseWithCursor { [JsonProperty("beatmapsets")] public IEnumerable BeatmapSets; From 7874045b1f82e34dac1df8eb8d849f25a878ab97 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Tue, 12 May 2020 21:12:48 +0200 Subject: [PATCH 1541/2376] Allow disabling SkipOverlay through AllowGameplayOverlays. --- osu.Game/Screens/Play/Player.cs | 6 +++++- osu.Game/Screens/Play/SkipOverlay.cs | 7 +++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index a2735c8c55..b24cef8eae 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -83,6 +83,8 @@ namespace osu.Game.Screens.Play private BreakTracker breakTracker; + private SkipOverlay skipOverlay; + protected ScoreProcessor ScoreProcessor { get; private set; } protected HealthProcessor HealthProcessor { get; private set; } @@ -189,6 +191,8 @@ namespace osu.Game.Screens.Play HUDOverlay.ShowHud.Value = false; HUDOverlay.ShowHud.Disabled = true; BreakOverlay.Hide(); + skipOverlay.Disabled = true; + skipOverlay.Hide(); } DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updatePauseOnFocusLostState(), true); @@ -290,7 +294,7 @@ namespace osu.Game.Screens.Play Anchor = Anchor.Centre, Origin = Anchor.Centre }, - new SkipOverlay(DrawableRuleset.GameplayStartTime) + skipOverlay = new SkipOverlay(DrawableRuleset.GameplayStartTime) { RequestSkip = GameplayClockContainer.Skip }, diff --git a/osu.Game/Screens/Play/SkipOverlay.cs b/osu.Game/Screens/Play/SkipOverlay.cs index ac7e509c2c..757dcd21ed 100644 --- a/osu.Game/Screens/Play/SkipOverlay.cs +++ b/osu.Game/Screens/Play/SkipOverlay.cs @@ -39,6 +39,8 @@ namespace osu.Game.Screens.Play [Resolved] private GameplayClock gameplayClock { get; set; } + public bool Disabled { get; set; } + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; /// @@ -105,7 +107,8 @@ namespace osu.Game.Screens.Play button.Action = () => RequestSkip?.Invoke(); displayTime = gameplayClock.CurrentTime; - Show(); + if (!Disabled) + Show(); } protected override void PopIn() => this.FadeIn(fade_time); @@ -121,7 +124,7 @@ namespace osu.Game.Screens.Play remainingTimeBox.Width = (float)Interpolation.Lerp(remainingTimeBox.Width, progress, Math.Clamp(Time.Elapsed / 40, 0, 1)); button.Enabled.Value = progress > 0; - State.Value = progress > 0 ? Visibility.Visible : Visibility.Hidden; + State.Value = progress > 0 && !Disabled ? Visibility.Visible : Visibility.Hidden; } protected override bool OnMouseMove(MouseMoveEvent e) From bbf4c687a8b155499cf50634ce545888f3ee0770 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 May 2020 11:09:17 +0900 Subject: [PATCH 1542/2376] Improve xmldoc --- osu.Game/OsuGame.cs | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 6e202b6315..7ecd7851d7 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -768,18 +768,19 @@ namespace osu.Game private Task asyncLoadStream; /// - /// Schedules loading the provided in a single file. + /// Queues loading the provided component in sequential fashion. + /// This operation is limited to a single thread to avoid saturating all cores. /// - /// The component to load. - /// The method to invoke for adding the component. + /// The component to load. + /// An action to invoke on load completion (generally to add the component to the hierarchy). /// Whether to cache the component as type into the game dependencies before any scheduling. - private T loadComponentSingleFile(T d, Action add, bool cache = false) + private T loadComponentSingleFile(T component, Action loadCompleteAction, bool cache = false) where T : Drawable { if (cache) - dependencies.CacheAs(d); + dependencies.CacheAs(component); - if (d is OverlayContainer overlay) + if (component is OverlayContainer overlay) overlays.Add(overlay); // schedule is here to ensure that all component loads are done after LoadComplete is run (and thus all dependencies are cached). @@ -797,12 +798,12 @@ namespace osu.Game try { - Logger.Log($"Loading {d}...", level: LogLevel.Debug); + Logger.Log($"Loading {component}...", level: LogLevel.Debug); // Since this is running in a separate thread, it is possible for OsuGame to be disposed after LoadComponentAsync has been called // throwing an exception. To avoid this, the call is scheduled on the update thread, which does not run if IsDisposed = true Task task = null; - var del = new ScheduledDelegate(() => task = LoadComponentAsync(d, add)); + var del = new ScheduledDelegate(() => task = LoadComponentAsync(component, loadCompleteAction)); Scheduler.Add(del); // The delegate won't complete if OsuGame has been disposed in the meantime @@ -817,7 +818,7 @@ namespace osu.Game await task; - Logger.Log($"Loaded {d}!", level: LogLevel.Debug); + Logger.Log($"Loaded {component}!", level: LogLevel.Debug); } catch (OperationCanceledException) { @@ -825,7 +826,7 @@ namespace osu.Game }); }); - return d; + return component; } protected override bool OnScroll(ScrollEvent e) From 78f1b230e924b7065d629919f7b2ac0c42dcdc47 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 13 May 2020 14:43:50 +0900 Subject: [PATCH 1543/2376] Disable right-click placement in the mania editor --- .../Edit/Blueprints/HoldNotePlacementBlueprint.cs | 4 ++++ .../Edit/Blueprints/ManiaPlacementBlueprint.cs | 4 ++++ .../Edit/Blueprints/NotePlacementBlueprint.cs | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs index b3dd392202..c63e30e98a 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs @@ -7,6 +7,7 @@ using osu.Framework.Input.Events; using osu.Game.Rulesets.Mania.Edit.Blueprints.Components; using osu.Game.Rulesets.Mania.Objects; using osuTK; +using osuTK.Input; namespace osu.Game.Rulesets.Mania.Edit.Blueprints { @@ -49,6 +50,9 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints protected override void OnMouseUp(MouseUpEvent e) { + if (e.Button != MouseButton.Left) + return; + base.OnMouseUp(e); EndPlacement(true); } diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs index 400abb6380..3fb03d642f 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs @@ -11,6 +11,7 @@ using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.UI.Scrolling; using osuTK; +using osuTK.Input; namespace osu.Game.Rulesets.Mania.Edit.Blueprints { @@ -46,6 +47,9 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints protected override bool OnMouseDown(MouseDownEvent e) { + if (e.Button != MouseButton.Left) + return false; + if (Column == null) return base.OnMouseDown(e); diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs index 2b7b383dbe..a4c0791253 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs @@ -5,6 +5,7 @@ using osu.Framework.Graphics; using osu.Framework.Input.Events; using osu.Game.Rulesets.Mania.Edit.Blueprints.Components; using osu.Game.Rulesets.Mania.Objects; +using osuTK.Input; namespace osu.Game.Rulesets.Mania.Edit.Blueprints { @@ -30,6 +31,9 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints protected override bool OnMouseDown(MouseDownEvent e) { + if (e.Button != MouseButton.Left) + return false; + base.OnMouseDown(e); // Place the note immediately. From 8500f8fe767c4f7362cf4c6c38685de270cdf77a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 May 2020 18:12:58 +0900 Subject: [PATCH 1544/2376] Add basic implementation --- .../Settings/TestSceneDirectorySelector.cs | 195 ++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 osu.Game.Tests/Visual/Settings/TestSceneDirectorySelector.cs diff --git a/osu.Game.Tests/Visual/Settings/TestSceneDirectorySelector.cs b/osu.Game.Tests/Visual/Settings/TestSceneDirectorySelector.cs new file mode 100644 index 0000000000..416d978c69 --- /dev/null +++ b/osu.Game.Tests/Visual/Settings/TestSceneDirectorySelector.cs @@ -0,0 +1,195 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.IO; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Platform; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osuTK; + +namespace osu.Game.Tests.Visual.Settings +{ + public class TestSceneDirectorySelector : OsuTestScene + { + [BackgroundDependencyLoader] + private void load(GameHost host) + { + Add(new DirectorySelector { RelativeSizeAxes = Axes.Both }); + } + } + + public class DirectorySelector : CompositeDrawable + { + private Storage root; + private FillFlowContainer directoryFlow; + private CurrentDirectoryDisplay current; + + [Resolved] + private GameHost host { get; set; } + + private readonly Bindable currentDirectory = new Bindable(); + + public DirectorySelector(Storage root = null) + { + this.root = root; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Padding = new MarginPadding(10); + + if (root == null) + root = host.GetStorage("/Users/"); + + InternalChildren = new Drawable[] + { + new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + current = new CurrentDirectoryDisplay + { + CurrentDirectory = { BindTarget = currentDirectory }, + RelativeSizeAxes = Axes.X, + Height = 50, + }, + new OsuScrollContainer + { + RelativeSizeAxes = Axes.Both, + Child = directoryFlow = new FillFlowContainer + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Direction = FillDirection.Vertical, + } + } + } + }, + }; + + currentDirectory.BindValueChanged(updateDisplay); + currentDirectory.Value = root.GetFullPath(string.Empty); + } + + private void updateDisplay(ValueChangedEvent directory) + { + root = host.GetStorage(directory.NewValue); + + directoryFlow.Clear(); + + directoryFlow.Add(new ParentDirectoryRow(getParentPath()) { CurrentDirectory = { BindTarget = currentDirectory }, }); + + foreach (var dir in root.GetDirectories(string.Empty)) + directoryFlow.Add(new DirectoryRow(dir, root.GetFullPath(dir)) { CurrentDirectory = { BindTarget = currentDirectory }, }); + } + + private string getParentPath() => Path.GetFullPath(Path.Combine(root.GetFullPath(string.Empty), "..")); + + public class CurrentDirectoryDisplay : CompositeDrawable + { + public readonly Bindable CurrentDirectory = new Bindable(); + + public CurrentDirectoryDisplay() + { + FillFlowContainer flow; + + InternalChildren = new Drawable[] + { + flow = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + Spacing = new Vector2(10), + Height = 20, + Direction = FillDirection.Horizontal, + }, + }; + + CurrentDirectory.BindValueChanged(dir => + { + flow.Clear(); + + flow.Add(new OsuSpriteText { Text = "Current Directory: " }); + + var pieces = dir.NewValue.Split(Path.DirectorySeparatorChar); + + pieces[0] = "/"; + + for (int i = 0; i < pieces.Length; i++) + { + flow.Add(new DirectoryRow(pieces[i], Path.Combine(pieces.Take(i + 1).ToArray())) + { + CurrentDirectory = { BindTarget = CurrentDirectory }, + RelativeSizeAxes = Axes.Y, + Size = new Vector2(100, 1) + }); + } + }); + } + } + + private class ParentDirectoryRow : DirectoryRow + { + public override IconUsage Icon => FontAwesome.Solid.Folder; + + public ParentDirectoryRow(string fullPath) + : base("..", fullPath) + { + } + } + + private class DirectoryRow : OsuButton + { + private readonly string fullPath; + + public readonly Bindable CurrentDirectory = new Bindable(); + + public DirectoryRow(string display, string fullPath) + { + this.fullPath = fullPath; + + RelativeSizeAxes = Axes.X; + Height = 20; + + BackgroundColour = OsuColour.Gray(0.1f); + + AddRange(new Drawable[] + { + new SpriteIcon + { + Icon = Icon, + Size = new Vector2(20) + }, + new OsuSpriteText + { + X = 25, + Text = display, + Font = OsuFont.Default.With(size: 20) + } + }); + + Action = PerformDirectoryTraversal; + } + + protected virtual void PerformDirectoryTraversal() + { + CurrentDirectory.Value = fullPath; + } + + public virtual IconUsage Icon => FontAwesome.Regular.Folder; + } + } +} From 2c83417c41e29be9a5e474df65a7806dbf747577 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 May 2020 18:08:43 +0900 Subject: [PATCH 1545/2376] Switch to using DirectoryInfo/DriveInfo --- .../Settings/TestSceneDirectorySelector.cs | 122 +++++++++--------- 1 file changed, 64 insertions(+), 58 deletions(-) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneDirectorySelector.cs b/osu.Game.Tests/Visual/Settings/TestSceneDirectorySelector.cs index 416d978c69..beb5353115 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneDirectorySelector.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneDirectorySelector.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.IO; using System.Linq; using osu.Framework.Allocation; @@ -8,11 +9,11 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; using osu.Framework.Platform; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; using osuTK; namespace osu.Game.Tests.Visual.Settings @@ -28,18 +29,17 @@ namespace osu.Game.Tests.Visual.Settings public class DirectorySelector : CompositeDrawable { - private Storage root; private FillFlowContainer directoryFlow; - private CurrentDirectoryDisplay current; [Resolved] private GameHost host { get; set; } - private readonly Bindable currentDirectory = new Bindable(); + [Cached] + private readonly Bindable currentDirectory = new Bindable(); - public DirectorySelector(Storage root = null) + public DirectorySelector(string initialPath = null) { - this.root = root; + currentDirectory.Value = new DirectoryInfo(initialPath ??= Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)); } protected override void LoadComplete() @@ -48,9 +48,6 @@ namespace osu.Game.Tests.Visual.Settings Padding = new MarginPadding(10); - if (root == null) - root = host.GetStorage("/Users/"); - InternalChildren = new Drawable[] { new FillFlowContainer @@ -59,9 +56,8 @@ namespace osu.Game.Tests.Visual.Settings Direction = FillDirection.Vertical, Children = new Drawable[] { - current = new CurrentDirectoryDisplay + new CurrentDirectoryDisplay { - CurrentDirectory = { BindTarget = currentDirectory }, RelativeSizeAxes = Axes.X, Height = 50, }, @@ -79,29 +75,39 @@ namespace osu.Game.Tests.Visual.Settings }, }; - currentDirectory.BindValueChanged(updateDisplay); - currentDirectory.Value = root.GetFullPath(string.Empty); + currentDirectory.BindValueChanged(updateDisplay, true); } - private void updateDisplay(ValueChangedEvent directory) + private void updateDisplay(ValueChangedEvent directory) { - root = host.GetStorage(directory.NewValue); - directoryFlow.Clear(); - directoryFlow.Add(new ParentDirectoryRow(getParentPath()) { CurrentDirectory = { BindTarget = currentDirectory }, }); + if (directory.NewValue == null) + { + // var drives = DriveInfo.GetDrives(); + // + // foreach (var drive in drives) + // directoryFlow.Add(new DirectoryRow(drive.RootDirectory)); + } + else + { + directoryFlow.Add(new ParentDirectoryRow(currentDirectory.Value.Parent)); - foreach (var dir in root.GetDirectories(string.Empty)) - directoryFlow.Add(new DirectoryRow(dir, root.GetFullPath(dir)) { CurrentDirectory = { BindTarget = currentDirectory }, }); + foreach (var dir in currentDirectory.Value.GetDirectories().OrderBy(d => d.Name)) + { + if ((dir.Attributes & FileAttributes.Hidden) == 0) + directoryFlow.Add(new DirectoryRow(dir)); + } + } } - private string getParentPath() => Path.GetFullPath(Path.Combine(root.GetFullPath(string.Empty), "..")); - public class CurrentDirectoryDisplay : CompositeDrawable { - public readonly Bindable CurrentDirectory = new Bindable(); + [Resolved] + private Bindable currentDirectory { get; set; } - public CurrentDirectoryDisplay() + [BackgroundDependencyLoader] + private void load() { FillFlowContainer flow; @@ -118,26 +124,26 @@ namespace osu.Game.Tests.Visual.Settings }, }; - CurrentDirectory.BindValueChanged(dir => + currentDirectory.BindValueChanged(dir => { flow.Clear(); - flow.Add(new OsuSpriteText { Text = "Current Directory: " }); - - var pieces = dir.NewValue.Split(Path.DirectorySeparatorChar); - - pieces[0] = "/"; - - for (int i = 0; i < pieces.Length; i++) + flow.Add(new OsuSpriteText { - flow.Add(new DirectoryRow(pieces[i], Path.Combine(pieces.Take(i + 1).ToArray())) - { - CurrentDirectory = { BindTarget = CurrentDirectory }, - RelativeSizeAxes = Axes.Y, - Size = new Vector2(100, 1) - }); + Text = "Current Directory: ", + Font = OsuFont.Default.With(size: DirectoryRow.HEIGHT), + }); + + flow.Add(new DirectoryRow(null, "Computer")); + + DirectoryInfo traverse = dir.NewValue; + + while (traverse != null) + { + flow.Add(new DirectoryRow(traverse)); + traverse = traverse.Parent; } - }); + }, true); } } @@ -145,48 +151,48 @@ namespace osu.Game.Tests.Visual.Settings { public override IconUsage Icon => FontAwesome.Solid.Folder; - public ParentDirectoryRow(string fullPath) - : base("..", fullPath) + public ParentDirectoryRow(DirectoryInfo directory) + : base(directory, "..") { } } - private class DirectoryRow : OsuButton + private class DirectoryRow : CompositeDrawable { - private readonly string fullPath; + public const float HEIGHT = 20; - public readonly Bindable CurrentDirectory = new Bindable(); + private readonly DirectoryInfo directory; - public DirectoryRow(string display, string fullPath) + [Resolved] + private Bindable currentDirectory { get; set; } + + public DirectoryRow(DirectoryInfo directory, string display = null) { - this.fullPath = fullPath; + this.directory = directory; - RelativeSizeAxes = Axes.X; - Height = 20; + AutoSizeAxes = Axes.X; + Height = HEIGHT; - BackgroundColour = OsuColour.Gray(0.1f); - - AddRange(new Drawable[] + AddRangeInternal(new Drawable[] { new SpriteIcon { Icon = Icon, - Size = new Vector2(20) + Size = new Vector2(HEIGHT) }, new OsuSpriteText { - X = 25, - Text = display, - Font = OsuFont.Default.With(size: 20) + X = HEIGHT + 5, + Text = display ?? directory.Name, + Font = OsuFont.Default.With(size: HEIGHT) } }); - - Action = PerformDirectoryTraversal; } - protected virtual void PerformDirectoryTraversal() + protected override bool OnClick(ClickEvent e) { - CurrentDirectory.Value = fullPath; + currentDirectory.Value = directory; + return true; } public virtual IconUsage Icon => FontAwesome.Regular.Folder; From 49e4fc6cba39028b5be157b975a26ae85e2e2161 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 May 2020 18:37:14 +0900 Subject: [PATCH 1546/2376] Move to better namespace --- .../Settings/TestSceneDirectorySelector.cs | 184 +----------- .../UserInterfaceV2/DirectorySelector.cs | 261 ++++++++++++++++++ 2 files changed, 262 insertions(+), 183 deletions(-) create mode 100644 osu.Game/Graphics/UserInterfaceV2/DirectorySelector.cs diff --git a/osu.Game.Tests/Visual/Settings/TestSceneDirectorySelector.cs b/osu.Game.Tests/Visual/Settings/TestSceneDirectorySelector.cs index beb5353115..0cd0f13b5f 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneDirectorySelector.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneDirectorySelector.cs @@ -1,20 +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.IO; -using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Input.Events; using osu.Framework.Platform; -using osu.Game.Graphics; -using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Sprites; -using osuTK; +using osu.Game.Graphics.UserInterfaceV2; namespace osu.Game.Tests.Visual.Settings { @@ -26,176 +16,4 @@ namespace osu.Game.Tests.Visual.Settings Add(new DirectorySelector { RelativeSizeAxes = Axes.Both }); } } - - public class DirectorySelector : CompositeDrawable - { - private FillFlowContainer directoryFlow; - - [Resolved] - private GameHost host { get; set; } - - [Cached] - private readonly Bindable currentDirectory = new Bindable(); - - public DirectorySelector(string initialPath = null) - { - currentDirectory.Value = new DirectoryInfo(initialPath ??= Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - Padding = new MarginPadding(10); - - InternalChildren = new Drawable[] - { - new FillFlowContainer - { - RelativeSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - new CurrentDirectoryDisplay - { - RelativeSizeAxes = Axes.X, - Height = 50, - }, - new OsuScrollContainer - { - RelativeSizeAxes = Axes.Both, - Child = directoryFlow = new FillFlowContainer - { - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Direction = FillDirection.Vertical, - } - } - } - }, - }; - - currentDirectory.BindValueChanged(updateDisplay, true); - } - - private void updateDisplay(ValueChangedEvent directory) - { - directoryFlow.Clear(); - - if (directory.NewValue == null) - { - // var drives = DriveInfo.GetDrives(); - // - // foreach (var drive in drives) - // directoryFlow.Add(new DirectoryRow(drive.RootDirectory)); - } - else - { - directoryFlow.Add(new ParentDirectoryRow(currentDirectory.Value.Parent)); - - foreach (var dir in currentDirectory.Value.GetDirectories().OrderBy(d => d.Name)) - { - if ((dir.Attributes & FileAttributes.Hidden) == 0) - directoryFlow.Add(new DirectoryRow(dir)); - } - } - } - - public class CurrentDirectoryDisplay : CompositeDrawable - { - [Resolved] - private Bindable currentDirectory { get; set; } - - [BackgroundDependencyLoader] - private void load() - { - FillFlowContainer flow; - - InternalChildren = new Drawable[] - { - flow = new FillFlowContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.X, - Spacing = new Vector2(10), - Height = 20, - Direction = FillDirection.Horizontal, - }, - }; - - currentDirectory.BindValueChanged(dir => - { - flow.Clear(); - - flow.Add(new OsuSpriteText - { - Text = "Current Directory: ", - Font = OsuFont.Default.With(size: DirectoryRow.HEIGHT), - }); - - flow.Add(new DirectoryRow(null, "Computer")); - - DirectoryInfo traverse = dir.NewValue; - - while (traverse != null) - { - flow.Add(new DirectoryRow(traverse)); - traverse = traverse.Parent; - } - }, true); - } - } - - private class ParentDirectoryRow : DirectoryRow - { - public override IconUsage Icon => FontAwesome.Solid.Folder; - - public ParentDirectoryRow(DirectoryInfo directory) - : base(directory, "..") - { - } - } - - private class DirectoryRow : CompositeDrawable - { - public const float HEIGHT = 20; - - private readonly DirectoryInfo directory; - - [Resolved] - private Bindable currentDirectory { get; set; } - - public DirectoryRow(DirectoryInfo directory, string display = null) - { - this.directory = directory; - - AutoSizeAxes = Axes.X; - Height = HEIGHT; - - AddRangeInternal(new Drawable[] - { - new SpriteIcon - { - Icon = Icon, - Size = new Vector2(HEIGHT) - }, - new OsuSpriteText - { - X = HEIGHT + 5, - Text = display ?? directory.Name, - Font = OsuFont.Default.With(size: HEIGHT) - } - }); - } - - protected override bool OnClick(ClickEvent e) - { - currentDirectory.Value = directory; - return true; - } - - public virtual IconUsage Icon => FontAwesome.Regular.Folder; - } - } } diff --git a/osu.Game/Graphics/UserInterfaceV2/DirectorySelector.cs b/osu.Game/Graphics/UserInterfaceV2/DirectorySelector.cs new file mode 100644 index 0000000000..5dcaacf0a4 --- /dev/null +++ b/osu.Game/Graphics/UserInterfaceV2/DirectorySelector.cs @@ -0,0 +1,261 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; +using osu.Framework.Platform; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osuTK; + +namespace osu.Game.Graphics.UserInterfaceV2 +{ + public class DirectorySelector : CompositeDrawable + { + private FillFlowContainer directoryFlow; + + [Resolved] + private GameHost host { get; set; } + + [Cached] + private readonly Bindable currentDirectory = new Bindable(); + + public DirectorySelector(string initialPath = null) + { + currentDirectory.Value = new DirectoryInfo(initialPath ??= Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Padding = new MarginPadding(10); + + InternalChildren = new Drawable[] + { + new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new CurrentDirectoryDisplay + { + RelativeSizeAxes = Axes.X, + Height = 50, + }, + new OsuScrollContainer + { + RelativeSizeAxes = Axes.Both, + Child = directoryFlow = new FillFlowContainer + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Direction = FillDirection.Vertical, + Spacing = new Vector2(2), + } + } + } + }, + }; + + currentDirectory.BindValueChanged(updateDisplay, true); + } + + private void updateDisplay(ValueChangedEvent directory) + { + directoryFlow.Clear(); + + if (directory.NewValue == null) + { + var drives = DriveInfo.GetDrives(); + + foreach (var drive in drives) + directoryFlow.Add(new DirectoryRow(drive.RootDirectory)); + } + else + { + directoryFlow.Add(new ParentDirectoryRow(currentDirectory.Value.Parent)); + + foreach (var dir in currentDirectory.Value.GetDirectories().OrderBy(d => d.Name)) + { + if ((dir.Attributes & FileAttributes.Hidden) == 0) + directoryFlow.Add(new DirectoryRow(dir)); + } + } + } + + public class CurrentDirectoryDisplay : CompositeDrawable + { + [Resolved] + private Bindable currentDirectory { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + FillFlowContainer flow; + + InternalChildren = new Drawable[] + { + flow = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + Spacing = new Vector2(5), + Height = DirectoryRow.HEIGHT, + Direction = FillDirection.Horizontal, + }, + }; + + currentDirectory.BindValueChanged(dir => + { + flow.Clear(); + + flow.Add(new OsuSpriteText + { + Text = "Current Directory: ", + Font = OsuFont.Default.With(size: DirectoryRow.HEIGHT), + }); + + flow.Add(new ComputerRow()); + + List traversalRows = new List(); + + DirectoryInfo traverse = dir.NewValue; + + while (traverse != null) + { + traversalRows.Insert(0, new CurrentDisplayRow(traverse)); + traverse = traverse.Parent; + } + + flow.AddRange(traversalRows); + }, true); + } + + private class ComputerRow : CurrentDisplayRow + { + public override IconUsage? Icon => null; + + public ComputerRow() + : base(null, "Computer") + { + } + } + + private class CurrentDisplayRow : DirectoryRow + { + public CurrentDisplayRow(DirectoryInfo directory, string displayName = null) + : base(directory, displayName) + { + } + + [BackgroundDependencyLoader] + private void load() + { + Flow.Add(new SpriteIcon + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Icon = FontAwesome.Solid.ChevronRight, + Size = new Vector2(FONT_SIZE / 2) + }); + } + } + } + + private class ParentDirectoryRow : DirectoryRow + { + public override IconUsage? Icon => FontAwesome.Solid.Folder; + + public ParentDirectoryRow(DirectoryInfo directory) + : base(directory, "..") + { + } + } + + private class DirectoryRow : CompositeDrawable + { + public const float HEIGHT = 20; + + protected const float FONT_SIZE = 16; + + private readonly DirectoryInfo directory; + private readonly string displayName; + + protected FillFlowContainer Flow; + + [Resolved] + private Bindable currentDirectory { get; set; } + + public DirectoryRow(DirectoryInfo directory, string displayName = null) + { + this.directory = directory; + this.displayName = displayName; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + AutoSizeAxes = Axes.Both; + + Masking = true; + CornerRadius = 5; + + InternalChildren = new Drawable[] + { + new Box + { + Colour = colours.GreySeafoamDarker, + RelativeSizeAxes = Axes.Both, + }, + Flow = new FillFlowContainer + { + AutoSizeAxes = Axes.X, + Height = 20, + Margin = new MarginPadding { Vertical = 2, Horizontal = 5 }, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5), + } + }; + + if (Icon.HasValue) + { + Flow.Add(new SpriteIcon + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Icon = Icon.Value, + Size = new Vector2(FONT_SIZE) + }); + } + + Flow.Add(new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Text = displayName ?? directory.Name, + Font = OsuFont.Default.With(size: FONT_SIZE) + }); + } + + protected override bool OnClick(ClickEvent e) + { + currentDirectory.Value = directory; + return true; + } + + public virtual IconUsage? Icon => FontAwesome.Regular.Folder; + } + } +} From 246812e0b1b550c0bebb63739ad67a4b45f0f124 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 May 2020 18:42:20 +0900 Subject: [PATCH 1547/2376] Change breadcrumb display icons to match design --- .../UserInterfaceV2/DirectorySelector.cs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/DirectorySelector.cs b/osu.Game/Graphics/UserInterfaceV2/DirectorySelector.cs index 5dcaacf0a4..1cd4ab6f01 100644 --- a/osu.Game/Graphics/UserInterfaceV2/DirectorySelector.cs +++ b/osu.Game/Graphics/UserInterfaceV2/DirectorySelector.cs @@ -145,7 +145,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 private class ComputerRow : CurrentDisplayRow { - public override IconUsage? Icon => null; + protected override IconUsage? Icon => null; public ComputerRow() : base(null, "Computer") @@ -171,12 +171,14 @@ namespace osu.Game.Graphics.UserInterfaceV2 Size = new Vector2(FONT_SIZE / 2) }); } + + protected override IconUsage? Icon => Directory.Name.Contains("/") ? base.Icon : null; } } private class ParentDirectoryRow : DirectoryRow { - public override IconUsage? Icon => FontAwesome.Solid.Folder; + protected override IconUsage? Icon => FontAwesome.Solid.Folder; public ParentDirectoryRow(DirectoryInfo directory) : base(directory, "..") @@ -190,7 +192,8 @@ namespace osu.Game.Graphics.UserInterfaceV2 protected const float FONT_SIZE = 16; - private readonly DirectoryInfo directory; + protected readonly DirectoryInfo Directory; + private readonly string displayName; protected FillFlowContainer Flow; @@ -200,7 +203,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 public DirectoryRow(DirectoryInfo directory, string displayName = null) { - this.directory = directory; + Directory = directory; this.displayName = displayName; } @@ -244,18 +247,18 @@ namespace osu.Game.Graphics.UserInterfaceV2 { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Text = displayName ?? directory.Name, + Text = displayName ?? Directory.Name, Font = OsuFont.Default.With(size: FONT_SIZE) }); } protected override bool OnClick(ClickEvent e) { - currentDirectory.Value = directory; + currentDirectory.Value = Directory; return true; } - public virtual IconUsage? Icon => FontAwesome.Regular.Folder; + protected virtual IconUsage? Icon => Directory.Name.Contains("/") ? FontAwesome.Solid.Database : FontAwesome.Regular.Folder; } } } From e9e03d038d780f13d62a3b29e9c6243148f10ebc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 May 2020 18:52:10 +0900 Subject: [PATCH 1548/2376] Tidy up naming and structure --- .../UserInterfaceV2/DirectorySelector.cs | 87 +++++++++---------- 1 file changed, 43 insertions(+), 44 deletions(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/DirectorySelector.cs b/osu.Game/Graphics/UserInterfaceV2/DirectorySelector.cs index 1cd4ab6f01..427b9a1eff 100644 --- a/osu.Game/Graphics/UserInterfaceV2/DirectorySelector.cs +++ b/osu.Game/Graphics/UserInterfaceV2/DirectorySelector.cs @@ -34,10 +34,9 @@ namespace osu.Game.Graphics.UserInterfaceV2 currentDirectory.Value = new DirectoryInfo(initialPath ??= Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)); } - protected override void LoadComplete() + [BackgroundDependencyLoader] + private void load() { - base.LoadComplete(); - Padding = new MarginPadding(10); InternalChildren = new Drawable[] @@ -80,30 +79,30 @@ namespace osu.Game.Graphics.UserInterfaceV2 var drives = DriveInfo.GetDrives(); foreach (var drive in drives) - directoryFlow.Add(new DirectoryRow(drive.RootDirectory)); + directoryFlow.Add(new DirectoryPiece(drive.RootDirectory)); } else { - directoryFlow.Add(new ParentDirectoryRow(currentDirectory.Value.Parent)); + directoryFlow.Add(new ParentDirectoryPiece(currentDirectory.Value.Parent)); foreach (var dir in currentDirectory.Value.GetDirectories().OrderBy(d => d.Name)) { if ((dir.Attributes & FileAttributes.Hidden) == 0) - directoryFlow.Add(new DirectoryRow(dir)); + directoryFlow.Add(new DirectoryPiece(dir)); } } } - public class CurrentDirectoryDisplay : CompositeDrawable + private class CurrentDirectoryDisplay : CompositeDrawable { [Resolved] private Bindable currentDirectory { get; set; } + private FillFlowContainer flow; + [BackgroundDependencyLoader] private void load() { - FillFlowContainer flow; - InternalChildren = new Drawable[] { flow = new FillFlowContainer @@ -112,50 +111,48 @@ namespace osu.Game.Graphics.UserInterfaceV2 Origin = Anchor.Centre, RelativeSizeAxes = Axes.X, Spacing = new Vector2(5), - Height = DirectoryRow.HEIGHT, + Height = DirectoryPiece.HEIGHT, Direction = FillDirection.Horizontal, }, }; - currentDirectory.BindValueChanged(dir => - { - flow.Clear(); - - flow.Add(new OsuSpriteText - { - Text = "Current Directory: ", - Font = OsuFont.Default.With(size: DirectoryRow.HEIGHT), - }); - - flow.Add(new ComputerRow()); - - List traversalRows = new List(); - - DirectoryInfo traverse = dir.NewValue; - - while (traverse != null) - { - traversalRows.Insert(0, new CurrentDisplayRow(traverse)); - traverse = traverse.Parent; - } - - flow.AddRange(traversalRows); - }, true); + currentDirectory.BindValueChanged(updateDisplay, true); } - private class ComputerRow : CurrentDisplayRow + private void updateDisplay(ValueChangedEvent dir) + { + flow.Clear(); + + List pathPieces = new List(); + + DirectoryInfo ptr = dir.NewValue; + + while (ptr != null) + { + pathPieces.Insert(0, new CurrentDisplayPiece(ptr)); + ptr = ptr.Parent; + } + + flow.ChildrenEnumerable = new Drawable[] + { + new OsuSpriteText { Text = "Current Directory: ", Font = OsuFont.Default.With(size: DirectoryPiece.HEIGHT), }, + new ComputerPiece(), + }.Concat(pathPieces); + } + + private class ComputerPiece : CurrentDisplayPiece { protected override IconUsage? Icon => null; - public ComputerRow() + public ComputerPiece() : base(null, "Computer") { } } - private class CurrentDisplayRow : DirectoryRow + private class CurrentDisplayPiece : DirectoryPiece { - public CurrentDisplayRow(DirectoryInfo directory, string displayName = null) + public CurrentDisplayPiece(DirectoryInfo directory, string displayName = null) : base(directory, displayName) { } @@ -172,21 +169,21 @@ namespace osu.Game.Graphics.UserInterfaceV2 }); } - protected override IconUsage? Icon => Directory.Name.Contains("/") ? base.Icon : null; + protected override IconUsage? Icon => Directory.Name.Contains(Path.DirectorySeparatorChar) ? base.Icon : null; } } - private class ParentDirectoryRow : DirectoryRow + private class ParentDirectoryPiece : DirectoryPiece { protected override IconUsage? Icon => FontAwesome.Solid.Folder; - public ParentDirectoryRow(DirectoryInfo directory) + public ParentDirectoryPiece(DirectoryInfo directory) : base(directory, "..") { } } - private class DirectoryRow : CompositeDrawable + private class DirectoryPiece : CompositeDrawable { public const float HEIGHT = 20; @@ -201,7 +198,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 [Resolved] private Bindable currentDirectory { get; set; } - public DirectoryRow(DirectoryInfo directory, string displayName = null) + public DirectoryPiece(DirectoryInfo directory, string displayName = null) { Directory = directory; this.displayName = displayName; @@ -258,7 +255,9 @@ namespace osu.Game.Graphics.UserInterfaceV2 return true; } - protected virtual IconUsage? Icon => Directory.Name.Contains("/") ? FontAwesome.Solid.Database : FontAwesome.Regular.Folder; + protected virtual IconUsage? Icon => Directory.Name.Contains(Path.DirectorySeparatorChar) + ? FontAwesome.Solid.Database + : FontAwesome.Regular.Folder; } } } From c048d9b6ae215eb0923ba39d060e5ae1adb0ace7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 May 2020 18:55:06 +0900 Subject: [PATCH 1549/2376] Fix incorrect assignment --- osu.Game/Graphics/UserInterfaceV2/DirectorySelector.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/DirectorySelector.cs b/osu.Game/Graphics/UserInterfaceV2/DirectorySelector.cs index 427b9a1eff..59de931df5 100644 --- a/osu.Game/Graphics/UserInterfaceV2/DirectorySelector.cs +++ b/osu.Game/Graphics/UserInterfaceV2/DirectorySelector.cs @@ -31,7 +31,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 public DirectorySelector(string initialPath = null) { - currentDirectory.Value = new DirectoryInfo(initialPath ??= Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)); + currentDirectory.Value = new DirectoryInfo(initialPath ?? Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)); } [BackgroundDependencyLoader] From 00efeb7cc6db07eac414dab21366b041fa46f6ba Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 May 2020 19:19:58 +0900 Subject: [PATCH 1550/2376] Fix spawning too many sprites due to not yet populated sizing --- .../Skinning/LegacyTaikoScroller.cs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs index 3ec6be8a6c..03813e0a99 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs @@ -17,6 +17,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning { public class LegacyTaikoScroller : CompositeDrawable { + public Bindable LastResult = new Bindable(); + public LegacyTaikoScroller() { RelativeSizeAxes = Axes.Both; @@ -50,21 +52,16 @@ namespace osu.Game.Rulesets.Taiko.Skinning }, true); } - public Bindable LastResult = new Bindable(); - protected override void Update() { base.Update(); - bool wideEnough() => - InternalChildren.Any() - && InternalChildren.First().ScreenSpaceDrawQuad.Width * InternalChildren.Count >= ScreenSpaceDrawQuad.Width * 2; - // store X before checking wide enough so if we perform layout there is no positional discrepancy. float currentX = (InternalChildren?.FirstOrDefault()?.X ?? 0) - (float)Clock.ElapsedFrameTime * 0.1f; // ensure we have enough sprites - while (!wideEnough()) + if (!InternalChildren.Any() + || InternalChildren.First().ScreenSpaceDrawQuad.Width * InternalChildren.Count < ScreenSpaceDrawQuad.Width * 2) AddInternal(new ScrollerSprite { Passing = passing }); var first = InternalChildren.First(); From 49e616b7e561ab940a2fa10f294af179f2b39e91 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 May 2020 20:19:14 +0900 Subject: [PATCH 1551/2376] Also check for directory presence before migrating --- osu.Game/IO/OsuStorage.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index 6dc25e871c..b060add03b 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.cs @@ -51,7 +51,7 @@ namespace osu.Game.IO // ensure the new location has no files present, else hard abort if (destination.Exists) { - if (destination.GetFiles().Length > 0) + if (destination.GetFiles().Length > 0 || destination.GetDirectories().Length > 0) throw new InvalidOperationException("Migration destination already has files present"); deleteRecursive(destination); From ad1d050fb437673f35380f8751c7a3f7cc68c7e9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 May 2020 20:29:15 +0900 Subject: [PATCH 1552/2376] Throw exception on copy timeout --- osu.Game/IO/OsuStorage.cs | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index b060add03b..71b01ce479 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.cs @@ -96,20 +96,7 @@ namespace osu.Game.IO if (topLevelExcludes && IGNORE_FILES.Contains(fi.Name)) continue; - int tries = 5; - - while (tries-- > 0) - { - try - { - fi.CopyTo(Path.Combine(destination.FullName, fi.Name), true); - break; - } - catch (Exception) - { - Thread.Sleep(50); - } - } + attemptCopy(fi, Path.Combine(destination.FullName, fi.Name)); } foreach (DirectoryInfo dir in source.GetDirectories()) @@ -120,5 +107,26 @@ namespace osu.Game.IO copyRecursive(dir, destination.CreateSubdirectory(dir.Name), false); } } + + private static void attemptCopy(System.IO.FileInfo fileInfo, string destination) + { + int tries = 5; + + while (true) + { + try + { + fileInfo.CopyTo(destination, true); + return; + } + catch (Exception) + { + if (tries-- == 0) + throw; + } + + Thread.Sleep(50); + } + } } } From 1ac9c7c15a88fe24132084b93c2d619cfb46b2da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=80=8C=E7=A9=BA=E7=99=BD=E3=80=8D?= <「空白」> Date: Thu, 14 May 2020 00:04:31 +0900 Subject: [PATCH 1553/2376] Add license header to WebRequestExtensions --- osu.Game/Extensions/WebRequestExtensions.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Extensions/WebRequestExtensions.cs b/osu.Game/Extensions/WebRequestExtensions.cs index c8e3c564a5..f92f707d30 100644 --- a/osu.Game/Extensions/WebRequestExtensions.cs +++ b/osu.Game/Extensions/WebRequestExtensions.cs @@ -1,4 +1,7 @@ -using osu.Framework.IO.Network; +// 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.IO.Network; using osu.Framework.Extensions.IEnumerableExtensions; using System.Collections.Generic; using Newtonsoft.Json; From 0933217389f9b08c4d64815b31fa352cd8a4ce16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 13 May 2020 18:53:47 +0200 Subject: [PATCH 1554/2376] Simplify mascot scaling --- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 5940ee8b69..ded1fc0933 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -5,7 +5,6 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Layout; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; @@ -43,14 +42,9 @@ namespace osu.Game.Rulesets.Taiko.UI private Container hitTargetOffsetContent; - private readonly LayoutValue playfieldScaleLayout = new LayoutValue(Invalidation.DrawSize); - private float playfieldScale => playfieldScaleLayout.IsValid ? playfieldScaleLayout.Value : playfieldScaleLayout.Value = DrawHeight / DEFAULT_HEIGHT; - public TaikoPlayfield(ControlPointInfo controlPoints) { this.controlPoints = controlPoints; - - AddLayout(playfieldScaleLayout); } [BackgroundDependencyLoader] @@ -159,7 +153,7 @@ namespace osu.Game.Rulesets.Taiko.UI rightArea.Padding = new MarginPadding { Left = leftArea.DrawWidth }; hitTargetOffsetContent.Padding = new MarginPadding { Left = HitTarget.DrawWidth / 2 }; - mascot.Scale = new Vector2(playfieldScale); + mascot.Scale = new Vector2(DrawHeight / DEFAULT_HEIGHT); } public override void Add(DrawableHitObject h) From 43450b54853712698da7909e31568a5ecd6b4566 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=80=8C=E7=A9=BA=E7=99=BD=E3=80=8D?= <「空白」> Date: Thu, 14 May 2020 01:57:03 +0900 Subject: [PATCH 1555/2376] Resolve remaining InspectCode issues > CI should now pass build test --- osu.Game/Extensions/WebRequestExtensions.cs | 2 ++ .../Online/API/Requests/SearchBeatmapSetsRequest.cs | 13 ++++++------- .../Overlays/BeatmapListing/BeatmapListingPager.cs | 4 ++-- osu.Game/Overlays/BeatmapListingOverlay.cs | 3 +-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game/Extensions/WebRequestExtensions.cs b/osu.Game/Extensions/WebRequestExtensions.cs index f92f707d30..80c8b147bf 100644 --- a/osu.Game/Extensions/WebRequestExtensions.cs +++ b/osu.Game/Extensions/WebRequestExtensions.cs @@ -6,11 +6,13 @@ using osu.Framework.Extensions.IEnumerableExtensions; using System.Collections.Generic; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using JetBrains.Annotations; namespace osu.Game.Extensions { public class Cursor { + [UsedImplicitly] [JsonExtensionData] public IDictionary Properties; } diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs index a49cb70c37..0c3272c7de 100644 --- a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs +++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs @@ -11,15 +11,15 @@ namespace osu.Game.Online.API.Requests { public class SearchBeatmapSetsRequest : APIRequest { - public SearchCategory SearchCategory { get; set; } + public SearchCategory SearchCategory { get; } - public SortCriteria SortCriteria { get; set; } + public SortCriteria SortCriteria { get; } - public SortDirection SortDirection { get; set; } + public SortDirection SortDirection { get; } - public SearchGenre Genre { get; set; } + public SearchGenre Genre { get; } - public SearchLanguage Language { get; set; } + public SearchLanguage Language { get; } private readonly string query; private readonly RulesetInfo ruleset; @@ -27,8 +27,7 @@ namespace osu.Game.Online.API.Requests private string directionString => SortDirection == SortDirection.Descending ? @"desc" : @"asc"; - public SearchBeatmapSetsRequest(string query, RulesetInfo ruleset, Cursor cursor = null, - SearchCategory searchCategory = SearchCategory.Any, SortCriteria sortCriteria = SortCriteria.Ranked, SortDirection sortDirection = SortDirection.Descending) + public SearchBeatmapSetsRequest(string query, RulesetInfo ruleset, Cursor cursor = null, SearchCategory searchCategory = SearchCategory.Any, SortCriteria sortCriteria = SortCriteria.Ranked, SortDirection sortDirection = SortDirection.Descending) { this.query = string.IsNullOrEmpty(query) ? string.Empty : System.Uri.EscapeDataString(query); this.ruleset = ruleset; diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingPager.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingPager.cs index 4c8902d314..f55e37ebc7 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingPager.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingPager.cs @@ -24,9 +24,9 @@ namespace osu.Game.Overlays.BeatmapListing private SearchBeatmapSetsRequest getSetsRequest; private SearchBeatmapSetsResponse lastResponse; - private bool isLastPageFetched = false; + private bool isLastPageFetched; private bool isFetching => getSetsRequest != null; - public bool IsPastFirstPage { get; private set; } = false; + public bool IsPastFirstPage { get; private set; } public bool CanFetchNextPage => !isLastPageFetched && !isFetching; public BeatmapListingPager(IAPIProvider api, RulesetStore rulesets, string query, RulesetInfo ruleset, SearchCategory searchCategory = SearchCategory.Any, SortCriteria sortCriteria = SortCriteria.Ranked, SortDirection sortDirection = SortDirection.Descending) diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index ba92181ac5..e26f084ea4 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -184,8 +184,7 @@ namespace osu.Game.Overlays // If the auto-size computation is delayed until fade out completes, the background remain high for too long making the resulting transition to the smaller height look weird. // At the same time, if the last content's height is bypassed immediately, there is a period where the new content is at Alpha = 0 when the auto-sized height will be 0. // To resolve both of these issues, the bypass is delayed until a point when the content transitions (fade-in and fade-out) overlap and it looks good to do so. - lastContent.Delay(25).Schedule(() => lastContent.BypassAutoSizeAxes = Axes.Y) - .Then().Schedule(() => panelTarget.Remove(lastContent)); + lastContent.Delay(25).Schedule(() => lastContent.BypassAutoSizeAxes = Axes.Y).Then().Schedule(() => panelTarget.Remove(lastContent)); } if (!content.IsAlive) From 9ba1a8af885b2334822506ee5d789ca19fff9902 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 May 2020 09:44:21 +0900 Subject: [PATCH 1556/2376] Fix mascot getting stuck in clear state on rewind --- osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs | 6 +++--- osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs index 105baa84cc..407ab30e12 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs @@ -96,9 +96,9 @@ namespace osu.Game.Rulesets.Taiko.UI private TaikoMascotAnimationState getNextState() { - // don't change state if current animation is playing - // (used for clear state - others are manually animated on new beats) - if (currentAnimation != null && !currentAnimation.Completed) + // don't change state if current animation is still playing (and we haven't rewound before it). + // used for clear state - others are manually animated on new beats. + if (currentAnimation?.Completed == false && currentAnimation.DisplayTime <= Time.Current) return State.Value; if (!lastObjectHit) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs b/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs index 01cf88a87e..cce2be7758 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs @@ -20,6 +20,8 @@ namespace osu.Game.Rulesets.Taiko.UI private int currentFrame; + public double DisplayTime; + public TaikoMascotAnimation(TaikoMascotAnimationState state) { InternalChild = textureAnimation = createTextureAnimation(state).With(animation => @@ -40,6 +42,7 @@ namespace osu.Game.Rulesets.Taiko.UI public override void Show() { base.Show(); + DisplayTime = Time.Current; textureAnimation.Seek(0); } From 76af6f25f1ccd0e0f2e6697ed7fdc8d5546ebc15 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 May 2020 09:56:30 +0900 Subject: [PATCH 1557/2376] Remove pointless test resources --- .../Resources/old-skin/pippidonclear0.png | Bin 72589 -> 0 bytes .../Resources/old-skin/pippidonclear1.png | Bin 40613 -> 0 bytes .../Resources/old-skin/pippidonclear2.png | Bin 73308 -> 0 bytes .../Resources/old-skin/pippidonclear3.png | Bin 34541 -> 0 bytes .../Resources/old-skin/pippidonclear4.png | Bin 71177 -> 0 bytes .../Resources/old-skin/pippidonclear5.png | Bin 77056 -> 0 bytes .../Resources/old-skin/pippidonclear6.png | Bin 78392 -> 0 bytes .../Resources/old-skin/pippidonclear7.png | Bin 77056 -> 0 bytes .../Resources/old-skin/pippidonclear8.png | Bin 71177 -> 0 bytes .../Resources/old-skin/pippidonfail0.png | Bin 67970 -> 0 bytes .../Resources/old-skin/pippidonfail1.png | Bin 69118 -> 0 bytes .../Resources/old-skin/pippidonfail2.png | Bin 73351 -> 0 bytes .../Resources/old-skin/pippidonidle0.png | Bin 68649 -> 0 bytes .../Resources/old-skin/pippidonidle1.png | Bin 69329 -> 0 bytes .../Resources/old-skin/pippidonkiai0.png | Bin 76964 -> 0 bytes .../Resources/old-skin/pippidonkiai1.png | Bin 75434 -> 0 bytes 16 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonclear0.png delete mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonclear1.png delete mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonclear2.png delete mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonclear3.png delete mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonclear4.png delete mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonclear5.png delete mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonclear6.png delete mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonclear7.png delete mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonclear8.png delete mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonfail0.png delete mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonfail1.png delete mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonfail2.png delete mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonidle0.png delete mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonidle1.png delete mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonkiai0.png delete mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonkiai1.png diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonclear0.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonclear0.png deleted file mode 100644 index a5f4d03e2a61075513c85f24ab39280c31f591d9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 72589 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�F*Q&jpbtTl0ro%YGC`VnPkLo4Qt@L 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 diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonclear1.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonclear1.png deleted file mode 100644 index b239cc561c5cfccc67f04badfa8ab1b54ef6852f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40613 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�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 diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonclear3.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonclear3.png deleted file mode 100644 index 31e0b91e9fdcb82988b6b8d7b2505a38fdf70358..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34541 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%0Dr63wml-6F*tONlh7MSGocvsq)+w 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 L&GqVZUE=-^HyYM< diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonclear5.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonclear5.png deleted file mode 100644 index 417af6c61166c727b9ab50cdd14a498c68744e0d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 77056 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 NziqdsJ&+yE=yzftZ#5;QXZo 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 S-AwW`#1E{fu22Os|L{NMEB7e? diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonclear6.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonclear6.png deleted file mode 100644 index 5cec7a105d569aa2f9d4b37f709496b29eb96e2a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 78392 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

    3s~@flweDA35(#PV^FB#0ww8-D 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

    qC|Je@A{CPaWgAH#&us)?C)p`xW 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Ɛ$)7cJ87~{#3|_#o2xOh1UuK?bBmXyMpDjq6-3zT?WMe9&2jV@xa8^EsbHy zdl6^hx6-&8qE#u`q61vqt0>R4Nq)Go$B==U$1p6l?&G`I;1#EU-je=&|r1vn!!= zlQ^hUL$gip`cl;=5t2h_b$e~f;F`DIJ#6b?6Ex}614?7*)Kn!QEfAE3`&)1p=`63+NQ(%@}w+G>B=SG&&j-GaPj_uW zc!)kTO+Y(G?}Q~0n9aZmdhA@QGw}=NMl57lA}*@uVvJ>_itfdo_;@5G8i!#XNv)O@ z(5|@f>+SS)7-EE>WnzG}2)t`rF4n=ZEPQKoh32f`S6DdnOT@=)Ug|21A8gOhqQh9C-Je`YTQom73jy~Za|ME4< zE$U%&ndMgHOjR~hl5WHCUj8l~bw6#M>;tzV`htqKMoiLqT>i(fmdj%dCkH;+x+5Ij zYAV}t(I;_b1MQ%7;TTWndUcH#D{-Ni&<&*LqGm`aBqiKN zz%k1%HkOp|ZTB>M%bGG!&Yd=u!p|+?3H*9`TwRO8yQXP1y@$acx9;1ONQl#Cy>XIS zyY|DZ8n@?XdA1k`?-HgSe^BHRoIIPg$Hx#zjr#S?#$A$d^G@ERo?1#wi{9u~qpCP| zR+#>5WinD@@W;Ak2s18i_xRFwd|&vMsG;9TPr<57SCkAZYva|h1&Vs=u_t{eAc~&&&+a9j&CNJ#Dy{wm*&TFc*9D)+1_46%kjWc(TV9O6# zFNa*%saU}tT`QO>Uj#<@iJrLit$vmEjZwvzj+KsouHVAlpu8E_I=A-*R$jPZ%=m0s zzBcOiX{(=gFFY1&EbK=CTQ%cPO!?p|Y&pFViHVsKZ1{&+`k2C^E?vx_02&v*X1zH6 z{|VU@7fvKm`bcSMsW=vF*`PitSsbWSrXgHC^jTb-t$+MC>(tFk(V09KFHa}9SukXB z{!%KgMViE|O&KpZ=25?-wvP{--AxM|`6(r8vZi9)QYhkX8g~hdEtncl=EOJZguyk- z3azx#h^iIgVrvV2`&o1x@~C@lBXCf=x=&f7SF6+B#l5S>exKD~#G#jODcITT=c(O` zx`DmlT0c9DV=Z$(n1{IN!^$>8egnJrR~GlO)~x%uSRLjLn}PQ?Y{Z!hYmt z2^aA@Di$@3UdALB-1_?Fcs3c1@|8@hCE5~Kf)3%Am%mnesgE;7E7ydJo9NP$j70cf zHrBwhDn2dyqKGjxb_GYzAF^mYr|htQ?FIaEmCK;(ZFoc9`e;9)yRxvKHD=wP+2x0+ z1190qJ$n&wa|3i{olyZh%bdr;+-G&7B$bE4Lfa>WC5L2n>UfLt;H!iBR03mkHzp$uN8(>^;YV)*&3_` zH|M|s?D<2V0rXv1!1oWC zVcjr#EHV^k1uPX8rXy2aSYq1fYQ)F*A@HL9qOgP_!p*MTX#k3qY;M?-j?L#!<$C#J zJB{fo)alaUkcGI!ieGXw)Ho*;B|SXhSgomH%lc%LsH%vbN@_M%nDlXXHD;SBi(*~Z zugF)>q;+`}HLK|7Ti6=W;d}AXH;WM!#>@8cq+%U!6lcD$fq>wEJQ;1~a^?M-v@lkk zhzQ+_l}nZ($x4TI<;>;S=Wi0L_SHI|&(zL_t(+4Wpv5NxX5h77mcsA)-{RiUM_9Be z#xG90#uQ^_5Kx)fYg2`V9?Rn$Tg?Y7aXNXB&BcXEuXr!1sS!979Bgq2f<2M8BU++W zIkhJw&SvCEc<##G`V{72u@qs%b-XklL0AM36l*G$RMts&Qf8lFZ8AKIXA&0=m#h;G zR9wY-9h^*4k&}b+b|pefNh(~VWu!-9+fjY`VNS6#`x!b-1`^^Q?HOprO&FDA z5|?sCO_?`&LcnRHn@w*n>8`KYWC`x#yE!$i+{ke%YO;mq7K??VDH=%8s|)*ZyGHQ z+;biaJr+4h(#lvWF0|Gh)aMCc-na2G4h^}5lPfJBci{kC(-)sbrLy`-VB}JWAKWhJ zGevTOz1h!56?a(3HC`_dM<;$ws`8WcR48VpDZdZFF6=Ur&Eg0?o|bpbJW7^=laon| zq7p4jpvVtc)UsilVes;{&B6VoxHFi!W(~G1+H2VJ_(0?AVm2tDVHYcxV-$~~(R2OL zsVbMw(EpKBxA$S;OY@XWubi;sw^R7#kd$x@!8eUFTrHwr19uFqTFyB8SDSDF0S8tp&vSyilbhk0aIX>*5q|5@ zX_@Td*|F%Efs{Nhnv@zBBt%$$j5qb$sv4@L`DYrVFt0>tJ@r^oSO#SN6 zI9UFh@{{O@J$QBY_c*f6`Xy2d5?;ePv&*(*ky1+7Ij>+k@UZby;Zd}uvdyUkUu{~2 zgFCXHU=k9Vjv2GQRm7h-xzxq1d0)cYv?992?Emu^2DO`jsVkP^>h-nad+9y<3^>8D z=tQ`b`VAUKeQmNdoqpjl1FE+||BD_AZ!9S+R>abN0uMY-08a+QWN0J1D~9 zf{esLT&T^SeI6yf)lwoE*x~P=<0X)bxtPQyDKQc;fsanvRcaNbnSOe3+@r&2xgsMS zTkZrab1;ZU(S~S0Vu3y3^3b~Ff}nd_FlE|@ zICE0pOMVhlL2J;#bI)9lMM!>X17iU=(h8$${_ zkaC>G%2+BcJn%dT9CzVh1zkzS%5i|qFCr2rcCv7&<+SNl8GTsO!SEF#!;eGruVpjW z@5kG$lASgw1WDnKt^ra^+&uPX-Mo;PysH_KeryfS2Qjp(r*5f=sMEnz?Kn%s_jGqR z-5^PM6ej1J+dpratHYyddW)n`TmMafBxE#}D z)nHCO`grT(4^ir9wdx=uoTLe{pK0fJv&`vt$B+9eDJ?Yah? zYxcs6UyROj`hd#a^8t9j@7w4y{5AY~@;K)+=}Q237IOm z?!u)Um6i8lSIYq1;;4v6Pf{!@dSy))g)ys|O5 zJtKqBbdwO8i=r(N(d9)vTc@JUhR2xYbokm|m^^ke_G}I;^n?6CR|14VHwdN18C9xP zM%4wdLQ&e=#_9HF6T=ztYwWWr@RoK@v}cevuZ+M4Cdp=^}q1!Q!gPZ^5GEx zj?Jw2`Vc<-XDw0^^=tB%Ez=FJ&UzoEEXbz`x^@q<2EL0*U6MwI|Cmh*OCFI^aiQnJS*^<1O-i^9|J#?8NlV$Gn|(PjEZrJc=DyZP>D$vL(&0&U9S5`{?i*!(x?7n97z1LT2?0Yz>Fr)6t}s?S{vch3fp*VbYY>ux;6eY+tXo z#Px%xh4-Q_y6E8O3y;#?O2y>7hM~77>O150*WZLsnWyzTg%`5(@+u7M@FH$p{cpkv zo$X&f_y^Nwzl*4FeeE~*qRsGD-`;4^{?SX3Uq2s#@txj6^N!Esv+X}3=hqOzyyq2u{a&IP{v@S7$*;{m)N`J%`9sYn10! zBUOjBTYpAmh(6VlUdyPuRZ+iDHry70E-f9gv8FmmUhdgBW%&!kW1U;|ftR=KhR0|} z0)jVV!i*VM{@ErZXE7(r7NHmL_qGGdpgM7OErpV-e34IZ%;xD~wJ__&3GnvrqU>V_ z>iCVnFlfL;EFLo(CyqoRT69BC9*f6^gFeFR-+zF_czp$O3d^LX_0fC!2<2Y9AI?qs zd(jcJt2Y77J9NXJM;9U{YM&xvDqQ~9!F{msR{j>=%4o6Y(f370hN*)lpdDl5*{5C0 zez-)ECE2l5RCk^XW>=`8F=IF7X4q{US&{wuDo?-A0hP>3(jO1telCHH}r#+qnfbAfTd z-LrTb3~W^aPfebV;`(}+3KUHL+d8~H?R9Xfg108Ug32|rKS3l$suvz!T`hgVBfsM1GS(<3io&;RhH#i&`WU7_+|PV5a90%ItFDJ|a&xu{y8GG2A(f!lulSn*o@-}ipT<~>#zjm<_ z;!_Ye|A1zL(Pu*TS8$|IA6LeY^onioSOV^DmaVc`kfb}hqGhj%@G6nB^>NbEbQn>s z1m5T~3LR??7apdmqI6!sLaq7((Y|d7cxcn%R<0_(nK%or8w^9UM)mRS zfPS&uXw>qcfq<5WX&p}v3>_WwH?06$yHt5J>!n{KsUWFP$rWGxGaGYXpN7fpTch8A z?&v?TANuz1h;C2UK&=`YgoJ9a{d5AxO@9u*?wN%c?JK6X@U>Q~tUocvWen?=cxT9(V_lSyaU>SGfq@nEWz4i!y|x=kDEq z@a@X^h&{bATUT7%sZIE5>3g^rvemGMa|62I``>1xbPhun`?dz*Ubub`lyHHlg!)e! zwrrocp08kFeQ|8FX&9JJ9+5@|Sx-#!-e~{MQcP$!2F`BQFO=EDw=$YPVcTO_BuN~S zcgB8#_;@ZoA`Yv)7y8w%4xdLST;&_=K=ql}22+3j0d4H`-sZg!4GtZN!^VwAad`hq zga&REf|7YwKj){|yR?E+vA5vrvr!1jayS;9WSm)V4boDLD&Df7=(FY!GrioK=v8S+p?mENP z!|5`jF+LdaoD%0(iHg{bFPDFY&GRNAKGZar{vdR5u?YC<6MX*1hlr0g)kpIx+73T2 znvJ@REU$oU4#OYceEf+JS{`CyNHitUyIKvUo>yVQ=h=@?r-~tE)nd|d;l4q3{%-qc zKfts>FTJsAS{OMuzTIF89OMQ@?Uc*;lM+BQzQ{?tvLs`3<84 zcW%Vu_o+)Tsn-O2b?BrJlp{EDz#u3o_mzp!IK$qx1Ds2I0=Kd|;9PPZ96ScW&ak?s zb;&?_n$b1o!K1LSLrg=(razz(t78Ab2OlU;KK$@Q<N$)37 zYxvtpb2UvEEexbPyP}?FQCzqY2yK$(b*P+*)Ii<3t&!%Exu~eNQav;&TN(Co3Ah^( zA;gzXbk1p5p#xs|czmYJC0isgBm;v641`W+s)W(I?$daFMn9{pFP~i~cF|F(*thH; zw#-|N4?p=8pD+3qYY+d8Yr)5m5O)I^Y4;Dxy9z;hN}SvAaQ2!32e%%=JER{| z^Q@-;^C)m)-G{Tj#)Uh~^fC@5S&W@SQ8cgE9bf&r0F4^v?uHrO=2dJwYbuBHOtVoa z{^C5tR^h{a?JRt=^G~E|GsV6iN=ctC7&~M#;-7eM#btW6aqB<8inG^{5Tg$hWfvxM zeBS?MO#Jy}!yeltG6v|;dLk~~Hf2#dI9I~&KWs;@f%!APh4oTzUx~u$b$bwU`2vpJ zy^GTki3p9ni=;SlzZq6_{SS6-LQvYm!L<|YU0RFnTrAd-L(-CuAS!@$#uPYSOasF( z1M`!aR+_sWisXUhBrHZdO!cSrRYPNp9T{U`8*kjiwQG+a?|2Z@w+SD-4p)yR%J!K% z{_xtdP^w|s-3%40^;Ng7kF*p`?zp>m42sz5^mK| zzP8UJlUA||L$Ms!;pE4Fz%=Y!?T25d{(|W}-o(?*$D-SS33%zd*_ggzA=aH-f!qG; zkraFKzrteYA-t0Aa4zw+KIB#W6D0&z5W+&zbzH&N2)z_F1zCo~@{pW(E`)Dp@*{^5 zIdaD}f+vW^$tDgiuD)3C($^UM#Za-CJy_iC`KR8%>XVC=?KX`Dqw)B8NOQJqP?+n& zEc|Jvu{W!+_ra2{mt*jF%a<|cO?K);JpP)!6#tw&iqJ&SJBz}FB0G1udDcSl zqQ2->!56(>oP-YjDk}n+8*W}pK);^jaVzK_!yce{?Llbzdd`Y^+7d1icd_C78MyAB z^;j4y?@sSC0TJD&81_7z65GGWcK_2jc4CK;Qf>(x41c%FD7>-yqg>9bnQ z7&irnlx2GF^Q7%uQLI=ClyLEci-VJB4sdaFgqxESJYAjP;pzfUPdB)`xglM=uCsGQ zs-1%ngE%C_#UeQ|0m;cpNKVlrIW+|_$;pUMNk*bB8QSzDq^2eyEj3=W7|V{S@K7Aw zItme?hr&>HKGdSIkfiW8kq~9PpV(vhPBa<_&O~KsEc-oSNtWbPTnMioP7AIm8pmAP ziZ)y{C5AQT$#M8@#}7G5!QHt&03%*}4vBFm;p|Z#?{w~m(B7{bUd@`AA6~;B$Nn(h zknZlyas1>~lrNXP;IvNoXZON{?|dWxH`cAj?a%-?YV6=(uYsGR9g27qM~fH6pkZqt zMQw8n|K8XRFX8Xge~Qmz$BJG{UA)tE5JG!RQ??5`MUSk*cWV|SF6xkB&x7HB{d+xL zMdZNu40|4p=cyeyc6=rFoZM-VG4%26fnOJVh1OlNHyJN4;jI{#0PI_K80WVCgQcgh zA~cMt{kGY;R!XqAwo<$ldzW^?h!l)xk`?iR{e+k*6#^J>VRkFmRp-L}qQ|IAr_kSQ z604FMabZY}Q_eXHkvW4*w_@;GaWTr4?}R@VFGbs@vw!pIM6d1nr>0=->0iWF1{&8J zgeNC_t<1!?1gF>t>|FRdF8GNapW()=S78V?9a@s>fm_4j%9Gyix6gi!4>tddv=mc@ zTEj*|(Baunl)7Dy3+{i;#GJi<8P~XZ7(<$lLCJ~V8n&{=%~&oohqm zB${I#Xq=ybohEy=@(O~O+by9>QIZQNE{rd-dM0a3F*||>nc*>smC22`@E@?NQc^V5 zQQ<%d<6x|z@8IaGt_beTNu!! zrQ-Vcld$!i5Pwq{^#9PZ+AuVJXOUscGCcjy;q;Dq*l}#PGGFRJlqlT^KY#HVIt;Sj zJcAXM9aO+VQqkpKCYIg|Ybz|c z2VTMIrJut8_6EbA|I)bfP_%sa2g8=-c-*;!gX`zu;F-U~)_wD3#3lQDtC9G6%iKq% zsArcL(J?-GJ_BL5BM^5t0C)XwBPci+;gJ!Dj1ECmLKLEsqLGlI7m@$e&Uye04ca@^ z6JA1P*f~@bk6bUOl6cO0pb#4R0w7&?6_Nh^4O`0C3VSOw2uvwuh=2pBQe5(YoXm4E zhbM)B1$KiN4r9=Xo{N$alps!J)d)K@>TTg%6*E|##nrwnHXpnIoi+&3aY=Y8Jp##f z^(DS7A)!`Vw2nB96ZevkrVCf*L|h5Igq`cJqDI;Js8lNli*{QP>iX*U{=^%<{(-QN z?S{Q3BEqiXc!&;>SC6A{;88eultxN18<~!sOLRp_qD4kLPTq-xj>h%@G%Vi~UM;yO zRrW|J>W!-9YT(kTt4P*nE-zA~cq_d6(#xpX)Kus+x9~hJ9{ny}|MqLF`Rh|`-Ln{n z&#uLVn>%qc_#lF#&LS#a|6TMm#UC+#EIbL$R%iJW=i=|e(PJ>|UE9Ilxq%Rtio(-i zU#&`xiyUvsBdl?i^5ig2oUzM$qA{++*d}75FJRM^ z<48`*SxB?na}Ck6I&%f=l=%_AUkifA*}eJ~EhK5kER1T}1kUb;`b+64xO`_FhQB-o zi{JhmDFt+C(KGvf+7i6>`*K8tK5P}wbR@-{!R0G|;j_?lkXwD<*PF z?kBgNi;C*Ka5RIB<1b3kAyQWim7-lxuXFZmLNQY3TeT5ZuRjc3N{|q%yD00Rh5Hjd z#EX{cBFlz2)$V{+aeg>2zDG*3eh^ukdT=@ZbapX({-O(X7IY#t~4e8GriAadMj#WDQ6VISgSAF-OLA|y}1!L73plVNZwIvGyIUKS$KQ+O&Z^)uBq41MM( zJS}+$+L)!#X^oX3IDpRyWDJMVg=D8#4(8_()-b2E2y{1DMu8`cdE|^|D&9+I@O~UV zll@A`D$UxKLjQ*C#4~ZzWh7wprQ0aJO+SO*QaDE@Y4{wx-nKow%XBsD6<;vr29_TB z8KWmn#_QduVe^ki^3;=IcF>;XNAPU77cgVvAEKvgs-Da>UnY+F5nFctj5j~-iT!7n zVata7=+fW?xVkk};_rIWkr;atC$6r+XB!q^^{3;Z`F#f5?&-_c6gF7irCINn;p>|@ z-09#{T6i?sD|q=JQgx<`tLEjaqxpHEK3Rs%!Sb1{{NFt4YS47$-!oSBJmTZ8;i&&< zTs(6Q#jhNNueT3UymDUCtRP4!=7W}q$FUp!%Gen3C{hnCYc)h-wdTrpPH;T64@ZwN zIY>NrtBL7@N1|a@>z1cyXXN9Lm*Q5azG$hFTWz?Bzm=lZr;}%h4v}LK=D6gOTU_YT zAuVZ_IB@Z>v(H^=8!l~}6ggi!DjLH7LB$R*dmWk9K9*P)Yl{m*VI0h7bw$P= z_^64E2}H#VdvqMm@?+V@%S(gwn`zj0LEOL@$q0*zMoV27QX6Eq+Afe(=?OHb-3X;3 z)bNunp6;oyWcafxz`Z$7e` zfT}yG!nx)+idOh=OgZH;EEf*m!KT@NV)kdhVAg^^@b86nNJ-+gt;fZq0mii+g1Kwv zqG1bfxKf($gF?gK|74%$7(RM1>bllMREPt@buPkyg(w2aaFRAD09QlK;`mA7eVsar zYOZcbEnCI#DYlQcazj)Nyo&RIm&H~pe9JdMo!YISEmu?jh8(cxhYt`M!_Na6RC)>@ z{PYf-ova&nVu_UR|5`52g+AQcx_U?S>N^pqFI^DVR;aj0V#P(`ri8>K4<{q_E>h!u zg*M`S(V26FgGXPnH+O=Zp}UO4C}Y_idM-yqV`z*^yRvup@H}`)uE}jGvI_q(M_-vO z#LD(D@v}8{;@AlTpTR>Q%zSzp zZUp(mKl&bGlLCdObxZ8MFV-eLuV#f#nDOFxbequjfuiy#;zfV!*rrR^vg9YMx^^8A zq5J>yH@E^|rBbahpzkOoH8+I}Mun-1n0VuUZFoFs45JCVoD=R5|`Go((Z=$Sa6$JrMT){DAK_%`;4h z)xfF5hj4JUen_rxlPbKFeMpV}3u)Tp;xIYGsn|P8wPb6M8ovh7!7~k8%0NEj=`0Pw z9tTqt^&tRBo?tC;Aq;^rBt~DB-A%6R%e$F^Q0F zo>vBZdo=5hs^h;$x_!>GitL0KIPLrmtNz`B;HYy*Pcy7np9kd~`F*#s;;ZlR=Y>m%jyfoe96M+x>>VqjYr}!4{NnqDJ+_T|@O3QydMXCB zDh;>(mRH2SpCg}+!>OBVl;_nd^~2Wfi&5Iwx^tWU{mTy{CQm{_ETb)U=<>u6RDZ=Z zFDGuv+gNsNg)%XcV`eTTzEUa#|G%SnDGBSLi{B_V^pzCsotwb1*c;F|<}7snAheMm zA}+$XqAkNqUIw)?yil!0 z&a!o?HY$tT+b`f;;02|p7!aeu(`k`NsF{n^8SOMktJ(;)x(r79#+^|;#Tf~9zR=ng zL#j@=2oEbIo*jgnU%YgC3~kT~)1DuT39~1lM9JJ|cyTU}Z{=d>GNKE5ckB$!jdb|M zI3YPXP#hn9ry(Of2G@g*;O5>VsP|NVA@f$8IhY6NJd5K=4;K{E6hUJ3?9Y!Po+q|p z-QH!QqYx!duQ#T(e;S=e!PS6m%C>v478uaw86=lC&H8tJqCH%1 zUB>P3>!KHziqxdtN_gyHsn?G;DZ)$n7rN-#NQwCd=_#zfk_0>Zif}GI6OKh+63?wW z?<^%|A#^Fm1qbP=Ff7K@F~-7WrdS?hP0xkUd*O6^jwsW6;b<)DjVUAV&d#0~(q<5r zY+4KlC%!fN?B8}z$?ggb(cjb+tMtX-{x2c1PRjb5GNilMn@nZDGjm7 zS|ldKLz^5egojmQiS1l%qi#rl61;o4P0ojZ*iTuROtmqyOQ!-b7>-A$w>u19M8 zCh_w*XVHM@yg3z}00)l&Vk2j^;L&6t_D&0=Xq8lcy7M#`n?++<9|u0^GG%u2C051S z;=&K%2m-qxTyyw83yGEVX@@z32VumwFL5(Rd^7w@e6aZ^q^AanzvznIO?#o@^9zvf zkj=r1Y@$ik!Z|ub^m{_!m=FQ`gfM6lBax99gA4I@5R#$6`3tuZA0>>s_+H%`jKb{I zU!t7os9NRdW`nrsB+MN775+SPK>vLNo}R5R^4rWMW*#@(uAapF1+PGxa6ugOBADLg zIlQy>GsCOa;q0Cp7&G-FTw^JGLFuwjW7L3&h;R5{@2J#V}w_k{jyM&l93vKm^(d@nx^On z|BoxJaRxT4B6Eg*HfFm@a)PzRV9r^p(!GNQu|VbP((2*2})s5=HsJz z?b@MJ?E#qg_m?Q2O<`fLI8b=<0l6Id@hs+w8pWMZrc7DG9-vC8$3Gl%Y3na~VG)$_ zX@#y6jVlmZ1umww;_ErM;jgb($7F%A4eC5pSV&l}*D1n zmHAk|hcQl;Cf0W$Qd4pE>M<;PV-7cSgl~CgygXzmJiPQ}JQ$i-a_%xbPd%)RO5Wpe zc@2)Ax~qttSJAeZv0yfQvRMX6;34G+Vkd5sl&Ft|(f&!IKBhyA?V=GZfZ|_UgHC$R8hV+>M0jqsn%%67A9a>G2k2E7I#t z8}JpzG-;$PTdkstStvy23-Qc$BQzSv8gMH58eB^M3HNgQ;p($ah{m^K->Y!&940)G zNs4$ldoL8PtrcQ&K(uXQ-}^#P=&j|CpqP`EV(4V(p{P(?=(*4~$#7UckQd@Y*fZdX zCNONu~ zUMnvSPTr1vT0)vO)4KVzL-%_wk~}7_#D$VUFNJpt2Uj?KnHd`FPAJ}sRvU?BXHH|| z7y7vfIlP4ji5J;T{g~FFkV^#SX7kw(|qd%UE?g94U!c;O5yBFHL(3WwW_x zjaphe}ZeUrYG#w-I3Or#vs1z5@YmwrTPvn)j5S~Euu@`zT>`Ln`o**jx z7~c5tPlVq!OHs@wPYtY#u^opQ!(%ZK`>=8UKX45*Rzl5N_AJGX>j%W&cgJ&$+o0=^ z%#*-^!o&|3(#m-e4`S8K)%bbsB5XKu9gSTbFlf$nrQ}OaIkfI9mfZ}6HsQRo%@9n_ zcHMJ48L=>+(Iw&U^Jfi)A871b9(`Z$r996qY2qsW^`&pH=$NUBLq*^2XffeaWLQ|4 z`$0tanu?bOPezeqri?LWI3Y`w(=OV*m`9NNZi@Ep0 zu@`zT>_Kob5=CG*;C4H33nmSELwR#faB>7b`1LiktT{G|*mnLGgtpM4X{zdi{6K;g7hm}tm3(_Cp*)53WTZrWAR4nD zW$CM&z`=Y&3NoQ-Va0ZK?-;7*3pq76l5CKd;zHO%;2lEmg|lMmPB1jaI@rqD*Xfe6 z`NDQAos#p(eI9PW*FSv@-_nK}S{ZTJbZ!r{zfIR~=Mib!en7~*{fgH#wsl)HZ~osw z{MJqFP{Oe^e!3QfH|Nj7i0-4YWZZk0JNiW&%n*k`oPjEpGSIzpE41jDyEWanE!mGh zF8Lc4tYUbm73%kXL3#eTVwaJQO=nJubFOy{eM>xnejf~1p68bBOZVf`4I2>_tY0#M z3(id#JPGj?tyfu@pVfQD59n6!S=ebzGt^TP{zh`>^TJSOTI@<<0cl!&QilQB8Hx+T zVloXmAIWQR;YsAlWRHOsk0ls5_QExNX$=&CNlFU9yQ`Pu(%wviaM_|xLoa+faU9%> zvZR?%wbT&&eEeTH|Fb~Zwl?%yR$RU&eqjR2c(*~1w@czL!J{}%%{jm*LN5sS(BQ4!thLi9b1OPXm3<;k>mq;QCsXm zP-N8J0*6_7_j90yH82!QkdhpTLkB}KVst+^Ips|AYPGC}^rQYb6c7xqm%`doX96P7 zvP?Ck`BXH#%R1Qq{W*4C_(%L2Crs`#0s|(rdguVMqa)C?Un{h4+8n;Yu?Vp9Mrg=+ z-1R?<>o=1Slu{faJO332!5bComsM(pWta8e?8N+kR_M#T3rdyhgr*ZdQx-~loQc@6 z1gCC^V<+$^(iC%ln**;BxvSgecRmX5zVSW|U)&@vR;@V4o*3SI2%LxKttV_>voTsU z>;V6>H;|CT%J3vZcrRCw8vnPrz8s-(rugJ_pRn+vj9W!7RVPqVNI2htGn>s99o4JKb?io2YrO=*RwZP#1j0!uU-Xh0#k9t-_n%8$d)aUQlzA^{kX!A*seQ_ zw(9#WEz8$Ih03{0w+;@{;p3Oy#-8)r#K$IyPj*1ZTD?(h+^2?jF{ zNf|#fk`b@WkWUJJ25HIqiLsLZ!jM=RvoK6hrOiDTDJ=O!et9m$+B=Yp81qd0W)6prt_ zjB3@}3Pb5;(dVxHaw~q=xdEwKea)Df<-4N7_*qmLW&3f(^Yp*id3cSodZos}2k%XK z9rd29Ya#{t=?{ za9eav47W~34AK&}3V}HwPOm#Ojx6E#c%SF&kQ)C7k`wivECG9IRAR0H&YOcLm7QEk zK9HYRCGQS;DeOgX?hD;iT7Q z85aYu;lieUh~*Gu7SLDS zYO_zsJsm!KW*U~A-eF8RE?>F_x=ou?V3SXEO*>%d2S1@-!)M{(oH@mv<1yNZ8A!U< zU#aJXbPFz){{vE!j~kvT(5^6A!h<6-#qyc_?(&#tkEwHCxGLOo(Tu+Ay^tLk|J+Lm z)K@Oa5p9Mv!{;xKMA`DD`3`4puf@gL(+pcV!*Sno1P5_3JD^_a8fe%c_wz^GT!6Mu z6~Xl1Uc%pdf5)=b`%$51KZ{nVT=&HW?D3~g>+hMerJ5qP{RrjxRJS4C(py{17Cq}*`A_T zk~!BsBQ+Q)F>{a@+zYyx@5HvcZ1?{k9B)a_{QJ4VOfinL&|}FIFO%dg`7JKA1U$h! z(HzjHt4f!Z>B%e+YaDF2aQiQe?KMSNi$14xf2k`z9Xb%DeD!PXbCTVjOTXd5+;^4j zobcDNQ^r)^c1;@Q$ZL5R{H*dmK)0vt(6f_Wmf2&RIlg=UM#M(yOX_p2_JQ?lKh_f_ zRXhSNBQWNg@|@*up6b;zN9&+nIu(K0lV2CYaulg4ycB^l-cRG1*S>!VaIPD|#zpZ_mgs*JaoDk0NL;b6Y%xv*0!#U&pp zh+wT69^(n8bTB=cR)UN8&`K)dG0w2vbAAb?KJzJJ8A!_^13wsu_q%n5m$$woI!mJL zJ+}yFzt7nK?X6H#n&X0vf8e%X!KH|F#!$DNFm8=*_& zbSb8LS4Eokd%vIxaa1* zfay=Qfv1;#4Hyc{fs=D^a`9|sJ3AEjG|fXuPCkdGPxi&so-=Xda%_&X+;Ymf(_4|0 zWD++=Cug|cJe22^32cE&0lZA~2D4fTUlhw`ozH#i{P6MnA7I__e~qCq?~+|GYSM>D zC}X`c{dqx5^IoVk{&OLSx1miDf|9CJI%Dj$>%?;kO;U0qbSWW7iCc-Jkg-U**B82& z1yD-mKc=K6W*To{kBiX}c1`7YO97Cd*<$y@6U|1GtTw_6qLyf!te{CtOU8wpXW)_E zQW)}bIhq4Pn)Gc0yCauzEJWOZ+F&6t>9`ws5SqXY_%^eA4gMMpp2Wq&`;exr=OM0G z-94PTeFCf3o`&|QKPonBjH1Qzr^I-CSTPh$4#K4{d!!0Ok`Q|x2XEhno!@EHwoA*j zp8Mkj#q-3$b%+S#a2+s27~3w-Snj!O{pA!s{dNuxo?})RFE^k>$xayi!n=sCWZTOR zYZIdpaA^;Id8f(!At1H;#d}J*ODH%@@u&DOl{r1-tWtqFUAtQ;?x?XZ4LglKWxueK zk$MLyu|J!aec_1he$hCt%bS(qG2@Gpyr&?=Mdd*P!*Chza*k+dbh;QEJ97%4jHh>yLd%msPA|1i{T&;H1x;f1zt-adS^a1qX4 z*&xmvJG8QGnmz1&FzbWS z5k^T=`y$B)Y*Ad)3xEQ{B2LOMtyX&v2M?V>y(fmCY86LCEV7G6V~6H_IwS7j8Jr2z zB3&1uh(l1wQDj_?McLM!4DWn2>7K<=r|VEu7oFaKXmNM*h7~dL3?T4X!8ND}>T^yb4(YQlfB%Zv4^P;z}FP9<& zChR2Au7so9lZN7#7U25ClW6etaCGpg3eRL01SUBkH6;WcfZ5f zcW#d|dD7LrGNw)&{m4+{`ThQw^}@TDcVG`9L-*=;+IgaN&A#X|bvBYqme=pIK~f`w zu=<@AIK2A@!yW~CE9{Z47LBD%C@dU=;@}a(K}_@Kg-xNMN5fg_EZ3m+F3f~R4V|Tk z>ox%C$$OzoSb>bx>*D%xgvNn4n(b#FCP#cOeuFXdhhB<0e#?wh#y%hafk+#}R>Xy< z9*in5>Na~{qALXrOZ3Ml-^8h1*U_YXf0QXLu6yx9c5(G|Myp<3kbe9MPKVnmg1|o) z9B~G^3xO!#E_=lZQ_5C{SF4_QvT6gQ1SKLUMs%1m(vXmF13M4DIS@C$B1If z%u{u|g{s5-l6=J0#6@l90z-F};=+6&4R3((*ekfS{~j7V*#o}WT=h}K$;AO}hjxS0 zsXI6xmX4HUcKR}KFX}v^PFzOyw%w3n!P@BN=t`79xwgGfDj*8iLn$!HNKW<_0&@)& z-0PrL<1#rKFUl^y72MHxR1Z{0uYwboqmhurdn`j4^uHK%4gP+2@T3sHP(6A$?_h_`@-nfH|l+#E}{0Awq%f$1O zLfG$#&XPUs?CB8Y!$tiFQWG{KF?P3MOR*bVi<-ZY$BV~?5fJW|tbwQ!IjmgA-cTdLG~WF&~X{vAhO<{?9Kj#>{^wuAOehY1B)^5r^F{{7Fik;sm3yBeTkCvGG(QFJFSKlfNSy{@x;8aY1_Cu^awGL@I zeb1e+oXAtPnxJvtDOO#dBtNj%CSv7>V-OmmU(cRUG~7LkEAt$5x9Fdc`oMgLSRor z^l*!@3fYj86J({9?ygLqDjX_ePzSMc5@HoyM zIgTbJ$|Iv}_KP99)NO_u(eXHcN7w^l8OoP^k(8RT^^+%faIMNcI(EPHe#jBIG$7_nl(P(x%+4C}bFwJU?#rzcu z(5No=p(I7X+k-_oHYm{>L#U_A)4<`ksa)U$IqjA^bwT0aSruN{<>M8z!2Wz&N5w^)dMV z&xL51%LvcWo&FdpA2cxN&K!gq*+tnbvh zcr?I>hV|k8%$G=YrHv^Zcpl#Zw~$LnPl*#(j0UM*6_Hx0krL|5?zze@94_rgxDfbX zj%-kR{${A&$}!P6P{c76iVEEsdKmOL_z%?gnR_eQLA}|jxNHv!pP35{A1OAf=&&)0 zz*5b8FF%8y`OvFYBhH0`aMv{&(HzUd-l>|n{;R;wsfzNb5uL8=icUsq2qJFj%Q^{o zOqkU0y=YwLn4(NErV!`!I7xDWEsG1Gb$F1#(1Uq^z>LPcKNg{BE+afQuEgNw5wGKb zpAeV~j)@BFil9@So~So|7IdDB7G#ZE@HO1o@D(;*_Cr!!roq_?EIls<&coW67JJ_Zmfl_Y=JnT6$Gz*myxuaMZ^6 zGra#)0pd*EMxxQv;XRwBccVfN$4)eR2QMKym0<7aD?FZ3;uDIC{i4|$njJNbAInM5 zmyr-E?nOiC9s$FD%-3c}j0cyZ%;QFHMv6-TU~7xBL;wIX97#k$RO{kGvL!H$s*S=o zzb{6s#@v&0BEnNKbLbmber6X^wY=APg*u{Mm0svEEz$#F^W^yVS@FA-7VZv1c)W-tE)TEhZM6qpfB zJEG{AIfW)fX6ty|JcE>jKjF9Y=WzEf=k^&JRVZ-)sAfqGS%oT*?%G0`==O zg`-yqL;ra=NEU7ZCpy8=9?z&)4Udy{~+#PRcLtsQ5@KKAF6^wu`w5%`@I~BjoP@)dzX`JZ6hTV=@9miFCyoLQE-$<0jElSj2^7OJJ&%8-(wcEknB&;*Q|I z%ps}b(%tyo9!y`b7DYiekdMcM#u+9WsuAq>7IWz7~qL?|+3YanU$_bPv)~dA|T-JHCx? z)=jr~evD3h-uhJ`IJ@dSwCp+@Jtysb_dA>K7jRK6u?Zqp?Y$~UTYD?DcTqu&($=WG zY9t6Tx==gzXpJhdirAZ?rBx(mRqZW!)6e&M|BCm`FS)N=x$gUU&bZG$$JJi;K_%HL zO(>0DvK>9|pm96YHJMr3>6>y;kPQpzoArsJVywqcbuZ;JsnPi}R*&+lU0V<8rq`@Y zedHN71xP@jg|O(RkD2+2!+buu6CbztRmHs^xeBMk5QU1xj8JH+)rgi3GqJnaLLm~E z=l2b4Cd+2KB(Jg$vr>Mp$t5)U9am(^rH_Qb;W%kpZ__tDF-5n84e;tg1vcW+$^v&` zxH|v!AEPE-e$PjBS>x7E=5K|aoSj1#8=2UCBhCy1wuG(fICG!OGT)zQl=NXlBf>89}Gnx5sjGmY&fbA!EN zbn19GqD@Iim--bf(n_yX!@^>q3k%;;)91v!GNpha$ zEz8|Is9o51u8fmES%^Tz4C8{ppUM~HH=~bvv^WY_wMT%ti7QO1tf*XgHEo`@dwzry zx;l{i;G7V+(zJ{#%2tV2ii{v@UnFK=ubzYonvcccJIVCbV9rOZeb+IMIIFB^oCHlw zYGFQ2+=tvI?&h7rsq1CuQ@!7Z;kwLRhkhqbiWFXohCmpX0^wh4*Cee{=n@8mCW zenYGH@s+mtcs!}aRa2aKTKxY7+E55pXk?J4BX+mx$qwo4ZJJcCDE6 z>9r??Wo6NY0IDni-Sw0YqS8R7&nD)#Idw!N$#r1NtXQg1J4fimD^jyZ^=?|S0P6O* ztcL<8rY1gBer%f7)(Ihd;S2R2DfI&@3D%#+ulp{0AE9~e%Dj9o!+VE<)R#9LL| zV*BY=sMkybIN<#&(DVnT)ei)=_AhlH)|li@DdQLyA2H&TqluE=n#T&vQ8-w9VpH-* z`=9vF1ycDdszdP3D_ik1n)gZU3Q#vX??BIFlYJXj6Eq&kp4=Z9(b4ewoE{X+C}O*z zA?UtILBl2c4*EShT3w1LAgo2Zpg?BC3vou~x9XCTHk~R(fRT&8YLB9C#*jMC2XW|? zkO}k@FD?#3nxjb~br`KdvgF)DE$97}}VVV!hq-Xb{e!nd=S zOrd52D>H`~l(A7tmYkOP{8u%sHbm`XEIf#rNk!Np(x|w!PxW;{v+X4zp|AN zov0#fLg=X!2d0@Z{o^G(BR;&${PUfP#7A@aZbypAZTnU@!&`k?hxHzgv(54;_mX`~ zyyvCaTju|yJb&bwpmjCe{b4cZn2M@e=IFi1Eiuh=!+Hy}&$|=KiHy$fKd+X=H(Yc- zdr@6|7_n8ES2&&0ZFVn5%XEp^)N4-QwGu;BmmQ|rTz($x74^1i4_~f0Qkk%P4sjzm?_R)HR^3#ycuFAJT=?-B| z=%qBrJjBmNl*%3bb)xPbLEY>!y1?`?*X;2WNJ%UB@+#h0ow_xE5WoK@xhG}&u+vR! zp|_l!JuM@(Tn9@w{3qFgqRT;F(^+&gV#0bO=dDoi-`)KAM=qwEHRh^x|~+W z(XyJo;!K9~PFzYlpKQ?R&+b(Wo!^-`ICG5WDkYS`$G3YUruy);lqEEg@{2UXcWRi* zZ7*8MkDVlT3|mU2e;>1XyTr}_vY{XZ311(d=nGb2H%WXYfb`eS~*Fbz!7Go+C`Tg8cnTSwTk4ssgxGAr~OSpAcL!L`I7K~F6anq6c~E!c$OOyKmlF_Y?&))_ zmlUu;;UI<4>-c{BcI}`FMZ{%#>Z>rO$3uag-DlG>CzXx3hnJndy9p1rk0QDK0!LYI z8qKJ*@Mhd5W^!WrLgrxoLrXhPHdR7_rP0rMMe!Uj?rvZs^QEP8jjYlg06nob96;b! zp$;~(AL1;^NUn8g$OWZMWh%nx0@zbtW9To{-UkDy-N^lN0JH~IOsHbv487j#*R&kt zKlzzVeH>@rMn%$wmmbnD%r2}i59TQAnO;}fTjckU$Y{rX5Iy%P8bpdXe^>wMN8 z|Do?&jGya|Z(PxbI`3S5Gt0nF=MscDY-_H2UPvxgUHL<2TAZ9dJqhyJwR_wgygWP`al_mwhKqs~ z&?Ep}=s&up2D7&0)G)dFU_tDJCHV^5yCf*7$qXuiR0s%R)q7<-2(&f|R5=h`J1)C~^#+?tfn$;5b*xMDu$p zAgJL`U-rsHLZ_Z zR`fSS-px~I28M_bq#G1lGXKL5+`{gplS9M=9y>Pu6w6)wrPt)<=O+A<*L8B|SgvS{p_CY?$)X{yJA9U&3W9>U9Fu)f^*)6ifdO_B9o9PsTviTdY)z*u> zIeP4j*Ncsf*L#f3&DC($0H*svW&3k``aWquc|am&CuT5 zIqC+@wJXk>T-Ki+a&DF~{}ltby*Idwt9jb&z2}KF5RJ|cQ<`3 zZ96O+Ms{kqH?uz_551m)9jFP#fYXuVHT2z@IYal2gY3PprjzydQ0~o0=$TX zMQ33Yi&Y~r(GQw*%lBX7V?Qn@)$m!QbDD|njcPLj)5tw|qa9Pd8%}6^QkKN_m{Y4z zp{xBvJT*DOW2nfb{G;{BsQC-_tD?H650d}3$8nL}P|xZ%Za*SP(tTh!EZY`jKdyoM+&LN_d?u~rXu9~${y%&q;?W_Z6=lZP ztxwY)V=^y_<)6tiRTa30{hGVIr!!IJ&B#940y=Wc5mNqIHl8}}EnS(EXJ!JS-SZPr zlD~YQDG2|3#M`pygxb+~L4%Pb?1ML<-P;|DzlcqpD z7+-paEOj@0fiZp1R`4?mI`}8U#0X#l@Rm;LUR`aif-YF0M?%TxC8 zLr>C=JD>DYl|07|_n4umV%{G7PI_KysIid#fjh)ZPnYzE9GJ4bMUh&) zEm0X>;CC?g0i~UOy-~k!k>kY;HMKx*L2)q2F7TCcOset1Cj^&%-8u`?&`s-d?OD74Goh7CvGj_S1OV4#P z-)9H$x>9N`JF)PMCxf||1BX^NQ;o~E5#m^QEro2is%}81gl<2^LapFs*<-6I9i(0X z5a{=x@FACwT*a!#jM9UYm}zVLHe>uuPMS@uIdWQ!iE~z8{5hlUZIx$S88mi*o_tjJ zkzJlEXp5GX2z)NEI5yVagZ|*BHTHc*za$CK>?l+d7_C%NPH~=0?icap9MapOJ|RZ#t8erM(tY zb~a=*gP)0r=imcQ>Fw037gyWvL0P%tI5+OUeI`A6-iroy?v^w>BdZjaroql z+-L5~4*@Hp7CKFhpp|G`=>e%ly>j@QP zdlebL`)V>e|Lz!*alE?zpfLyzhe;#ap*4ov1s~nD_5a{)Z(M+tQE5yLTIU6ai-VDm zNrsc|`tx6teFTciv4ZN1QiOi`scB}xdd@0pg`RX#itc);aBkGo z>^BrUuw-?uwe_y!(8g)m&sPba@4C#~=7CfpSIRLnf{Dsut%nse}>=|bJ+P|AqTV{b8K65K$7x~_U`Dn$yvK93K>d^|nl@onKhCe$=- ziD=l${hbO+vd?$rbXG#<2b)^kV+~zgAiV)xNR-73!Ki2;>y5}QRu%w&%D{Ik!NhuFY7=4AOw&;&)oW8vdl5rHlhT`Q#kyFz zB1;nrbpMrUqFLBY4Y4$_yxiK`rgL7Wdf`%a)2fybww#{P=*)2YYi27yZM24nK}a3@ zVCX5F5B2nj7H2)i62QmW<5a_eRV^dr{qWF}$Y;y)1! zxNmnt`l%k;i!}!ISz)4Gj3#W6C7>Ww8x`U+cf6HDCZeP*(B_AygA@^dy$<8g)mHEZBUARn}u(^_j1Ke6sjGrQpFHYj5t+ZCNm?ke+#W^6$a7 z?}mhQp6fALgTag=qsfW4cBfx_5#`bfEkZn`P;m$zR&91?V|#I}PMhybc4A^B{atQI z)%XoE+s~@U9%G%2tv|_>81?fTr;B_wvRf*L&v8-snSpAwPky`uRi7=f4*_VnPRh1F z4bLm`Vh24^c|;-1bzm(ln%#PWbmw@PU$CY!TodR$lJV8wTzlukPEvh6q*87oX6yvl zrt-FZwqf-& zU^EVGrxFtF$o~jT2+JceFP;)Bm*#_MKDR6@1COK`;lV%7{_wP(_t;(6IW6!?5R&%9 zF}~Pdymqs&dxh)jXF2GXC*qclzr2Z7aEHEJY{1T--Xl(fg&6c}4}qhoUvs zLxRVkSWY6yOHsP%oL~H2Jy8>hQm@m4(B7~7G|*(kc@J|XX8q8dLaV2hk(4UfceAn{ z#trk}U^y;g?>qM6qLlqJMJd*sP_mZ%OXHW|w@jC_fj97=tCn+)As*V?zB8SmIllRmIX_caQN+9l25qzFWMB{b)^ZTWlA?#b1e|m%4koC3s16% zj1;FP%V8u(WX~|UNfDaeA>SbHp7b&eP|27OJ*hA~%eay?H8mJ5r0cb!)bJG1KXV+x zZ78=v_&E4yL{ttNRVh@3{|krf6q+{q>VA{4Eh9**CVy&P)tdmBxgdXC<$PEFPdc6| zt*SF=7WQIp(qv9J^GyB-=!xxtKeEpFTYr$VauPBb<_9aNT}CB(lox9gR z%NRk0MhLQ8-!dK-hj7;~0xA4A6S?@?7(!$=uPf?GVp2v<&iXwTtR<2#&VPa}tSOZ4 zlyFy0SBZ!M-8>!(yi!+<2$6)SReZ39aob((>d10GNEteJz)AKKn3PGb-9x&6AmuR&# zSw($%t&q+6t0zFT-os@4H)CwZOlMyysM*pzF`cy)gzN~~y&SHx>M9kWAuGx2w=^o6y=->;UCp8Rmx-}hP-1JPPcn_iiT zZs_sq1>-sDMNMCFum)1!O@8i0o8d5aZj~H{FD! zT4$bta@VK0Kb$wH|MvYj9v?X}#4pOvQ2LFKnD6IC5~}bbJW{m6qIQ&W5|CH)G$$Dv z>?frVJS;4Fuz_(Q?SVT`50PY~7+ow7^Yct+`lWmd$k8z8-(e-3epp`5$?Pr=>_HhK6`&~in7dT35;vyiPfU~Q)aAanW$}ov!js% zmV}6(!eq>=;lFziY=tGM<4}wvWtTaBXnGpOEb~MA2&^oOLi~Jprps6fH#UQ6;)ONJ zhQ(d_Vt>&g`N=0vehDxp{7rj=$L7lR#4@Ur^?7GRXFb$P%9fEFv$P+k`x1S^~>>m=1n0X+M6OfExY$_hYh~>t5WJCpF6n+4d`AFNYnLtL!f6plAfJ} z<@fj%N}#O{Q^mAC#1RI~_9o%R{PsqOJ7SS<9T;AN%3J zfuE6GpuGqFsl#JAscqCv$57Se@p7@Z?_WERVA~ag6FTF1iRy0t;3hqaNg$JScvxt3 zuvFo7o-_Fq=&Qb*-@TAgyU`B!H*pa5mn368lAfJQ0?k}zDx5DBZnn|{(nVPCwL0`B z^t#Yn1~>}A=QI9yq)InDG6s*Gmqn4D_alA${3>I6+kUeCpsrm8PW+5mwV0!v^O`AC z^??;=jXJ{`m}GmV5?-B(h*x|^(=JyIfy=bGab{;Ct;ZJzS$8jU@uI;`ClZ624Y@JU z{YtT&b5jEl*V48lA(_};x9FHRZRXY~%+meZOlS#ayhet`M&?*2dQBBoOe zRBK^0LadfE3fS$jg#u(E$4okrSXxWOMnLG6AAxwgJ_TprXPxtxQ<-zy(U)(!Yv{Ap@U(Qy)RpPd4bMM}3~C$G*m& zyZRbq6or3DAgLnrXvh!w858bD#8XMcG+~5qjLy%K`+Hi5y;+iQlPczsk@;h=XWWmdI}tGx9PYfg??FFRlOcnfp6$8$%z<2?npfduVIsfI$^w_{;;W14ydzE> zV`2^hhzY}FjzP0sYjgC@#|uN1B+UlwUNd!1-_wEsD5X+uHk^O=f+lYH`K$l-KIM^w zT!UPPP>BEe_(DS|(K0$zW=k~8J$81>vif7qVkY(?ThEYTWqXMV8E2`w^L#(FtS?6? ze8ukP&!2_w(&@qm{Z59C%B`(lsM}>md1*2~@Nd;ZCTcpxEXQHS`^#Z6&aCzBwXf9e z0BhINw{kX(OU>voA>KR>$S<{JnlL38g4zMDM4cXA48-F#ff`gGXR*Vk919*^gZc+e8%Z&GfK zI#2ZTUCdTN-e|j}Ax|)cd{NvYNNT4{be2lA;t?f8tLN8*Mv{tk)H*SNnbmZ*XZ< z7Fd6Nf{^$b$D@nl70>KmQMpGXiKl7b@#rH-k9GGjROHya1KD?VyJPs<81~Q4Xi|7H z@0Zp{^G_q@Nl_sV+zUFeh)t0ffYDK|9SI$4l@F;?LD6BxGvSRikB=0Qd|i=17Vsef`XxnOuA_Im(_f8IJwwJGDs6!qD-xI+ z;xu*q@BZrUA(%K4rzzj-M%3KCkL&qdCOg;?qVDXbm_$H5IjO$Y1J_Q!k@0&xJEm(O z8{koH8*9Ie9DT0da8${Z2leeyoAB!@AfJD{I#sq{#4dcI`u76+>)!kaM^*W=Ky zbGT>W*ysQ9aEJ2WGc`o3Gqa2~;^x?$MM!caxpqmaN^PLJ@rLTrW{A`KNra~advc|8 zUEVas=vJ2W^U!f^Dh8%fjj*6>Sf#gmRFy+@n(-S0Jl6TioQdiVbn?VkUC245%D;Ch z1p`u<*vd4FMfv#;DaXdTiGJ$Z_zE;ozVGu-J`h>n!rGl;CP`jGl-Km&K2@M~l9|*i z=R$zP{Y$Epdp)sz*?1=o7rhC@Qb(KgImBJY`*+_=#CV8Sn66hU zQPhC1P51Ab@JJp>*2};j+PzShUvhRH=ASId537kA`|M)C|d9e_gtfEHxMz5UV8)ZM=ublkwPcOZ30$PN+Sd4Xn zgtkA9>2fvt9s6ZCm)p_S>`*BN^j4AgKylP-b?>raWctF&`zV*&^hK{SCGI6db#b)y ztQl{bHIW^tXh=}(-``m2PG~RfwcSH}z$O@GG<4K>?{~UlbL)Qq7%#(#YG1cq`xV9= zStHcFfL*vGiKi|+nvraeJe4Ij6+=cbI2cvk9q~-yWI^hyQ8j7u244%7kc4@f?2={9D)bG@u%EhzHP+(!6ZKcriWT(PD>hw}-I8-LaT7 zP1uhkt_#+I1iR@O3L4B^F4ZoFR3fPv!PWu`Fy6B(IcT;IhXa4yYW7giQ(X_-yvW+S zE>C{xy1U|gF(RF$$kD;I;q^bR1ea@=cO>mTS*8j+~(g0Gnma- zSmjPlc&95N{FLR@>yuAF_iM-Ty;FiARD==orw!A@1L+u0xrq~@35iWf&r@=-?Uj;# zlY+W>hFzH5ZC+)0vojejmr8dM^ez~`yCUZTyq}=y^zh%cdaVq{hUlI-R}_Blv}kI| zTjSU%M6o0WkKLh{h&cBY;N6n(a*jo0V+S6g!R^soHd)-EXK|3xEKOPDe9q6S#jNTt z9go|i14*GT2WH@t&kw6A8bj}cEikF z4n5aQjgJy~6YfFqK+}ixtm=GKAyORFZa1(D6T*AQj&)O~+RYhJ1I``Nk9K2`StCo$ z{=3QsIqk+l@N=3_=99;l^naV(H)`KHBx;&c5H)=sNCTvqntj5!=H|$Obx((Y;Cye< zcR~EB#N$c9)2CsYDQ`}JAq+Mx#|#i=X@L5TpAbUWW`Q7D(@HXJ4x z;RZO_O}b zOA)ZvD=rea*`ci$C0FEhC~k%-MQNyvAP7DdBQ4aQ#t)CkW$azLx6V8U1YG)y8F{9{ z5yi01_`u!y-Uk%aGf7jBCGI7ri1tEsEt9&M%LYgwrCP6Ai?`Nib1TdKnZ?$_v~&W7s@uvXU9Lj8bt`PQZh^qNYs?L9j5P;gz& z^)}eQBIzr>stKB?u;L^`!u6&m)!ZwyjqxGv)4q3!LJZtlA`emc&Lxay{XSZTzju_P zg<~_D@F+wgI5_V?$XQm%Np6 z?`)eEKlF$N-~XE#p#%WZmGt_)TtvIW1-!bJsy~(+MeQ=9*78f-$uuj)-Au~&e1n`}~^UKKpc`O$2>X_G_&8$qQUR2wfAo>9ib(o?1N3empN^P?zFl_ z(`s4Sv1PHs-JXSMRf*AttcZhp-J;@B|s>sYRe^H0@Va6nU`E&KT56%0}S0F6x!}CvsqO?(Mt^n$rr#kW4W7<;Ew8`E9A7V?hP@Sy$^QtSz zZ$`dLuoJh-0K(4NZ9-@Od;LImGg@^?J0*7C*D#^P74eH3AeI!UVnnYM)0Y`N${-nX z8yJ?$q#VM|HM>4N#qy!dU6GP-yebs)b5$s(FBa98s@}Pz#Q=k*W%?f&PW6(F28)N$ zeZm6veLmm)zixToc48KFxuk745ntYR9W!PDJ^}EoP`)JhA6$GTD7DE^3%|buHP=?A z6KhZQe;L){2eqy*{7OD};rt>|JB@3|cS15=8wPtS3D)r6x%82KsfYx!(H>2{#`%XIOJ|*U{O@oZFMDTFm=)yx4>4;Jq|R4jj~FlX34-59J*?MmX+IGEKx@+S74;LVl8{k6TWWj|+vwx)a| zVns6Vkh|mCpiv>u#)C7VNJ!EMGN4w+^U`PLzFc2#(T%^80_1$^AUNaqMF70^&thac zokG9aP<+=nOR;DDX}5JJV}Gs;g~sY2$Gcitm=8e{EzV|bjE8Bc^XX$fsx$ikz}LY0 zFOlUanQd}S5rh~3t+~D5ltu5#z0z|TE|^)rs>+%Fx_i}aw0!rN@WHiGTbbf0dsbMK zI%eV_?gV-FJJ!znGH=nuTnPvOG0ghgC$?J(b6ECg$tAM1&${uese$Ey=77NFXk z)~-jUHWSdru~ZwQiHx@H<5zJ26x`T=0>$;|?Y9K&YPhqQ_&w|{b26p(@Zyz;HNX5z zAgdm%>l?AcuFcus(T17LRl>ylJ{wn)5lzm-Bn+ed_d#WbidAG;u+;lqY1*%o`miHb zvqYP)SGj;b%PDyKi83;^wM&4U7*`^0#6oPB#ce9Q{|F$3E>R zAQzKRT~0PqQKPt?Qi)fWBEl0u(Pt_Q30rOelP;aRfDB9*|3?P)cIM`_#%*lK_#fsb zCR*!6z4i`}%EEDG3SuqCY{_Ybi%JWu?-u;eWd~#-CbC+%pOgWRKg6R{Y?;~QnBSrL zTdbY@{@ZQO+>U5G5-r?>Vx{l|P2MzJac!a{P5iD(2D1M9!J1P5x0wFtF(57~`FgbeEi8#)$p62=|GO{{BO}o5uWgN5$S4x75kGobhMFJM H9i#sbzl81d diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonclear7.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonclear7.png deleted file mode 100644 index 417af6c61166c727b9ab50cdd14a498c68744e0d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 77056 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 NziqdsJ&+yE=yzftZ#5;QXZo z(P_*@YRqNhUfbYaOoHy!=xk)>FwemR)5Pi4QJDCc!eple$Q~1sg~=pE8sU;^G$qTb zQi$K?!h(bzlLHIp*?^1twunz|SO~C&Pc^g~(nNY(QoMwn9`aN>{J!W%ywY<#JbgPN zD{UYC*)t!{fBG^0H|8yzIL^+FT1foI5MQkR3NDOmY~{ zp7c;cWc82*sgN)+R+^h>n9x@oYMrCB6g!bZthZRj(UKgwSWtAcQD7ig`mD6jd#YJe zg;(dkjqj&?h)(r{*X3FZaaUI1<8^cK#C=a;_VnG-TK#uQuIMwpIczeP9av|Y!5`?; z87}ucEIqa+S#?_=+~3yYOyW)yUeo+j*O0rv{ATF61V*5)pJ7>k!gI59kM%2!M<0&H zp7Se^nU#!)D)->?7e2(mx1PLdEw;Ovb9*jf*%{%H=nWmkluX-|kK!T=#Vt*DZuc%> z`wFd=&5SJWFc&5}B|!F=*m%&W@+M)@>9o-1luVeM^a_*5q!k{MttvmHW*1zX&Q4BJ zs`~B13vb|~&GFazpD=yQ6X?)<6x_UOV&9o1c=M}|@!JzK5OW97oqTbGKYIK}{Ji}y z=ro3uWTiSB!v>XXzLh2DT{T#o>|*XPE07So7AtlvMQ-+O79X@SN^!|{ufnC)3(6?$ zxf+ElM>#LjICZYo}B+Y~)}jew)*GW~qwFn)gG zDID2edSk`w_1Dif;(rT1l?u8NuATEp_eY`V*k9VV1uivNp2+s$JYHt|POQU~#UGd3 zL1jCS#0PnN$#x&Of+KrO z6eS)w9!!&>XmuKBvPt%z+Xx55t8%b^@fPE&N(Qt0Hm#4vq)%TIV$U_g z#3yzP#zSrHfpWz2(zdNgYuOh;e&NO~+rlEYza83%?0vQ=#Z+39ezg!1VixMu<>K$d zN2E@LH3$iIK)Y9-M&mw>q@@%pD`!q*VaBI#;JL|zF={|>{QKi7$pb10xx)M`nerLl z|MMMWrynrxHNaI2Yyp$0ZdFm7T)M17Y|0tqmejdt@0H;JS(v!bE=(naO2Ymt^G!H9 z!!pa9`6(4vb8!vD(BAz~BeEd&elzy`vk`|1f1e@9$>wJi2i(yI!z#w!cp_c~@~JHWy9X z^ng-%=XD4ziO$gtY3b{6=FBN^Qy8Ky>A87m?OPW?ku{`kOTvXKEBjayJ*4R`K7;)i z=8H~Q3Uc$JvF+eiT-kL5s`IhPcc}`UlN*?0Mb9WlnDC5D;Odz~9QffUeDU3PShR1c zG_Q+Ge1uf(iHuZ(0JMx~ie~)=NRO?FzxS={ZWD3~8)O{#S259Nh)#3Y;(ww7Hn4bsz3W5by!99X^|5dq-{ts5+T(K~^^&qug$GL0Auq!$f`;Y9zsWmH+xaTl-|GOG1=g-ID1@kax>1_OaU_N3K4+>-N zAOxug?rYK%4~=*Li#Le(6SNL*jHWgarYgbo&WsyHz@(^eKll-c&KvJ>L-c8@Xw1xF z;Z}N13_aOBrV=9e*CNG39GWCJwyKpcm5ead}p>zsH zJ0VQjHCq~=wLT$jd!VIfIL=;95xp*UQuNSilEn>h94oe5g5$nuH0;|JPAc1370v{I z{{GLg``8c0J&tG`IT&3BO+E?h`BjQEt3$jy#| zqbdMZs&>VLOMjUAu(<(CNKFO!#hsEjwQBP(tx3fbqkMU>E`c^dw?3X z2Vp?RA;_rF)VQZ?$o8*>jv-7lD!Vum2eH>4`x^&lzhCCb5d8buXLjOD)KZ){rA5Lu zLp5)i+{|Oxcl>`?w&rPk{Z(5$^JpL*8q)~(4eN!+MvTMz559~wpZ$sxn@&oCb+<#e zVEZRuV8*63;@BDPH#g7Lcz?*lSh?j7tXTgyzI*2vJkb7mR0-?{wM!$>nXN8*!a>sG z5Z_+tR{ueKHueR~pY=7SEcyZ?-yD2{K-glcq)!Qg8ZlS&xSYhwLlU&1{L=FHk|Bg+ zI4pKbh-?x#zQhn7Q<6G~#{6ZaliCM!rY^#eiQIK-{(j|We6x244(yo=r8)v{^}P>q zgI+T}Yg-)jI`}RA1iv0VhRc`Mi%Wvj2n+-U_raSzyWpX3C&IYku2nd5HWveWjzw&O zq2fVs)qa@Jt}_z*JS}aPHGzvi!nDonq;g1QiG#uq5A}Tm^#)FmDlpg*jtAuLoP~p@ zw_#Is5)!WNHO(z6JqjuH-_5NGLf!n(wrU7kG;V~tBZtG?x0sUDl?e7@w|wvsX6)XL z#A}9ngZ=@%F?GU&81=>=XU7&A@ulfHQPb!<4jNLW8D z+FIyW%U% z+p`ge&aXjM`dR6-+&+54;C^-#64FlM;N`toBdq9}RojrheFqfTuJEZDZk6lpcHx}A z6|X;wOB*+$TDy*Lyu+B}h5gZ3`}y1WV&^ubC2bQH#0m8y2H}f$CSkynU2Yhgqv7`w z8WSdjD7*?#U09ybAj8rUH}i&0A41S)YiaRI=1&J0@gdVs$w=E03O23(*`M92_yM({MECRQ$5sTJct^ zw`~_5itu9fdFWWZIdU5IFg{y0A8Yp=z^0`ep*?aMK4Ep>5n$Vzg+*~WMvpIM z&%u_(&H|*nxbAW>j)DwCPO*eUJNJB=rHY@=}6Bo&0%8J%qGz|NXZF{yt(K_ z*(n9`g2beUj-ov-niGq%{`vMijF~)GY~C79Y8^9pJl1TQD{YVOFdj~iOqU*) zH4NDI={AAl9;D%goZ@GeHC&tORz$%LJgHF9PZ8` z2=xp=n2#TF#l?}GmxpXkzVtIUpDi0X`5DO0PZT1@B&^~xf)#N0ZiC)6>)@WzkDyME z=B3K=v?S3&lV2J?22rsq;NjZ=Z*=X6&Tqb4;?Kjd-O)wsvH0IP*mUj?@-hu`2i0y3 z@o@9bc;T0i5E@cMJnjydJ9RC_zduwwa=`1oAH<-kGYwBzn3}>G7B_c^eD~S{Oq@PW zbX=vecnZ)0(bkK`i%edN$u3N##9Wvd!ZQ~p>9qXu<~-c@-f%v=C5*K`-T!gS+VYpQ z-8FIyB43iT(aHy2JC|bbsbyHVeIs-lLpm^527RV&PXv2=K$D+?l&n)W>s`L8MQ8l}IR4nW@Q%KtC1Fo{Y^NvDXw<6~ z%huDz#p2L^@8O?=+n~uUG&|pfLKy^a?}i9>_e0|v_0XL3hwXO{H1JmGq3AC7MR7OVEGF?mjUDBhb!?wfX zeCrJ7+Z=k$O@kX?aJ%+saoLz1YF7D0Ir*?BZ_Tl^Rk+LOi$GJUGnDowT!h2dSZPVN7RkI~}G;0d)x{;{T zq5%?5okjeKV~D#Hg^lMf;aJi&Bq!|?_m6R*K7|tk1G?d!rSVzIzzNTF!VN!*B7AabL(Vi4dN+o)6(Yel0^nUs?I5`$G?fxd@$6v*o#cyHb;Uzb8Hk3{^(4$U!3?2I< z+6`$e4ZK@=2#{lvsp+EoJa0GlEt`iwj$c50+-6~>3MQMH+TUQI%|4iRXHx3AUq@5TC@Ab!41BCZ7{TMZ48?D0y_5#lM))N z1wGUIJ3o(=M}8I;t{U(6pMak48|E}wicsMx`5MFew}NHH{`d4xc=>w&~-j*_Diet{CzwpGj??9)G zl|p1M-aiFtt!-DJv^4xP=|058-#E6y#R5aCc0j8LSG4x33g4QI;a9f~sx+$yFJGn9 zn^vn0p&ceduwrb8fCU zX)c&C`c<@j?PcS$cMdZIs}{fbwFD>6cWu@mhtHdi8Rsl9cYUX5jBPR_kNfOlyxTy& zoahhHYho9k0>!L6shb+$(+tDM^?^$XZj$)8JS^U@O{#~c)#RXCM15q}ZfktDeBcsy z5%b;|k86o5je87mY@kL$6_j(7thBxTUz^Vxj}Gb7MtMz;K=B$B9X-Z$#Ul@mLE1@IoQO#g z-gV(|D4iw=2QDAR@eLQzq2B>PM_bFp&jL?|0H}cxKGQNVCyeczwnXfv0H{x^?yI$$sZ@8T3tV@7%YZ4f&+mmzdwZ~^L{|zM)yfs zU@~W7mSV*x&y=Y+DVq$1Z(tkaR{4Wr#vix*iY?zgiJWvpMrBc?T|SO=R}SO+=>m_K zqV(~-PvDK&ufVAs)wK%|eSziMKEWfSzL7lk>p64fAH4eDQ#KJNFX8dGtkK-KWuRGb z6%2TwCEB-DqgFLTd7iSYhJJqEMdWD=<8w+UfB4j`D?Pqbs)$>$BxC3@|9-gy^Y@sp zFIq>28W=BTRb&p-Q9aEDd~tk}c7KKKd-sSJ8#3S= z9KG;J%WimV<^-#jpt@Z+R{6zGqcCH}YN(uU97j8JVK&}->_w#8K%81OEnLGfTcbgU z@cONp&`=tp_a8%6mZ2iPlan_*OFAHQ3%I03ajVms!=#7Bi~o}6EfHRZ99LpYmRWf; zdhFoay`?*#D36K4WLgg)C$Hqf#QVNoV`iKi-ZnKS2`=$v8_uJl->~v$L`SWVwq3ni z;iay_@WaaQ(C496Wzqv=?`+b%$@t>F2M|_mpm8s7A$B>I3(rZRNed7*|P2yA*}oEGD~&&Zd$xT<#3c zPYWtRbq90){dxuF?dNck1TAB{Xq>Oa)NqD~=+fD}r8~sjV=@;es=p*h`T22D#udIdh;wiQzFYNLt)}h zb{*{Ak_EWlW1<-Gz?#OGbh>P;U%E@0Lr@axw5bL!*9hscP7{waXKl00L{X>~evXBQ z*NJbJ0Y_CBp6${f4}AA}iBjKf#RFeW#1l=b3ggLw8@y38SbTUjT=#JSOxvPYIAU;Q zWRc??b6R&CpX zZ}+b+SJ{D0=_a;rQ)Q{lwnV2+Uikcr&kzt$zTFHfEquHA2kcvZz<9*Ap!EO)$rA+d8zQbU_sGL8{_crAEPkO z@BsS<_rQ$VFGyKX_6VO)Kiv1wW9Zhdl^E@Y^q_=Go00YFE7EpVLhBcVNzI!GE#(;Q zFM>7cuB?F<5L6#6ZokdA=Xe-6q6R*C`F(i$v@;$tK&w87H$Hz2iRUUOv8Z_jYdIT` ztIkAb85dM5qy_A<3(d(Ft(ug%94!SiGV4cFESY1$W3OT3L6b$!2|TMiFqp|~JkGwA z6mc8kN@C)LB{6ZH#BB+wd5I{rV&>F!;t05T7+QxH=AD5@w5b<cL&~l=5Z979PYM5yAIuiW;N8hD=tbw?7r%2zi*C{ z+r;F!$k0>G)29IfBFbNr8-a`BNlI61K6oQ+{(b}A+FY*gC#UJ`xx`Eiu3cc@OR<)0 zh%1RncH_}kY8v*%#TRgH|JAE9UkxpLL?XoB5ND!$x0=98jvy=Ut_7ER54+TPtfD+*&ynPSuY1v3Bi$oL`9K)%t ze@NR^iQ?dJ%xK$MDyHhFsPF3me>-&BS_5Vh-v8=QJUn0;oL$SC`^S@3;@jumlA8V5 z62Wb|!rjwgjnEQ}-N#}E)sMrg*GRvI@bjyU8Xe20wqFl2DY+Z?)+(q~I(+@Zm&ngE zB|K0^t`Ne?txS5L|5&k>Yz%W^lC$ziO3W^y?^GK0dR;aStXv}nAT0soOzmrx#hDJL z=fa8o0jdCQ>lWaAiZPv2SW9>J?1csum^f!->hayPe_&jfhtZ|QRJ3e16wR8nMBCO8 z=-Ry#TD9zo*3Ac@TkA=fJ^B@_|8bv0R5uft<~o~6bDu>024$UpcO_kbZ(P+Y2i>SA zQMcBOTk6{@f?YkNV7{Y+?xz18-x+0mD)g0cyaDLO9c= zwIs)uOrZ}2w!zgbCOYltz%~^bD-l-6?vp1h4uo=sBE^}Eaf*z*OYk{SHEk6(9NsJD zErz6l;NTv(eeMI&wxu|@E(V`Wc^yOg-h*epdlCP}{EGO?YfwZ zgMz{XNXqyJi*|m5Y0o~25k2q3w{!lK2&$rKwk^6xU|ORZCTk@x=Ku=e?a60o6@~nurc9}X7-hM1rF6`XAGw`~=#nF|EfcojX)U!Kla3G=g1F91nR~E$pQ5h)vL;PdJ_(Hx8~I z<%9m0GycWzZ@y~WyLpfVo7X6tjHKjcLS2x?6UKA-v;UR}Qq zr=ypMnP|nURj?1!zSB!EXVK4C^uz)bR3Jdzb3#Y-k0>j*ayjKNJa_$U+^ag2xVYiA z9{r`>JdTD;p-rozj89c!+M83*r&0Nd0@uB*jNXap4eU*K^Kkb7+BR(_G_Aq+m9I&c zJOI^+@9aLyO8*rFCzPK&@EkTpm8*xx%scwjsmN%) zadaszyh?&OF_HCUVxl_bg*O#J(H1A-@Wu@moArz{^=8D;h~wEsaMttdD+fNwXRs}~ z%)$u|h(zykhRne0Aty_PkEYDV`}+?gE4BOrO&P4M=NH@yp|yu1qQMw6Z8Z_?yG%yU zep98_`c1|osKx+O(gH7K`lXF{e#sB`X!>hX9~o;gZuB^~`xx47Gw8b}j&atiN6z4T z(Jr!!anEs(oSlX&`>7#p6R^d~?8ojEW3fCrX;f$M!RMdZ%yk*mq&?g{4U-YIDhX%y zR$rekCLV2x=S0z7MmNg6(LQPv&h_jP3=VsxUJae2BL%I<-b16*LV)gFbg3R(! z>RKD!Yeqm*t8!-Ffm^=9#*;gQ5;th0+u9AnxO<04+t5s~^0&o;AQJ=wxMD`T{%AM)za4(L zaJGS%2YcOQZNG2l{DEf|J%`c~(To_g6_-*hw|aWa7!3b_rqXsneiD2x+Q!{*HC)Q)B1^h{1)O})v*(Rl zIBZ{e79a16Do;vI{s^lv6!#CDgSqc+!iYychTEVg;nrgY+*%ESdqfks2L{2Vc73=s z>HwEk1K`?e99)Mzi^$u)#`Fnq;Gv#lOxBLN@Fc!Ib{>nLULfsT3N{~{+^dhc_y(6s zLCzWY#9ZB?t8CWc>Ek zVLZ2Bj@0DT2r4_SSCSXgvMa4ZN>zYs5fjZ+5)-pdq#MA_|M~gnaQH9Fm1C`1ZfG82 z2&8I?FCaU&a^g%}yL2F{Y<-)HTaX~dVbb=spb_(+zdrvQX{lvJKiRkE?jCpG$%j8h zliOc_hqLtyAKYq&qyCf+@tm--WS0wFgrDu)i|B*ax#N1=I}M&b24XKQ$-vR%>T{`d z9(pId{2Ys%wk4(D!ns?_pC{0~XCq8*6e6|sFvpq8M{#UXxdwI>f}cOoBBboQ<8xvW zf4b@eO$;0CIv!fv~f3AZCf3g_s&2|Y`yXqpIeRUQJ3r)``Nr^e?Oebar zguG@+ap6?~t|TU2Y+iIu8AwdL5qy=P(E=~am4ZnVqmE|97!1A-_q~Arb^7wN@CZomlkKtZJUdK^!_>Y^9hugP> zv#X)@*!c@<@#PEeT72z@b{)JC=2Nx^D=Mo99yw)cvsDT9YZ}^^ZLBSKM^-n|)AwW3 z$G;o*ZY^}}HxwaPG@Hj3sc zxE8J?CW1GGrYdg&EhhR_c}%LCw=@2tRq3PM#tjp*enTsTq#Tvh-rUykPB@6%+_J8w zUhaWXp!izQi($24{XfXhGPK@yajS!edk%sB$Y-y5K`M%X-cO=oJww%SrhTuCPsG~K z49yBH0TW0%)e+M{BQmls!}suN<6hOH+1Q8S7j$L6Ao~R~B!Aqs7kT-&o*o(gztQMX zpCRD#BlbYDA+7pm(P;b>cpHlfGrGKL&%O#Tf2b%nzWWPO(hOO)&Mvjky-hcGh-ZQm zURp`CmTneD4(4IbgAXG*LGElV={nA!8CPOwTJtQtGPjI^O2yUmFfTl*hjAvRjGO## zTCElra#L{rl;vi9x3R4e>T8(w=j3F-JI?kCZA)@4KwVU3N%(pOUUwFiF6KgOV#v3K zG1jGFZv>Bd9!^$RGnRmEy?a_VlPX;knQ8lQc*_>6;vR#$aG|on`YS3pgOW@>E7gO7 z$d(w}tc_#=I||NSI)+s*|7hI1b-?l6FTAlBTMdmz3@9v4!lkn}Z&lZ66ohs)%R1_w z$&5$B1%r2?D!^qHD-Z87HGK>VYma(kCP{ImhII{brJKdRJ$ZQQzDKd+5`d13UJEn@w${GC22s98DtW0uv}L#NVfr ztg6`DcwkR>`!$!gOLaLoo>q3A)qxs!Jc}S>?r}Z~l+=5)lw?w3vIF%WLmSkA3Mo|)uBMH7@nvG7`AR1*XAKfL z=`FZ+gQ*|#Zt+{=hIKDTn>be2LLSQC>w^$@4y zX*^>8h>72cU!VKLs`#EGpzFk5s2OmjKdS_L4%^0E-emklgoJc7?35N`#lPE7WJ*}8 zJkXKG5|)xJNNLN)JlsEXCXU7~5z5XqD5&3<*~Q|3Oao*o zb7j3!HQ`!j0&fI|plmUj{BK&F7JE}ttt$7@@4;d4@iU}PA5F@IceHIAovHOMrX&WU zu36QDE5XICC(lM3_k;!DUk9GGDp2Ls3PjXt3Re%q-(ZH(;-8jF+m?WFra<>HzXeO} zR5#r2cm_2?9H%V?OG|xy_z3oItemXS>Vk`_Gitm1H<%ieRu*r)8PtmO!0-lTC6JD% z$Km9DLxx;sV(FX9uq8#PALHb)U1VK!zjwIwEZi)X|8y3!W==zF@@g@eHHC|)?>Jjz z(3!3jt~((q$t8!BxKb74TEt`?XQF?VR+AxZTueJt@=_4B)$$-~&B!1$3NmE6Wu_g4 zcZ_WV_ywBs0j1h?%Mu2zgjxe6x$w|`hump64hWV9Q zfW;+RxTtwsst+txG->dJ|D-G1GtyJH;`_Hhk}3(@O7QjmuXiA$z%~n^`Be4nHw+#= zhP6X^*$1)WgU>5in{H^<@aA0Y{LKsJuEe7+K901kty29@ zBe+59h}jQv1|BaUSy3USYQnYdHqBPv6q%TKqf84z^0Q+QyMB{Zu8}Dnnu(F_D`u~$ z_~&AZP1|4;YRZotC$DR#iZin)v#^Yq0(`C?kFo`vBO0TYS==kVu)t!M2!q@;Ld%M3 zYBg$jrP?O*>SmxHG97K|{&xkwy@Hu#%cJ*U@tdn|8J8Z%hNnLZw3LUg#KKvR1!y&K zI2zP2tfiw@;&BXQ-N3E&>H|p_82nsdSTsxa` z83FrON!zwS@D+7P@^Y>n=%yzuub5W40e%73cKg{9L9S-*I8`2st{Ea}Z=lmV>$Qw4BwDQogpsL8e}HQmd( z?87>pnEpB6ZPn%S685p;H@xWRQZX=qoTFs8V%@zkqzn!O>OvsOkp)GdCRufZ{we5@xj{F(5MXS z^a6tWVCub(S>1h_F{(4iv+>NxIe6&RhmoAd))j^yN^6OeY9qLDPzVzpNJ^Z1Q`eEO z6jG{YT#uM|bIgf}?mXsiO71+3n1O7KI%riDh>>$9BD{tnkeZNq82-C#Q!b`Bw6T~~ zmBp1*1=l7#KAh&nR8U3B*-J~y{*I@cpT)C4dLA;&QXt(u1E8vDKmCz(K0%m1XpCtQ zu_J(W2S45PH1$J#tndyYpy<(Cn3N z9J5+>B_-i(^tIzTSHj)X6;oP?C&SrYYR0$zASWz04^86^aB(XmrsCqlYbqR9@en$j z5mTO}>2Ws+jTtfddiV&d$+&X^a0Zy3&*#PfPqzT*J*vk2Xeo00O~uIOELCKIhO-kC zh2`j}{mHjR!mk_uG%!0G`IO7qM8;@QfT1q4(>NzP?p`iM4 zBlXA|Ma?Z+O}vq&l!uIr<2ba)y5f%G9-fSVAVcxVW64M1ci2>;s=`o(M#9sh%-62c z6koGUKWAVrX2g`ATUI2pGLc$PM&QBT0Tx?Kdhx90sLL#-Ag{_#Z?6a%Pnm1H*i9Ypa6kDe!oF@|K~5H zzk5CKex;-WkKXkQ+BO@3d7sZfV%j=s&6*4s&-U;Lc@y4s*1+BWL1>H0O@x_+N1et8 zliGr3;eeyy*2Im}BX1PldDM(DF>z(IH2v48Q?TvO0jpMnw;9|T1M9Q*pcAyk=dgFH zZK@X6@G0wRsnKh$nK8&&n2S?ct+#?)TJx0)W=UlgmSuv~2?>LTg$>QpQ;JYnQbtU* zeQdDGuo9pOi^RRX`oP8Ae&S!2AS>fv%=vjC-kSKR@rdJqgpic@Ut$XN351^wjB2i- z3-UL({YX;HI!EB?LH$kbT#E7z;pg+|_+i%D(!Mn~AE(FD6Q9J!=)b86q+|a6z47IO zAJMVxwJmQhKRR?I4@0^=i`FeV;^$?LASZv9={w-;1y`T(@T&DYJj1?&t2f)Q8=iNq z+SE3NBqWm(wHGJf6{J@Ml>tlU?Yv>Uc?{gpydW`2GYh)&b|xp`%t_1LdBmjAW280{ z#DR;a;B)-u3%fQ6DGLH;>eNWNMngloI70o(tg9>)$7kh2zxNLms|*T+0p!7bI$7+d zIJj&-3i8W*`Sk+r(_$iuMn8_;joInciok#!Kvk3hwVA8cQ9xo!O)(TXHAh5X5PWSs zOlaZP5RcgC*Y3rG?OTfp2q%}qVvGL=i{j2>$xFXU`!@nDoejUo;I6SVvHJKQ;()>B z=Izr3AHVV;h71dqyc$=6?z&Ij+=ezyreNT}R@fi?4YV4zNix96r5@Y^pN4nsb?^v& z5zcN`#yrd2U+yrYh<}{clH=X{TO5`W6K52hwlmnlTx`=6J~Q(eqJCN|#X>9vOZIoI zXJ~m{TyO#>w^}x+uZNZm`F)1d$;!QelPj)lm2QBiuvFSamL(4)=Ov?jOw{%`cVUmI z*_n@jTeKTsOue-L$MznCPHiZq!(x{8>bI7*?GdF;&geGb5hKSOf>uooR)8Zx zC!WmQy#JOTY9UM<#!@Q-fquZuk6*#SHsX3Y3A3z3gX2*v@zLfj`0K^Rl0|bZFh&3C zhZo@fxzFLmg=OO38vN~^-tF+-+z&BsX3KJk82tRkKTl&y-xtuL$w16|dmNI|*Gk2V z4RC?0*8pJ|eFg72OW@`=1x{v3Q0CC7_8Ye((4WHDG+$+FNpa)Z5iB7lLU!konD``Y z7a|S8qQWHnxpzCvs{Gs#Ok=%s;1IZb8|vq6zi%F@>(gX^M2cgu2Mnen(irfgQ|>C+rV zUB(*s>=OkIJK&y9T}(l8YvAqQ5w0F(wb`l)wE{yh@PAW`dyWHIOoi$zmPR=FV&EN> zJ4Wg@^2dvFW}{EL(cQvbcxvQJ=+<@$CONR6SVBxx?<674F61~9r|_n^(c#2s zZ2QT&1+!!3-GSEOhSc$*yu*mw^pmt*Q54p03|}ukx%jIVnx&hd%hVC@@Mzkxx{m9Ff4|#|{|$Q%o!d;owC5hecRM~s;^nnc8xI*S9vuY{y)CSx z)lxEplWUmqof|`29A(^+^cE{UP@j>QIO$fh@$3wiT1;|w9?f4q3G_=b@F9^?LEcGh zTKTtC9WK2*oiS@bZ(-%odML%l__Of6XsC}>QM9gI)-)|XClRNQUp;x=#N0vSabojg+%bAOX1zHFKkWV*7cZ@qlA=s-@`0Q0M0nL&49^-1;OfQtjMh(D zbeb}P6JV^I#3aX;6k@V7SV~N^nmDsyu#gU3y7OooOU^Q#PK(uX=Wuklb!AG%zI-2rS;!S+Z9FE?qZg1ERR!Ea1sX>uaUN|gugd%d$(@l#~U&bE+=nC!fxAk zIVI6;-u^8v#BDMh5WnjVp)@-6zINqvb5QYY`*)Mn!;0|mX%4>*!=!CTKz_#|n9{1O z`>rKWt71`D>V$&!15o?XPw?c-w^1vMSyh!q8%`Kpw-$Pga16pXM`kv#e(MJD1X8~m z;NjC6qo+->KIcl$PWR0ykApq0XWnhd zE4%Z^LcGBevNQK#c&O((D-st0}L^D8k~H80a^DYS_O?Xj0;_d&kGv6(vZi)ZkByj2MF3ADkt{ z*DS%Mbl{&e#xA=8wE`QVs1+BnIu86My@ZC5malE1JLqDT6P!v76IfN3;ke_~Md;mh zs(8~+I%Z3F`nJWu0mCf5C-#J$e;&l{WHwnXLyKC?&}XuBR;n$)HBR?_b}zpE_EWqy zW)|AFm?*BTn3P;&LO~KrNn8Vs)J59J2~Pfkj7AGG`dA_(kKotf;>Dg{|Lr$rElM=3 z@L}Q}>M`mq^Z1fNOm+rqh>17PJUfrXM5{@1;1#R#@N?7=T)u2w9C5%+w4O8{EB`s_8q-ggQt#@^W=>Ei$VO=;?#DbH2gdy=TJB)3&a?X0;k)g- zyzoHxj#3bsGv?(n$8O4M>es53_@Rb@ou79C2fw>|Dmv3rAO3o_P)_oUz7(fVuE4Ly zcS5tr`eX>EoaY`{gRj4M99xboF{Ou-lpgAPJ0{H=FYQ|brpf;J$FJf`4d$M^S2Kil z7%6Q#8nXLM$JiDf#YnSk=37*xhD&i7G0E_`=M6kD;YpjNqSH#B)}bu|EcZ0p1(^N% z>4Ps}*J*YEmqQ~?=-j*$Mm%@7ali6F7c;X(*`gvf130iF4Ie)9H>UM}30+%G$0MJ7 zj4yT_!lw0qA^j4ox?B;yaS9S-Gy$GrUl_^gB|%0##2b}ow(3+zjaw4v?U5}eCGo*d zVXgC?pJxzYezs{pz-ohGb>g;oV%FUI9z$cP7#oXnVz6UKME2X!8; zm|M*wCl%klekTfY4YSEM^>4%4-HQ-pFpU0#Q%5r~V)Q-8&M~C0lGN%%4#numP&DZM z7+mW%la5~trFtC-_x*)+2e;s8;(>BmL?JZ>$=66o;#jG;2PUlM zFs%7|0a|vnKCQjFz*OodM!tZ*4*eml@v>4{0U>=cw{I`Z_LhU+Zm_`KDHv`(0|fyI@#MmK zYv#i@kK79bq0va|z3AOxQ$R8%)ATs8W@eHVX=jv`XnB;5btGe}6K;xs5b5|m$HXVed@{$s1!k5ngE7w(a#OC9F zi@j^>0WP|pGyVZ&4!X~H);NspG&hE{GWz=7g5#y;9Q7^WIULA$3gc-&?DXz3Y2xH?^ z4(uLCzj4J)Zv~Zto0%{2-*QzVod+Z<=G;275m%bl6}{UIMr7k__|L8aUw=0^pSXlA zF~_A@PD+6q14C*;9YOB14w~>LP@UL=%bA9CQG)X@xYsCzM_${{5ftcx@iWFi9qo#< zS*0jerHIGMvWXN^WWz#xU~J35c>eh}@Zg)%5D{U005|qx3GSMHJM_9_;}P+@oArR# zpnHsa4h$zJ)b*=@)8~_ga=f`IHz95#dJ=5nU`Z3 zI!5-v3&N^qHkmymJ5!5S@BS3?zJ3QO>Hmr&H`1SK=(Xv>!nuIF>@&zrk48pnG}11g zLh7Yc$ViJut~g$pe;(?h1YreTLWwR*d@VyR7V2hacR0H>hKpwpxcZEMo8O&q4|oFZ zfe(uPQQ~-aIJC&ZXB)?T|FoLy_+)4N{8J)bc&YM)3tei6UV zUx=>{L?Jz6yBNLK(Vb4-hRO@hVj|?^A;u+>OoWK*=wy85zLR)Q^1?eCKO=okY)hrN z_&Kr7c_u$6eUI6z{J#8I2SQQmV<^lu)y?Fhz%PU_-Ic3&NlrH&GzzL56(uJAJEk`< za?F))9fUA#Y@iTn9vT$V1?&D;g06$CYlE=xsh=?Whc_j~<>D5O=O#Rf++hzH-?bcG zsqt9))V_m}T8x=|Z^>ufo1>mm@DV1)Ab~sC5M>Db*v;KOCV= znxpNQo@mr6#NtX0*8;Pf9v(jz8;^cdP6;(?ejBEB>Wl1tccH|^&ZqYl!lk$nd%t)B zhhpf`yE>j5d_M}NerVjgF<3F=pWolZiL=Ya-u12A+O(*ZoUYAPNzOZ}IRxJK? z1vVz9AthrZQlWT2p&_1jadqnPq0nlhzcUtn#(}lN82n%zv z6a_PjEAF>fP-(cC5lv0WxDu_VmO^MXkwK)uiid{>#eqo&>yapI!(0=`M&RD%_(w#_XpL)y8l)sf#UgQkG%lZy#qqd{*c_LF z254if(qIKV!y4gId?K>5Pm3Q~g43Dla5&iOntqH7&Zcb`MC1wiimhRjC!at}RUVi6gL^KLC9-2macQJ%4V*@*kGr%Wsz8ldqOw;nH>Zd&gF+joN|37q%j&;4Dh? zSL>$iEX8g-L*9q0_aHbKOKvH+F-nS$peV=SF%cj!?G?flak}(KO4en~D5x^rY_cr1 zDZhi-lury_nI~@WJC$Z5 zAS;Urywm`kkq-Oh>41V*U6n|q{)UR zqB%;P9n00{Rksx~W8;x2NcT!a)M|y03bgkquHOd!F$ZxzwcLVA69zRIfJa`N4wp)p zlW@`L#-EPht2bW3XN#6&>ALk;wPU-OVC};C6T7e}`d=JOIDmw-Q^?LajiQ2B>3)`$ zT)ogRoIM2*jfO|at8nw3ASO~(KUN(by24dZOYJfwFgs?a5L!wMaPq=io-w1KN?{9P zBKWA2l=$>e!g-8V)8(QfVKv>3i29bh;;1-6w0>B<=dhGDq%KZ`e|iCex{oq`!g6Tp zcR)b;A)HI1OW9z7oG-3}q~p7hy)yxgdbEbSSLFwE8MBJspNOBH`UrEr{25zgR*RW^ zS#}!N?+h5&4h=l?=Z~exo5!3*gC6OrB=){a#Zjh!yaZv(d(@tPd%n4jP zxEBqD((|to39Wxwi+Ov7&f6bdijr|KF*-73~oDBM2;2afC#*Q$Jc z#?7-aUVi*3wCvHqc*M3~QPuZfcm>N3uSZ7O5#;5Zg{m;#kohLvH_MX0oZTA0&Hp}l z1ivnbX)v5z6t`VPQJk|tTz`}GOd=vNF@=*sC#H!j@g+OO%@UIgnV6Um!B-|j7^m>L zxDZu$;ZKElgCufpm>t5A^7wS7iK<>ZI@ zO-5oy>zWwVwl^OA;05%UJPe&D^hej*`(xCdqjAS=qtHy{FN~%@r0YUZtm1QJc&2Ct z=I5Wok;EvRIv$P4_(KS-Q4g8`%X`&S7gUYABI4p+M5mmUEJ7Kzf?J|a&x+Ns^C-^4 zv8cUJtIFEZ6D`8~_<>rcf~5hY7N=?x9sR?1b(vIwWN}At#Ts zb2Um!^O2PigXjx+IGW>xgHZ>NynH*(N978uqX9g<-M~aRT5s04Aqg&&jaqdG-@URB zb6@=d->zJa&Cy$tlVx5Z!U_ICy)movK+JyVZrt?LK>F{_TP z9oz$-5%Pf8?+Pc^n#K?P{|47F>WkJulk>UQ&JaY+O6As}RCfugoF7p_SA~Iq=rdwh#7ix;mH|#4+@bg=5bw9k7oB^{h_a{TGYsR%nM`Z z;>&H{iyfiRoqX|N-@D*)*E_~fSO?b8D_Z_G{){(dXPbYYJ2-fuy%Fh>KUwmG0NOoEQ681+U?QATvE+%19Oe{{MUq)`h z`Eqsh%;DkP8bg~8z(X(1MxP-ytR_ApD?^J7?=QxZ?K`nAB@uZ!Vlr569gj2og1ci< z>#p#cF%Km!m1HW|0IG$LV&U#3hI=f~yTxR*edar9+ggM!orgCz{3Hypve->n=n(AM zvlw;iRL(Vsja6gpsC$uUtXg6Ymgb>ib$0J4$Va@ou|m2n9=lr)CF&E<7OsR&wNdP- z;Ox-`p26=3!nVF^vre@cIZ3nSb{BL{u;ChAc_)O>YT^JUM>#qAGWy=L9Dkdjb<0fX*C%4I_VP`}`wc<|oWD_lp9@Qyx! z(_8+;w%BMC6dV=9lar@w!3oZ;VQ_P+32(PRX$Gbf1fbOyi&0&Sl2Rc$m3RQecSxmi zt})h~JP{Do6(gIp!R^nyfPRAl;AsPI4Z8;KUmJsM3;)2Mr_UofEy}p4*a$)9EhC3w z>o4WzI~DFhtaN6@Ox6sV zoKKOPW*S3$FuDGKS$Xue(rThBPa!5dg)LoqyveflM3qbPnFJ-NU!5)&M~XPEbmwW%wr)H4brJX1O0XW^*7Zw}WtQpZ?biwKe)bM(*Q=bY zw$yZB&g-8cE&H(XNE!6SQPKn9;@(31xQZ#*TNQfsacJ|tggW~T=vBMKGe(!R6I{Id z!87CyxcV~rrQ$OQP42hQs7 zX;K%dlTdiV6+DK{6rOZ3n+yGaYc$r-u^tx>zr7<3pk z3EgY7fwL+I`jSxSoPETrm}uQ7W^gSyyQ4vqvADBsD?B{&emwC0EHrExRA~!4c|F+? zxYO9K7&U4n+~b^Z;<5%>jp5;Bc34z=G&%=20wUS5#J&)qcSG#uOcdmuN6-45;M#G7 z@g2+I8-EO2w*DohVaw>>crYG*WpbsYh%>R}w-BR}IUW4EfIv;L+QziH3 z&7lOv+3$(Tu<6;oD1_D$U3jeaK)qC!ZKJ^MU~6KMO=GG7nVeX%h|^VPonDL6X<6vo zbsXw8uzUu~xKzWM4X}K(m}O~FBqb1&R*G(IyIQ@r>001Appv>xQM=bLv=}-G9Y)N= zfK~(1(z^kCJ%loMju4h!1iZZJ!_%t)JiNuL5O2SR@DFT;;LsM(>U~YA&yn><;h7#C z@zk&L(YRyXiW+wnLST>^`rY0eeYy_Eg~RE%n0;BCV3A~To;`mFZHJqxE!zz=bsE9{ z)E1mePs5ltJyF!c`UTZa+kVH66RZwzNValui^N-VUqSQE41L-jhxTOSnRh-wem-0K z7|^=)NQ~$_4!!(Kkm}I@+1cr0yrv7QV7riQg1G9y*^O(^Z)FHIP^yvK+ojq#LCkBR z6&8#-`+Z?WZWq^sEszA9{NUz0PU>9i=68o6B-`}HBHvn{P@yZJoV*M&}$7=gqK$b z-1|k@qF#GK@V`G{!Pd37f8;de^q62gYB_eieh*G2uoRjB&BDiF`_W(FQwhZqxS;mI z!LzYEimmt!lkb3#K6s-4U}TQ`pS15AwG~;X7Gqg-DzZ{pNYqG}9-ZMH_=K>aSpV4m zK-aY)=ELM2dc1^gFVP9Qp^oRa;aI8UO;8J>VF;&K{5!GlB&3AvR<9ktaq;LN#ok!S zLMj?-WAL}!a83 zRu)?l{9K#7bW>{N79t5Dj?Htn`=7OTIUEybTAYW2YzKfMKizv?AS zoxTKbyz&8JQ|QIu6?4Pwqh5q}Gxo}{Kj>;S5aVJyeEL3s5_jv^ghhKdZu&{8@FBs; z2hU7=7`?}`NuBNS+uNJ)+3Igh>H1F2p}4nmcc|`oUD_u!wHv^-^$7ISrbF!!g4FB+ z@%db#$WtWcS*lMElbGh>-})Kosy+fqB$j5ASJpulXkpWD) zGkC+4iVt><${;3^l001^?Qrx_L$?ybv(HZaGby2=I+4Bn2OK%|lQ?k~+}CXsM$H^pp?H!;%x}y5w)uq2lWqILXD#qVA)~eO`4cga(*U3xR7Mmh1qSUC9ke^zJ20ezMw3^oH zIP0}o@x$v-701KZzcZeE{xvjdW&H!lg}I-9dJeyS@B#u0j-%6miF0)Xx7+Apleh)7KE*!sm+k2_1L=f2V~?Ll4Cd@n$qnK+Lgru0nxF+NYuXj zC29LsgKy#~Y~1#z$ujixX@_^e`~(q^wpWNt7aYX|onfkkl|iMt4XREL^;L zNbWjzuQfCmtRCnE$rtA>Gf#YA`&!mD5}1C=Tn?=JlfyEm7m_gVs`{yzW3e6(sf)Nr6QAHN;l4d1=j?!{ACaMr1@>}Ztu z4JyehxntxQ_*rS?WQ!}b@Tu=H_mh{zgYS$7S~kUq$A`eBg42ebtIp!|Eq>=vaaJErKxj!C6whIik4W7#6Mk zSt>hJmGIm6EmBhVOWQSrI%3FUH(nUZ$y`w}keLIV5z6k{na|?Y6@Q?pFb-qew#LNw zpNDq^!wm&R!1o_~imY@)JLyumVtmV+><)67JTme7?0ok3fecCD6FrtbZNah3aZO@L7nxM zuzKZshzf25oob(PO9CgyoETHPaDb4QsFxJjIV#JQXATYoc`(pn$ETRpT&30+G`TP3 zU4~bjJ9$&pm-iaRf6nt3QJibF~KWkmECStS8}rxHEZE&duA+3eKp zjJtZ>feADFT=g8X^MLoKJc3Pst-|ib|6s?`HCQFKKly$+{)s*ebzvd~bRC5IW<84z zy;%R&dN8Ga=_gz9*{X%6BBr4s1JL7%53J5|a};>R9K_~5YfaU}BWv}?{I8#uY8l!J z9K3TTe}q*BS-(#*_5}gf#>2yBLe<6kg}_&W?!CxfqtQdB#hFYe6q_oemkO)tG_(aP z#F%o2i-)vU-SC!zt3ZQdLF8r&Ygx*L6(8uWkrN*bH&B6HqdJI*f0HvAK4r!%y+WKw zQ=xriPc&*}-6FdVBYPrd-6@<&jS*k2M{;%o8mFhjv(pgcCn^s?+ke68{mZ3nCBMM# zc=OYDQRf<2hB|S^s)8UKi;lu?N8_4D|YZbniYIcl92HHe>rBsDg=0 zm(qdRPri)QOhe0ZC+A??KkOdpTH8lDePtkE=MUI)a)TIQVnXcffrt9shGBQww*CC# zFZbe&A6_w}MoVzPsJ6Gk_s)67eH*~Z86_=yp=*cU@K4Rhh3p(*DanOsHP9FDgpHe;2GPKA~FNSZn27Zm2 zBEa9-D(jI64`2Lm5ti@atlqFpqjls6)V+Vcw0)~$|At?Xc-fG)?H||^FTM8~!s}Vy zUgbuJJzs=7?z#^eHP_A=P`Bm?^mzJnl)BnJ3smP5fZ*<<&^NFS5-t@ZuaK$b23=ZO zk^^1QKav$CrI%Mh>a`<5SFl7({tg-Uq>IKharR+i7=iv!#+l?vvI09sRT7iAkMCC%WhH3Sv?+~`vTNzwww{Bs8=&gyXRHts(F1^Wx_Y^C^qg| zFO(6x69c^)_Qg|gOtxz5Tn}`=4)1*r(sK;)I)W`r?tAndsKY9^tdKUcIa>A|jiy<} zIGb4rz1FlYM_S~n&0i)q3c%_ScC-GenxW4AKujQ53Qq!OXLRE+uFZwDtoC5?@hPxx zR5dY~zw&Q$yO|I^S<=GW=qwdl__swfE9;vz>sk+zfcYdx;JwNoEH~_;33_{af7r1%VM1gBP6y~Sm zLeg#=NGn0|{_Q9&j6kza*N-{TbMe2)^Km@>Pva41jBYayUZYqE>efZzjz!pbe6z45 z@+G(Bv(p|yyFs=o^HV71)^`ti21nv}O$|fV$;BUc4}Al>^&5~8>K zj5EoGa!Wyh-SFwCW#pImHfCjZkqu<=Gq$>2)1y5l)b?q||X-DIcT^MVM zJsF^ezIZ=$g{#E2CtN(5idQQh9tQ{8XcQNj)_pO#it!}|omo+wSu09>$$?PS#6-yC zBo7ikJya*A_85A-8fSAISY~QyJx0+j9<5ZBBn~rEH)r0 zcpzpx@usj`Y8sDJA4)yE&}7hUs991Maq-8^GWU!SlQDhqZYfE^$=MUmuJy&!W&0!z z`$7qdGv9(%Yp8M}K$jhfNr^8xD5_&hMM9#f&2q%@!dph{S+>FC1J27ihF5=EghT(z zYdS1Pzp)K)&*&Lw-HNdQ7io>tUq@D;_@|pM$>SRrg_D>0(}k7eQ6IJIH?%1!Sqs)~ z8(3d3my;om)-Jpra=TSc45T`da`_^(YJ;`p@81etMzTh5#lcRr&)ohNe*X7EX}rkr z3Fv|c$KQp*I`&2M^(IWi{gYmTuEZThs-pjhT6F=cjQJ=?9u7_RyHbVlTNQfs2`SEK z2DD5NJ1PqOrs(D(Cl6*go6ll>0Q3O3D3@c5rsw8yM}Ln zdH^2i+#B_qjFI+Po@dd%6>wSnrnFyCc%0dR>}*cLgwpbjfL~p9^Qb;BP29&_hro~y zQXi;+5uwI+ZfzV&;+)2yYr_3P;90@MG+w{Go0IV5xQFoD?ym$Xm02Ke?#*!Dh43#Pa>b$>DbZH8T)9!<=aGkhpw^r8&uJJNU zdf*1vFOitYR;iyjFf9?NOwJ7Hz==y{5T}--wky&=K2_7z5Fc+ zs8hE+oc!t-4&36%6G~S`)36$FtAMvEUl_jMJ@pHw-~R;G9{;l(DS3Fc!aa|E30*ha zxPNU4*R%Wa#p4a3(U}N_&kGk?(Z5Wl;)paUV~L=ORZ!(05?0ha6eJB3R@4W=(qP3B z>nDBo2AyiBM8rl=Q?OtwZzg?v)G>~KlK;<89Wf?za1hBt@b}R4ZY_jSW@nwQ7||J7 zsHd-oHhrvfr!ecS#jt+R?B0RnIX=*+E=sKiqLQ=FLp-~>CY_C+v>ZM$+pse(T3BG& zlFR2nsu~{g&S=`R9lWcf>W{c`OMlvg!|_{CuR(h>Y~CN5+V)tP+O{~h@eg51GlU=} zAVcm(&(VUEEp(|;hwWZ_7SGO{hwnBoLSX@ux8xz{ggT+aaM#Pfz^R`7#p%2@eUJCP zm?10#Q^D9au+Do%OM~#Y8e+K$k9eqWj=xH@xnQ#~s;t8gD)_AM=0t8K)E1qSP$&j_Z{AHoXJ==Y0>o zx9!)y-3*+C;`LnoGCtk#GgL*UYW&p3bb)gPFtcB{&5J@`GgH)PMbYQUtPoxuK4TI| z(N%6BsRBV#F(^@O5+o(8MlHWWFRn#xLG<>&E=pgt9qNLO=4s+2=mSEy=!@%@7%nie zmIB8>6{^tdf)5NwmN_|byMYiXg`5omcW#cuhzS!A7$7}c79%jk3t@qcp-+uQd~PU; zixQ+Fr-u^~(LOsF&TR)8KY3$tO-YvqjS+YDvLGi`94HmDwj!kF#$es9eK@${7;1$! zMr5;4i)ZjRivX7ptlY8_>=C5VCgI$^ML4&0CnDehug2DQgLMQXZd;Aq0z>jch;t-H zOc`*^9$2j8zISyr<~{g2=Kr(^hhx_X>$!YVji*mr+|~a+)OldOP1n5H0!)pM|79+I zIj{-&Vw@R4Wy=9b>wxv(NJ1nfS_xdY!^KW~C5us6lc+f13l?$>l2VCD+j*1}ZxbZ- z3-s!}DAh4HNqo>HR4}*wt&h^={|Hq`bEP|v*MJ@v#g*q^uqR@Yg`i63 zU}9zglcl99xhAMK+I1O#@QeZ^6gmm3$&d`e?_*h^Ra=fBb!#*lwr+>uTDGg(b}iIt8jif( z7Z9B-l#7OXsT9SUM4X6Ti`X5z(IWdIT$^>Qwhq6QfuOUy5uFkxZ5JX0O(MFZQFDK3 zGQhP+j31rcc^(U2`WExv_#R*Xy%47oH%b<6*)RiK-5a1^^NAQW=UX^;=x;n~KM2^r z8mpIogWadrpj4kB#a)e{sYgPhr9@JaEd&PrnO4a68E>LcK{LsS%9^e|DsF!6IwYlm zUV9m(nq$xvtrZ0DD@xS+#c$yFa)z@jlWeM6eUw6#!D`~BG;zA@NYr#^C~4vj411ay z%EV+oyWzlUBSeZfm6Yg_o_`hx*T!Syq_Obv=0_|?jR-$9>opkeM-CxYUkCX)=Y+MB zhZC2RP$%gef?D@OiJN8a3%!Roe7g)o&xmI5D$pXiphP^#hE@|)euooJV(t1fa65Jp zZASE{RQH~{_{j}=4MAjb5iY2Fk(J9eg&JuAW^wTa9KNt0aeEJ;ZFU@78nzSXSbZHv zZG}j2j=RsqNb7i5;Ug+45pg?Huz6EF7JvFX=D+tHzW)7tY(KdIX*tnSnJROzsbibE zBQfpCj}bU*I!fH_tKOWeDi6L(U&nVlHX<=`tvF&JBr{@?6Ncz}lSzpd5iJB}hETY! zfvzkP6^V$#K}wS1AfY8iQsX{ri|{$S3Q1Aa5-G9DgsyNkv<1Hj%j&Q=mL|rTt1zZW zRMvSiZWbk)Gf?OL3cb(^0<<7`POOh7Uk{SA^riyGf;|$GOinUEQKK*!f}`G&b$jNy|;THR33Kfn#Gh7X7Lku!)b2}4fiS)tqtaV9MRzKO92Z{8JpPs>}-YHQR* zXpgZNBFG`N$VZTZc*aW&ODFOKiT-=;0* zjPnIPC@M;l$Vq4qu^op4aYvDGHSIUu?D3=;(oCj(d&!^gipTGr`_$+ta&h#Aj7=Wl=M^+CJ;#`YCoV(HGxI)O!bOhlBxbN)vN&p|Fj z#PgQ*hyn+KJrNTjlM-J!)1h!*CMS+`K~6d(W}d>C?Kv1aaR@xEGyvE{uGNUX2#U=? zT#+}@GsN>U@Xz#D26Cz8CR8Wa?q$KJTpxEy^7 zopaAa+o*H3mf5K&^qxLw5!n>yV=khw(2$*XRXF*=!>0-A*6x8jI}AdPakEkTwuezt zw~6rs_5-hsM5q_Pg70^3MttIWF<~+!W;nalLCe;|;qToPX&HtJ9|C+9I0NFzlU!tL z$=pJapCghK-CS3a6fG$d8c9la_c10#cOL^cBrNu*rw^FlMY#r1sZgp*GL}VJE{JNe zRAjbPpA9Ex4?$G)8rqVmN>QBkI6~9)Gg}2ZOy46CpnUGBL@* zho+32^jgHFoc z1E&Nv;tM^XDdzJkF0HWWa&pgLZ+tvdF;VCs9&S|w$7{W=2?#^y+L7oYE~am-rtq!N z8ovI`5ER-14H~sYkH)Pqs!40~>ogXTcfN>{4#S`gwa;~4AsfEL->ZJb=2L5-E;3eW zaCS#{*bvN{{upMz_5>yj>xq9h9T#Jio+^nTsO?E2d^V_vNlYZ5t0`monUYD0u0DRA zBt>^0iH+YuO>a(8WI{RPk3=TLe1#C4G@6Kt-VB+jSRP2PiHBab2RdQdX!HLE?+{#{06uz2pWHNMEm$wFyqfs}BuHz6NhM-$Luc;A~Y{CXou1l1@2 zBkvrB9?iNz6|X^Dp3c!8?>!Q3DRwEw^kv0O2z;23Ks3%g!D9_S4$1CdN02YxVy`3 zc>AZ%FlbUc)T-rC z&6yF`I1y68*Vs!3v2(>~bnQDFHEOz90$^=u5FE zh`WH+-aZvtv`QAS?}C~y5X*-#2M*6wrhdqi+3VB*JL$u zRV*zUS~(yGe&p{Z+4Aoza6s5AF%j~>mdQzeaQMoH!eluyTkmpiG}f(*LbK3TXw;^b z^blKv;4m-rm^c*S*&c|?bVWvXlKATdNXv@B$+*ktkZ~4H&AVBha?K1lZJWX9tII^D zQxGz;lA%?_<8Znj$%hXjcV7~k_iqoEDp-{(k#NhJn126Qw5ibIB zpwobQlDqjzczFrUyWu$Y#O^a5k>WqJU}Se535ANfB9Naq4hVjSIZ4UceRA>yiO}4d zqR@@T9R8z1_?zgHBtg(;CzI2E$*QD0S!s&np)NWOb>SXp3N}KMzYLnZ-=WK2ELBY} z)hA1zHFM+f@0w@hDQ-LmhZD{1ssbB5U3EazPq3HnJX)Hg8>IUH;oxYbq(M@oOyu!9) zmLCShnU-vc44D8n-Hf?qxw5~j%SQj)LTE#NDuvHy$gc|@+x8CN4xORh8`*?#E~f5Q z=k2d!!WB+}*W|l{$AX+g;|D-<2W$8YTrHMVKOTcCPc^Ud)6Yi_FmbC4R6KIh>3DLQ zFmvzgkYMc3;q~KuSf7ud8dIIL9Pg(!XKJ|ICw8}Jsh9t7zK1Kp4C$?Aw$~4}aT=J! ztz4k=wElOb^?SOfZXkZkSR+;A7PqGm3A=Cp z##BymJ;{yCh|^f~Hnw6p!GQPcS;^WJ?>sS|Tpe71(zB_-#!fAMYKLRQ!7%J)?W)akB{uVfT zoct6UT~0*KyH^f^;@OE!T5^!=@@D8@Qv+f_5L65{3~3A@9Rsn*dE=TeLSgy>bRj%r z-dv--SxdeZA9wr(yeb1jTD=#m4bqe{oewN}ipRl?bLpvvTVW?Ba?Qm$N?Wm+F~n)* zQoI{5iEDz`W5Y0~)8f35wB#Yr9#3J9s4rcb29= z_ysf1yLCI-NxaRrE4lG*Utklt?T|D^s=1>!HGZ}XFD9%KZKQ9Q`I)pS_xA!l9S^&C zl0|$P_`GUX|J78AhGQ{rY_4IH>^O5wWCvT0x~q*42tMr~E{_7m(a4>)m^pWkKg^8r zUcHaxTS(EPmoI6wSUwt80a5eUh=-{k0jX0+xIClfl(J zJ<2>NLW9#I#x5#Dht&#R7y_r@>$G4txI11!iZdS$MjK$#rjCHay8-&t#gV(2wG3H# z$P<qz~#IQaod4+aeU%cB$ zGgX<~{A?e$dCcUfWz@UHYHpskTl02QlKu0i>-AqU2^39u`754UT@$i@vL@wK;%x7) z;Q24O0`ExSw~muvy6H3?ZxUwePIOqKzB{kUmPFxJ4Yc_zVXPX`sV-ty5v&%zPI%sc zU%r`?kQspMgV#qbko_0-0cF2wV3`1sb7oKD?2{3sPSDQ(TYk*!Ec-Mvx#XmUFjdYx zn;9%Q8=kECygPh?n*l&-_U7w`v{D7vk_dT5wskM#UN2B=I3px`#x-{ql-&t}#NsL; z-bl>gNGhrMCPXZuV4`S=$p#SmAwLNc|hr)bY;HI4Y}yI|ih6SAvqw<@1Qs`PFcSd3zJk9<)V)hpvS495}W(i2mG zlkP4>h_*$aL4G$w(+2L~m=7SKeGKn>{Tc#?u%Ge}^%qnAxU=sKrrFa^H_Yu9wZFf) z>8Yqb9oX1yVU29$$;rKC{CX$Qq|h_7mXn5*OA}SYAWk)z#+%0{qO#<44sFA^M{c!( z)Co7A|5EzyWPpfT(Y^mz9^Y*6MC&u23nO-ezeAWE*Wa3+4rF`vIw`aRRl%L+bC z0a|x*ciF0IpSg1In7ZUhD2~r=S!A8}7N$A)GGjiJX0$SKqu?y~g-1xShSiU< zWz5Rg&|VF;QgEDKT#tW6+c+eEV)8dFbsGX|g`kw5V%qEx@d#jobkz>IrZIvg@hjDE zZVL!Z;p5<>8BV`(al@B0P_~7j{Nm#cffkV6Z10^o-hQncofRDC;`Ue_kmrVF^z!RO9f?mKY5`U&5&8oE*L=k){yuQY!9jTrv0!NB=0KgAD(DBVa0324fqlhpAe#ky1y=-tA!gY5 zoN@5vx2l%fd3j~)dvIXLmIB?2zTxbh>4w2%H({!x3y(HJo3jkN-abK86C?b79RQ2v zAfThtS|{1$_=Tj6Lk1V8EW|3$nvMJJKsH&(0IA-#;%V7i2x}u=4$^? zIt@6z;6wbi+WZN@udw3Q_q`?hx$@_FvrU+#TLT_l1b5?)%)fo_yauc^{9zwjac3T$ zp4I)j$FG97q|48xB*sQBK}&F2a#SzzGkWG}Ryv)&3sf*19WFkX&;9iAq%~!Be9OUH z&ot>)-bL)eY?Fw6@2Qi}W*c0|33qp@tB^!Pls_~P^Ap{pcq~r(6gb8iWkfk=Q1n0n zm8?8N%-$Ik@X|Y)9%)iZy%l+#Gql~?nUvMW=Cle z{3+A?4CAb|ohMql>0MM-H6;+4V1Xh!O-Z)t8VHJX=Li+oo<|(laxmkZrX>;)5`K`# z3SG+&%VoM2U-IxfeiNPPL+2)7kWOi5>_r{kc^g&l&l%FNFG)}dUlHUNP0c}cRiW5x z2p8Va|L{uYKE$X{%SA2PPM7@CV4TwGFio|QKnVw7Zl2QJ`%2AtXFG1*ZjQD3bbPG! zo)@Ra&&zr^G}nTf5n-|L!-b@}%XEsKesAM*g#-H{LKbj(N$5|Vm<1G9uf;FT#|_1k zB|0}{_=8u)>!0V{YcGISD&hT!J#r`~gH3e#yO=B}3+S9O1|h7SkFlusWwQxQ2T{&x%FVF8zXlXb2-cITr#lls!S?O;T zuKuOFfF7l;66kv-LR! z<95-6#0D7`TRJ!io<8fPau2Sn9sK^isD8HxaYNrW-JP5D zgnGj_BI0%mpHLSEEsiLmGO4uJIt#IOeCVs4my)+jEM7M?+=;wpljX-G)?W~*pM4Iv zeOotOh42yV{YzA_&vz%M9dlXPkz*AUaxedQm>}MZ@%zVO6QF+<(X>5d%IL~c5uJ0N z`UQF8$WY&${oo&7r$NN#YL$o``$=KDt^2fZ(Xih}zy6eNeL|1~-GXC^{oVH*W!jNc zz<`2g4sFhqv+&b0Qt(5~UY{4Aoxl^!$<_J+4Dol6)+RQ5#G#n=6d-$d-#KMe`y}p+ zMSJtdUWa%vvT=HH4O}(t!39;K3)-EpobzL$DxLx)d~(I6idoOD3OZaV7*>b^HI}UJ z??e@2sP(>f?Aoj{`Cz~CjS*whlIk@9cn@7M0#lhmraunX1Snyqy5=8^75$9()!*}q2l`UttTSd`t2x;GiDu`nN7J z54?u9%zxv2_9qjVBaAt?E9)n*qnWGZ=(khIrhYK8QW%pFWXBpf0cHOqqXmVq80S#a z>E5n`Rs5KzPeh-jU{MEI>8#_{n5hiMEJaC#0dWdAE2bM(>W|IUvmSmmJ5pKJt0u4u zoX37ZE?4*GsDq-1Nb$m&H+{1u(?K#5m*y4=JKxar`BrO6gz1Y2Y^2UV_g6}mA@l4r zLUY8B5uM~+e`!mq7rkNPVSV0$24)-42vtu*lR|m(6jqUaqawh#C>WC%-B0D#-qc=W ztiq>&nPgSe=A*wle|@@2&~LDv^RU+4gNyroiop9Oyb*{@JqpPMwoDmWs=9vV7zUqi zvAfubV)GAMD};g05-OPnVw8DE-kqmI)KMGA=**=njAt2eI0?_2A)3)r{El(IwrE0Y zjKoH{zi+cf4r7>Psl%(wyI%fw@j-Elx~CS}8H3_>y)|PB&_eo)w|Uzk#u}+G`*jmNkE#fWX4Z`Jct!3v_Pl{(V#!_21h=;+r#-7neA*%!nk(1Ni-(KKD zTB|D-UV4Tw7m5qh#Ws`Z^rB3pwEX6p=!va890Zlaia1SxVd*5I0*9Z{uL;<`3MriB zN5!(Qk=QI~HLJNLn0W2L5#WX?#LG|*Z_Va!7#SaB#cLS5VH3N%xt7)twtVo@RZPp{QI~^NX>zFe;BoG$3~}`% zjrl^-$VN|xxVvsf?zjNN%cj&A_1R*S>&+HZI1gV7L;nyn8mh@XeyjCgeOK?m@=djj zG;D)hgHjUX;4;u$lJS$sAeSm(EkFGoZ$c3?R#N0wuwEoMn5i!Z2BTxK_%6=4&SxL2 zAlHNwrof0}+gbnqS{eq!4rgU~4FTqNfGFfQxQqf)Y^VI@!0+y9)l+=2^9vHw*R$@r z1}duQ2)lB98~ev)2-X7W0%8RDRg3&>XZhO2HeX4e&6|k4tu@iIjuL$LqlT&8p$}QD zD$U38mU_#JlHs>L$x52ldMaT^V!Z*d2*|`Bfx(W3HZ-#(|0)~5fdfrkExaZG^#=3n zSIfhUB*J5Mtniriz1~DtDi5m+0MT&^Q>U%*_IW515?i-yeOLt5Uh8Am}=Zp#h`5lQTJy}_&3yXkg-(HZT zB<=KFtfG1~Y=B8WqP*<#+XdXId5Ggj?>zveKV@hR;f$ z-4M2GkZ@R#F)?G+Ite$~Z)-PmN3~DrbJ)2HY`{Z%k2mA;UHn|%PC5v9t$+3B9ZY(W zD&CM8-8JhU=0o&KLp^OODD6u}WNxDXUHn;Lsg;vIT3N2-?|0S<8oM^zReAkmm{09sASpgu>t|WvYF_EyuMIq> zRVB^+L+qv~+qKkhHj(U+6GOfI?VzIakDAJ(N-5|u^mH9Ouie@vZ}}TdYe)o;hPUM3 zsDjr0*Q|q8Qme#>qYlGUxal%n#)g&10`wX`UVplm*s-?8pQqZTuu@Zu<>gT++*WYg z@SS=%o(~WRFgzvQpRJ^F+juZ+JEY$E=1o-`J&GQ^CdMyh%=1I#>rTr)LB{0~xfw-ZQc~`153-WZZGQzt7?SloA-St7NfeT%6`vfF&t=MPLOG z*c(VXEIg|8vG^VP3KO@!Lwn)RotEVF*>mNtv9UKOx58Z*xjc8&F?bzFoM^n#mpIaQ zH-VrSt%&DqJM$DdD?+F}-+L$J%g4jMTobFPoeh{)wR-CTI>_}^`pD_Vk7}$eU%Wxp8si)Yl?RN~+R#Tz}OO`LlcwUF5s&U3; zQ|K*rFc2L`Kas2+K4ILn;L>DtpD)r|XA!m@+aZv;?U>J~l9`x63@m7|xQL7#cSVNT zMkWSVS3HR+ZSQnU*czOad&~G1^GoGmf)V@X@Xl zxb$Zwt@hPl9Y-`>qg7dstCz#b6+4{8^TdAPlVp!`6kyf!=3Qe?i#x|JizEcJzq;u5A9qrEmw1;@k z;z&-DqTUW+%@OUm#9QaaEL(YmJ0C|=6Co;RH)$at2sSn4 zB?#izx75%}|AG`%$D5AC(yfwEj)7*f(S{R3DT9V`{5&=s^l6Dri~a^gEA6|l$|dE7 zS(<@?CidA>s_YWjT|xO!vGyIx)qa zXzMc}a>^XAAg(c=B!(wte3D0b4tWm6sw|5oB^J7KeeXM!-UMzs_p_G7Ahvr6k{jm{ zphE3MP)el-i+p_cR_&kx)AW#-mOxBLo9{|&+l3bU{j^{SN02Q+fN~7eCkVSZkuwQWN%yHRM6e9lg!a0 z3DZHx73Kd-b6C2vWWAnU&i7S~e%Z7Uc@U&d&nwP+X~h+tWg;gen8~pxf}>A`Bx>-c zS|oL|oH=k)cf_0ci}68RF6qRNZYsXc>o_3?h3oJPb&l|vD?+x`{@EGbY;{5p;6u9KDO7*0XHZ9_|vb&!R=Id$_M5vY90lC~s^U zUXzFYy-C$-Y1)zcrIh2{{sM<5)=sn!?&`52dP8Q?3P(f>Su$1-cI;1rWqI9Dfm7of z4nRCabZ1e&E&v*B-RJa32xI=aO4n$h@|#L#}Z_6G5IDfX(}y+;oV@!Ta~?z_si7z{$30m*|X9p2J2osqk}WUv#?r zHWCif4u0a2NH!q){44PGkdrB!VPv^!vty8mE(;O||9H)CD3Eqw(i)V2^^%2tB!w*Q zo-!a#4C&M>%Q*``VD@I+*%gS#o&<1m%^Y=00*f&vqxQRw+?~>W_=?Lv3;ezN+k53CeNrrBxN08lu(b{@19fgLl6dCLKj!pzIs}O zo3Vg)a?wF)qA8V^cG6}@XLXcmmXOsi{f@XKnytcp!025zbJ8=YAXKe{{)WSh@BDYU zTqn%$^H7mO0yix-EPOj`-fGRp45az=@G1ZUfQQXf16gpV8cm|O>M7yNwFx}I5U#c+H`7;lx;yoS;#Z|u2t4uZ$yT2`2)9I4Y4wDx}Kh*8aEqu zxFVBSj5L63Mw&o&Yz(F=vG?0t>h7v(YYiKon+;nH+fsbFKx1|w8;j78b-&fwLt=GT zzvkbM&q8q+OyX+;{Dq0l8Un=3o?`gL*@a_JobA}a7%%N+m3(lT=dZy(bc47coy%Wu)4dbua@2M3#HAq|Ou=&|ttaVxWPvwN0 zQd=>-4KOR2^?5RX&)A3yAKu`mOc(<|ZUjSc}kfsag=AVOM?FXJi?F@eAETGowm^7$BHaG^ed;$2#WsXHtdJ z+Q@LOLxAeo{}!o{mhVzbAn0Rc20?xDUT2d2F=tVcY%nsKzMLm90A=v)(O`fI#E zTyQg>k7|6LYxK=v#B8RgoIwn_vB43Vw<%W0D2r{OZ^=R00?s- zSsvBIIu$Gn`r*39SQm~>gKNb{d`I&#>W8gC8mrzYs!b{GtBBSmt#|s7Z`oZfm^bmF z1=&&)|IJ64k%N4F9(T&jB3*-<@JGVfM2fHDjaBH+8rS7~mEM~^*tN&w=(YcW85_5d zD;^m3=}8zXp)EQwxw+Ns{VKbhGh!EP&FE#B>3ai(NwH+ao2n4MJqJm2vnMh;nIqx* zCfU#j26k~Yy78=t+$J7*FxJgWWH5{yJN=M8iP$k`xCm%##jcqr8~njB4^hWA4@Ja? zQl7zJoO{eaaNb3~102fy3MMU-$-2WPqZ38%h)uBq#wQz~#H13}9dCpUy`tIc)KLci zr3aVnD*)gr2LkcfuH6s$^W50jn6JW6^;(d*cf9AQCEz5ejBOaIz{t!ZLQM$bgt4!G zy^HPFBGuMLERT$%H3J7$n=$fFrVL_#OME^t%}Gqt=Jg&^gm%(<4;AV&wU;DoY3a=% z>{eLA##whv^4~m8EgCX_;2DKg0nS9xy#GDpq=g^dX3m4voP5Z4#^kUs%<;zF(m*yt zWo4CukiTR%fFan%;cBD?WbITramRTXqfDh0i)8D{BJZ0azI>KFr}}XVer{>R5GNM- zQ2bZSEni8GaS=`N>Y$1-G9&gecs>0>hhK)rH{-+P481DrDvlRt3TJNYNQhs zRfcdi6*Zs#`!f~Yv|elM2KJcDW#On9@nlgu`{_lqbZlPU@KoSQW+O>2as!aXW=||H zN&7FO%PV*jYsgjH>QkWv{QB^YpD!?~`O_O>7yW`DPJf@!rc62Ph^>1I*>6VdE|mw zWxNYlC{C1ZU)TXfUt~vIdCl`PB3-!v?eqVQMX+)~v4TOL4X&W5ScZCN=DmdIGab3N zgne{`%Lz93X%=H=|FwZo6HRjlgr3-OCq#b?t@+OGOAh`8Eh|t^c)H{LgHL7a_TDo% zDyp=Xm;sOoQu#L?t*p>v(#V|DbI1S_&hy(SY?+2qm-%5&pRU zdqmmbHzhp!w9C+W&{mEj@%WdV-dH*jsw-+7@dmQ~GLKEu79iph|8FQ-iGwAhGD1(b zF)boyD?van(|~dI!{t`oFu&Ozm(xSBbFu><)4(LONI5;_iL5>%`+}->8{fp~cWx=x-489psFU8>shJh0#P_BBWl9xF9P^4{a0T zu}edU2qhU(D%JSiAOpW2)wokjxFFFzTy(l>+&)MatAeZ%7w6y4)PHP8*n)Y7f{k&K zM4)vSVPZW=RknQ#;jqJ${rSntzM{cd!~+Y+@9z7cftyP$V%25IG#ro}|76qP`KBwvH~6bd zAYF2PBHf?SUEj;h-vEkG1b{rDP>@<({GoX!#_wf%uHV)(s z$Mx%sN>@`%Cae1Fbe{2ZoeE~e$=Pa6nRt zyO-Bkr3^rAWp@UYP+UOo)%>b5NK={hPuVOjfz%5Cij7AE>*CowMf;1vIN)X>YffMF z-Qx?|6omB+yDEDL2X`$lB5?Qvhl_`XZ3{kO0htF9 zhIIKs=`}r}7*($s+4&ob?9k9=u>bQVWB%^{Hys^A4`?4V-?2I&o(FV`wQf5?lpGa> zvn1s}53G^8RS=eq6L!*Ud*k?Ge;#=~=EEAp-gxCwS5Q4vpVRIo=V6wm9P2KDX?IKI zVvWzDIHDPs4k1Zhry4!Oq*T)=TYci#EZ1;qVj^u--a`K05Akp*Vt@>=?V>#l71Lsg z-58UbE%ArTCTHmmb}N5(e?MN;yGLR}Z=djjSCa35AX}!jvz|{^xTc5>n`j_9J?v%{+-1qoR0pa7_qwWC>w8%~nb6Or3K z{1>`Vfe2g^Y&9pP5!(Dt2;JMjA?g(+0Wd2Y;DvgSfRLH;hXcp7zcLaLAuc-@K9P*h+bI!zgzm;YD`Gy($!ZhR4n z$I13LR*T3HMBsM)^*ShxwA>O=t+WITGmg(wK$aV}V95OyDj2o}O3c6Ld(`y!rll-f z|8dLz<6DMd9_j62eezm_>Bn$kH!z zOQ9ULW%a1SKzS;7Ujb6arbhnfDu{CMXUMa#caA79<6yz6fy|w(f$HeP;ULb)Ev6*G zD|P`&qLHMkZi|2J*g&N$L?86X1Y~eez=~cI*2o5Jv}h+68wJbD|79D_+S4=nOCWDZ S-AwW`#1E{fu22Os|L{NMEB7e? diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonclear8.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonclear8.png deleted file mode 100644 index de64264914cc8330093550eab1f0ffaa01e91be9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 71177 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%0Dr63wml-6F*tONlh7MSGocvsq)+w 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 L&GqVZUE=-^HyYM< diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonfail0.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonfail0.png deleted file mode 100644 index a25325815275dfe5e34358f3509f93fa273ba40f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 67970 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 diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonfail1.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonfail1.png deleted file mode 100644 index 2dcaeb92e3bf4650be65948ebacd9a7ae8bc24ed..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 69118 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

  • ATKFj*pA9VSx9n&4rRut6B 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 diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonfail2.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonfail2.png deleted file mode 100644 index 0f0b1d4f59fa046156fb8911ba14cb73ccb60710..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 73351 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 BEpq?> diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonidle0.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonidle0.png deleted file mode 100644 index dde0adabb403485d9f92830f5ab95a4b4396ad83..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 68649 zcmV*XKv=(tP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D|D{PpK~#8N?41W- zQ&kto{~6tT11)8??7jEidk;bO68Jz7St^1cihzg;hyt<^K~QAN-h1y|TDtd0)1=9F z&wF`E3LR;iq$&2Nm%jJ1US96|-*eAB_gs-$twvUeL?X>r5&r>%A6xuJq2oe%ll)c? z{mMt%!oILA?5kMOKPDU({uYr(vb+C_uuO_X`@aGGkb(e9^tSaySrBO{EA$*Iy=~!n zra};6DuO@@f73PFLJ{`$UqeM7+83T9yhhk2-hBRlmLkzEd=!ax;YVL|GZSR`D?CO* z{H7r5zm|^CW7e$LXtssd2%qyNnH@6}s(eBPBtd<%kk|@EsVVx$=(Y4beQ2MK3(rxo zBD|K0{(B4CLeW2;{{%&%UHB*x?ZSZ|tn`-zR1ji8Y^exBtS`vIb7`N7AjX0q>pzc* z@VG52cACF*jGiZaeqMO!E;1<|Fy`rG=VE1>^g^t?1pk!$|yzlZ*N z(_^$Ryk}N7h4B8FoimZu>nxTc(Jp)xiMC}RTBD_YZ z#KN{9?DQNe!t3?LTM%<9^c>n3K2t#d=AdmV`sb(L00`$!2oem>|@`APgjp!G z@5@SQR$Mj5=(Fgvg+f@m7@HD7=&8a}A)!oRC4!ZEtdIpyVom}pJckhe3L;DIBZ$7f z1+Q;)(>4`4E)?MyJx3_QG5zCa74jm{E_@V;w($_ek%U@bl=TH!5MV)+1<|E#5?-O` zTg<}abc{rqTo;0+ELiIK5@oSi%>C9>#3BjAQXBS$wh&9$DXXm@60;(rzf$&B%8t`t zEA~42%bqWmaF5YndN18yF)s?NP%RVcn(i3=<@fn*{&`TVXp=Vh=W70{RB4c>#z2u4 z1*I~9J&kmedm{ITdk*(n%`w9vu@qjU|Kc~_i1z8ZB*G;4k*tsfPwpBzCI~TE-n37G zPR|pBUEeaNeZkrnwuR$a2|0cKo9C1-@^Gz+C}4_2JAViwEm+WkI17UNrU27l;TRRN zn)UxWvJ%Kjam_Y~ydcWC7G_R_HG(V_@ggdSGhy%02z@3lg|)RctZiAbwqb%yU&Y9% z(jiJ$K+K9LT?vs=0kuL26)SXHq@uq}o7DVYv38$n6}Qb^Pmi%}C{iIwld-3!F!7cl zEGZRX$#O)=6o{9nAt5ylGNlZP^kgU$iBQO+kgkYeg3msW1YM;@Iy)aDLZV6tHLysxf+Z{nFImDu5%x)#XSAX!AOcb3oE-yn6zLo2glMg!|#mzxhBAbwaR~NqkA5 z$=80H6%zLZ&0q4n3ne=tC#zhr#!0-XcSOtrkEN6X@c43A5 z$yD4~ano#*OQWJ@n_ibIcLuYInf0r0ElXi#TNci)W#MAygz^sdsO4J%Wy+R;SLO1k z*s40*d>mlIniFeQ^nb!Mc6)Ce`z?NX@DTTJ{mveA!0eGfA+B+6%`^UcNM#AI33-OZ z=MNG7>IEXBBakRdLP}aHWa(*0Wjz4})5mnSoz8?^!Nfj2J(_g@Vwm8iGYej&b;(en z{am1LrIXl_NRw6mkQH*dP$3}~#9ZGp&q~B|CFDh-UHB*x?VO>DK$b8S!J;Oi7DSo^ znu?dE6l0~C=9u7X$Wfdn5-Ui=I(LTtwHJxK;N)5v#oay8vX~PBeapbVK|R##+Zc|{ z5?JTLHotS?5oUe26eo{v<@Sd+`w;fS=W*LbWDZ&Fw$ULFMZAVpLKvjUQBWqwAUrw_ zq49}`Opzf*mWp_}45`XwD3l3ElZ7+uJ(6?FaAMAcodn$gkrafS#Fhj&hLu~akX26B zxFF>Embku%3qt;;e_UV81&g1ZPLXIAHi|?$YXq?-Nfi9i&a9A7Qw*~p&=f*fl9lGH zkdO72A8b01NP{q>^&B|9o$#NCpUZFBx9@~#| z$``nHHw7UtPqN#E?mbdsx_5X)Vjr{$Y>)qayBLicJHn36#7O+~-XhFB@;A~_?!eEx z3%W1brg@|Yb6}ki1?lw@c=_-K9)*P=C?Oh&DX$=xJ>w!`XrMGbUGQjRilFjz^U@PZ zs7c80vqIK53AiBUf}racLCC2H);I}y1}mJEA}o+1(bl0a)?``Jz@Z@4G*U+vwV$SR zVug;=Ychzny}dU&R&0!>6>6bk^QLG#s3~k6qzHb^?x>UZac<}LI25M9qx)x|QnG_0 zdsy3+W!-7I-}oEV-nl;h*Sr&kEd3ZIOVIDY9X1*}m!8J7AD1%g{wN%r8)3wfeNahq z;bSSn=s1Kvf%yJ8JiLDg_aY+jIw>4U33oZ)xgg>>_{-lEauQ^UT)D|goL(UdfrMQc z_|$i?kR|@66>e$)7iu?IEkdFDkp5;>^XYtq#)ba=ry83g(dH}0n_^9F4#7`NRy7GV z#YdA^lRukYlR@R)Z_*w00-K}D^!MRkfxIU=LPM4KYu0l775ora?mU1z=>~V)*0B~G z9K8`8c7*NmH>A5;TP*xw5_(Q;0|#=*n1Cynk}-1Z1iW~%8&aEc`0%6kNG@Mf^Jo#4 zfK_5F9IyV3ySK05a(EQtWid#VML>}jigZOBC*Vp7i_9Kmqj}} zu_mjU#F~87}h0!e}|%m`i5TbHA8o?FMNJ}2p|U~*7P%X#(7i~H(Y;B+t#579=Ll9doim*a6@7m_75!cV6%uiJ zKAmr&x4!>NqD^0h3RMIWV!^T|Uj_{(mS81Yu^wEv8+wlX7+pp*g$-4^Oh{l=WY?VE zu;tPjTzhyGO8FD+IEC#6R%(TS@?xAg8HA*`bNq3MJK9z0h%bIzjHApjJF`mT$qH2z@>f&#yGpEND%N9~4MCr2bI`PXAZ+Q;Odw)az0Au^y&2h99s3Cfl?kpnvm z)=ylAshbu+t%^kH;@#1C!Dh`P|6Lqk-N&o5#}E?y6p@KBh)PX{Op$~%MLd*gvFuvW zy-xRwrifTkOCYszhqbjEY;B!jZ)FEhTWgeevq2>+EOVH<1LqZMCb5=q%g&H9`Z#ri>U!k!nlvekc~9@7c7SXvzK)$M}Mw=98=549)eW#{rU82ja9t|#E)(Go+y`U5IyHed1` zf+ow@J-!L`^DDT0{{bFEgd-y9HBx2FU^AjPQt!&aIRNhNKB(Yojrv}!>Umd3Qk~A3 zTI6KSyPV#R$QKvz*Rv>uzdpmxhkE*^LlAJX!qXI_V{ah{I0-XZB4pJHLMT|_WZlw< zQpFcsC-fTO-Xn{IM44JHy6?)eLKgKvR;b5Iyd~DcL^FMabIk$ze6qyJhjf?~vcjp* zKAkh2FInFD=UQlZmqnXSp45;k4O!PzX-KRozM90kJS)9ep}QwTg&9=04~EW~ho&7X z!Jec(3&=OTd+}kc{9`+Ay`T_2s>=+q9ZLAMMc?wyaI-6cYgVst^!Rh6#FNXA1lt$G z>vzEyJD0+hZ2kh^#J*7UA2on$L~I&f-9>KBTC`83R zg*@dEt0aPvW(Gtquy?8eH`ijQ=IVgj#oS;3?<8&G{w9K z0#1H#g(6D}oWzi<)&s1NADqGpsp69rE{Hezkm{bSQ{NH zS3u>eZIM!~6%r54!j6ltkr>NvM25t=Bu3Qfh%f$J0%s@VjyQ&~YWV6k;K{yi8M0@x z;o(yiTDR=N&g%?2r&5?P7#k{-N8Qe2_g?{#L=fu5FYmkiquD%M=Su56YLx- zz}D7<362s8v6q>|kU?AU2!cpqg_+FQ6b_K^?PG<6kStkN`#ABY?h0A4bPtkQLiZj; zVUY`x3?*`nXzx3zR0=7V?$zc%x1{nvjLR~U#rEzp9jjyLH!O+>ga*U0ZvmWHnGj?Fv&aH@v zCD$3>x{D+ps9d@wdNpW@*w!PUX4aGAw*TRmo3FS@ECh+;sJiX&*^VV}an0H#^3#xS zSPfKQ!lq9RiX>68Q`j;8FFd|^2xZGOM6ZSK!;$(H*$~32YtwqY5gl`c>&AcD?-N9~ zAE$Y=a3fa8V1Ilo_MbnChY`0dOQ;JWIfx+%2>Fj`vH|@i>zjm|TqRV=$zUR5i3EEH zD|Clx?>#$PJEQB2aK7a3dBc@L=lg&ainFD^Pg$XchJ4)=Wl|`F`&|=lk~n=I8o5Jg zu!Fi}WLcB%S`c-PCKbA2?$RY_*`a*40!f$Y-t|~~G-@`^KcT!Gdn#>7zlmKc)tLIalPSs@`L*8^2_nxIBjc7Il= zVse#}Tp>2rHYU{=;e6j{Z0KCcvi^+~%HQ&s6>{;A3#Cv9w{iRD1&biqWMk4`BRL32 ztSOAB2P-7l^f*^(Z0-E;QHPJQ>-3*!-6?y)PGdjghFLq%wdW|Df4oI2*kY+4T2>i| zF{8hLa?o6@U^`tsjNfkG)(SSIK|?Ke%;;dT(!|AnW?8 z1DHQ!CVqPmiAr{Ay!XZD1_?Gnv({Z~G!A=}3~8yR9q=+G5=^<2n$u>v+gtT@w#Q4D}gCCDHq?tEgBEyI_Z8z`Gz+%L1Ra`6Q0y}5z&&r{ra5vE07 z46_jL&a9AiO>U7^tdN_86Koo26#UobfiebS->B-r$$&^}jTh{jb1Ve|4IFmCBEZjpejAa}^s)1esM?E|bnw+(42 z)RSVrq}T_|D-Ob#kCq_0=SQ6HS|7W^Kj8Syo9qqvu)k7%-LUZMS?~{_(=&{cg~v?(-Bx&2#g_-q)qURaHk1U@RK2=k&Z-Xz8v1mPB}ZF+zgY-?*sBoeaH4K8SvvbsIvUv9zxouWFI|aH*3}kVTu!ddFtR~oDEiOg_TPk3Zf;vV8R4Ncpw6D} z))dRee1HL;cjNYR!-3VOF=^=vM1~$jxe9$Tw2U`W(`DHAItBM1Uq_WnwK1qnAm01& zqihE?vm%vQ@1r|>j=!&b$L+VSHV74`{>*J#GNPluW9Olr2#-COPLX%L|kk<`x^#%%1dT=816F5!hfuVEM3x*g4Pr|M~aBi z3uz=DH~Ap9utH&AG-^#gZaPMm7y0V57RdsjFpIXnVAJeIL9oeBMS@L_@h!ZKC#H9q zg4tVVbIWjLM>6}FR(`Mo-|pXq)Z}aYL9rbwmFZ-nrlinwV>xTtT;`}f~BEe!(MpuI07jtk?32aD%$iMhwfu*XcYREoHWitqoMR)=<5A2+7L}kSDyr?ZkLIN>(Ex zA{KGcx1mzfe44@!C)`RV+`8GIB-|9fNYjEzxM^^b22-h`Q7dFo!d+NJn*^KsIpo6@ z1e;tUbSDk`_n%sx48a|4hhy zXl_8}P?vrEM_rMe#OD;LP_h>eU)TsYiebqkXZAhE*lC|3IQTENCx)|IGkn~rJ~}R# zg$e;y?DyfHmlvov@j=fe*nXT}YAi6YFWS!F-7uDh-G%-5ZRf9uj-}8^i!ypF5*i>c z;Z+koD+a)o^)79E17MTK%O;DCfcV)ds3PyiY`&4)^oy@~Gd?*@FkeHN1jb2RBtTi|FIN4^sVy=g|ANPw*s3F@vff0%KgxG+FEYgQjo|~5VLU}e!q4U%Cw?b z>zuH*uY$Uzs-i=K_OPngm)!((s~8tqLS)H)8rdr5)Q?2Q|m6anQ zk~dz1Z>8re5?k1P=Wz2Bj(K?7V$p z(`goZj97(P9r~h2oq=$4rKrBbCMCR4N&}NLn5u!agL><<-Ye+}=?le~lM9w~$0ELD zVYdLhO0XAN~S?p=$D=cA}szPj?DiFN+SuZwVr5OTTrLU zVb>z8+43#!Jm1NF6PoW(L{>PuHo*stdtm6e<*;ixk=+2fAC%4$QY%QCjzE)ev}Laep@_4r=zBRISttgWL4Z^-3;El`cb? zL`Nvx?2-o-m(R`WI}HE;!NjGV_iH%MxBM%%$ZU{2Fks91>^N}>YFf|f@}xNU7S z#G8}WP^>wH4b${{5^hR|VG+Vz7(`nT>^H;u-mB|9}ZLg^r2rP^W57bZPS*;u~cz_?hA7MmceKo+KwCF~N9l3>RlHKKXGO#y0GL zNF{J)`ARNiFF&YRl@l>*Nx-^*McX0h9-eQUj3{b1E$y5?BH zBDS@IRhLgOywgPVuiq6`c4Si(9!R(;{810GX!^23!cFO$DGGsRuTdYG6u!WO+tNi_ z-@>M8rf&*1T{up#OBQd0HH(*_=eQQMnu$v0TX}Sh@5& zwCPiacC&=O>Vw0o;4=7jGOH5z4AblY}H9N7zc8 zVQ1qEdwVxHIs3q^cxkwn@PmU?g4FnAsN~5=Pn97(O$J3;3KU6^NKK7HNMbx9lf#fI zdzv9v4P^#=W0DXBbf3}`$__}WXelOGh~uWm%{G*8siI9EOx+V|%>uwmVkcPcX}P144=JjR!ID-mG6nD_@m6|mSRHl!4MDoQS+$jq19ttcFe$z;21o< zPdBx8qC*i`Ahjuv>CHOAt=A&W5z8m}=_UMg;AdRne&c-l^h6GD^K6LfzMg31>yPxx z-5@Df3sy`nt+JM}9FYe<#`7CTasB>fJWGr~Y}8q1K*%%84oG2u2U)qt%5zqzt18T# zG@Ec+qG;2H3Kn)|E)hD;33jzIjq%g!wP@Uy^1Wow>(F#eXM(-=_D*(b*)?Vrpkcjk zXkLE+l4`ZlJS%_P`er=NJle`_+d0+6#zjlfVYn`tlqEq)r>azKf;8D(Sld^{%oc4B z)%Q!yqo#!>B02u?2{t{BK*)=|>_|TDI5R?xysd*j9G$%3?(PIP7p@vYMQcT}+f}6& zF)NLxbJI5aR_q_SoLOzDN@mTavh7sX!(o2urrZ7rarxjCucWd*Zq4)ia!S# zv(WVrCg4yvostpJ1RNnBx3G|c$t$y^iMAlvWMS*4R~4c}$Z^cwDdqhe;HRZOp-pc} z;Fmq*PMAO7Lu|P8r=}YuL6vHK(Y-;}JPkT#h3%P+PN(o|PTcRt^wZ^F?jz3>E95s~wynwzFk*Af5fsF0@+3iMNVvtJ787 zUpgk1#X}Mw3`Oi~ghhrSGCmd&Ny!LHOGZfa3+-B>0=5pd@oCrhVBLJI=1AcpiHY^u zRnu_v{x0_YbC;Z~xPLdiALt3ECNp5|O*x%R3n^k!L=dj-S%Je3jv+Db7CWZZNfK@v zQ6lT?HY+r0PRo~(#Z6y~9yh6QTaswghmjzYU{m}O{W^kRQzkAFaZa%Pir2!5dEcS? zC<>>{p11_yr{P~<$%&mvPoqW>C{?Z(hBR)E*p}1|7?I_8F!Cyk|N^3@#4sp5y_B%etFE@jU99$~9A`%wWv@^dOoIoo-gaCe$JcoV=z*5Ho(8fh*5;ux>gZFDRCl#OP+d5Z7-$w{J=uch17*N6}oWS&Kw$ z$9izx@Y#U*81V7Wu<@ixa0P=%k;>FtXQ-8Ms|cIw9W{G}8)u0F8jV?uT4lSly{zO% zj0*m}F!H_6AgP>v2(PJ;u%E%*zb5)kS&lm8`@xDbBl3_YQB)g!5gK#TpYl#FUXy8S zL5sE^*c1&y{%R6v5^QR{$k|K^$O*QcLoqBGJPjijQR9|9RNZe~2*>0FOOTw%XWbJ^ zeer(Hiiqw#m)kc5;%9fU<@BGLJ)YDga&?&=%GwtiBFC-;f8ncMv(UQ1a15O}9bfEQ zjLT1VaI3lq`H^Ynr>9#xOc?(QL<48OwUC3c@!0=8wmywUXb=rLS{}6Ea<7{GFm~b! zI5f_G&X|0YE@zr8bj3wzSH2u~qr5A)TDzj-#KkCGyo=^YMv8m1N4KdW^t1ybC5O#utF`1u!w@N`VBp1lAo=hMVmgKnl73iO4CGzRhwxF zDP06ku&u1!u&~z@jQfd}pUauBuyl+Y`3WMUc`KK?VlApQg}6b^lXpjh-KmE(gf4myoZLPeuAwV8BPU{ zN}0+;n-7nm2F0&F8FTLrq-3tPUKsGV>}jCPP-KF1K$?#K(5xj|1Bg7IU0cMqR8i%?6v^ zv=PRxpAM-Bn#MFGaCX;C>|4JMe>@68OvEYn`EO2O5X0KOGD^7xpncWKux>vLa@Ty! z$jK$NlFyY5vvB%`X6C5{K@E9X|IQfMVE}Au=Im}WCu#e>#Fr-zL7sdQ4i1el^V`FD z&WCNuq_g`u`o%S@T>VLgCF8`B;+VSVC_EfX<}3c%R3yg+I=1GMJw1Mkp#2bjmF2n8z$ItrkOuZT#0`v zpNQri#p#D%eEAjxTZc)-j6{Y;u)# ztkD}Iht9V+!RFw?1bO#}r-KhSAoeao{$jV%wK1`6?+m$0ROz8Ozh)ui1*J|o;Na$m z_j^r*mv?K;5e~Add$A(8Nl4dB;wCAG%R*m3@MF^)ej!qXW}%R8h!bpLO6$>>v1=(e zWjiOR!*F8fU3|BHGt}w4ADI>-m{6~gQ6ojDyMN=Gaag)`Chor4#Xd`z{wju(Yg3GG zHX56@eUDx-R2jSb-HRFb zj<+z#l6G$U0s2>O#FRl7sdFjlER$%+Fn;C~)aw|S?JDp2A?TF?lX^|Y37T1^ zPU4c^7c0>O&5AptLb*DKZ!!={JM)I9WrowWf3bS|3dBdB&>SgvXsW)iZ)Z$s+8j2` z$7zmQCi1Dw8zVAL-h2sx?=LDPL&HJ}#hv6Oi!*kp!M7JGn5rR!K9{>Jl zi$w`GS#Wi#_QnU@hFQE|6J$uK)y&dnhDSg^-jkRWCT#57(67V$aCWCfV|X6DIt$s+ zRhm7^L(1&ks>7F|QN;||7D;d@@{PO)C0H@Ko5b9rOSB$$Cx-^&$*-QS66Zo1*5C`p?J5ijXDkA7$|mpi~B*AGd*1_ zEr#~Bx}tCA4`E#``#kHG0~2kP0#?@kka&_!^WVU(N^6X--LuFPA$ z5_4d~B&}QDPvBEr+t`qfQM@)qlTb9A(cL8G6m9w}iWbpdeTHfzxkfm_mP(y4wq8pN znoo0T4CB%LM9f>gnEk*}+&)>qqiR;jR~`-PglN3}X%d!h{~E!e|1hEC2hymE?e5(c z(^?L}=JnrUl(SQaN#w%`ot(?U+NCVgoGmLX$|f!h8_)mCE)=bUTJT7% z12ME-d$j1e2+~qHXMwa7Bob>#teoIrUlLZ{y2Ul#b;t~L?ludp18KH1ZE9tjFO3y)u=z|hV(Drf?+Pvre8p?vMKnSx(#GylUqdFWoTKg z2WD(qXi%gGLFq*YcAp4&DxX}3)^}`Py#dgjUtv5i*oQvBU*Ar_?rYmKte`?`H1??7 zA3sc=f!Vtkp;`k^m~-6#H)mFy_$l&9iLanau)M_r&iphFvSfaGd_iO9Tn|&4v__Rd z%VFhV8S&SKVpXaseElk;U8RDXm03_&Gf8UD^?%y+COK>S+6|=3Ej!n`ch@;+Rk^2T zk3-*0V$^67bBeYwiH2$-^?#`QpiQ${!nX}R|8zQBJq`0?lUro^kniyoWpwF)oV zG(dGLmU7cr4rynO;L1zQAaDWW=GhjrnsL@Q&P1x-Y@NqI_{P+BhcJ9 zm8^o!ZOY)xRvqs2@kdRkxy;*`avv4qe;8#(`G&2QG&u!SUG*Zrc*rwd)6M zsAWOoTdF%gYTFsMoh)PR#Ju9z^IM2J7VhtmS4c3bH|zsv_om!FS=*a$-eeoX)PBpv z!8Z`Ynoy{(c7TiYtt~4QyG@2FS(D@@q2qbBw#_BlWbV+(Qa_c#{K#p=2{zHUW+x_! zlQerdLyOuD88nLfF?qyjT%`e$x_zSA&mEM3ZrkSNNQl!#Jh-^G!q-D4V)4GOQN1oT zkOfc65iJl)O_f&Y z7~lsX8(>hxZ742h%Up-dO)c6a*mQFGRyHk1PyKvO zwB20mV(Rk6xpFy7AHM;K$>%kDfV)o{G_Tnv&mnJ-Yvc$Ep>zX%22F#b6HPziAZ4R89n?i8 zgOz;S6pTU2k|%519F{hnoDi2yL*bNMMf$OAm+J^fbqGQ$Avgdov9(yCHFNRd| zLu|7Fnj^WxF8n!m-#Muj9U1_c)3qIXeB3!ti}I3K@Bd^j4sLy{IbvMeOc@TJ;{25S zSFbL>_9Vse7aR#Ye?VLTTiG`DH88cyFjzOtIRj)7g$Jt!15n+U>^z=xA;FM`&cVLAgNhp(PzI@xJ7Hz@Grjw-BhWt8|c9r`!9-fUcV*ZB)M~l$p z&7qxUL8%Y~QG}|Mo5H&7WX*o=i8#6&X(_ba8{Ys7tx*w!m*hQM(29NIU}Xr7ZTSun zd=#QlsaUr-dY9$9jbv?ayZ;PU$u!~0{D{;l?7n)2{R;e)>w-XPQx+3@PK0A43rMM2 zgi)bPdL065XydlyV=g24$`)?hlA)X`^#;v>i(7`p!pV3g-zFKZMcW!53kFl#+{?pQJtO z5TC&3D=!GhP4s@<_V8*tDEBqLh(dv+*(i9s^MmiSMA@!~&yb#8a7Kcm*tz(k1GBbm zXgyIL5@;Hfr0_qADk1j*J(z0^Hl=8jIMYc9saMIL(|{Gu%JwSO1ieNKHQ36gC9}ql zoWrckF#dqZ0bMJXKvb8h{C*w@iq|DHDpA}SrJB(x<{g?D>0PrKC`;SAv!`+GqVXdt zS`2B3hQ9oaQI+yFcHOuOk#7Auv+?f*$}U)NH&C4lEl{T4T+Xs7;@w1QWshzFl$42= zs+W-upuB4N1Ck_bb%)M@odZSuX(0oax($MhBu{bMrV?#B2|5`%N$LvFNs>9NwPCwg zsDKuOwK0u3vU|;WJPtde*<-?3q6Pe_^@d94H_8*rblm`283z^oD^dHBKWg@Cg_0gV zxcBrF4u7)*3IlYF*0W5J@hhe0gblljeOt7E)Q0b2JPo}9*HhcMZF51{4nyOvY4*&I zgKI-{Yub}ti-OO$QiN&otK1y68u3d@xrclA3O-WF5NxFOXji2jtfU#b?zCExEwmFcs2`;Lh;(6Qf&cAE?zS_~q6EoZJ6g^N0>I4wI6BQN6-5ht*7#SdBwnta?(UH}_r?83n2W6^KI`#Ada5*qnCqw)J~;bWkS-@{Xi zK~)0T)#lItTJR&z`ZWjkVNY>|m4c##cWadG@tJ0?h<6>a#0s@Mbv>%6xTjFu_(!v6 z+1S{+p-Jh6%s|yjE$Jg=mm=>2jrEb$NT#^K*0!lcn@oPX$|OM~+MQV8prqj~{Hnmp z#;|A?av3cC`8Q^o^2z>1VowYT^h084ngySKd|c?d@g&ASK*(i2k4b)MKCA`m`1!Dl zk%k?2ui@)HbFg{NPAne06r*~M#I$wa;MAi7%qp*hxox}Pqs_}uxr$w`!VKGgFa*x7 z{D5R?@^u8ArA&vWNBQptB*xLIO6G?m35K`r2^%Nf?^eXSPksOL?B*vM5Kzb-;?;w~ z;Ub~&mL}cCz}``tl7I|anyW&7SgOPn4xK9(i77;z)R<=ckZXiSNvSJ9SDSNwbrlL~39*J{npnn)=|z~A(oJCRB)Az=I2;tt{x$#LdtR_AUl--QG!Za7 zq=jV6l2I!}kr-yhGK?+S)LM|KPbNRLBGd(-CB(Q^L?X3Aci$2y7N855&koAsw|wg# z+9kcj(&89exg2EPl)A(?q&MsYdrdwM;`FO0xN{*?vzK=$S4f?q15u*5CW*Z|35wK* ztf%0Crh!ATbizmYbmt0m8(A&S38=^_@7uKlq&AdD42X?7kM!f4xNTG6aPuU>Vhd&# zCS~qx-?XzuF7{f4X^{ptg1aNh2~Sw;OQhX7uGzC}>{;8=xk-CiS?k8n7&OL5);1Zm zxtbwqY|*Co6I>(03`pt z+t%;F$w_M*k{*(kO`ZsS7l}c7Y)sLnbEA_H1|aE#NU*hLfBVwjXgG*wvFA);60m09 zVJ;4tpw(IiRjX=}#Q!bpG+hOw64v|*poyE^4L;NS3BTqx(lFcn9i2eD7mu7_tftnI3zMcocXG1>oN>XfU% zsyXQ!f1h4{7Q{ti6$TvC)@bOTVd*&1MIrMaxkyMi4RVnf+tMZzU&u2;J`nBd>TaI( z(7HD58phvW{ekjRMqktSJ|tn1X#+Nr)t3HP~0h)scQWDuUj7R>7EHA_~3P2CJ@@fGnOLtU{QY#nqt z#-GQ=Al29kW+Et&RMV!NVP{8ybR49M6h%VDEg886N0AuwD^n{$4t47E3!<%c08}Xz zfG%?kbB)jFT{ypUoGKY%kBy&@q)b(BINDO?NB-PjUNlWMG>5Bukq~!Zvu8diS9;fmZH;bW z6j8X4_*a6Py>9unXjwGmN4J&JB^be>22WQgsoH?-kA6yHe)#rKqiq%0}>(QD$)8N&83>-A!bHtsnTR6AZ zII+mxKJJEco;9FQ$Z>OD=158^uw(B(kSE{f4~QHwpi%%74t$KJsYvzmM~jlWu9qV9KGrQ; z!(|rCFAjFVr!zl-jg+UF&c8}4IAqMVO}RWM;MZUmiLpeRS_dIE zn@)g4n?9VQN4YYn+QhIhz2j?-!{VanEKgGye>HTw` z!d5B5)jy0IUrN{QcCb!R(^UzYE#qPP>MCT(y2jKjh+NROej`{HS=mKsaPcn1>V?kQ zPL4m1{Wp&x>E$iW5zEG2EkP@PKS(pWNQ4~Q`n?68rjR*sf6vo1xRe*0L zbeLpVVyCk^E@AJ>NY)DQv+eAi>%yyxd8}-KZq=)>O2PZhD6DAvg>#6E=63T<^;!<- z(0wA3L-;U8Vb3x46{{F3e+0I5|*BkuaWQ@DBN2c$%sw|?dYuD4L7DXeXT znarF{QXDjCCi%4~^e*MgM| zG%Z;vUke=M1x4!-aMfFJA~fbA{$9FAvzI^UVznDm3ti^E&mS-n_iw}_DE^UVk2Q8y z`A;wVCYK`OG+X9EjBbHtxN(3Y41;yzkaTMwcFh}#FV{`O+#R3d^BwcB@W57lfB1K7 z-!UEcejktIzvn=4b_3)wg-^uA{*_^4mvOEJ4UX@-au!bx&PRG$vgW7-Q==o?+-b;9 z3t8FprN~7>72c$xO}yzEA;sgYY70xQU=AA4Xv9&!jN_6fKZnbui3pXga?ebX!{W%-yCY)Sm3@Fcb zZJ*-IsMYE+8NT{{?QBE;@+;c(VdR4$(Wa;o5^b$r9ay3aT8*J8_BnI%JLN@p!r z_Nj%G3KTVHegYeH%P=7MAR!8OF0q@y!boIB#b3+z;_TzUG)Dk0pLX!;o4?h3-o)m_-1Nt8$+~7r0LViyeHQP_d!UrheJB9BKt;dIuXF82S@#c%fQnhxsV9EKiC@RyyFKgi z+sVHX^mI47X1UG2sN%9v-g*277XG^tE4F=&^S_Nn>iJDjT3{AE3Y+tF*2QX5N`>M+ zD%7ajjkCHHsgLmE#p6gg^qpl#lmvEwqjQGPKVe}65^W)hBvbMu+H_CR8`B4nYlJ?K z)&$~QBQEZ>P^Ok)>n$GnAMTe>?+mG2Gzvc2F}rUy!t z>W*T5UAg7j$T-r6e&eXngV=L}=kgBj8KQW0;DNm9b9pZR37A;?c zW&wlQo`jp@@%^d&NV)hs{{#!fx^y-8>m{mVkZzLUD_Gj(A|a#rEkx>UW{>FkTrz_` zgg$`GI+_FClofJ9b1g*Uy6@qy{cGXok!6FyK_5S_`*bWju}NF;de#3B#mCL#whM~i zCbvaO3NMIM`A63+gZX<_ar@?mu2Eb9aAMstJUDd(+n>BfP{?J@k4LzBw!?%r9TD4p z47YD=PA?jX+fR0C_DqYKRjcvMC!pMbuQW%B5Jrxi>=d{fb)y5_Nn(CK3=n$ zi}IQ~dJL|=AhA}%)x9;QzV|WgD>mbvXGxIi2Je`GWk**)q2PK)^kpb%5H0pf=?kB* zLYC57U8nqrHu?m@9^;u%AKH8eZ_i&u$jiUAf-SKsjk;xrWBpHuuxbA$ zw3;?9Pl8R@+5#2o`k=#t8TfJiS}Yzs37u;WhO1{QcBhl~lWuqgVx#}XkEb_d?S}b? z|9d_Xl}QCM;3r}NEYhTCC1mO3N#OA>XOG#_zJQ}Mp8`5Gd>^)NUI2N5`P_R3Q>zO* z&$5~w4jLb%fDUrn(>AB4Z<1Hhrq|Jx68zbeAd3HW8NS~BXf&$3W-ohgT)WOKi!ac) zis=L>D=7c8XxmpVUEI~7Wfz^T9<@#aGqODl5>jzY5$gf&tEF4{>XgKM8e762)eE-oW z_^{;=l<;E)i)O+Z)QJcU*^6(EZo4VbZv7(freONbEHoa(7)mVRLXV(maAqlyWT&2cLp*jadpLd|?NF zz~Y@N5fV)95CwPK)4eUe`|xYL|J^)TS)13i^$PIC;HC4i@t5`JU3)ZZJSei4N165r zC+}~?cdI|bjSZh5O=0>;SYnZw8HYMcDkbghnQu~d-wyclb2O>gf$d4SMrO^K(@2gA z;h$h3R4HGHiMH0jCAFcMal*{VOkE^-6>TBZkKUXHBel(*n_C4`9%z`gbYjbX$dWWE zSQ$;qX7KNrmZLlH!*5?9ICL-jLi{K+Ejd=VOn>bC=T~$XR*PFW*VOD^^c_woYLyxUkysacCriA7ZI{+!2mYS_~?tbaB#80=%J(8J=KcakBdEyOMfh~C?|=yY*W}eWJuK^ zj1vleZEB%2bgJ@lkI-ube>Pd#l*pNTQz>iSs(X5)dcBNslv%O+{B^Bh(i)Hf)y*SL zF*{aa^^VV2gQ3fFLZk5g>yN|V7k@>Cs;2c5>+?($1zCz_>b`XeOsriIWhSjGP@xu( zq$VRtZk{AOR(65#s;o^8`JY3nR^rBQQ}N5$tx(FJamS=K0T|tE0v2vqiDD&lb~l=f z@(s)5r#)-1bmnIGxVPsbO9&c!dGu;Gmam?U$TORfo+tQ&lkE8cdG2vqFU z5MyiBfk;ALTon#KI)V6W|L_l505-mXaCIoDAE`(BsxR8~MKkDhUPPPBb$T;;Q@RqA z#G5nWrBVm9saOeix}Hmx$XVm~B!-VOCfwXBBB@48&7SE<{(S|0Ke9yYCnT)xs$ky8 zFY)uC1#r~%8O%dzl{=(X{B+iI*0mH{IYQ04f0lst^Lt23vpTl`19DGB>t4+RmPVz!JQ+kPCACUmKC8;@wUK4PD3bu5O&c ztAP&DtQ!&qQlor}HoYM=t5l7IK}d3saM(Fif`5ZXn!W6~cH|*a({#dF+Q|h<2YNfx zk@oviY&!Onp8uMC0q2(3ykZ@uE+51tb22y8yY+{S4Ru(+CCj?-oCans1NqB)yxYbM z)bsISwbL>hD|3>TF2~+gAL7v6U91A|t|%wBX85#yceMU|3g?@(WUOr@7`1#ZR)6sm zv%1ND%v+2kj@vJ<#g4_JAdBaFlE#8+iLKs+NoS(1$VP+7O)`1-7}k^J6RCwp9l@Qg z3%PAepn<=qt8+=z>rIAt&OA7J9tyc& zX^YXs*ZAo~^--SMhxI4cYb|V=c~{x5AO8C1cXS+-Ax)xLY23daD!J+M1D_9h32O_9 zDUA)&^@L1|7*(p&*4*y@9pp&IAIm1C_9&W=<$n*P~MVI@t2p z?`T$G7*h$p=SU6yt(QBo?CVj8y?cT`W(3GRVn=K2X~@aaCTjL*hISPwt%ivGr^G+^ zE+F}}nL=4}z`ImUNUSmh7?5BK1Cb=!8O+JtiZ(r8@MqI6Lu$j-Ei0)N$~im1$J?;C zJb&XJdm-OEN~}u3v2+b?+Z2e?75MY-wXF3I(i~w;S;>CbbYLB7);6<05s2O<(7l zJc>1}<5vvQ3~!$`_ri593MZG+NUGgYvuAu#wk$$q>?zG2;O5i6GDxQm_R`9EUBLg|CKv0v9)3Lrh7We%k#N9{&9Umrgdn*hqA5Lhg~AEN!AfhkEEx zh8h#HwiVd(GzckC=9}%C6;|#ZaJ2S#bCL~dtS}Hss;d3Mxe;x85eYPjHks}u+FF0M zSmK4MwP+@L&L~)`7@VX_Oylq9VX~Qh4j~Wm$IXlEALsx(Xf?aRTDM3MM&~J~4*e2}Z!t+8s8Q6o+L(NTT}i z)4);OWHll(;sTTx*K^wijgP&vzG%~_lE%`7Bhe<^&7kPHK@e@h($+eb>>Nv@QfI@u zxYrNfL9$$DY1b{8p=)4NY;PW9U7;JAJ)n1kA?VdDci~ePe_DsllUH(izjK0aO3ELS z8|b&1y7&{;MbaewS0`cX=_6doT|ttdG|vdT7i+Msc!Tqu=g2Q}ap4iUzj)Q5LB}~= z`=ZJCKJScR3smgV6pKEagO0U&aH(PmS_$g!D?j4EijN@6$M;pKbgHjrR&zFa?=n!K zbqzH3qj5flIu6_K+(#O-)C-z&9&X&EAw8(;Nun*dNVH!#Hn= zU2MD%SlzJbkEbWDB2C8omnlBlt12Z2G!4;_q3p-uRaDKsf2SU8VWW!{G6*MoDfUO- z#QM=+BPAQ*rZC0}SJH9rfX;VrC^c(Yp+_|edER)!tRA0 z{%-EN8u<=$e(O^Fdvg{0u6$!i84YJNX^y5-N4|4{El{FX0H(}ajIoVca*K3us8VtE z!CGwjawL)rbf>-wb$v45T*vM7|f{H<`7)mD?^zD*5{9TiRp{ zlV}Sr657{Zsl13bsXv+SS_cDtSQ%GO*z>)h9J%-4A=ly(4o>BfT+4Jby&mW-UTSSu z7G*0?z)S9EKfNEMNh&P6d>vaRu6S#`NNVLDJxj;B$-iLNqH(yoZL#JN!=Z#!@6VdT zt_Q6;L=`a&f84wTyNDOuzPXXSddRw*26QcqK=~4Wnyq)2+yAV??sMO<%ECv9P%^S{ zwW_1(oS6j@YHChgecdo%;c|S`um;?`gvCu%c=Y-Yte)8$aq8Un=+gDvBNDN#!JWE#TBfv&;c-Us3&-Dew$r;i<4dX3d!JVEEHt?p|& ze>PFlNoU5Srp6-V!E4Q4?r1Y}BHCB5gG?5IX{)}%k6-V@xjpx=diW$94mpGf2N41j ztuc1mOawH_Tf%i~)^_!oI36zU!b%%z9DZ^Q4!2Hg_RWVRB?0M5)BDt=Rwa;LOygsG z2T4m!#M*7MxQSB)%@1f)sXcm5UI5n|R&{#UpdLkw&u3zKoA&T4Mfr(%^EN)|G=7}j z8{w%j+?bFdr0a{e)CS^QTFO-i4uH2e->a1+Uc=M#I}HA8Qy}#%2RmDBTn9ngOj?_- zfk=8T)iQb$VN{dD#euLvx^-%^_j!!vs|KR?m@e2Ak^z_VAh&Ik4@Y5Imp&+4z85>9=~bp&z{*d1BQ!DGAn{Ds^N(0dtzpC8E(YUO z&Ko_-`moO;JyhXH&}+zI3o!hTbrn2qogooxjZA8W^hKK<;F=~wMVp>XFCz65+#}p; zDbb0WT`?3la6;qLGq;f{<7Xw>+LuSFw`se^PIexUT5GI%l?+E;!~+d~%y+&pf0n zWk^>fX!cBpt;F%2UG(6>#)Wtuu}`xH)`Mz^5BrTk)pi9p1J@EDF=;ra7yj3w2P#(V z!-SK!|75bOSUI;hf}>yNDBMbwZhcH!D=TjGo!nBbMkCle@xJY-s9TU+EZ9kHrJNm@ zc5}K+(1sw|q`P?%ZBl!BF})F4+T0t^q?*zmeg;QmguHmJoxbkK1W;byu-w~uMOK>q zFpfG)?m=u49$(3QbO+(?3N-&@7CKjQ;s%mbDg{(AcTl4e;AX@y^QQf4qJMxNyTa5q zG67XQ$6qH7aogrXkuGQEw&^WxM{7GS8T`9O)UA`)du0>5N%%1pYx^pg)}a^b4*u_E zGJKN;qkH3n)*Vs3dT(uAPm|xm>V^IAD(rE#!mU*6MBBsG5q7z>w2A8P^+B0p{EFx* zWe5(wd}6Z9goaSY-Q~@&zmlxHp9Z0*|n^dYrtXyS_v;977vtH}Il zv73#f$rn9Hs@)FNitFa}hrhmrlj{y>_HqYV+cj!CqEX#`s8zcGQWCCWE3@369@nLK zH3n1xn|1FC4eT7ZE);M1DCGCC`kVfE75X4c;nugbD-pY0I zwY?Sklr5qC+`F$Mp}74ow_OnU`ILk>W8x?hF49~2c6kzQ5^Pd^Y7%Kl9PZu4Qaf}V z#ac=Od6A?GBP;EchdxanXkR`M5^HL-nDMBJz|Y6eA|YP)qvr;irr+b^_UPqV65hTY zaR1&(>|DDZ%DlZ@mb4zx3|-6munWw`QBumzgQv}BZHLRuGaJ&~&a{noL9qANPmqwn zr%j-)f17Hp&~EzYx%RvMSExU3AcoXxiAD`Ovj&d$^()h!V9mFq5FPb2Q{k2=__$jd zuc}@(?|F;LHR}jhPg-~jh>O04)ThU|?Seq$<`1zX!&nEkCc^k(oz| zR%KASye6wx4kSh=BT8>-bY)Mi!ERC#8x4S$r|yS+9C{I3rv9MW%ROWX5Bg~~hE()` zwVe-szxNv3rW<##kYe7e`IdpBv#u9&FYGRC;*6W-GAm-`rnj;QZ)d~mPGRBc&aZIk z`F_nFGnGoTNAKPfSOZ_A*xAwWgZI#{Qd88e-A7M!%UI`+#p;_!pc^2E<7aI`Erj4gOKz_y!pp) zwJ!oYh09l&`V+%5_{k{G8j*3m)!b zbZ6F6$J&)x3w;Y4&z;1x$9zRH79|@rfPN zoWX`mtT&|Q2XJX2-YE?lp+;B3-G~2W8clv511dH~V1=%1k9RI7C7s1DD`p`+Lgy+; zV``M91Y7Hw^K&f*8+vO4czNwUQd9W}2G+JEp!6``nvk}AKZcK&Zsz6l7e}%E+vP~h zM|zFgO^cylm!YUuwGFO3I*&hRe`i!TiK=96C{lGjlj2?#AoJxDxtp4#G$Z?aGDQp@ zPt)d6Hy3KP3On|#MQZXj{(#sHJ*w75{g400g}D`B7>%clz?iyC;N#m#a|DQpI)v@( z79!@=Z7xG%nn;aw6+cMsVeby7e0J8#_v{OwVthbEQv5Z~XeMMOe{!DFUe3fvMnECwW3=t;JfZX>*SLA0RdI$jY0Bkp z=1|9D>z$j}K4+W3$!0R6`=k~aUAsQ~OVz`=JNI#FU)~~9^r1Vg|$3)*r?DTGkcEtYgaPx)kW-sO29^FUJggqY=QG|hz*fgFx z8Xq^+%T>a7_UdmO`{#Q^zqpF@@P|lO^2T2|cQ05OXf$R;@v5cK&X;<8V)prR?0p`F zRFOH1mWD&Peqz z?QF<5N$D~X^~>niA(SOv#jsj*6oMVcAXvPz`vMsYHai_DbBB7(5?!YQa# zQQW?bV@>?8T?Yg-;KRv^$Q@(^Hkv*G|EpJ5yF?g8GF`cM3{Nhvg59NEP^scU6JuJ` z(5+3GAL>-B3tLBi-0M~3Qz)F8!hHW|Ob2NBtHkHRMZ^=4jE!Dk++Yw(0YZNTtqD zIn!ePg%=vKy7cEah>Ou!*&;^_sahBHCk*BHi^vlc8rkW~`50WM0YqYcaE{h?J#_Uc zE~LL?qLc<3`#|{D=U1Z1FXj97Lvb&DRTNsVA^84@41=HMqL_m{^j2o44Z%G^h4k8> zG%NIEQVYS#Ci8*oqubm2qij9HmVY5D@=1bD2>gvOdoVA^U5jDV@K0guz$ZedAOGmR z{a7$$4x;k0x)xQk37ckN;m$R1%tc#7sb1Ir?^pEcHx-Ni-GYeF{Y(k?>ty3l9Yg9j zMfr9gBF)*1)1G6ZpwtZXm=bGiM_73kZudy?)!jJs=q%gfBMvH*YLBiTe{S&$@)icH zY^CTsZ7!NsZ_VV2k0_BPUcsd+5!|+mLn-(-FizeYx8jcI>`RRUNumme$VH+y&tSYh zzOK%1POu>l2kEaM+6GzL^c;E-y_iH>JNM6IHVw*J z&G1354_HgB2?JEeW8S6p+OtF!$3)xo$)aqe_N;||RXtr{Z^X`e#rmybZO><%4v&5S zT>qQfHYdeAyqKk(p~a`lltH)+6>WMly^vfaR5&WSdl)=nAT=c&acO)&ya>*)RTr!& zlGCOv0|e z~wwsLcOBUh8-R)4MvJc^4SH`0y#y9SRo@3`By7LsM4eAUT7ZUCCT&Bv* z3r^NH+~7-L<;AYA5EQ1FK__y>;Hs5TXSm@L&5AILiX9u^gO+XK;lt`RvBiw<^_0OS#8&!Q!Yy9Y=gLD>26r7kdm* zvHv10<}gB{_v2ph4$R#=1N9qr$ACr?@%y5Eh>TWot`B`Ec6-;NzcFCYNBHLGdPGMY zW@5!hXV4_0GQK@9omu_WrmjX@V@g+9u&8A0chU?Pn-XV3TYL+HI#~Pn`~X=JpL2#9 z+)>U|9yK0yTXbaa&qw`4#J+&)@?YGx zxeygE&Ga!ttTqWYS-4~{=gQKiLN6e-=OE#A@a6M)7(#Nq94ZwrR1zlG;*3G$Cdcjc z?}%JA2^*ID4~uus$K_{RkSe>%9?4(S2qnKtNxFbz4>n@lH&fA~{(D$DdL^E}P$D^n zHCD;MFQb>^qo3E~{*#?d4C6E}bcS!SuK2j?hv+hQ9TM7&E)Z8pX2ftS6Pw;|;cQbl zMLXr>FSrt>{^qE);Ie5YFg@AhD)GHw$TSj&?HjUIoFX7m;XlJz6Vf!h2QKE)$s}vLpo) zZGIq9BDICe#ysJEc292L;=<8b^2d+(E5k~jS_sHcJM0R5mVZKfZm_~isbJ9Gg~O1p~O;aV+P#pEbDt4f;=u7 zznwdQ^fW%bu$@yK3~SH??j>~fpa|1ZXH-A5aBGB05?$s_W8%Vo#)c(?Fw}G$366r? zj6OUOGu&v+OFc+;g~D0dBFblx6(V{E-lsxweUvS{B`X71k-B-*z`2W85KzW2>+sJX zFJbiDaZt*iz}>qYCUoeD*p{Y^Wxc=RGn}}y8S3;*@^vurw6&`YKbMkdT*?=9YgI>y z22D|-x<8y)dqTcRYSQT2QFoXI9m(xMe`$R>`WqP(i`#n+W80ylxD|dK3fTkhtuur2 zp16B8Lz60=s9K>H66)*L$+I+Gem@2KZmrksnT{zfCcw9U?iWEWNJ9Sn9N!;X$2I&k zxubiX{+RXa&xO`EUl@sFomH9z>0lze@50J$_-O4ME}DxXf<7KT4~`X@Y92K$@l1Dq znBE0p;RiKGKtAQ4S)s1oJyt?;Qe1kR6rRj;l7H%t&^}yjePCy2m|zpBvNWhQt1eqv zTO-}xz${aFPL?4O&b$ zztCiw{-3nMrh{uRtLq1Fa(TN|jtcu1&tEV>ENQq)sh~(UN3tl96Qq*DBihs-ef-yV zh>j9wK04$5O63vQNw-i%5$^`YN;;ytx5oHV$#Cq^Go+cRWu_2V>x}MFeZeLS5^a)Z z!P3^wWAt#aha{(0>yfTdGSTKOIBOdz)K(e%Hbx_K?N>N?Yn4{8DLSJI>%#uI`4@co z=RA}QG@XU-?k>iR%~P>)$zoJ3*O$Aob)@6ko#RN~y;8Gpxuj6WDib5Ia)wx9nmS+# zQe;usdFvvpS9~4zE720ICl(DpzKf7sv}4sO?Ed8q%Lg&9ktWY%9Lxkz+h8NjU}@7K zQd;`UH64bCw%{Hivq*cFNkkHE6&FKMDVU|L=7rS8Mgj?C`8P7Q|E$Dcm$tBP#(xgE zL)upAfW>RSMg5k=&9>B2n?cpE_SZ!i(4Y%96y<%9nVL7HXt_xRBT^?_F%1vxu^EIrJ z&C_z{1v&RynHGuI9_dzv!#$Fm5{Z8w@l&UXilrN%!K4YAy&?)5QPC=FxP24fUA}~i z-^@f(CK($_*C~fGZki|(bsR2Tzr?x7Obxk59N2xKmo$pVuw#g5({lvj*2*+xp3Mzv zrBXW(Nfvs#5k;E>@b6RmSc`a4a|9^n)*kEj{Q^J#f)fUBUwcg1_8q3yt;&Ul34}%- z!p)VlG<%kZLMe;|m<}oB4YsoUWYZKmbYRtdZK?svx;Ug-B{;el{zTa%LKhbQh`&Dh zA7pyT+}?E}uH3+rSJx5w`T&-kKaZ>Hw`(5JLD6HK%a>-?h__R(ybeNYPHJfcMC?bg zv$ob3ZDE7{a@&T8HWhjv72ypSdYcBIV%D4B4^*=)rH%0_ee#Y)xbyU|W)HBjtBIdh zuQE6s(UjP+Ud5#CUt)ZnT9De5(;QXdwRtC4LHYa^w{2=1Z8EqG1r63>h^h%M$nauuM)N&KQpu5|>0nT3 zxXRWzH>|_WQ`pYBI>NeqVyPcK9WfJaJ6lf5Piv_dA8q&={cAOXHaBp33^rUm3Wp~* zxP41PVFuBblHbnS_~RPJglg;IHD5A=BbeJ4N!;;%m2z+{X8tUGtfPuY{~X8hxnJP$ z?3p++XAbtx_>Aq($H{pMapBt)czHbtnw)Ie!U^`=g;;$0G(v;+A~q%(X{`KLc>3}w z6Clzfo@e1N@Zd0?l0cupPR&uwmCr?@QikHi1@mP6bF|NpQ(Hi%y_&x{A-sTBT}CEv znk_QDwGK(OjWG*QoUX*hqnnWwFH9*GV?f>h7(2fU=X19tTs&+r>&LIqrg|H8@p!RH zNw|oAcl^Mt+RhL^+%1??3o@}iV9aZAH=HhA7g0G zAsF}d7x;AlcFaAv8K3X}311yvj@f%xv15PYv;F%q_47}#Wc+xX{dNsrK8WQefMv$n zMa!}5(seGc4k1sAXO?CxxBad|6XYJpXH15cl6V6TFH%nQjM&(T(X@oSW)Hah>6%3tOpZDWRk09`tzQIX8lNkh(i{w~SQ_qSO&>8r z5e>(g@QxZd3{!si2pcc{j29t)ajO|o3SoY7kET1XPGaZy2uX2g5f;1;dv9#PjGaGV z%10CMIq^xNJH3Cyniya{V=-9l8DZpCqBS~ODAOrIdk5{P-7gjcB88IoY;(wWooCTj&OTj<4=x}K&>a;0?$=$lZ$(^5o!?{VWU4p~Sf>{ANGgM|1ZR0fFrho>W&OSY@ z*<%KQM`N@cK2o!18c19(AAOCvqetV@9n0|i)gEn*liZM=_5$Id`>^fuM$BBd40DE! z!I=eLZY!-GZnIw?0*YG74EIOD^YIv1Pp4y1^d~kBxOXWE-yJ=!U1><4czGhCp7RSc>r<*}O}N?f z<4_d(cjx*Ue%nOQuaGRu{{AyWwDo^Y?RKKv@WCoFsN{P2EJV4q(%-}`G6*}LhO>KK zjYVeLlecZ+C+h9j8pBPndW=egWLL!>pa_YFuWG@uO$_ zk8Nxyn(VITsBuxGKE#vzJCUwaZxfnz4RP6cb~(B zU;o0H@7CkgS3lv{+{HLN`wM(CeiFt{?uyqD`!%o1NNQ>{;$QLu&iXjm1J&GhYbsuS z9m>@YQ<5ypeoQGhz683%w0BujYI3GNpv+K`rL7nHn_o-Y=Hw=Xhv@_^uy_Xy9K)|@ zksYsI13wI$#k!4uz{R>4st@VK1=b(=<1)te`xKp<4M3ab4O#I)n`R}^q+S!$uht)J z>(0QoEs+Q^hVi4JN*!;EY2AfgP=3lPtpNS<^iFQOAW^bbj4Us6AhJ-=zJB)#WT}*E zou`?9S(I(5i?cT-!4IP`WAgv7_tq9}#%w`hm3L2RfsmY}!r2uY@a3qn=-p)i`VZ)f zvC~Im%F>T9{nxpev2_t3g(U&i(Xwi;%eUBnZ5txO_aiap6npFvXZ(=+gF^J^9yZhfQLCbmnw0rqtXK7hu(9X;+Onhv z5Z%w*S~nIc)cwzfoPH@m;YqNmXca$InPjkNlmA$$_heN*q6S<}Z^w%;iWuRk>E9M@ zItxk5Gw0mpbPONd2Z_n&S?@rF-=8Mns}l!t{O&d+#h-;l>JD4SnsD`Og%YK@qilt~ zaPw*lsSW*5{3TMW!f`Wb8$SB_eN65)0q2kBKd#ly1$eJkRrV2lxJz8zC0HHVz-<>4 z$&{?vTv(u`O#_gx!*6Q#03Yu_RAN?~v55SZ0ghd@ddfH3PqsBmuq=d{3CjS-<;ABth>oU~IxcJaCE-V3=B8{ERTXjNv zz!1?UwWUI)vQ}-C6l!GPSR^&#`jc^JMr2+&eD0)nM4cw8O#NUatoZS~Od*%X2k%Wl zs+=D&j(mL(p&>Wn;OLL?75k!FlYyAlvIAx}Z;Mab_QK?zlQF7!KTK!)pSSLgNzI0% za@jtdMNiOYAH2H@pH2S+hqnu>sOO!A?OMUzQ)6wjYr5~Mu0bjYlFevoXSV=HL6IC4 z#OjK$n1u-Kecjjhqmv856eAm(B2n4Ha#YXR_I~qMfcxL{zY0 z0r}**5bNb}P0!;scYJmOx5ITp>FH7v9Y@vI?7a=GkiOxEi+C1F)(l{6Uj~1nsXr%#2EYv;n^4uK^>GvLv-Tw`=oS!L4%ECQDsahMC z)~!bwn-m1)z5LzxS)n4)PN$5h8AOtH=w{(=n4Eqo!OAA-r<$y-W$DUTgxz?k*~=aY zvxN0qa#hBL!(bHW|9~t>*EMNhwHC5l{N(g`71nQF#EC4eDAS`}RWz;hKAd{5fL(*Z zu(5SB$mSQZFW}K_7Cs)m7@evQ=B#eYqw?VCZhXCEBd#BPJA}wk?Cr&qmx(6$zt6FORESnboDq8cAw04g`m>_u`%O5^H}<{W|OP zm^4#$-`5Ke8OKjUE>*e_$~Bq__rRvG$^FN3Sd|P!t8uH)yB52VIX|=t_Z}R_)^C?{ zR()QnR<$;}Nc@5cD&=dWJ}5XBNoulr214cXM5rxn>U7%k8;DMRq1h9mT}cnvyXh}v zkw0Xy$@q5b?_BJ(rJ=Sk_sD}om+|F}U5Jc0pgCfCWb#yIjB0k?z`tb+W?+@(wn>9- zhZxhJt|OgEE{V1tB-kW+bY`@jqiB=53syD-4^oZhz7`GWy?7pE*jN%%Z3&s%8qz2R zFqb&oI)RW#UH8b>r3T7X<)@cs#;HSL`2FTN_R$K?_ua3wCtOSN>%Qfey2BQrdTE-9 zPK{S2w%mD)->0qP_VYrCYE@uuD|Elqc>M6bX50LT&9qX4A}yXPmPGQkJy4`R=C-YD z15vkj18&<`{QkuPJPI=XjJmvFuHW4dBA*MNa1wTj4 zBpb$~xJ1pCap5iP43nQout|IAFK2NZBHDt|XRx#>0_1gMltB?Blp|JOv=tOil1r?D zZy_n+j%E+&TBmlFE9cMzn!i>qM^gNGZojy1LpawSk)tojP$Uu?^zAwZo<1G9eaic= z^6VMBxE;#v=avf1E5gB1yZk1eh3D;kjDb&?ikdCcqD+fHD(kIT3Usp^e?bG>eBJbP z4@Bkuy}4~;AWKQd#;X@un{1|uOS#0#+A{KktC(sY{9zvMy`r2{88cYUsj-pLhjbxduYkCn`+}xEUpG$aR zvSASm(k(yoGi2`vSoL7~c>Xj3%B^$6V!Rf1VTwL4@k!sM>G`1496CGs=cT-@uzqrnJn z+t@gDo(V6vdfYZm6aM$XJ;U5ZZ$N{oPI{?oQ`1e;^)du|CnwIGWO`J1AU;jk9hryr7_4=f>4D2oEWkRO!J zPEQL#QvBPwO7e?5<~7ol8G-@y1)GF3NB0Op=8z!Tf{D(N%ETom%&-eA)ferohG?_E z?!|d1(lp&Akt3>=56JT3Uis%HUPe;mM2OL*G{5qMsVLQ;KO|QCXT(Nb#_8YkwrrD= zt&?VpUGVfYh;rE*F(%ITI{&&UNlH>e6)yZ%1&2%moDI?SAD1ht~;w{h|b3ElQT)TzbYLAvlzEc`aMC%T_O| z*(x|*gflDCs8iE(htw*2(I(0}3!)7?O;#`i)ObBbnppERmJ;!4MVmHAcS%u5G&wJa zA)-w$q!-ZHk*Tg7@|VkF5cR|`cYuVoVgf47s_cdmk;KkhGkeU|&I=`LXP(!X)>+tl zO)~(=aB(UPNwwD8wkav;s>w^h2#%K_hs=tZ;^?9$+A1d6vVseTEAD9?t1BTs2C4)j zrcTTwR*(N>U0vS7v#|?A@d`D#ZDXN|3u8uy1&g-G4jx_wyT~#vA$vZgU?&{e=(#Af zqMC0>rkuRjAU-MCsEhrkGYdOBC4;d>mbJcXgzJ7ABHHvsdXXU7w2U!lDyOAJAUV{q z@H881oqHs=qWv;K*P7W`yTIEw^9q*4?u!@Ex@12!J-y8`X~4rpH*gn`l7iGcG``N1 zxL1QJJr&ZFf*Xu1?U#K5E@P3L_#Em2o)t-z`}XrkT)aKu;!+Z&+PCGljfEmUiv6aR z6W;hHVzFzE7N)786v;1R!ZmvZgLMwed=XA1%dy^mF>X6KDH@`v{O2DMP=!1}vMj?j z5Ih@#d&FQ%TX2sEqOF||`q&o18;D-K*DU;?BDd<*CeI zHR52TIP?xulQRrh(*>a}nqX~nl_)pv5q)c$L(MGa`{8_E>zuH%*11R0m9ktVMT*n& zHi%PigAoi&U) zmyanq7Z$B?W)%#od#4eV!1orZ+uO2~@p~C6sRUxFg|0Tuu#)Nq!3zp5g*45=D?n`R zbN-Dy1N~rU#k(@o(_SMn*?4m*5<(v{ZPXgUq`73qQ=|t8AnC8c?h(3@f{TP+Orp)* zXX)wbcodtQ<p+o z;(3{wW;8_b_RhKSB8P#$i%wgfC8Qc{&B!OQu}Ie$v$R1i0$FdQu(|SS6Qn5mer>Jd z#-~IfJdE@_dmJ2f?h$&s#0R-l<>G!3&n$a7lQ*CSDpAjgZ);^fKV7sYT#P8; z;+iMXPSNwJkZ7kDU@i!yog>`r%=7tavL}d-D45t>MY;^PV|AnT<=JoNQPTKP6z=6p zK`hd*|CWRZt*Z00{0b5hZIMv}EprED4K3kon4uTot8<}6%aWmz=RZ7G!89>cugA_H zQ6EX-F*4o7F6Zk9`#k7fvP?I6DVEwmWybT*jB-xqvGhf(E}jlAm=vwb`)3ia z@n^H!7A;HrnnX5ST*a`J7W`meW=L(yqE^>Vn!SR9MBB}tpKVwWI60SvM?e|PUiOp= zD93J6-v6CUIRMl^E5Ary7_Mbmp?q=`DXAI4Un#7LM4Q|rBo12D%Z;T?FC@jMH=#`N zoV}aIJ}BgR&I!RwlyG8qs7}pd!VRC+>!9ZWMA2{{qVI-&U zK2b`N-l}~=*yO>|PKeU=63G84b}ZPq*~;9)42D*Z{P8?438^7`-Vbvkd3^)2B>r=) zY|Fv7QeAG_*tjsg_HrnAzj0=`*q1?x8Wvz_Gn+2Z+dPBcnNi-&6Rsr;YgnE910c2H zt!;T~IK+|p5Bn3nx`#m1bVhyEz!CGc^&OY2~Kjt7d`ia^7+EjV(ynfC$_%JhPmr^LvvK6;&Y$&Z~ zd3VdSv>TQ*ZJGx|p(LOU0_a@KV<_k&minSgIa-a%Fgz;9$%BDR*DfVlcRj>n zYozPtA}I(Io`JBjGmmJ~!UHctj5kTa5NK(!8&SHj%2Lj5@XBJ+mNBRkK-O%*`D`=8 z+P*S+4>3&!%)C&sV?SotnSVNciIVl;SKaWej*f0NaIn_&>Y2fotatC`1yZUTF|qoF zCK~byMMmDW*mujH9uW;y3}-Lk(>A)i8{6nv9bI@3l2`T(fOmX$SkrYCn(Y>S})g# zwY4+S?F&}4Qv>S2-righIBFaZisTj|Fc*r~PY|9c+-zbrE8)gj#HOP9z!BQSVwOUk z5;ak-xpAlL=Li3qe(-iOud7BR_C)(q_NY3jyXHvFaAAM;x-q9$Su6-_D2l`=Jd4jD z{N&Nlj~pTG&9!I~^g>d6dK1BH=ZH&B-my-X%T9&Sf8nyE_cu3KY z5qcxXuFmdI6=298A$KnhS6lN9hQ5vqhWwTJqDaKgP9rIi_aRDc%AjO%!v>-9O0hC7 zs8Zax%a`Q`iB)L~7%+nKC0h!VK(9}e4oscc&1@zrR&9+MrE~YY(9!@Uopkp`n2cmG z@2DT1Kx}k|^>%51E7L(pA~&Key!MwV$A{Zp?)k)mxfc*w$SFr+=o`0CnNLh+6IJ$p5wM1 zU7BG0ClffoTwb7@DJf~Xk>ir?=Bu>XQCfd$y@`<}-+}DDc~;#fQX(F+I>JxtadIvV zmx|T7ZBs#3{K&yWm}O_442n6?v|KyX?vZu2%z~ozxZ!YhHEm{20Y~;Z9lfnl`-9<{ zBZgDSTQ>_jDpip$ol%6{gDgcW+WLc#q;mNZ?KdrLGK)A4^O>qMSFTn(YuT*yD?}`Aq~G=qA(ichm)lp{2E(t zJ%Ku%wOb-bjI7fL&3oj%TX%BJ9;7RHtB4!);28@ z*u0J(>i78{q*l3+K^{LJW<;`kpNNwy^X1w*7Zk$of2|ct`cDRNrh||~ZY^!9<@82m zwrgY5)T%UGe@+XvXHS_r)nV(PORM_emEqHuEBTkxY_V??aRWE^yuGS5O`NRRt_S?f zH0JgrBLBs;O<(4#6?~JllWVZ{!F?oa77Z?2ybT5~o}K3`X%soK=Ky^dzA?47D+?E= zf(w1p$HAjG#8UHQ{3Gs$2P4%CZG=LZf+q=dPw-TAv4WGE=@pE+)gziUH0w2e6KlI_ znDg;MczK&ABsMSf{c<+yml?peOjj8KDz-sepV}zhFmFlatUZfCV#OPhGNl~qJmw0b zNj0}4qqv3AHP8kPa0!++X>JAy%Z=2~3+WAnus?cJ?vrRGng`JdInF|@*}GW=K?>HJ zNthG99S3lO283D_fxoZbMWXJ$)6#K#D-0^@0vm@KoF%pQ<~E$$v{3VC{!m`niY<39 z;@RVU+`g4c#_TDz&V7d(B?KlIC5JHL~VwKoXMfBJF9; zZnpeFNuuI}x+ii*El=|;wbf+&2(+lyoZWdjCnqvC)B~N;aVY9`GS7uA&@9tthyPUfLM%8bJ zfwS5mFA*aIO%nS4yG@9At%*?<*`Zt6;z%rQ86*1gfI4v0%V20~)T$`lKCxS~XF4RI zw;)U8lZ)9nl!dFet`}!&?A#nNxN|?0E8j(P#5_=V-}Ii-(EIZVCbVCThr7EN-*5aC zHA?ko_nMKCmx-KEsd_(5s8$B;7krbi>)>1=kZBD+=A%eUf+&HO8OS}Vmlu#K(@rcS zeI-9BS=ywpLgYvWa@(n=HxNWyFyA>6Q$lg=VD{75rA$y;mGNcb$@{BlFj9JXKeufN z9V(E8z+dWwxNA7I;+IS(+O!%}8-2>xN0|W4;;iZv?7#30b}jn=Y6V>f!%!wAL2+Ug z_U)X3WtVQ?*~35CQFaMfW6-l^2h7;~Ri0))3jDeFZ|uE$hJ7P`^x3;aD-^408CEut z=Ij9x0& zoRdHW)~?CA+_Zu@$*%%;9~iz2y*KOH**H|=cbEme;qp~HznA$y zwHO}E8ptbW|G0`7Yc@fd#<#LGFfus6AMp*kar>4+WjFIivP336g3N?d+G$eFwP>AR z&CA&d{!MCX_RL4Cd7t5fT7j^4uCF;_d?-?-s(&A>U$q+b`nA*?EnLuGX^W9vuyy}- z{P@`}bgKG3yo+^$m2D-h@s|}ecHrdJ3>Ex)V@|vGFnaVNwE6OLZfqgH(D<3PCUaxD zGL8vg?pI(UF})FWAR>01GjA@>m;{TkuPafnI@XWf;j zP^Z7$7+Bj?MfXY#@WtA>@DIp)E{Qwmg3xEkbc9CedKu+P_C(j|KS1GN-4GSQNxhMhU3dG zJ0l_PhUN%oVJ~Op5-ayvq51J7-WfzXx1vo4D2W1%PE}=v!ZgOQ!ZkT zkgT9dF~gfr#NRhJa{G2pbusyW%aByoFl)!^<$bJJ*ohCfW7s*>!Il+k(6(RZi~Z8q z+c|G17Vg}Or!V%gFJQQPghvc(`--UGQyKs3*BgE3bjx!*Hu3a+0y=aUj))jtOtM$; zju_c<6cX!d+&LD6$Kh4@X75I%rR1E+(O8r((F~wR;as4KD@UoL|*#$ z41E%1+tI%1y@(LR<}eMP1igKoIuJ?t0WMkcU5IWQo-9aFsuF7Z>rzR|60hR(&%Z%* z7U9^`?)3YjFZTSg3?HO(r_ql z{#+Mt((r@fSiO7+#x@-TPmd0;b*Rm(Wp>R)%svv?ah4ocXGN}DCP^Apv~#WpXSWsz z^y`Je^+w~HK2!1Ir(ffz-D}ZcWRYN-8qxs=z17=86UhagJFeZk0i`0t>nL3+X)0+e zY3y6B5hAyiHbG}f!?`q@gu*!};|w{erFQnEaNzH&Xxb`!*TMC3QRv$LeI&-6=JuNg z4np-AKXcoLU>E)zE58~AS<)5$02A4%eZR)swZoyeUcy_D>*&^{C~R4>5yx)b#H*O6 zP$(nV>&48%v4*>2IaDuE8XX!`M_`L4sM|3R&PK=~M0r`3j$MWC4{S$j@-@v-X2trn zV+~0=M7JNWIbwN8WeNCu`DDC!V*02K0rFLT(qkUH+E3CPF%DGIkN!Lkhi`4=_KW#- z#o`(B5YRO9URpt*U|sw>fBb`#xI|W8)m-83Q960bQp1g|NHUuS;)*t5eWxapxeMQDEiQrjQs9Q@b29vww zt%1nAh&DC*q{`&iCZ9$NRwxo)Dv?;>hv_>pc41exn>94Ap-Y2NxcOoSx9{rS8biL` znX7yoiVdIPm(x05D@_{se%utiw}4J1dq}+5J%L!(rp2>D)<0P$v>*kg6fkDD{Qj*3 zOdK*DSDydF&Q(YXC4yIRW??sLiRcb_j}vGSHbo5)KQ5F$v(e5aOxK8$M(($f_< zdFE1%X<8lZfR0rue2@S5goOKWxN(fzHWcfjU!bzD&ZUqhyNyqN+kkab*K3Ys4Ot}~ zuB@w2iggvL12w7ul>z`~BMNqGyb4PuY(T4aeQ@O&v+M-H7P+8u<$f5~szad=Y@)U& zjXj%JEgr;#aPhCkL=pT5vD$S+MQGq|m#0;R%ncb7l-Q|Auq_ArEnFOQA&Bwv3}JgY z^W-E_lCwWN-~8C@>tnbM7M^W(7D4X#IzZuIcoXXdzFR6T9@|L93;lyL~3@SnR!4Ge3{)S ze7XgSu<&;)Zq!~?5&tdRoOF?1vRusnAOXGb@(GkmZRjZ-BI}y8G|Ndf0(li}Y6XN? zZTcYXs8MLbV?4f>vxQWvRcUxT=mvq#1{*drQXjdiH^z;b4`(-uJmirjUcslk*J5nz z8F+C&_py_CLC%`2dP)Y*Dw^nJg`$)TU zl-tfNu`P#UY_HMq@z#X`QnIKhCZG!rtw-ngKE|h=Kf$pb_Yf8ZE<7-XwQDi#Qqf>! zEc*uJ(xJi0cvk2b+^z#?^<;-f_ zdbShs(I?nd(erDGT;N}-CuX+kiH4&VBf9fcs6~0b+yze=r@T)g&MV1@HxPA{!dCMS zrDJ^>tLu5WTHC`r(7Z06BHk@*+?jTX_%si>vb0}cd()pyA!UN4ovlBc$V>DHK@ZRn z4y{Z;BZ`zH_(RPxcz8C(l}m?FvQ+j-^dp#nHmKPX(Q&+3m-p|5_H#FI+j+qzCKNZ; z&Bxi7H{O~ILetM}>?^?2!5_ZPPLPVEkXqS7ERiBzsbm&3E9}Bkio}#OIpSo=NJvY9 zLKP2%Jcd0dihb#v{Z_O{Xff~F=oeTD(hg%G^QIu1!jF8zT&z9yoo3H`P#p5;S$tSk zen?G?#lp|KL7vQ~T4-B&2)@|3swh;hh&*%k$Wu(7*#T-*I2@cBil+`;R}{kZ(}cbtE+ z1*h+?#mPHA1zZt~llAJh=RE?Ke%W_KTrelu4> zG4!i1P^3P@v$K4z>iiJVZ5lef{{?1s8;be?1K{G(5)x~F_J{#jHCrT{ zC2i|i3$C86;aj?UhKAu)@L}w^au?PKy7eC46;i4;hppbgvAIY~xsONZ{?P2@osh>j zSnbq|){7ibwzBz?L>2Llk)8&Th@TayR;NR)G$4ZH{sF|r=qA`PNG$aI*(BOoyFVED z5p8|HHktDj081apaUu9AUcJ=rW=Z40P2ujM$=t^{`t%V*x>X$VOscytqB~DP-I>2& z^v5eOr|U?JX)+Ae1Ny?qcr-0v=U#b*_ja@`8w2+T>m(R4OIX6SI$+BPBk0?=I5h+OZWfYiXfbL9_*b zcDCe4wCNm3mC1Y|OPiJg(XJaA7k>-q*6pRuZ1M5{TKMbS2yyY3VRPbl&7N@}H$_yJ zk6<-&1)9!Whxg`g#OyZR(Y1R&IJ@h@1u3^f>0;e6sly!nz4at++&O{sH@D;O%Rl4C zf0tm==9w6=avX;JI0_TD%)#_s%P?u&TuiM~85Jw`XRoJRS~P%m4lDMpz|-$0z&6r& zkt+pFy%I)BMxRTfVs1fkL6g}rkGxLS-B7mnUMO43AL9IWyo&fwqvkAa-p59kwn~>` zHB-`%j;pVqF@e__rSt*{X(FSSbTr#gKY{#;HVHJD^yH7B=7Z*Ba;ANnJO&4kAAW1q z^f#f2Hr>0o1EovlP$%K=Nz<;%Z6EdockmJ?PrXD!%qgzB?BmlJUyoRTy(hQe^KCQH zqDyI%@CIC*0IMtva+(!YueCp>?pTN=v!`J|{cc<`AA%+f9lY~9wk@9lmw$7frCAuL zR5pOi(QIyLOyT66%Y4PkBhjh4is)qJ3^#W%9L>AFLlN&75|zGa=V%~9cH>_pBxt9t z6BIE*9t!$PT}cw{9IJVLM4O`LrE^%O~Q=U8J=96jrG!-k)}!LVIDZcDi8&7}zW3>PIJq>?>;Z=!--1}rIx+@s`+mUc%NHR}zQyGw z?p3cDC)hQb6ytsfV?giIdSDG~+P@lOn!c}%E}>Y?L$`Kf&#KShcs*~Eh%A9NC1^%x z0Vth19vdILgzR;;X;D!jiEw;o`!t z&qwQ2K7F|xKW|$Omji_}y2QCmEm+y{YkZoU*CD46ch>N{>ssQ2p-R^o0G=MqOw!Kc*6RW{*D{(t zGoy~fmWM%*<s{XHWCN z4N$chFWN+8+*znkb24B!;bue`5-DI)7pm^U1ifk{chXSws=pLVj%hNU|t(62#hkLQJ%?aIro)vlX&rZD0in_A6FM zv~v|TB48{_n?zfPBB9VfT1A-ta@KZ2>vCwiuWA|oImoYW zlUJNiY{9PE=h%BC!Pc=h##bwap+Drj_`tJZAS9gp@|pvB(7@`AQ$hG`>bH=k8g>c& zOG>fqpKma#)2HnF@tOWo5-#GW-M_;A3iSXA5B1Bl9*QO=-R!87@yC-_NPS3i5)4DG zOw@_C$Po^18Dss(uU!lSjCFk(NEno?deXqEs>6fMjsY zIapsjq}j_IHgRFtcH$s6J)RnZ_i9$bm|wp%DCfkw$>Z?j__5eLc@-W%NYXYq^o91X zI*AP{=EKgl6c_qqD6}%pg7srDzQx-Fx_|UtBxL$iws{(|=WNxe zh)ekQ`}OQg(q7g$J4-OJT?-~^d}x_EJq8E%Z{xOeOVX}y@G|%qJ2@#DRcncnbLYUx zg%2yti7EljaU(&E#i!3;+^DHo*lQZL&)AF=13ts==QcqmQKRC(!QA?ixq&LcXFm@| zzxqQoM}QY0r(tzqCBIV$NcOJ`Ul;SP(5??9@7nt~2(sITHRdt}RdZhA#S(kiWU^Xw z(ST&mq%nD+^(ymA_`^`l*|{6*SdZ!AvL7`^EfZ@d7bT0cI`>9T?8VjFNKF+XHj)=) zv4q{&Z#;);-0q8~AkR2${Q)b2YlK8QSFRDln3gulF`WRN2$}vQ+GK5W(9$};UcZf? z=Q)S*4g6{p$`;opsDB)G5q8&ew?Km1iA~sck6j;iB8nI9h|&E5q>;_*!!??>zcr3!)>h;pOu?2#=-Ypq2^s!2FqHWM-I!hrGaz zTg)catoZ3|H()CHiK zB-%Kammw$c_rhOuTp+>4MS@8kTWhob(KO~$xwN*gKRAt~xU;ZvsD>#G8==Rf+^teV zEo=X2%`ortui@`QtIMZDDSw89n3D*38G%Mq#&b7%Uh(s{fwzk;3L}|y-|g@PSmRUm22<23CG1=?Z*08_-=acVuKoQje%&|^kB?@U zCT$eXFa8O${@jY!(Fbt(`cj-+|BvR8ym34HG5Z}Np-hW`l_;0fkIe+#|NbQan@l?9 zF;Ozylt(h*KCL-I#()jf))__n+OhRWP9(#cgT$GdRLWdNrZ=g5u3aO9F-4o8yMs=I zPKcsN$l9hpEaxJTr$*za6DOIqO}d^v{T2*IKndMiiq~FUg#B&9)(mvIauAn8;@OpV zM$f8MQDyw3Ec1tC2mNq82KiyhnvJk`pmiSDkLXYXbJ{jX&AJ&Q5OT}4{dW+Rq?cEh zU7^CXc4T7R4yVS>`o?|8Ztl|@$sFAXKRXw-g7s`NzfY(U)*oyt zOhnnFBRcjxj{dt6N%7q6W)y-i-ocVH=aCYxiT+~7#i|`!kt)+Qgt;N?ZV+xH^VeUc zii54xGDCO#SVuA|qwMI?9XRuV*`gXNn-Gg_GKeV+K>qvej(&6**%k*`5rQ0rv>|Jo7DLlcwh0M4i5;`Q&(Yd;b`oP^=l0xGb0S^w9LM$;wyKTz z$zepZZi&>|3vND*nc(LAYm#uwLSDn((UqIsII>PH4E|x7L4%oDk-&bg&EKzvEQy~X zMbr12l`O`sFke_uNvu%U$1uNkiIVvbugr?*@FV!^)Sr+iX#Iw7VWY_pL6B&tL{mpF za|)}qT}p1x>Z$`yJ$Zp!tBf-~LO1i~H9sIEIOEtsP{bu%$fGa%#eshi5y6WcrO35) zD7f&^tSH9n00j+XMjDgZOE)iOu(Am;tRdEu-vrip#>QOH>~We%V|^5fkuTQ>VQkSR zs0k*Kre8)FkfeFrT=OrN$6(dPOL%aF8l3FuJ#Ppq`syM{ZoWPbhf7*Fd6tMuHemwm z2BrKdR_~pKIU}cW>q6#*WY+vV-m?N_%ei5AvpSgg{Q|f+=l!xJv!819(9f~;`gzvH z=SOvlm*{}Dl+a@&@ICu>7!MXjv3{LdZy^^A#$o|R45N*%Rgny!>#`;E&`>e6sf z)gr%`?E-X06@C9-TF-&ajJ-7KD8V zcI3E7LR^zRs9vl#zoSmV#YbyUw`vdU+j5U%dECEZRBQVf?3`!apGIVFq&c z+3QCx!1q_KLYel6J7#5D38U)QLrS$K+R&7-+P!RqR!1@jOZp_}w&=THxmnhr*BKmIpV z%Aa84lL%bh_&2wo6X|J6oV|GjPapHXamxSGt6@_}O*7n)YiRNexka)g=!F6sZm~v8 zO|k~Kx|iVo5a-p$LK}T7 zUG%o@eC=h;owj91xj8<&;X1uDbFDL4HgZJ+yN}Z7%f|=)SkGvR!rlg9;}v*k;uBi2 zsx%m4?dniuh}xtD;zfN%W&b+AOl}-hH5ZM_h70kTz&zDb*`T&a8(6h%!~d9&e8ZUI zpkS5cTXSaWojbN2K6LoiC#Y+yQs4ICf4)=F2loY!wvPR7oTOoW{?8fO0m290`1LK_ zaouJ5-$OGUm&Mkwy~~@~x#y*eAJ=VPeXUi?=x_rnV|3qr)#CWuoLnA}jGTc-s8#D8 z!J{p~Bx{`4ue{E?Ne5hKuDeyG{NUAtPv6Y?Tyx&N#yl-8*f^wf5cp>|Z7l1LEDyLx zT8AYEiUjl;GgQZR;YTz2@$1(Gll9<)%k<|Dt1HbA)1P`*cYpT2qt*ebAFe(6ObtDG zUnQqRYxq^SX-u!(&36P;%(9j5>9QNI)Nk*FCf&-Q&VWcZ-Tn}r zFnBM;#n=DDI~n^bGx@yqpJF}s<wfIua71 zwsK~weX)?402o2@S`2h9fUs}6-}nzMMOc|SwrA}wDxTQlaY zR?^aF^%%N~GbFBQOD751~h$*5F~>T{5KoOH8_(jqieAq?J>OtqW; znSNO^#}x9Gbul_7P5ZX(u9963l`k<>O-fSD$Rk@TR**oa+G3+(l;mmlnq2kFS*+Av z`v=Y~_*P#oFbye~e`L4T{WbLHy`AS<17V`9i^vPjCpB!)f>IJ&>CeB{Y2Dh{=D1i( z*3Qzm-~6ej`Dse+&{c`11(w$l`F(zUIq_cI^vHL-p^yhG@v;=Q;1KPc(zDw@n{TEq&n3+x7N$k2pC*yv5#-MHyA+iEFI`dk>Xw z$9zxR)8tow)P`)XU)r>3iQ4Amnql|B&M`=uHC;(l?$O5CuPbWiyK1^*p^Dn= zrV>xAl6&o;zG>|>^RM|vgfS6PVm@Q8zF)9TiNCK>?*sR&QR2pJ`oVo4=<4U5QP$ev zYR;%c4edQzyPrHkg&Fm`qOc~>eB}cDv@BcR!Y!i-qGOt9WT)OL=p7(XZ;KAt*lk~%J(H|_00L2 z_vU21{lep#^zGM1L%gHdG&!@D^Jv5+KKYhRCd(D^?3XkNbpwyVSTVM>*K{0M)Y`<;Ph0-pM zxH;FEW4}LNbJzGZbnhe8s!hdC#KgpCMg6f#pZz(*ygM-Y`d5}$qw-gq4~dcg!+Z6* z>E))({X;VrF43=lFVio}*Q?R|@6@=@zJ|Et6y3hN2DjNobLK8rK>^+xQ`HLo){iT_ zN}IbuJ@+3}BgDPI41&IRW|}U2;6Y`r4-ht$c0jK&8hY&I%5PqOD+y}@{-zzYX8uoF zyQWOeUhzsKUt~8c*wfef7@kKN}SrP}@H58qGDszPSr#=#$^xH_GBSb8fX!TI(IPRhYk6 z^H$DL!JiA&anMMIxP8srYR_GE)ry7dv_5;jx$}M%6)e?^bsnWH%GYiO^mTGzR~Pw( z($}y5rVA$Cstp?g+lmn;wQG;VHS*xIl-GINA##xhUqXtyFZxX1FJ5ep0&~rTLqP(u z32ilS@L?)!6WDgm!rOgsbyz(^e=axb+z?7_fbyTU!QA^azbRj%EOP?{=8MVH!TkoP zd8f+lcL}-UD%r%edITUoEk)hh^;M7DO)8FVuBBN;ragqmrWcq|$68bGm#83bkxG1} zA&_aA`|8HSj?mu!-g^3C7^_huY1jRRt7~c>EnRGsc1ek&*iHmM|GgDD>`)m3+2Dz7(mk04{5*-XqhYbJa29H>q4MVj$T zXk{+Mj*p3ebSfRap-m38-KZQcN<`1n1Yj^n@61`dQZ2HZso(y)Ryb@;Nr{o~ z=Oy}L9%?(_pJa%=aRbA^09knAI~&D5O#Y}Q4jDtrtFjlX$GX*u>lX-DDoSgnU3ckg zZsj`tvlhkeH9h14&B=;a#%j~Zhj*{k(0Ow(!@O{jS0|r-f!3{O{sBa*Yu6)m*r*ef zyIbu=PSiKj)~wL)E3yp8{<5INKriaFoQvs&T7bRJd}5R;;ci z|F%KSroXf*p_4qZv1Zg0@WC>q9gy5cyB^rjIj#)Y-G(~B;;2YKVrH7!?!Bk>9dnol zwjZdpoKz(wc2HDm7sbT4RaA7Q8G(>aG)^(GO_iA3U4z<>(1fe6*RID@K6cvNR(0&t zPGe3uTw{(Lry(tOSD%g}b;9V=b?~S$8hh?(+I954N@-gE6Yhlc5!zQ-5JYgvi5F?k zKltMvK*r3tB0}35b0p@#fqQ8BPm4``57d1F$eG175R+BexCgddWrMmR?af;xN}CI2 zctMX9UF^*^s<=Qy`i)epcIB@jrO~wPyPMvA<5z9U+0q}{xW)dOIObU0@xm=?w<1SB ztcWt|iY#JY%{3k6zD61pvn5;M;xZ?L5BvU_{C&biIOBxcVTj~1-XcL=_=uy&0RMy3--c>^Z1J0y3|h?5U+jw=h?B*kX9904hy zL!iwx@6u8OM~~3((T8cT0VC8uIazyVG}3$cgKyqmMaA2}u=Hv_*`#o0@^2gkc>j8^09ev+WA=3&(bNZ>tO#mJ4E-K#L~S0DsS0#zO4-;_pA|wJV-2 z!|>rC^yuho|4(sAWfyd0G~QKXF8`O)<6E5i*i|~P>+a@R;N-l;Q>3@=be>yylhZWl z@(0wj-I&0Ml6-wR^HV)@$xYi_X1_URP5DE)MS+DKQPItG`rwgTKk`iHcxOO#>p^PT ze7E3H0KI52+rZjGFV(odeVvZCt}u7Wk9zfrswX|7Cbel1P?atkHdt`fwpG;$o8{^+ z{pS_^wz!Osn&=N)x^~9ZAr&@1Sg|ye>z~)LvWD46X4nbpu2I4UbxGO)E@qg(FoPwX z^h69H=+3xcWv%~JcYpeke*7dj>s=07cS=*6^s;F|dzrCMr~ZV1mx7qo%Wk_$DQUr6 z+9ezH>R*2=VO`m6&-}58I%NFSpV(B!t$q$ri@AktKbzJ?2 zHq|Av_dG&-v_-K6I?0&0wn}T=-Fdz(#2s^k_UW~csUTGw-BcGW9=ZSPU$kmr#mS>8 z0%XwM^e--PH(W4M@7Si|Y<*k9^zT<{;?vgzj~qa{__CQ$q=AQ&h`+eZic?-k(h*)q z#e(M$sugL_YN0+zn+v1&;cD^sFs>lXA~fKfGw&_kcHP7ZCw+*W?VKK-^CCw3ckNPc zy3}5Kw$ZV@cQs!eh~-jEalY{M$P+5Nn;<1tyQH3bJ}Wa`{!-xG4}4Y=D?#?Yeb!AUc~F zbNp&dY6rpVhZwAYAM8tIk$xdVrA0F!tJlzvam-o1X&w6-Ll8F|lf16sP1S zmG|`(dkYmCo2tZ=o=R~>Qac~kj~uUFtsw1a?b@M{=?p8Uv_Vd%zB+mMfl5iQql|}U zMioA?(o}1?Sbt-I~avs)xBE*{QK&Er|RYJp9`LG_z0q4b{+K$K|sRg zZGFV~X~x3SuhzKA2K7PO0BermLJOM0R`lm8Ke=2wsKz!CJ{jn^#JBLB1&Mmd)fyV}q zDhHphNO5t=D#-awFZ}kq(!Y5v_?Mjrj4loudY}djIbH*jsy?~3VV@Iq_JDzki7V^s zsx7QvJ5>))yiZxyQ&~me2*Pd8f(;k-3l_|<32$}s1wk?2|GZvT-F|-HL;w)+JH}AY z5HZS`?qKsS0nQ>~goG`SJ1QH3;`|On+E#6IVO$^=iWB>7V+=nW;?CJLTNh5aL9>1> z<2Q092p4A|)B{L}%}}e(6~}?H%JIC5&v63&fwy3`zFZiZ6?QP|*?Ux66o~pD)_ec7 zc#S$^U+1{8NQh5TW@BWRUkew!rRTqVPt7LZ5%}Xygsi=f(IJi7$+zce!827u{;}8V z(B4LE2a^)jCdkk6&hH=Twi9nu!FB{?v0#G<%J7MB7j*;fIrqLYc|e5`DFx zG)r0Q*1KuIVW&95->Tr4J1^9MZDZBAIe9ipv~=NHy6>BBmGbgs5w+8LO!set#>@(dhOD&Zv5LN{?(fF%^SM^oX4viF%%Bc2zPED@Tt0}8<3Bi3CHc(ZHPQU zH;lhS-_C`49KcHuFQjdK*)(7np)`qh0JuO%Rf-cF@rgSHF^Wpx;Yb^BVGKF!q$C$k zz%;%gM{WE3KK;JQr;tJKtOCR1-@3Ux%U(iL!M~@^b@QAd$lL$o5S%;*`>j7b=U!EmCyp!}sc(KJ7wzqqEk2?htp*a~CTvc&3IBW$H?!3ltq!wtU%x^v=mo>B$MDiT|qu#6!on0oCWK?%D+t zjh)QB-kwg9(bc5?yrpM8yf%2o0c88CF^1j{F$7L`4$I(YwQK`NAtw==b)vFCeUmm9 z260)VIcP=sUz+JoFaOQm_lg5`DiBcIfX_>75d--17xc?}%YD=z#7!~Eu z)N?<4rWr3>r<8>=0xxv2P2~0&rgMhxr`W`b^L1Ah(Xk!nNj^<+jVCEK%{0{LQWp&6 zbls#o_12SHCcdf);A;GYCtq=&z+eXzt8L zidmnl&!@j=P6tLT#|=10gN_WwVs8aW>B)NOsVT}WU}-z0lcM=+*K5PNZ=7QY_PE{l z)j79~t(HpinxZ1v6so_^A%oSvC|S!)-?DtoDwX(FYw_v@TC-w^E5hk7sVv(FTeLkQ}Y5M!0Y`-(|%Z_J$j8)`!3t( zQ%6W-1gBc^Y056P8?;M0#8%A%&$kEP|Fl*|95G16-q4&GgiVjlsv4U&1m%o`!vOH-T2HtOR~uh7j; zUt*{|INcwWtT<2Q7Gz|k8}$>muUmf3EG=CU+?>E5C4G?gKk1Ar`?7)0T*1Z;Yv(MER@6ryp4-gBklCB+0{Y%?^qS={222ksI+8(g6#ZNQVYxKy$ zDk=)q2M8OI#y^e9<~cn%lV=b#`~-nhf)F!tqL4dWh02EcIUnMy+CoDiZPYav%SChX zHkyR^NR}mX!yM!ns>oDLSrn;&l0%4>j)WbM)@>HIvASVSKA*fjvjJ zXVPqE1<9T|x04d9v0x?~Of-M@{Tdy-?=WTOmRUf9SbWnMViC6pE4HnF8iTw^3Xi}< zQQvRODr`&ce2(=T)EXKJX#*~n9?kl*Ud8Idw*F~@1stE@l zs2)9!HWwPN{7paU!SBCU@+(&aU*!OuKw`gQYhcCdX`@eYl5lM=N_=J2Hi(K!HH!NY zMaP%sJKwPJ3!QQ4IeOu~U$-l&BpigF5}pQmr*5ULxd?#CMts+#Ro&iE3T<`Dr%N^Z zz~Ne-9Z1t_0MQch!NemX5#MRJat`MtCclW=W=CR_HfkKQ$3pE^-ZVYTT`r`~d} z9vyp)rhU3vOU-y-(Ld(LO?%Dx;pyEf4sk=}kG+4qu0Hxm?bhpX^ZGdDWlz_`KmDnU zj~)%a%Ei{OX|KbbBwSI^&4XvEhZ6smt6?4BYRh=o5O?V=J9&B2bcJ~Ma6(OM z3JmqzwG607+$Pm=$?eEUN-+1QRp9=Rwqkq1eADYM&(bkRk5G0F?rH}RHe5rjAB2r^ zW|R*>BhC1`C2dF?U+S&KP~t?3*rZUkKKU4qhFDeTt?0GFzdpIl?lL9cgA=R0=0trvrP9ayq^0qC(OJQHe%z z->;a&(y}Znn5hX*T&W4i++O1oiYQ6kCfz#*9(U##Z#pg}Zt+`VHI>(eu)c?G`) z4^vJayzBr%MoGV5>GHjqqe=`GnxHscYUv`tRpY=z;^!*V6g4)tZGvSVnOZcgzAE@|kSUws(;N+5X_Sf+4cL#>kW0*@CW?zj4=q?##vj!Mi@fE7+l0L-8Fnf1O&!& z&efS)#G)Zri*VwmSHsU_$=kU}#lB)wvE!Vrq^g2mP0PNX$;{Nom79|8+$}{T`i@d+ z#%@}_>Qk*)@~#@kL}~a%r&fAXU=|78_h4yW@Ai#LvkSV|HoU1BI{btSmC|VM;F+qS zq-0y94Wbm==yG{d&kG)#N>{v6um1eD&Ki5E-o5988uN2+js!D^AfHJ$Lt?lHAoJ~s zM{bB9ejPtG*Vx`%Ba^e$$r(}+h~|3lm631~LpNqWvy9PWyK^HO&mA8)XjDNrQ%|bLYx2cA>eNiU0SAvNjRoX+9#lsSZA; z%xWwbl|X)vemeWWqZJd!1|n62uQH?^fG6c&fkimBsg!Kg*MGdG^B#Cq*NnbUKYuvO z)Wg7^Y8sFjBSYLK-8u&RyM((%Y94R=`0UZhBy49hq97u~Qwt2l4pMjh>wb+r^GJO) z``zGK2M{f8RundbjdL3zh$zGz%iqiEiYeiWtj^JpNgHreY%fPI$Sf^mQ^!35Jf7Gp zlXhGb?>gMsqit?dYv zwC(CVcTqLu9dNo1>^|H)t6DWK*`|A>EMk(5R9wbgriZng4EmL|_6t4y&D;9dRd?wA z<8RP{-&fa~^0vVL?X0wHBlSR`whm+$L~k@yC623u9ngtWhpc%HdDjM*Y<%;cFLm5L z<8Xdw07+gqXUi}tK>>9oLgna25T$W7gh8eRvY-Hae45)5HlSL9SakR zsO!qQ4yyyU2YdU@_jKX@XX>om{;kjdc;1YDoWVGNu<=2ou35ck=f&`0hr2`~g0Mj( zkT#$MY;TEA33K4}<{Q$k+GQlfCn_$kG?lBbc%AaIE2qBL)OGH+L?24~B=^yXaob)# z4|v~I$6v37s{%=?6qT-VdmgNucGb&-=OTc@^cFgO#DP`Iza3TiiR&_mN|C40#Y$-U zx*_q>O*hzBW5u#}^}tsz>YRK2tAqDEUH6@Qmwxzk@ph$qttyx($DPvDsPERz1M$t> z%x&q}C7^U6Qd}*l^Csy5UpXor(E;H?td1EajG>6*jk-tS^LJi{?-{oS#LRc_nO)3h z@6tKo^57gKO@~t5{5_Qgi9X+)@P^LW|8!mW$o+ck%O|yAT`=)m0FW^O*=$us8qb-= zkl(Cbror1u$A}?p5M>vsl!vJ8jb^Vo&`+~38yiECs&AYzX9wA)E%w$ES6rn-{%`By zcHwyXjJtI67vF2`%8!F*q)YZYU)R5V>o((Mm#>zdI`e!rYuriydi;_~&G0j9e*C6S zb;W(>%U>K=>prOKaq54?B$Y%(O265#vH1RT^}+8in`a?~RUu*$4_0jYEy1Je!(aHF zyxDiFq&SevrVOGJl$hLGZPVN8fKF}HcW{3VIpqK~X_dUqG{P<_20K}O_sVzr^}SE@ z>YsmU&hpvXxB+iR86fZ=W0s`zM+%70VY_H1+#OOce(CK&q>wh_QI;{9L7+&AeYmmx zjCmSm^w!I_+^Yl5=CE>j`Qk@(^Jm}cpGEAs5WrD;oT7(5d1#yAM)OV4=jKR#s<7Jq zhp#v2h@+2JP7cY#%n&kjKb>*OUCM1)c5`AE)kos$#d_}EE3{_yhru%yBPMB#V$*I2 z9&Imt^5wmu;@lSvdE+824^fIu>~2(WTeVHe&>pQ@Xh8qo>apKobsyZ(QL}Lk)F)lv zz)wvo=3if*rawQOqR*E8qo0?o)cUnQsUV-2gff7vF&Y}mnsw@sH4A14V!%y;^D5%h zbvPo0w2`AYp&0|S&X}?3Nya=$$sP2@JY04F~i4e#@~Si>oKt%b;%LuEBAmioMRU?hqU(})Sd4?sp7(Nly4Yf zl8!Q@T_y~yM&K`=BY*zK@)dk)2s|u%Sb1ReWnxDq$2V8IMj6_zWpfSc+e@wc?XHf4 zyQ^`NSjQ#7V&9!#K1<fGYx@%qT;+yP1%NpUDbNGj!JD5?f9edH+y1@MIf;knIhU` zMy)GW_%!F^@AS*mY5HNoA}!2Xt4#&VRaCIR+{gkawAb(VtFWkCvc?~Vs>Yd(swU}w;MutB7dHh{F-86!;}t16JHp16c&`rxt28h%pccbfU;xv9GR@#i)3 z*UXpF>-*VtdIbELAG8?$oA`Iaa{btnnXDadjVbr*(3;xhNdZod#eNS9-#l|*O zVp4|EVw2T8DM_tTlGG+MQ*BzeRdVZ&O7GG`O}n;qRvA){*|Et{>>hL7Mzz;hlg7=l zDci5#r_9kmzs%I!IrH@MqGei?wO;Gu}XYI-<1XYR8SZo?l3^W*ssQZGzM8S zw1KSo5Xc(8H8+~6;2najEh34u3HNAg%#!w*#$sdRGW6b~?^a1^|MJ#!oqN|~nq?GO zDWY`bA(M33%_lgs?b|yfedqZ_I_vT?l$R6O;XNUxuP#3Ea%~)RsB`S1_K^1OUAptb zmsDIB=qxvfr_m+yB%d5Ss%!Wi-?zVbo_U;S6uGg&xrS0$q_;hYR#Z%eVm#^ccp51# zIz=gQ$!Zjzs*L1DYMPd=md%=}Lx;9%(XpK}JGD{sPK_P4-pI5pCPyLm?XCCgmS^kl znJctn{z`3_|F^PNE!Fy*HTr5yq@hM7ANK&(;1T}9Ir?!pa)hHoaF{b^Kt3JjUTo4r%ZAd-F z{H_uw%VxeeUj-#a%8&9XFUF^w*eDfy0-RCgjncwJJ}qCiT${3fGZ$F8r|Z^Gq6B#o z_fvG-K>5tI=d2CosE34^tiW&0LI*Y zUS;t2PxE#3`L}BB%vYV>oWxVb#l`ZQ#!jVdn!dX(JnC#6d(CN15~!-Ek42{M|JNaB z>8C|61<#lt+~Ftl=tD2`efF8{k`sYr^$3|+DF4j9;LY9?3Y=0 zNSgbLUU=aytz1sjS6Ren++s+(ZgzUF36PbLSa21uREckuL*~w-&yc!zrFj9WHKays z22m+SwKr94e1_uV8z~_%S?OuXYS!Fi)OlIX$jggzVy1I*eag)#(&{z;C_m>9#~)o; zM91x_n8Y#04lv&x@K0`9ezd#=gjPEW8`lS^QB4ovERL}m`F0wzwum&+#yt{F+VS-M z|GV*39d*qS=CE>DHE+2N8GEfm+9-yK(6Z@ZowNV3I`oS3)N2>dHpzS4dg;=mC+Lmo z4~EoHQpzBmf7t`dX}gPayrUv<_D_2Il?OC`=?7aVLndJRbv#0QU3Ra02{kZbP)Rg? z^ETb~`J3|Qhw>*o@T8t8Ps-WBqa6W$L*#2iM9!mvkC46JYXoTti~kxHmqmG6D<*z~ z6M`A#A)2=oB_-L~uqc#BjS58)5!@oM_#$JtL+onCor0__BA&F3>A5Rk)QJ<1=V_&o zwQ8gG8-20n{rQSB$q@uo(-`S^%1Gi?Er#mUz5~_&u;Vr25PnfRO;anwyvayipyZPsGSMLj$#*X82_f=W41$%B*7~2nR7gH&PO=2J%?Lk zdqbKHk3=<5bZl$WCR!~MP5f0 zGW?}f?tkuvPkw8apO?AlpFJMXSN#~-KOgIhS?$u48}op_gS`Q*hilV3x+ zj#b|)9||7rn262Ys8=VRr4^y&`6+7AcyIM-(OAhzeobAnPU}{E7i|1y^pbg?j<~Zl zKEAe*IOk_g`Q%ak`WyajiWx*HzUluAX}F2Hwv7@)?56AtftTPqc3d7bM^o`h2jHye zjW$QYX3B9%@VRpwWB!wzt@0B@o2hIH4VC@5F;+LCvLSH<)=o#*prMeq{UV4OqD1LZ z%HalSw=u@H*~ItZ#CDQYK5)jv8vo!~l}<`SINl!p#%a!~CaLPW%MdM_KU24V^Mmp>1|mg3YNKH~?LRN6II%Rx$&QF6 z4_v1AenSNLObd$DLA#!&2PWU`?7%|R`r!8eY4VR#^x+Sm80E$gJz59udb&DYbdU4? zxPG+)Z?Grtrks9L8b;Wz8!z_@jCtZ)3p0;`b3az{60wuxohMHL_4&q#aJysAb5S z1>~)bF(Uy<6OT=v5ynY_gNDK;VF#~wemkRqtlR=8kxZ4q))r*}F%y(ekUr$iHbjs& zDgEf_A#nosL*GU+ z9kLDwlehbyK162@+Dq*^9paqx6)n}%Ur$rCx8h0QnnvC+cj(MM-JD(bEs8eYrjp`N z&Se)30KURGW<(ZBol7I74YH)vE|}RV{n+8C57LIPaf9HGrir7hA!`&gY8s+uTXdGH zGe)|^*u}=!RFSwbVzeQ2$2Gzx2=OugDk*IT{*ukzKl~otAoSw&=!6&%T11EuGMd;i z)}Oy-jB!5S7$R^cG>Js<7JD^)-e>yima~-f=Z?E!Z**~?zFb^Nczbv1uO7WCjtn{W z_HjC`dpl>R9;31T+}WK zvqB><+7|5$5oH(7pt846TRR)-nzUibaCME}m?>nGHDt{M>|53^EYNPL*(r>y+>Z|Kl*;|v%2fvvHIYes2|{3Z*LfTVBUQT9eekhyuD zmOAhH%eT6F3QMSuyY1g<-kko}pd?@K{`#|`N>{tpI!J>XJMLB;-v1=?qn$qADD8#v zZXkKoPR|AP4!3b4D(UMO$c8)Q~ zn-*1uoIy-Xj8WDe#l^-cF|oBWnhaKtHUpKA*0^drbeLlA-!4W8N!@}cobMrfU|(a5 zpHcmED0CK(GL?qBn~*ok8}er2^6{X-Uc`(--XQ|d%Ui5ji{8-vldsl8{~oDtZahs1 zU%XtWZh_jupTB9MiUTdeW||=n)7TT4ns4Zj6aW z6gK{8eAYy1*vOF(Vht0=M8_&Np@WhWJE(PPV|8hqsXe-NQl~z1#sNJ!6$Mua6Cie}EHW8};%3>7hmiuK0N4s^5j5O7bVyrV z4~UITRBBpZ^=Z*fL;LNeqG3lz+=OLJVV>Tcc&3&u3rytwYshK3>%#{swNuGQk4(|{ z+wN2Lx-XsMk-MLuZs)TxcHIJnKl$phTDqom#Uwkgvyrchx`(e|iZ(7UGXeR9F$#yI zP=xv6BH3xf{K8Qeq|JvwbP%>x*eF?CBF7qQWvmRjB__2|%O?G`YqJ&_*0a01@3Xi1 z9|1>-u|b=FRrmP_D?$=%d0y{q=@IY7-upRB@k1b)Lt#+NVX-pS885i?QI zO?2yF|I)cHRBlK4Lenkov->#BTFf4+rnhT#kWM}CKgw%Cm8e_9Wv|l{*PRsFoC3tA zO;Aiy-7dLzQKY;Tho~Sww21`dOypL0m>;gn&?t6iM$Ci>6M|2-#Em!J7<^W3K(8ol zlqsY}oHafPTq3wbAh{5krKatoL7fiJ_2aM9?ay4RQ_nv{gAX61E`0;3CQ))VCH@mr z6EyJX5z5?@sm6Z4eqXjs#l_BWAA;pu@TW6T3Q1cx5)N1@4wVYy4Y)-hb1D^z99Ige z0FtM|%`?Vp@R{>D^pei!`TfNzELf%0S+g{2$=90x`RiKv#T4_d-_<-ZQPE9Xs3f`) z^0kf;`~8!eI%m4Ml3c|nb=5KZ9iUz#xLw-@zU?{h{i>h;4ouSeOB!kK)*V$)56k?0 z@ktukxu<^q`Fr_`DKhhW3a2SLu9u=bM4Y;)3wX2ttGsMe?*pKH(PoGxC5j85Hf_s_ zrJWwNN!kz}jDueo!VdRglMCBMYbeH(pnZFtsGBdmPB%=tNc~22brxsToOh;t@cCT_ zbkN{~4^`%ZA}z`<(E1ICYKK($?YMDz86(`t5;v6!Hws2Y!qzx`5abG}rEWxpwI z%|G%O5-&>Iw*R=c;K|*fd4GMVdGiBt>5bBR>)i8BQp+xt|4nOVuh$3D@rmcj=PT5H zZM!S4d*xH>)+Bt5TdGfySHH~v!|80kw$=!#ZsyAaR-C>$VC2#m(PsC&kDD*9n~x z*9k%6w6^Y(a347C6Q1+FE!-!7$=4V|)SkFzh9G;Yb(>adnI5Ayu_a13=Mzd|6&sT% zk0(X`gjD&H8mlvC7*Nh(a=;t+AMm9k=b0bwm%O?S}URQCp*41RPyobj!&W3< zM>P}lA5PqWYqv@r0_VyIm%#tc{jd|5mcWs7jKxWU1-bJ+P%eoBLKOoR@8m@G#AYfs zF3k{klH%eL6(1jOI>R_6BzTk@7o}$AS2RzEQkv-i;>^c-qT@_QKu+aE`HYn$G%|#r zrs5>?v(gOdr#CffzS$P3WKr40?_R0*7ZvF5`GJmY-(Dx^t?wRl7SU7|t4uQ-y5|X6 zxgszL*st@k8g%s&&hz?$CqG*s-hRIRT3Wi)J<8KYag7aeM>7?(eIQv=(YiB3i+Nb` z?@1i)>{m(d=$#S8dG9EDC2{f&AZ(VWGb;^?vueu~v2<7=iB|;5FJ$}_=efsNtYSQg z=1hX3W8w@c#Rkrs$LMJOj>QDbZ&bcH7ejI&qwJ&OW0ak}R7H8X>!tr2c8+fO;11_` zWkK%ck^N8AtR-(c$1R#4q9gwkQ111KjIUnSeQ!Ub+>ND+qM|%KjpBaHJfYaO6D9dZ zao;Fk{wKi`0YrH^$&-A%VvPE>;gAgtPVVl)@8mD~UcQ3g6q|mjViHCL|5^={Z3qTJLRAy@OdK|w@;Q1sCdQ+YeUH~G zpFgFPlm^DwVOTh8gKj(bNEoqoJp>p9Wu|qgWy*zLuF$mbYPq-J5 zceo3Md&YBIN*KpHl8%;fx^*k@Iks@BOy-q=L-Wy)eBf)MqXN5^pzv)+P-4FN?h|j( znU7U%$$q|RhDYpqw!Zy~Hj20(u9NPl|2!bc%jW9$@9)v4pG=iMY&Z4jxPC@)vlzB! zhJ!w%FumDEP?jJh!w{8fi2Gn;?A*d~g?eloC3%LVf0EB3>L1JqfodKAxv%4zJZb6eZMR9?yrHgwTY?yblQD{<<<|_z#(boTbfn!j~1@Y)x5v{QP%q3%nd8;q+$|= zC@%9}^J;E$O~4x@@29pTZ(s;9CSh+yCk!_PzPsr_8HCj)N}PQw{RM`k3%`}GXok7^ zE%`DgVPB)NZ!*7+Dpx)Dihfq^%5f?#rWrXvO|wXiNe&z0$7FtG+S5*l+8}LIAS8pz zCXS0lPyc7kkty-G&{68negzZS%kL$onv>#H~!TsvP5QnDwXgNbBSC?Dz9N-UICW zfPLgON{CNTTtauf^2%2lHHvvjPqCz4yD+ZlZOyV%bWiVmt+=3c{A@BT;41vFE6fIS$ zg7z4lVu-uFqGQ_|rHgei)=tspd7${K0-s8Jf6MP>FUr5nqp`w2%~unsQ9hzPz2r$f z#gO($jshpEg%ahjI!gJu0r#2#J;VFPSZx)aFWZe6sQ|S>+SF+LveuW)j=Lc}ytY5r2&*$${|B$8%1JN|7}1`rxp-Qk=iNK3dFL~6|{ z>G0!l<6JoD+RqC&&fE7}qUW(Z(zdUspdfc#HH@BccX?Q?n%Zcv-v8uF4Pfzq6+ooU zf5tzpJ6@$PH;78pxle=^R#XUxHzqGfo`Ow^&d-rQH&4+;ImYr;lAo(JYu9PrnpIl2 zalO`UGM2kq8}nAFplE}9{v7#=&5++;sc_%$G7K>^#O+BOso0F$%n`S#W>Ml>VF>&K zWA7O<#id&rm|}`*79jJOMk*;W*H>Z`ERRO0miSS#!RO}wIgced=3OC`TQQ=V8j|j( zn53hF?wajKan6g%UVU@$$hpsqP*K^;6%n6pcctNPuF5V4wL#ht04k0p>ivz8rH^=L zN1cuCbCzEI?jDB`A`Oq6Gf9^|d%csA)qqL`)8Op<#Tb4#{=iVLCSY$7#EDrl<E3M@I8Pj{3ZBPe#t#kenocJS!HA?~Du6`Kz6aC>S8?9A>f{L!dy%v3Ma>C`q* z@3>;3fK?fhF`3pl4B~&iP%TziR1E3NDD>P~s zmW(aITC?ZND>(al9xZW)KU%^L{|pS@2d$SvBcD0jm^28jr517F$cyyQ7Y_!HoXW+}4i^bZffZb|q=u>zY=K&G-uyA#EC>;><;na6lgjwf z)wk2hdTZ*lYRl|J74h~{b9Lf*165SyD9GwIQd@a>41do3r$c3!&7>BDfRB`O);NYB ztZ|ItH{kuaji{+~N&SowuDTg_Wk7xr+#-0!|8#&FVKfy->sC$a%Y#pi&wm)Gyi*;lQYDC%Et;WR+^w1h>K!IYe!NF zl&2+a%99iX&dlTR-e>9M&mXQBQp9h-8pJdG|5o^eqgO8o1TyW$8H zZ5flz2$lID9Qc$u<>XF2ZUZQ++5n$x%>m(4nJwBVx?24}!$C^3Of^a`lZTr|&FBJU z#&K=k)B3{Zgc$0Onx&*vdnId1iwx74x2Oy#VphYkx&;!itTECFH{K5+pyHTsphNxL z7^5NFAt*cIf{FTBWDL^4pNcA{H^y*van9}=I?Wgzv?cYxPDME{sCX088ZIhAVb&et zgiZaS9GGxRI2FLM?UHv#2kiF?zND$c39t;H=5rxt ze5QPlb(!D}fk>#U;e^3+&Y}LOwEQMI6gmUqiKiMXOWfIy7~(Dy$M2#fNBL{Zn6%G`-$5ZBBGPz0Pr4d7xkSh(lJcCFpGE2%30-wX@RY%2H;)dAyTsi<0 zJBl0P#tpL>ao;CjZvE#_X(;e+yiLW$p#?jX1DziClv;z(LGXl&p4#A}9S*f|lWfDI zvZH2r?qJ5se^kJk2*&D}r8yXa;QFYDJDOSuC+YA87OFGFemJpuNHG+hmK;iZ%6D); zh#gGML?_?H`TFb#DN_iax=d$*YKOQXA9Bx-#voL`A?~7$cgmL+NQAXBgZ$wv6hf>8 zK{zD}8*(Njn8|I7=Pbq}Ji#UIN}x7wkgen>9aInIU{&@4&Hg%&nle)H_l!l_Ea1j= zOk6h>s-+POh?3C*#0i1g#8m|HhU!eN9|2lD#G7uQO*(V9Awb%kpdph_h3`XsL)`V* zkT)l8tGFRYJn9-#+jivYq8!vFY1@e$X7Zg%os&IFbKZGBq9y5?MN70{J?%CG8nX3|a_y0P zd6ukE)-*DRlhu$ADTK-ltW#%V;~J?rs`~vCq}{V+NNEGUi@fU?D=+yz6c`Pe-&3Cr z*%CKH3(6ZGH^faO3VBwEQVDaFK-_G4@M`eP&INDQjpmNg^l^cOylFhaAY};pBxC0q zBL=>wG16XG;-;*i8N~NkoM=L#1MU^5+cYLgx{RthK9^okbK_ z2t*-pMKtJ;4?hB737ba6DhwJBn=_D(fH-9=q_mTgTdPf@>aT1iRB%9#&Pq%Q?ZwE@ zc!)7pU6K)!WMdtT(P^Oct?F(l#7((d;wDuu#J!o~zE#D!F9gr*9Ps76E`K5J{s1yF z_En#bhpKfmGFpUS37oQJ9S47;ydiE}WC#Y=RZ;Qe+e3BLxNCLai*M_TAAZnVPkpPi_d7>% zarD6ss7tuu#u=mBNg2TKi!fSC-1XH-H(1ox#=cgyS)V8snFb%7pSMg!3yO5;sRva% z@}Ry@RP5IccRa6uR|=WLnQMed?)7s_Zzb=DEnB~ z;CuNU5IA+4&Kg63LqxcfoZnnrT&!r{uVQg=bX*y=xr4zk?}iJ_9SLk6k&wKH{=N6U z8h!O~+RbR_48zmi`?k?RV-MD_K8I-SoMrlJC1LaqxK|9@>`GV!kQG7PyLf}3_+RW+^BTe`b{waD+I|?YVjEo+JhNlKUp!ldYfd#c!|8C};s#n<#^%&E!J`0GaL{jWaPxRDc#3P<2O;CJBPJjECea9lP- zm{>nI*8nk=m2`Cj>bdoC+o%%j<3>VmvE-di0K6Lshucy3r%2un*M~aTiFc`aEU;S%n~_- z?TLxgZoN;?T_>KdtKPgzZQIA!>O2JFvt5kVzxR#n^w2#&t8p5F)q!oyqm3P7jLrq( z#{bU5X?-rl*_^nUc&DP+CGT4#nJUhG%@8-+?L|s;sTkhu2UJpwchDdzO{ex9qLXeP z>y%|>(AuX}Uz1uG2(xJ4d3=YCC@8CopCZ%1V_R42y;BMC+noNwUWj{}+$4Fk6b zN`^2iCLK>RhN=k=wkJt@_c=y)UwMg+z4b)JIcMtzspePw>%5+N;gu;G(sfMmtOFDm zOn#OzB1`COh;*?Ho6e}di5rn(7vP|{@%4OctPF8`bKX+X26AynE^+&drk7c^zkApH zb?yT+BP8f0ef}OS$p9<4S_PmwM6|ySlF_4VNlz>7ZsTa1)?g5^Fx8WVT{V{ z5O+brGF^55rMl!2!UzJD&9y{= z$_D5l8UmBVL^$BfMy&=2J2qY;dK{-mK7K+Sd(vj=7wye2y!)LybituFJCmV7@LO@~ zAkT;}v9Z0jO{0v4vCQi-kFCMfqAV^m(J z0vF^j*Rx;VtA4$X*5t>&saHQYu9_F_`&QRIa;-LIhhjVg+Wx3#_T#s zV;{auv7y0G)e&!MWtY~SojMo<--9~kl@OzsI|s1m0F z`%8S9zw{lQeCari8~T5mKDA7ak2=97)6DKV{eGQy{mELnI<$y}-^Y-5vN2ek2}rvT zAd*xzl^;^Wy?|0>)gi7D2N_L<>fX!FRO=1|Ycwp-iQM$sO*(Rq@rsHHRa?k;KV$gh z!*f^A(Cdq|0reqVtWc35ZstMQ#FoU{b{<5w;$^It9KUYto_I9x|gq4ZZ5$t4ro~XZWbnL zq%rcVB4s9t)&QB|LO_w?5*cO;;!}J=ipK3eP@~Qo$zg*8VSx|4f1if-Ajl;Iu8GZt z@y*+45;~^s(0w%m>ip)4+wOov_$vYbxYq)L#qT$x%~dK`d#01tv>~Fzw?ai5nBNI# zl+^U0y7HLAw8!vjY{$PDn2_w=e|PiAfzNhPG3v4hgWMoyDlnCmC=&MSV-G(z`DCJo zs0^nP`;7WnzU%`%KJC9c_T1yO|L&*iu`?gij45+#bRz|#LsqRdJ;&+))ftyvrProE zt70!d(E-1O1vB{Iap};x;1)r3N0M6vP}UF}G8$q-m4=9|Y14gm<yg5~+(u~X+Etb&4#DqMHDyqg~7 zR0Behiq>Cdeq_M+9uwDASB%_Whh1@Gjl}GGO-q_P?Q5s=3W6uJFD!J@McofOxcT9V z8#e=r8|5+0*yF}Ayo%OrEo%^CLefegM8GpHs7&czF9r16ye$5M7^5?2* zg$t5t@W&r=b>EqfYsA1~bj7O^wP4{JPW*Tf{04pt3L8HOE*%yL;o`BKOC-qx2+Zaf zfwXZaIHVREo1mk6wN!euhUXS34}JDX(v>Hkp@c;G9S4jSSaidJ41D#te{lQMx7xM{ zulF|mm3ogagqS(%S7@Y3WJy8RLn_L;-W)XyUEtk#hf0bWBv9$&bkuGK>$Hb1a#HNp z1WYs6?3urq_7`eW+%tp`?aU?aFx1vE_TaM$!3$_;Of=wwWVnux6{Te3hRJ&Y!2o9n zZ^7rs#CYV1ZK9-OO>?+4G1-a51Yni!~4Sh*pXI=xYrOG(IWV=FEqyF zol;VF(ZoZJ(RnWt!W9|7UABMU)AY^UGHC@^f%u*=R4h>&C|eqSL+M=YXeWGLf%;CF zkdl^6igYwieP;uK#FU|mZ+wrb^!3-F7Uw)wX2<#Nt&Y^>sZXh8%bM#G*O~F`!2^%d z&kGrFJHY*Thq39#m`7S=xQe#7)1$sf8}K4Z-Bx1a6$sm+B10yqCP$9-V!`Al_+VTKKk%2_3c-);YDA3vO$L)xvL8D(MC=?WbWueV|2i{CD$3Q zq66OPQP=K~aL`*&ICi3jVI%Gm+z@{lqlYD%Iun(+O&CJrn&9;iC-23!7nZ0YWc=$S ztHD)+%LX=};iIq#){dl!$IW5Mf=;rG>!4N3CTcqzmvCgE|L_*twG{$21V$4~Mly11 zsEaaey{EoWpJ6E*2FpeYB;TeTgz9#F?tJC0Jk==Py4~&FSMYf-Pf7sMF)elX#QU|| z?lnu;EUKG2=|M}_fP2IqoXjVoxM}kZ!%x%z&=4v#Bxp&KN=#%1lQR$|L`_^YbgKP7}+`DoQI(-W6>YehXU=hn zsWRh^yg|o|>s{mdXBI8->-p($bO^LxCj7Zj+HWL_r5Z&4zNr#2uT@Oq0l_mh3xB~>6>hl7G)Ag_0K0WNQy+bO zca5h~CK7DI@we-?H|Zc8&`Gg!l$4q_!|P$(1{WQtO@%A4UL zike2q6gKV?3KUhskPS7hKfv!_UYYrrUD%x;q8NyCU9;~-Nt9il(Z$9;l?tTBh;E@A5U5J{_miEtm z;o4oo4x*t}XbK3jq%uRE3?aka8-z?juuM=;!695Go*;Ax7-J&Q8pr&N*YH|gFT9WU zQ$c7H3{hz`49y}jq^~LnyRN_*wE9R2RTCgacu40qrqc-xuBg)xHlr`pDSv3_S4Vj; zqO&eS+;Y@S>*vO8_l_}JFP(Viob@j&Z^an-is1I`g$2v6i-gfiSB6I(I(F+OPb{7V2V4#= zvv7A=B!Hqos89!~kJQibg*f=QnaklE1n;0Af3dQcovNbsm#Cx|uB#3TjJm$esBET* z$|5$lt4<#~FnCmRSYul56(?V;g)8Zk9MJA)pNK5lEN#j1TBDY7QICSoZ3ZZqz5gm5ED@|8M{R3=K&{K~%A) zUA|_F5eDRqFykEe!$lY(SYNrQA}nFk$zh?!Fn?vCvgzI;6fvF{#U+$U+sf|o5O$=I zwuKcW)?E@FT>^s=O@-3o2MWTHcMf$ye}&QR8@Ayz+ZzIx8IWRvfBLYoAg4H z4rCW3TEpUn1@abcRB`Sr@@}|Pg)8?{-txT-k)N#W)kb0G!^He%mNCuYLNax3t%w5&HPoM}lV^@cE=3;5uw>AGC;!84j8589W#`g zBsFSezSMj(o|b&0{g*FTr!OYGptq+_)z`D$H)=oB2LNcF9JFkHOb0|q#N;cZ3V!Go z<>iKH2jG(S&I1aC5DT^#V}Z@y#?Cb6G{}Bk$Lh-&m0kHi)Y}`kg=`s(WoR@^V=H4^ zQI;!=FvwCW+t_9#%SD#SlwA9oB}`;V4dH5%WsJdC?#!e@bY;tyktHGfR?+=*Uw!|8 z@A>Jx&Us$%*Ylj`InNL0Ip=-)RPNE!1X;L>iNDg^Tg(&oY(B~K7^zM8cIGqSWwD&? zjQ(j=bF){3HHd$gf@2qH);^%UGte|QJehtb4&kqO$&nqqbZtEoBovP+t|>Ik`F#zA zD-7p5oNBd1+1#wQ%*5D*xTa3y$g?TPp{nnZ&w8=X#*5lXB`p?Fe?l~p_5=GhOPB0N zazP16-5Dzy8~nm~RNnt$B!Rguc!m77X6H1aR`VsKXtc6%GGgY?^3BKzWyGl4c+|{` z<}VXTr*w^k$`BrPVheBBQuvzEhKOW+*Kn<)R+Exu7b)d4&sd!k(_0Mkvn0J^R9Vk)Pf`TdFV@uC6PP4)*_A#1IY=8!w|f;jcn z_gcCF+&36-8lgIEvD5&9F=AxFj^3hddu#ThJ9qUzlZH{g7%5YEC&XPVwhfob)NZ$e zo&1AMJh!Z0{$=)#EIdeU=l(4)PfiK&T!(u@GRS6xasR_>Yn8NkB?wM}WFZsv{PKtk zB0%~{FloE@kb^{&T}T#c$-AXCCtdfms(NX=TkE`W`q?LQ(w7FLCjVT$RIIaR_RO7E zK-Z%#!~2dnZ;r3HIA0r_cP&BBEc_$VI6P-vQd&UHZECZw7%~Ht7N9zPx&pOm$`w(U z9pSEmzK@}+#afmuQS`p2P5e~ExKiPn2t(eFS_*gcKzfobZ$-(p5AF-SgO^9Sfm{?C z)A`D-z*7V5w(eICiOummht#oY9fdw#@o9H}yI2hBh=*N07S?p|Z5F*Q1y_Rh|DAV1 z5b{@e3NG;xECC8Vt!F7(*MJ$akl9vZx6M$~w$H`+Wy!LO~4un{R$(mnr9)o6T(Qk6v6Qy+5} z<^*H(x$>wt4c~S!2X$HmAJKes#covii1im9*}uxln22NXr+B>^?~@}we)w%0Lu_4* z=1q>FQmm<(|GYpQBaggyZb7W9ntzEwyjHvSBnIgBcFf(ZjeoP_Xf%4C4zL27KmGjl z>%Q18OOJrXM0>x|4*!66O@VPLYCMgk2b$I&qPEKQ!q5)dr|fXAEMld#0aBf zM*K6L5B=`ionwy3+(*kG{rCrzFK)tVx-V0)NP|k%=SE5QA0{5v?kffTmoVp5_+L(U!VqvsPkdi_?bbJKyA=;zSfsNH7H2_>gC zhNF3=;RPor%A^?goC3NCTV~Ix(vwCiLXo~JMbR@bpYN?bpFUc42=u6FU)$nkW0wZA zNyk5BX_kh+-1;!=e^FIJQr&s!U8REZbx!K*byBDIhD*vGqC7vvygF%dnsE}2T?+o0 zc@C+8wNqWVuk!d6CJXD>(aPqta94(Qnin3j!n+Qn7jrU0i}{SgQy4r8TgpCueKDm} zk3GB(x4F2%W0t4F;r0?o4*)S_LJ>)@pc^h1%Ynflu2bI1UuX@Q_l;J(mQ--o4tj=x zX?J^KWZ&J+7i(X&6qU(-3=keyB5L$P7ed=V0aN=o+Wi9g3z@@YKs| z9R3>}NZ+dx)wcTUQ`t0OGZ)iz@lgNzP7rHiQ0D36D^E{WtI73TS|NYq2XxYMQ{$~; z#9YGiu*E`pk8opXZ}uTA|6!~4{Tdth-*qt>2fK0ipFbo|Pun>j2Y3Ro^;;>}L{ZO= zIr9nPv~DvJ;Olz=c~mTy)woLFl{V02X0~@7qgGgCX5!t56!65w-FIPPP_;FEJ8;+% zJ4(Ic*OK8k)RkevIA3nZ0 z=w81yOD1+X|0uN|^N!Ta(hh`%py~rBl%P!YSY~1kF|ON`6#q*NN4x8USvgL0WuVpM zAL+(Z==55cK(+>Y5;k=7q-}ukld^Do6R!dZuW1q|iNTn$ z^Ji7g4p6acon5l83ge9_kEn;^U_oBZ&$RHuJdKZ#@u%nqt zvzF6)0xN_3O^Y4l*x%&hrijNa$D_n*GC8D-c^N)y8y{Mmi4(eq?K<}IjTZa8tJL;B zvWh#83O|DMZ5fd|wctFPKE}uMaz!+yl&KI>xc8=fQN&gSe-6&@vRhkCH)4pR%F>_4 z4c;AsT@Tn0xuN2J6+6u3+Mi)SFIy(p)s*?eFQ68`C9=<)u^M5>k1;Mf_9sTNQsgAx zlxkayR{6OqFgn9U#cM^sT`d=0T& OK*OB3F|9W7i2WDz654kF diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonidle1.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonidle1.png deleted file mode 100644 index 7295e95efe9c962cf5e5cee94ff928241b81765c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 69329 zcmV)@K!LxBP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D|D{PpK~#8N?41Q* z6h{}w|982#dq{|2!QI`xxRsWo#i3AIoVH&n6{tZ4D^lEx6|3Ox?h;5s+~ty-ee-s2 z?=BFzBzKn!&5usAvuAsIyZ?Ff=FJh(sdwRw@4hgdbbX3T<1c{|a^D)xSkW z_u`{%p)PC-btNmh`-J_%-%<)mUiW_~=1Hk&zxfD1B8jhVq5Z;NVVeqxwd9q*y06jY z)w;gCdeKo`D1tx>e--L&p$K)|*HF=g>cVq`*9hChtIz+>QYzXdk5bVt`RIzSAh1GR z_)CQZSSXgP&~tU4FVyKVE1jY%+`?y3A%wpLMY!nbudq#m`f4Gu6^dM4bobF~>3O

    8CJfgqHE01HJBS}KAVQxSw#5L@9f5@Bog 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%0DW8d%49(wA_m-Wzf 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 K%$b<;G5-Nz>y;7! diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonkiai0.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonkiai0.png deleted file mode 100644 index 911378cbe91f2a6fbaa7d2ee1f8e71de3f7e2d75..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 76964 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

    g`m? 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 diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonkiai1.png b/osu.Game.Rulesets.Taiko.Tests/Resources/old-skin/pippidonkiai1.png deleted file mode 100644 index f89568bca25c2a7bbef738d024c2f2f25ac398e0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 75434 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%0DYz4H_ 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 E2j`LGcmMzZ From 134a94e86d9ca21565b9a537860e2abef1d6556b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 May 2020 10:02:47 +0900 Subject: [PATCH 1558/2376] Rename enum members (no idea what a TaikoDon is) --- .../Skinning/TestSceneTaikoScroller.cs | 2 +- .../Skinning/TaikoLegacySkinTransformer.cs | 4 ++-- osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs | 4 ++-- osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs | 2 +- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs index e26f410b71..520961d3ce 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning { public TestSceneTaikoScroller() { - AddStep("Load scroller", () => SetContents(() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.TaikoScroller), _ => Empty()))); + AddStep("Load scroller", () => SetContents(() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.Scroller), _ => Empty()))); AddToggleStep("Toggle passing", passing => this.ChildrenOfType().ForEach(s => s.LastResult.Value = new JudgementResult(null, new Judgement()) { Type = passing ? HitResult.Perfect : HitResult.Miss })); } diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs index 1096b8db00..6e9a37eb93 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs @@ -87,13 +87,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning return null; - case TaikoSkinComponents.TaikoScroller: + case TaikoSkinComponents.Scroller: if (GetTexture("taiko-slider") != null) return new LegacyTaikoScroller(); return null; - case TaikoSkinComponents.TaikoDon: + case TaikoSkinComponents.Mascot: if (GetTexture("pippidonclear0") != null) return new DrawableTaikoMascot(); diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs index edad36f7d6..ac4fb51661 100644 --- a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs +++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Taiko TaikoExplosionMiss, TaikoExplosionGood, TaikoExplosionGreat, - TaikoScroller, - TaikoDon, + Scroller, + Mascot, } } diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs index c0a6c4582c..9b37af1111 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Taiko.UI { new BarLineGenerator(Beatmap).BarLines.ForEach(bar => Playfield.Add(bar.Major ? new DrawableBarLineMajor(bar) : new DrawableBarLine(bar))); - AddInternal(scroller = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.TaikoScroller), _ => Empty()) + AddInternal(scroller = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.Scroller), _ => Empty()) { RelativeSizeAxes = Axes.X, Depth = float.MaxValue diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index ded1fc0933..dabdfe6f44 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -127,7 +127,7 @@ namespace osu.Game.Rulesets.Taiko.UI }, } }, - mascot = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.TaikoDon), _ => Empty()) + mascot = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.Mascot), _ => Empty()) { Origin = Anchor.BottomLeft, Anchor = Anchor.TopLeft, From 149cb93e8cb314a87c7a0f7a97293ebac98372cf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 May 2020 13:18:37 +0900 Subject: [PATCH 1559/2376] Add very basic error handling when a directory cannot be enumerated --- .../UserInterfaceV2/DirectorySelector.cs | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/DirectorySelector.cs b/osu.Game/Graphics/UserInterfaceV2/DirectorySelector.cs index 59de931df5..ee428c0047 100644 --- a/osu.Game/Graphics/UserInterfaceV2/DirectorySelector.cs +++ b/osu.Game/Graphics/UserInterfaceV2/DirectorySelector.cs @@ -16,6 +16,7 @@ using osu.Framework.Platform; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osuTK; +using osuTK.Graphics; namespace osu.Game.Graphics.UserInterfaceV2 { @@ -74,22 +75,31 @@ namespace osu.Game.Graphics.UserInterfaceV2 { directoryFlow.Clear(); - if (directory.NewValue == null) + try { - var drives = DriveInfo.GetDrives(); - - foreach (var drive in drives) - directoryFlow.Add(new DirectoryPiece(drive.RootDirectory)); - } - else - { - directoryFlow.Add(new ParentDirectoryPiece(currentDirectory.Value.Parent)); - - foreach (var dir in currentDirectory.Value.GetDirectories().OrderBy(d => d.Name)) + if (directory.NewValue == null) { - if ((dir.Attributes & FileAttributes.Hidden) == 0) - directoryFlow.Add(new DirectoryPiece(dir)); + var drives = DriveInfo.GetDrives(); + + foreach (var drive in drives) + directoryFlow.Add(new DirectoryPiece(drive.RootDirectory)); } + else + { + directoryFlow.Add(new ParentDirectoryPiece(currentDirectory.Value.Parent)); + + foreach (var dir in currentDirectory.Value.GetDirectories().OrderBy(d => d.Name)) + { + if ((dir.Attributes & FileAttributes.Hidden) == 0) + directoryFlow.Add(new DirectoryPiece(dir)); + } + } + } + catch (Exception) + { + currentDirectory.Value = directory.OldValue; + + this.FlashColour(Color4.Red, 300); } } From ff6642190f0d6e85cdff7eb3b2998fef0f9fb974 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 14 May 2020 07:26:47 +0300 Subject: [PATCH 1560/2376] Update colour retrieval logic --- .../CatchSkinColourDecodingTest.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs index 57228210d6..7deeec527f 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs @@ -19,9 +19,9 @@ namespace osu.Game.Rulesets.Catch.Tests var rawSkin = new TestLegacySkin(new SkinInfo { Name = "special-skin" }, store); var skin = new CatchLegacySkinTransformer(rawSkin); - Assert.AreEqual(new Color4(232, 185, 35, 255), skin.GetHyperDashCatcherColour()?.Value); - Assert.AreEqual(new Color4(232, 74, 35, 255), skin.GetHyperDashCatcherAfterImageColour()?.Value); - Assert.AreEqual(new Color4(0, 255, 255, 255), skin.GetHyperDashFruitColour()?.Value); + Assert.AreEqual(new Color4(232, 185, 35, 255), skin.GetConfig(CatchSkinColour.HyperDash)?.Value); + Assert.AreEqual(new Color4(232, 74, 35, 255), skin.GetConfig(CatchSkinColour.HyperDashAfterImage)?.Value); + Assert.AreEqual(new Color4(0, 255, 255, 255), skin.GetConfig(CatchSkinColour.HyperDashFruit)?.Value); } private class TestLegacySkin : LegacySkin From 5e09a1b33485663dbb69fa53b44b12861849831c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 May 2020 14:23:12 +0900 Subject: [PATCH 1561/2376] Use Action rather than custom handler --- osu.Game/Overlays/BeatmapListing/BeatmapListingPager.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingPager.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingPager.cs index f55e37ebc7..dc9f30cab3 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingPager.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingPager.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; @@ -20,7 +21,8 @@ namespace osu.Game.Overlays.BeatmapListing private readonly SortCriteria sortCriteria; private readonly SortDirection sortDirection; - public event PageFetchHandler PageFetched; + public event Action> PageFetched; + private SearchBeatmapSetsRequest getSetsRequest; private SearchBeatmapSetsResponse lastResponse; @@ -82,7 +84,5 @@ namespace osu.Game.Overlays.BeatmapListing getSetsRequest?.Cancel(); getSetsRequest = null; } - - public delegate void PageFetchHandler(List sets); } } From fa3373e5f306e0f64afeccefa9977c02584b2f5c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 May 2020 14:24:43 +0900 Subject: [PATCH 1562/2376] Reorder file and change naming slightly --- .../BeatmapListingFilterControl.cs | 30 +++++++++---------- osu.Game/Overlays/BeatmapListingOverlay.cs | 2 +- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs index ac5ad96f7c..92822794b7 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs @@ -36,6 +36,9 @@ namespace osu.Game.Overlays.BeatmapListing private BeatmapListingPager beatmapListingPager; + private ScheduledDelegate queryChangedDebounce; + private ScheduledDelegate queryPagingDebounce; + public BeatmapListingFilterControl() { RelativeSizeAxes = Axes.X; @@ -113,8 +116,17 @@ namespace osu.Game.Overlays.BeatmapListing sortDirection.BindValueChanged(_ => queueUpdateSearch()); } - private ScheduledDelegate queryChangedDebounce; - private ScheduledDelegate queryPagingDebounce; + public void TakeFocus() => searchControl.TakeFocus(); + + public void ShowMore() + { + if (beatmapListingPager == null || !beatmapListingPager.CanFetchNextPage) + return; + if (queryPagingDebounce != null) + return; + + beatmapListingPager.FetchNextPage(); + } private void queueUpdateSearch(bool queryTextChanged = false) { @@ -142,7 +154,7 @@ namespace osu.Game.Overlays.BeatmapListing queryPagingDebounce = null; beatmapListingPager.PageFetched += onSearchFinished; - AddPageToResult(); + ShowMore(); } private void onSearchFinished(List beatmaps) @@ -165,17 +177,5 @@ namespace osu.Game.Overlays.BeatmapListing base.Dispose(isDisposing); } - - public void TakeFocus() => searchControl.TakeFocus(); - - public void AddPageToResult() - { - if (beatmapListingPager == null || !beatmapListingPager.CanFetchNextPage) - return; - if (queryPagingDebounce != null) - return; - - beatmapListingPager.FetchNextPage(); - } } } diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index e26f084ea4..4aa754491c 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -247,7 +247,7 @@ namespace osu.Game.Overlays base.Update(); if (shouldAddNextPage) - filterControl.AddPageToResult(); + filterControl.ShowMore(); } } } From 04c99735264be3cd769bc085d7958727c6b83bda Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 May 2020 14:33:31 +0900 Subject: [PATCH 1563/2376] Clean up cancellation logic --- .../BeatmapListingFilterControl.cs | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs index 92822794b7..3df4d5d588 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs @@ -122,6 +122,7 @@ namespace osu.Game.Overlays.BeatmapListing { if (beatmapListingPager == null || !beatmapListingPager.CanFetchNextPage) return; + if (queryPagingDebounce != null) return; @@ -132,14 +133,15 @@ namespace osu.Game.Overlays.BeatmapListing { SearchStarted?.Invoke(); - beatmapListingPager?.Reset(); + cancelSearch(); - queryChangedDebounce?.Cancel(); queryChangedDebounce = Scheduler.AddDelayed(updateSearch, queryTextChanged ? 500 : 100); } private void updateSearch() { + cancelSearch(); + beatmapListingPager = new BeatmapListingPager( api, rulesets, @@ -150,13 +152,20 @@ namespace osu.Game.Overlays.BeatmapListing sortControl.SortDirection.Value ); - queryPagingDebounce?.Cancel(); - queryPagingDebounce = null; beatmapListingPager.PageFetched += onSearchFinished; ShowMore(); } + private void cancelSearch() + { + beatmapListingPager?.Reset(); + queryChangedDebounce?.Cancel(); + + queryPagingDebounce?.Cancel(); + queryPagingDebounce = null; + } + private void onSearchFinished(List beatmaps) { queryPagingDebounce = Scheduler.AddDelayed(() => queryPagingDebounce = null, 1000); @@ -171,9 +180,7 @@ namespace osu.Game.Overlays.BeatmapListing protected override void Dispose(bool isDisposing) { - beatmapListingPager?.Reset(); - queryChangedDebounce?.Cancel(); - queryPagingDebounce?.Cancel(); + cancelSearch(); base.Dispose(isDisposing); } From c836c9319bb0b6d28fa378e4412b9d4fc7d47e71 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 May 2020 15:35:11 +0900 Subject: [PATCH 1564/2376] Combine pagination logic into BeatmapListingFilterControl --- .../BeatmapListingFilterControl.cs | 118 +++++++++++------- .../BeatmapListing/BeatmapListingPager.cs | 88 ------------- osu.Game/Overlays/BeatmapListingOverlay.cs | 69 +++++----- 3 files changed, 111 insertions(+), 164 deletions(-) delete mode 100644 osu.Game/Overlays/BeatmapListing/BeatmapListingPager.cs diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs index 3df4d5d588..41c99d5d03 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -12,6 +13,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Online.API; +using osu.Game.Online.API.Requests; using osu.Game.Rulesets; using osuTK; using osuTK.Graphics; @@ -20,9 +22,34 @@ namespace osu.Game.Overlays.BeatmapListing { public class BeatmapListingFilterControl : CompositeDrawable { + ///

    + /// Fired when a search finishes. Contains only new items in the case of pagination. + /// public Action> SearchFinished; + + /// + /// Fired when search criteria change. + /// public Action SearchStarted; - private List currentBeatmaps; + + /// + /// True when pagination has reached the end of available results. + /// + private bool noMoreResults; + + /// + /// The current page fetched of results (zero index). + /// + public int CurrentPage { get; private set; } + + private readonly BeatmapListingSearchControl searchControl; + private readonly BeatmapListingSortTabControl sortControl; + private readonly Box sortControlBackground; + + private ScheduledDelegate queryChangedDebounce; + + private SearchBeatmapSetsRequest getSetsRequest; + private SearchBeatmapSetsResponse lastResponse; [Resolved] private IAPIProvider api { get; set; } @@ -30,19 +57,11 @@ namespace osu.Game.Overlays.BeatmapListing [Resolved] private RulesetStore rulesets { get; set; } - private readonly BeatmapListingSearchControl searchControl; - private readonly BeatmapListingSortTabControl sortControl; - private readonly Box sortControlBackground; - - private BeatmapListingPager beatmapListingPager; - - private ScheduledDelegate queryChangedDebounce; - private ScheduledDelegate queryPagingDebounce; - public BeatmapListingFilterControl() { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; + InternalChild = new FillFlowContainer { RelativeSizeAxes = Axes.X, @@ -118,69 +137,80 @@ namespace osu.Game.Overlays.BeatmapListing public void TakeFocus() => searchControl.TakeFocus(); - public void ShowMore() + /// + /// Fetch the next page of results. May result in a no-op if a fetch is already in progress, or if there are no results left. + /// + public void FetchNextPage() { - if (beatmapListingPager == null || !beatmapListingPager.CanFetchNextPage) + // there may be no results left. + if (noMoreResults) return; - if (queryPagingDebounce != null) + // there may already be an active request. + if (getSetsRequest != null) return; - beatmapListingPager.FetchNextPage(); + if (lastResponse != null) + CurrentPage++; + + performRequest(); } private void queueUpdateSearch(bool queryTextChanged = false) { SearchStarted?.Invoke(); - cancelSearch(); + resetSearch(); - queryChangedDebounce = Scheduler.AddDelayed(updateSearch, queryTextChanged ? 500 : 100); + queryChangedDebounce = Scheduler.AddDelayed(() => + { + resetSearch(); + FetchNextPage(); + }, queryTextChanged ? 500 : 100); } - private void updateSearch() + private void performRequest() { - cancelSearch(); - - beatmapListingPager = new BeatmapListingPager( - api, - rulesets, + getSetsRequest = new SearchBeatmapSetsRequest( searchControl.Query.Value, searchControl.Ruleset.Value, + lastResponse?.Cursor, searchControl.Category.Value, sortControl.Current.Value, - sortControl.SortDirection.Value - ); + sortControl.SortDirection.Value); - beatmapListingPager.PageFetched += onSearchFinished; + getSetsRequest.Success += response => + { + var sets = response.BeatmapSets.Select(responseJson => responseJson.ToBeatmapSet(rulesets)).ToList(); - ShowMore(); + if (sets.Count == 0) + noMoreResults = true; + + lastResponse = response; + getSetsRequest = null; + + SearchFinished?.Invoke(sets); + }; + + api.Queue(getSetsRequest); } - private void cancelSearch() + private void resetSearch() { - beatmapListingPager?.Reset(); + noMoreResults = false; + CurrentPage = 0; + + lastResponse = null; + + getSetsRequest?.Cancel(); + getSetsRequest = null; + queryChangedDebounce?.Cancel(); - - queryPagingDebounce?.Cancel(); - queryPagingDebounce = null; - } - - private void onSearchFinished(List beatmaps) - { - queryPagingDebounce = Scheduler.AddDelayed(() => queryPagingDebounce = null, 1000); - - if (currentBeatmaps == null || !beatmapListingPager.IsPastFirstPage) - currentBeatmaps = beatmaps; - else - currentBeatmaps.AddRange(beatmaps); - - SearchFinished?.Invoke(beatmaps); } protected override void Dispose(bool isDisposing) { - cancelSearch(); + resetSearch(); base.Dispose(isDisposing); } diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingPager.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingPager.cs deleted file mode 100644 index dc9f30cab3..0000000000 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingPager.cs +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Collections.Generic; -using System.Linq; -using osu.Game.Beatmaps; -using osu.Game.Online.API; -using osu.Game.Online.API.Requests; -using osu.Game.Rulesets; - -namespace osu.Game.Overlays.BeatmapListing -{ - public class BeatmapListingPager - { - private readonly IAPIProvider api; - private readonly RulesetStore rulesets; - private readonly string query; - private readonly RulesetInfo ruleset; - private readonly SearchCategory searchCategory; - private readonly SortCriteria sortCriteria; - private readonly SortDirection sortDirection; - - public event Action> PageFetched; - - private SearchBeatmapSetsRequest getSetsRequest; - private SearchBeatmapSetsResponse lastResponse; - - private bool isLastPageFetched; - private bool isFetching => getSetsRequest != null; - public bool IsPastFirstPage { get; private set; } - public bool CanFetchNextPage => !isLastPageFetched && !isFetching; - - public BeatmapListingPager(IAPIProvider api, RulesetStore rulesets, string query, RulesetInfo ruleset, SearchCategory searchCategory = SearchCategory.Any, SortCriteria sortCriteria = SortCriteria.Ranked, SortDirection sortDirection = SortDirection.Descending) - { - this.api = api; - this.rulesets = rulesets; - this.query = query; - this.ruleset = ruleset; - this.searchCategory = searchCategory; - this.sortCriteria = sortCriteria; - this.sortDirection = sortDirection; - } - - public void FetchNextPage() - { - if (isFetching) - return; - - if (lastResponse != null) - IsPastFirstPage = true; - - getSetsRequest = new SearchBeatmapSetsRequest( - query, - ruleset, - lastResponse?.Cursor, - searchCategory, - sortCriteria, - sortDirection); - - getSetsRequest.Success += response => - { - var sets = response.BeatmapSets.Select(responseJson => responseJson.ToBeatmapSet(rulesets)).ToList(); - - if (sets.Count == 0) - isLastPageFetched = true; - - lastResponse = response; - getSetsRequest = null; - - PageFetched?.Invoke(sets); - }; - - api.Queue(getSetsRequest); - } - - public void Reset() - { - isLastPageFetched = false; - IsPastFirstPage = false; - - lastResponse = null; - - getSetsRequest?.Cancel(); - getSetsRequest = null; - } - } -} diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index 4aa754491c..225a8a0578 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -4,7 +4,9 @@ using System.Collections.Generic; using System.Linq; using System.Threading; +using System.Threading.Tasks; using osu.Framework.Allocation; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -34,8 +36,6 @@ namespace osu.Game.Overlays private NotFoundDrawable notFoundContent; private OverlayScrollContainer resultScrollContainer; - private const int pagination_scroll_distance = 500; - private bool shouldAddNextPage => resultScrollContainer.ScrollableExtent > 0 && resultScrollContainer.IsScrolledToEnd(pagination_scroll_distance); public BeatmapListingOverlay() : base(OverlayColourScheme.Blue) @@ -121,51 +121,45 @@ namespace osu.Game.Overlays loadingLayer.Show(); } + private Task panelLoadDelegate; + private void onSearchFinished(List beatmaps) { - //No matches case - if (!beatmaps.Any()) + var newPanels = beatmaps.Select(b => new GridBeatmapPanel(b) { - LoadComponentAsync(notFoundContent, addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token); - return; - } + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }); - //New query case - if (!shouldAddNextPage) + if (filterControl.CurrentPage == 0) { - //Spawn new child - var newPanels = new FillFlowContainer + //No matches case + if (!newPanels.Any()) + { + LoadComponentAsync(notFoundContent, addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token); + return; + } + + // spawn new children with the contained so we only clear old content at the last moment. + var content = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Spacing = new Vector2(10), Alpha = 0, Margin = new MarginPadding { Vertical = 15 }, - ChildrenEnumerable = beatmaps.Select(b => new GridBeatmapPanel(b) - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - }) + ChildrenEnumerable = newPanels }; - foundContent = newPanels; - LoadComponentAsync(foundContent, addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token); + panelLoadDelegate = LoadComponentAsync(foundContent = content, addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token); } - - //Pagination case else { - beatmaps.ForEach(x => + panelLoadDelegate = LoadComponentsAsync(newPanels, loaded => { - LoadComponentAsync(new GridBeatmapPanel(x) - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - }, loaded => - { - foundContent.Add(loaded); - loaded.FadeIn(200, Easing.OutQuint); - }); + lastFetchDisplayedTime = Time.Current; + foundContent.AddRange(loaded); + loaded.ForEach(p => p.FadeIn(200, Easing.OutQuint)); }); } } @@ -173,6 +167,7 @@ namespace osu.Game.Overlays private void addContentToPlaceholder(Drawable content) { loadingLayer.Hide(); + lastFetchDisplayedTime = Time.Current; var lastContent = currentContent; @@ -242,12 +237,22 @@ namespace osu.Game.Overlays } } + private const double time_between_fetches = 500; + + private double lastFetchDisplayedTime; + protected override void Update() { base.Update(); - if (shouldAddNextPage) - filterControl.ShowMore(); + const int pagination_scroll_distance = 500; + + bool shouldShowMore = panelLoadDelegate?.IsCompleted != false + && Time.Current - lastFetchDisplayedTime > time_between_fetches + && (resultScrollContainer.ScrollableExtent > 0 && resultScrollContainer.IsScrolledToEnd(pagination_scroll_distance)); + + if (shouldShowMore) + filterControl.FetchNextPage(); } } } From facde2c8e17edd2804dfee0921f29caafcd04648 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 May 2020 16:01:07 +0900 Subject: [PATCH 1565/2376] Remove unnecessary generic specification on cursor --- osu.Game/Extensions/WebRequestExtensions.cs | 15 ++++---------- osu.Game/Online/API/Requests/Cursor.cs | 20 +++++++++++++++++++ .../Online/API/Requests/ResponseWithCursor.cs | 11 +--------- .../API/Requests/SearchBeatmapSetsResponse.cs | 3 +-- 4 files changed, 26 insertions(+), 23 deletions(-) create mode 100644 osu.Game/Online/API/Requests/Cursor.cs diff --git a/osu.Game/Extensions/WebRequestExtensions.cs b/osu.Game/Extensions/WebRequestExtensions.cs index 80c8b147bf..b940c7498b 100644 --- a/osu.Game/Extensions/WebRequestExtensions.cs +++ b/osu.Game/Extensions/WebRequestExtensions.cs @@ -3,22 +3,15 @@ using osu.Framework.IO.Network; using osu.Framework.Extensions.IEnumerableExtensions; -using System.Collections.Generic; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using JetBrains.Annotations; +using osu.Game.Online.API.Requests; namespace osu.Game.Extensions { - public class Cursor - { - [UsedImplicitly] - [JsonExtensionData] - public IDictionary Properties; - } - public static class WebRequestExtensions { + /// + /// Add a pagination cursor to the web request in the format required by osu-web. + /// public static void AddCursor(this WebRequest webRequest, Cursor cursor) { cursor?.Properties.ForEach(x => diff --git a/osu.Game/Online/API/Requests/Cursor.cs b/osu.Game/Online/API/Requests/Cursor.cs new file mode 100644 index 0000000000..f21445ca32 --- /dev/null +++ b/osu.Game/Online/API/Requests/Cursor.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.Collections.Generic; +using JetBrains.Annotations; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace osu.Game.Online.API.Requests +{ + /// + /// A collection of parameters which should be passed to the search endpoint to fetch the next page. + /// + public class Cursor + { + [UsedImplicitly] + [JsonExtensionData] + public IDictionary Properties; + } +} diff --git a/osu.Game/Online/API/Requests/ResponseWithCursor.cs b/osu.Game/Online/API/Requests/ResponseWithCursor.cs index b0fe9eea28..d52e999722 100644 --- a/osu.Game/Online/API/Requests/ResponseWithCursor.cs +++ b/osu.Game/Online/API/Requests/ResponseWithCursor.cs @@ -7,16 +7,7 @@ namespace osu.Game.Online.API.Requests { public abstract class ResponseWithCursor { - /// - /// A collection of parameters which should be passed to the search endpoint to fetch the next page. - /// [JsonProperty("cursor")] - public dynamic CursorJson; - } - - public abstract class ResponseWithCursor : ResponseWithCursor where T : class - { - [JsonProperty("cursor")] - public T Cursor; + public Cursor Cursor; } } diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsResponse.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsResponse.cs index a4d2c0e871..3c4fb11ed1 100644 --- a/osu.Game/Online/API/Requests/SearchBeatmapSetsResponse.cs +++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsResponse.cs @@ -3,12 +3,11 @@ using System.Collections.Generic; using Newtonsoft.Json; -using osu.Game.Extensions; using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Online.API.Requests { - public class SearchBeatmapSetsResponse : ResponseWithCursor + public class SearchBeatmapSetsResponse : ResponseWithCursor { [JsonProperty("beatmapsets")] public IEnumerable BeatmapSets; From 6bb06e9d611aeb9255c28a5936f59c0cdc2559f1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 May 2020 22:57:31 +0900 Subject: [PATCH 1566/2376] Expose CurrentDirectory bindable for consumption --- .../Graphics/UserInterfaceV2/DirectorySelector.cs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/DirectorySelector.cs b/osu.Game/Graphics/UserInterfaceV2/DirectorySelector.cs index ee428c0047..6ea026ad3d 100644 --- a/osu.Game/Graphics/UserInterfaceV2/DirectorySelector.cs +++ b/osu.Game/Graphics/UserInterfaceV2/DirectorySelector.cs @@ -28,11 +28,11 @@ namespace osu.Game.Graphics.UserInterfaceV2 private GameHost host { get; set; } [Cached] - private readonly Bindable currentDirectory = new Bindable(); + public readonly Bindable CurrentDirectory = new Bindable(); public DirectorySelector(string initialPath = null) { - currentDirectory.Value = new DirectoryInfo(initialPath ?? Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)); + CurrentDirectory.Value = new DirectoryInfo(initialPath ?? Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)); } [BackgroundDependencyLoader] @@ -68,7 +68,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 }, }; - currentDirectory.BindValueChanged(updateDisplay, true); + CurrentDirectory.BindValueChanged(updateDisplay, true); } private void updateDisplay(ValueChangedEvent directory) @@ -86,9 +86,9 @@ namespace osu.Game.Graphics.UserInterfaceV2 } else { - directoryFlow.Add(new ParentDirectoryPiece(currentDirectory.Value.Parent)); + directoryFlow.Add(new ParentDirectoryPiece(CurrentDirectory.Value.Parent)); - foreach (var dir in currentDirectory.Value.GetDirectories().OrderBy(d => d.Name)) + foreach (var dir in CurrentDirectory.Value.GetDirectories().OrderBy(d => d.Name)) { if ((dir.Attributes & FileAttributes.Hidden) == 0) directoryFlow.Add(new DirectoryPiece(dir)); @@ -97,8 +97,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 } catch (Exception) { - currentDirectory.Value = directory.OldValue; - + CurrentDirectory.Value = directory.OldValue; this.FlashColour(Color4.Red, 300); } } From cb0b25ac55c7db5e6b1cc4a941971676690388ee Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 May 2020 22:57:41 +0900 Subject: [PATCH 1567/2376] Throw better exceptions from OsuStorage --- osu.Game/IO/OsuStorage.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index 71b01ce479..8109631ef9 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.cs @@ -48,11 +48,14 @@ namespace osu.Game.IO var source = new DirectoryInfo(GetFullPath(".")); var destination = new DirectoryInfo(newLocation); + if (source.FullName == destination.FullName) + throw new ArgumentException("Destination provided is already the current location", nameof(newLocation)); + // ensure the new location has no files present, else hard abort if (destination.Exists) { if (destination.GetFiles().Length > 0 || destination.GetDirectories().Length > 0) - throw new InvalidOperationException("Migration destination already has files present"); + throw new ArgumentException("Destination provided already has files or directories present", nameof(newLocation)); deleteRecursive(destination); } From 0b73063a89ba56edc8fe0d364fe105375fd63c3f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 May 2020 22:58:05 +0900 Subject: [PATCH 1568/2376] Add basic (working) migration UI --- .../Sections/Maintenance/GeneralSettings.cs | 9 +- .../Maintenance/MigrationRunScreen.cs | 88 +++++++++++++++++++ .../Maintenance/MigrationSelectScreen.cs | 57 ++++++++++++ 3 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs create mode 100644 osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs index 832673703b..8bdeadae5c 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; using osu.Game.Scoring; @@ -26,8 +27,14 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance private TriangleButton undeleteButton; [BackgroundDependencyLoader] - private void load(BeatmapManager beatmaps, ScoreManager scores, SkinManager skins, DialogOverlay dialogOverlay) + private void load(BeatmapManager beatmaps, ScoreManager scores, SkinManager skins, DialogOverlay dialogOverlay, OsuGame game) { + Add(importBeatmapsButton = new SettingsButton + { + Text = "Migrate storage to new location", + Action = () => game.PerformFromScreen(menu => menu.Push(new MigrationSelectScreen())) + }); + if (beatmaps.SupportsImportFromStable) { Add(importBeatmapsButton = new SettingsButton diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs new file mode 100644 index 0000000000..76f01dc4b9 --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.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.IO; +using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Logging; +using osu.Framework.Screens; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Screens; + +namespace osu.Game.Overlays.Settings.Sections.Maintenance +{ + public class MigrationRunScreen : OsuScreen + { + private readonly DirectoryInfo destination; + + [Resolved] + private OsuGame game { get; set; } + + public override bool AllowBackButton => false; + + public override bool AllowExternalScreenChange => false; + + public override bool DisallowExternalBeatmapRulesetChanges => true; + + private Task migrationTask; + + public MigrationRunScreen(DirectoryInfo destination) + { + this.destination = destination; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + InternalChildren = new Drawable[] + { + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Migration in progress", + Font = OsuFont.Default.With(size: 48) + }, + new LoadingSpinner(true) + { + State = { Value = Visibility.Visible } + } + } + }, + }; + + Beatmap.Value = Beatmap.Default; + + migrationTask = Task.Run(() => game.Migrate(destination.FullName)) + .ContinueWith(t => + { + if (t.IsFaulted) + Logger.Log($"Error during migration: {t.Exception?.Message}", level: LogLevel.Error); + + Schedule(this.Exit); + }); + } + + public override bool OnExiting(IScreen next) + { + // block until migration is finished + if (migrationTask?.IsCompleted == false) + return true; + + return base.OnExiting(next); + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs new file mode 100644 index 0000000000..d1c2f6d6ee --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.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 osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Screens; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Screens; + +namespace osu.Game.Overlays.Settings.Sections.Maintenance +{ + public class MigrationSelectScreen : OsuScreen + { + private DirectorySelector directorySelector; + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(GridSizeMode.Relative, 0.8f), + new Dimension(), + }, + Content = new[] + { + new Drawable[] { directorySelector = new DirectorySelector { RelativeSizeAxes = Axes.Both } }, + new Drawable[] + { + new OsuButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 300, + Text = "Start", + Action = start + }, + } + } + }; + } + + private void start() + { + var target = directorySelector.CurrentDirectory.Value; + if (target.GetDirectories().Length > 0 || target.GetFiles().Length > 0) + target = target.CreateSubdirectory("osu-lazer"); + + ValidForResume = false; + this.Push(new MigrationRunScreen(target)); + } + } +} From 06f507496a0d4bb8865d226ea9af6d220a5ab4fb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 May 2020 23:02:28 +0900 Subject: [PATCH 1569/2376] Delete migration source if no files exist after completion --- osu.Game/IO/OsuStorage.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index 8109631ef9..443f4fdb69 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.cs @@ -87,6 +87,9 @@ namespace osu.Game.IO dir.Delete(true); } + + if (target.GetFiles().Length == 0 && target.GetDirectories().Length == 0) + target.Delete(); } private static void copyRecursive(DirectoryInfo source, DirectoryInfo destination, bool topLevelExcludes = true) From d04079f6ab7911b5d7f8fd633019993137b0fc65 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 May 2020 17:40:30 +0900 Subject: [PATCH 1570/2376] Fix directory selector not masking properly --- .../UserInterfaceV2/DirectorySelector.cs | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/DirectorySelector.cs b/osu.Game/Graphics/UserInterfaceV2/DirectorySelector.cs index 6ea026ad3d..ae34281bfb 100644 --- a/osu.Game/Graphics/UserInterfaceV2/DirectorySelector.cs +++ b/osu.Game/Graphics/UserInterfaceV2/DirectorySelector.cs @@ -40,19 +40,25 @@ namespace osu.Game.Graphics.UserInterfaceV2 { Padding = new MarginPadding(10); - InternalChildren = new Drawable[] + InternalChild = new GridContainer { - new FillFlowContainer + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] { - RelativeSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Children = new Drawable[] + new Dimension(GridSizeMode.Absolute, 50), + new Dimension(), + }, + Content = new[] + { + new Drawable[] { new CurrentDirectoryDisplay { - RelativeSizeAxes = Axes.X, - Height = 50, + RelativeSizeAxes = Axes.Both, }, + }, + new Drawable[] + { new OsuScrollContainer { RelativeSizeAxes = Axes.Both, @@ -65,7 +71,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 } } } - }, + } }; CurrentDirectory.BindValueChanged(updateDisplay, true); From 4e4a779d6827d76323702fad328c451c3e3fb000 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 May 2020 17:40:43 +0900 Subject: [PATCH 1571/2376] Improve overall UI --- .../Sections/General/UpdateSettings.cs | 10 +- .../Sections/Maintenance/GeneralSettings.cs | 7 -- .../Maintenance/MigrationRunScreen.cs | 29 ++++- .../Maintenance/MigrationSelectScreen.cs | 105 +++++++++++++++--- 4 files changed, 123 insertions(+), 28 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs index 188c9c05ef..b5d07ee7b1 100644 --- a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs @@ -4,7 +4,9 @@ using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Platform; +using osu.Framework.Screens; using osu.Game.Configuration; +using osu.Game.Overlays.Settings.Sections.Maintenance; namespace osu.Game.Overlays.Settings.Sections.General { @@ -13,7 +15,7 @@ namespace osu.Game.Overlays.Settings.Sections.General protected override string Header => "Updates"; [BackgroundDependencyLoader] - private void load(Storage storage, OsuConfigManager config) + private void load(Storage storage, OsuConfigManager config, OsuGame game) { Add(new SettingsEnumDropdown { @@ -28,6 +30,12 @@ namespace osu.Game.Overlays.Settings.Sections.General Text = "Open osu! folder", Action = storage.OpenInNativeExplorer, }); + + Add(new SettingsButton + { + Text = "Change folder location...", + Action = () => game.PerformFromScreen(menu => menu.Push(new MigrationSelectScreen())) + }); } } } diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs index 8bdeadae5c..1dd079a8ab 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs @@ -5,7 +5,6 @@ using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; using osu.Game.Scoring; @@ -29,12 +28,6 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance [BackgroundDependencyLoader] private void load(BeatmapManager beatmaps, ScoreManager scores, SkinManager skins, DialogOverlay dialogOverlay, OsuGame game) { - Add(importBeatmapsButton = new SettingsButton - { - Text = "Migrate storage to new location", - Action = () => game.PerformFromScreen(menu => menu.Push(new MigrationSelectScreen())) - }); - if (beatmaps.SupportsImportFromStable) { Add(importBeatmapsButton = new SettingsButton diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs index 76f01dc4b9..b29cd0d630 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs @@ -12,6 +12,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Screens; +using osuTK; namespace osu.Game.Overlays.Settings.Sections.Maintenance { @@ -28,6 +29,8 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance public override bool DisallowExternalBeatmapRulesetChanges => true; + public override bool HideOverlaysOnEnter => true; + private Task migrationTask; public MigrationRunScreen(DirectoryInfo destination) @@ -47,6 +50,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance Direction = FillDirection.Vertical, Anchor = Anchor.Centre, Origin = Anchor.Centre, + Spacing = new Vector2(10), Children = new Drawable[] { new OsuSpriteText @@ -54,12 +58,26 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance Anchor = Anchor.Centre, Origin = Anchor.Centre, Text = "Migration in progress", - Font = OsuFont.Default.With(size: 48) + Font = OsuFont.Default.With(size: 40) + }, + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "This could take a few minutes depending on the speed of your disk(s).", + Font = OsuFont.Default.With(size: 30) }, new LoadingSpinner(true) { State = { Value = Visibility.Visible } - } + }, + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Please avoid interacting with the game!", + Font = OsuFont.Default.With(size: 30) + }, } }, }; @@ -76,6 +94,13 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance }); } + public override void OnEntering(IScreen last) + { + base.OnEntering(last); + + this.FadeOut().Delay(250).Then().FadeIn(250); + } + public override bool OnExiting(IScreen next) { // block until migration is finished diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs index d1c2f6d6ee..c1aa7f095c 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs @@ -1,13 +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.IO; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Logging; +using osu.Framework.Platform; using osu.Framework.Screens; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Screens; +using osuTK; namespace osu.Game.Overlays.Settings.Sections.Maintenance { @@ -15,40 +23,101 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance { private DirectorySelector directorySelector; + public override bool AllowExternalScreenChange => false; + + public override bool DisallowExternalBeatmapRulesetChanges => true; + + public override bool HideOverlaysOnEnter => true; + [BackgroundDependencyLoader] - private void load() + private void load(OsuGame game, Storage storage, OsuColour colours) { - InternalChild = new GridContainer + game.Toolbar.Hide(); + + // begin selection in the parent directory of the current storage location + var initialPath = new DirectoryInfo(storage.GetFullPath(string.Empty)).Parent?.FullName; + + InternalChild = new Container { + Masking = true, + CornerRadius = 10, RelativeSizeAxes = Axes.Both, - RowDimensions = new[] + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(0.5f, 0.8f), + Children = new Drawable[] { - new Dimension(GridSizeMode.Relative, 0.8f), - new Dimension(), - }, - Content = new[] - { - new Drawable[] { directorySelector = new DirectorySelector { RelativeSizeAxes = Axes.Both } }, - new Drawable[] + new Box { - new OsuButton + Colour = colours.GreySeafoamDark, + RelativeSizeAxes = Axes.Both, + }, + new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Width = 300, - Text = "Start", - Action = start + new Dimension(), + new Dimension(GridSizeMode.Relative, 0.8f), + new Dimension(), }, + Content = new[] + { + new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Please select a new location", + Font = OsuFont.Default.With(size: 40) + }, + }, + new Drawable[] + { + directorySelector = new DirectorySelector(initialPath) + { + RelativeSizeAxes = Axes.Both, + } + }, + new Drawable[] + { + new TriangleButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 300, + Text = "Begin folder migration", + Action = start + }, + } + } } } }; } + public override void OnSuspending(IScreen next) + { + base.OnSuspending(next); + + this.FadeOut(250); + } + private void start() { var target = directorySelector.CurrentDirectory.Value; - if (target.GetDirectories().Length > 0 || target.GetFiles().Length > 0) - target = target.CreateSubdirectory("osu-lazer"); + + try + { + if (target.GetDirectories().Length > 0 || target.GetFiles().Length > 0) + target = target.CreateSubdirectory("osu-lazer"); + } + catch (Exception e) + { + Logger.Log($"Error during migration: {e?.Message}", level: LogLevel.Error); + return; + } ValidForResume = false; this.Push(new MigrationRunScreen(target)); From 0ef3bae26a4483fd12c21a34ff38d3fe6f69775d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 14 May 2020 18:34:51 +0900 Subject: [PATCH 1572/2376] Expose playfield from IManiaHitObjectComposer --- .../ManiaPlacementBlueprintTestScene.cs | 2 +- .../ManiaSelectionBlueprintTestScene.cs | 2 +- osu.Game.Rulesets.Mania/Edit/IManiaHitObjectComposer.cs | 2 +- osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs | 2 -- osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs | 2 +- 5 files changed, 4 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs index aac77c9c1c..39d5f50459 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs @@ -49,6 +49,6 @@ namespace osu.Game.Rulesets.Mania.Tests public Column ColumnAt(Vector2 screenSpacePosition) => column; - public int TotalColumns => 1; + public ManiaPlayfield Playfield => null; } } diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaSelectionBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/ManiaSelectionBlueprintTestScene.cs index b598893e8c..d6dee92ba6 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaSelectionBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaSelectionBlueprintTestScene.cs @@ -33,6 +33,6 @@ namespace osu.Game.Rulesets.Mania.Tests public Column ColumnAt(Vector2 screenSpacePosition) => column; - public int TotalColumns => 1; + public ManiaPlayfield Playfield => null; } } diff --git a/osu.Game.Rulesets.Mania/Edit/IManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/IManiaHitObjectComposer.cs index f64bab1fae..9b5d290fa8 100644 --- a/osu.Game.Rulesets.Mania/Edit/IManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/IManiaHitObjectComposer.cs @@ -10,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Edit { Column ColumnAt(Vector2 screenSpacePosition); - int TotalColumns { get; } + ManiaPlayfield Playfield { get; } } } diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index dfa933baad..d7c0889c0d 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -42,8 +42,6 @@ namespace osu.Game.Rulesets.Mania.Edit public IScrollingInfo ScrollingInfo => drawableRuleset.ScrollingInfo; - public int TotalColumns => Playfield.TotalColumns; - public override (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time) { var hoc = Playfield.GetColumn(0).HitObjectContainer; diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs index 55245198c8..83049ff959 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs @@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Mania.Edit maxColumn = obj.Column; } - columnDelta = Math.Clamp(columnDelta, -minColumn, composer.TotalColumns - 1 - maxColumn); + columnDelta = Math.Clamp(columnDelta, -minColumn, composer.Playfield.TotalColumns - 1 - maxColumn); foreach (var obj in SelectedHitObjects.OfType()) obj.Column += columnDelta; From a5826116479b547084fa2d3d0fab0211ab83089a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 May 2020 19:05:35 +0900 Subject: [PATCH 1573/2376] Add test coverage --- .../Settings/TestSceneMigrationScreens.cs | 36 +++++++++++++++++++ .../Maintenance/MigrationRunScreen.cs | 6 ++-- .../Maintenance/MigrationSelectScreen.cs | 10 +++--- 3 files changed, 46 insertions(+), 6 deletions(-) create mode 100644 osu.Game.Tests/Visual/Settings/TestSceneMigrationScreens.cs diff --git a/osu.Game.Tests/Visual/Settings/TestSceneMigrationScreens.cs b/osu.Game.Tests/Visual/Settings/TestSceneMigrationScreens.cs new file mode 100644 index 0000000000..2883e54385 --- /dev/null +++ b/osu.Game.Tests/Visual/Settings/TestSceneMigrationScreens.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; +using System.Threading; +using osu.Framework.Screens; +using osu.Game.Overlays.Settings.Sections.Maintenance; + +namespace osu.Game.Tests.Visual.Settings +{ + public class TestSceneMigrationScreens : ScreenTestScene + { + public TestSceneMigrationScreens() + { + AddStep("Push screen", () => Stack.Push(new TestMigrationSelectScreen())); + } + + private class TestMigrationSelectScreen : MigrationSelectScreen + { + protected override void BeginMigration(DirectoryInfo target) => this.Push(new TestMigrationRunScreen()); + + private class TestMigrationRunScreen : MigrationRunScreen + { + protected override void PerformMigration() + { + Thread.Sleep(3000); + } + + public TestMigrationRunScreen() + : base(null) + { + } + } + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs index b29cd0d630..b0b61554eb 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs @@ -20,7 +20,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance { private readonly DirectoryInfo destination; - [Resolved] + [Resolved(canBeNull: true)] private OsuGame game { get; set; } public override bool AllowBackButton => false; @@ -84,7 +84,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance Beatmap.Value = Beatmap.Default; - migrationTask = Task.Run(() => game.Migrate(destination.FullName)) + migrationTask = Task.Run(PerformMigration) .ContinueWith(t => { if (t.IsFaulted) @@ -94,6 +94,8 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance }); } + protected virtual void PerformMigration() => game?.Migrate(destination.FullName); + public override void OnEntering(IScreen last) { base.OnEntering(last); diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs index c1aa7f095c..79d842a617 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs @@ -29,10 +29,10 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance public override bool HideOverlaysOnEnter => true; - [BackgroundDependencyLoader] + [BackgroundDependencyLoader(true)] private void load(OsuGame game, Storage storage, OsuColour colours) { - game.Toolbar.Hide(); + game?.Toolbar.Hide(); // begin selection in the parent directory of the current storage location var initialPath = new DirectoryInfo(storage.GetFullPath(string.Empty)).Parent?.FullName; @@ -115,12 +115,14 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance } catch (Exception e) { - Logger.Log($"Error during migration: {e?.Message}", level: LogLevel.Error); + Logger.Log($"Error during migration: {e.Message}", level: LogLevel.Error); return; } ValidForResume = false; - this.Push(new MigrationRunScreen(target)); + BeginMigration(target); } + + protected virtual void BeginMigration(DirectoryInfo target) => this.Push(new MigrationRunScreen(target)); } } From 16585f767edb0f877cd04eba61ef662f7ecdca59 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 14 May 2020 19:17:24 +0900 Subject: [PATCH 1574/2376] Add initial beat snap grid implementation --- .../TestSceneManiaBeatSnapGrid.cs | 73 ++++++ .../Edit/ManiaBeatSnapGrid.cs | 233 ++++++++++++++++++ osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs | 2 + 3 files changed, 308 insertions(+) create mode 100644 osu.Game.Rulesets.Mania.Tests/TestSceneManiaBeatSnapGrid.cs create mode 100644 osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaBeatSnapGrid.cs new file mode 100644 index 0000000000..84419313e6 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaBeatSnapGrid.cs @@ -0,0 +1,73 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Input.Events; +using osu.Framework.Timing; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Configuration; +using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Rulesets.Mania.Edit; +using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Screens.Edit; +using osu.Game.Tests.Visual; +using osuTK; + +namespace osu.Game.Rulesets.Mania.Tests +{ + [Cached(typeof(IManiaHitObjectComposer))] + public class TestSceneManiaBeatSnapGrid : EditorClockTestScene, IManiaHitObjectComposer + { + [Cached(typeof(IScrollingInfo))] + private ScrollingTestContainer.TestScrollingInfo scrollingInfo = new ScrollingTestContainer.TestScrollingInfo(); + + [Cached(typeof(EditorBeatmap))] + private EditorBeatmap editorBeatmap = new EditorBeatmap(new ManiaBeatmap(new StageDefinition())); + + private readonly ManiaBeatSnapGrid beatSnapGrid; + + public TestSceneManiaBeatSnapGrid() + { + editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 200 }); + editorBeatmap.ControlPointInfo.Add(10000, new TimingControlPoint { BeatLength = 200 }); + + BeatDivisor.Value = 3; + + // Some sane defaults + scrollingInfo.Algorithm.Algorithm = ScrollVisualisationMethod.Constant; + scrollingInfo.Direction.Value = ScrollingDirection.Up; + scrollingInfo.TimeRange.Value = 1000; + + Children = new Drawable[] + { + Playfield = new ManiaPlayfield(new List + { + new StageDefinition { Columns = 4 }, + new StageDefinition { Columns = 3 } + }) + { + Clock = new FramedClock(new StopwatchClock()) + }, + beatSnapGrid = new ManiaBeatSnapGrid() + }; + } + + protected override bool OnMouseMove(MouseMoveEvent e) + { + // We're providing a constant scroll algorithm. + float relativePosition = Playfield.Stages[0].HitObjectContainer.ToLocalSpace(e.ScreenSpaceMousePosition).Y / Playfield.Stages[0].HitObjectContainer.DrawHeight; + double timeValue = scrollingInfo.TimeRange.Value * relativePosition; + + beatSnapGrid.SetRange(timeValue, timeValue); + + return true; + } + + public Column ColumnAt(Vector2 screenSpacePosition) => null; + + public ManiaPlayfield Playfield { get; } + } +} diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs new file mode 100644 index 0000000000..5a3fe29770 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs @@ -0,0 +1,233 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Beatmaps; +using osu.Game.Graphics; +using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; +using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Screens.Edit; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Mania.Edit +{ + public class ManiaBeatSnapGrid : CompositeDrawable + { + [Resolved] + private IManiaHitObjectComposer composer { get; set; } + + [Resolved] + private EditorBeatmap beatmap { get; set; } + + [Resolved] + private IScrollingInfo scrollingInfo { get; set; } + + [Resolved] + private Bindable working { get; set; } + + [Resolved] + private OsuColour colours { get; set; } + + [Resolved] + private BindableBeatDivisor beatDivisor { get; set; } + + private readonly List grids = new List(); + + [BackgroundDependencyLoader] + private void load() + { + foreach (var stage in composer.Playfield.Stages) + { + var grid = new Grid(stage); + grids.Add(grid); + + AddInternal(grid); + } + + beatDivisor.BindValueChanged(_ => createLines(), true); + } + + private void createLines() + { + foreach (var grid in grids) + grid.Clear(); + + for (int i = 0; i < beatmap.ControlPointInfo.TimingPoints.Count; i++) + { + var point = beatmap.ControlPointInfo.TimingPoints[i]; + var until = i + 1 < beatmap.ControlPointInfo.TimingPoints.Count ? beatmap.ControlPointInfo.TimingPoints[i + 1].Time : working.Value.Track.Length; + + int beat = 0; + + for (double t = point.Time; t < until; t += point.BeatLength / beatDivisor.Value) + { + var indexInBeat = beat % beatDivisor.Value; + Color4 colour; + + if (indexInBeat == 0) + colour = BindableBeatDivisor.GetColourFor(1, colours); + else + { + var divisor = BindableBeatDivisor.GetDivisorForBeatIndex(beat, beatDivisor.Value); + colour = BindableBeatDivisor.GetColourFor(divisor, colours); + } + + foreach (var grid in grids) + grid.Add(new DrawableGridLine(t, colour)); + + beat++; + } + } + } + + public (Vector2 position, double time)? GetSnappedPosition(Vector2 position) + { + float minDist = float.PositiveInfinity; + DrawableGridLine minDistLine = null; + Vector2 minDistLinePosition = Vector2.Zero; + + foreach (var grid in grids) + { + foreach (var line in grid.AliveObjects.OfType()) + { + Vector2 linePos = line.ToSpaceOfOtherDrawable(line.OriginPosition, this); + float d = Vector2.Distance(position, linePos); + + if (d < minDist) + { + minDist = d; + minDistLine = line; + minDistLinePosition = linePos; + } + } + } + + if (minDistLine == null) + return null; + + float noteOffset = (scrollingInfo.Direction.Value == ScrollingDirection.Up ? 1 : -1) * DefaultNotePiece.NOTE_HEIGHT / 2; + return (new Vector2(position.X, minDistLinePosition.Y + noteOffset), minDistLine.HitObject.StartTime); + } + + public void SetRange(double minTime, double maxTime) => Schedule(() => + { + var linesBefore = new List(); + var linesDuring = new List(); + var linesAfter = new List(); + + foreach (var grid in grids) + { + linesBefore.Clear(); + linesDuring.Clear(); + linesAfter.Clear(); + + foreach (var line in grid.Objects.OfType()) + { + if (line.HitObject.StartTime < minTime) + linesBefore.Add(line); + else if (line.HitObject.StartTime <= maxTime) + linesDuring.Add(line); + else + linesAfter.Add(line); + } + + foreach (var l in linesDuring) + l.Colour = OsuColour.Gray(0.5f); + + for (int i = 0; i < linesBefore.Count; i++) + { + int offset = (linesBefore.Count - i - 1) / beatDivisor.Value; + linesBefore[i].Colour = OsuColour.Gray(0.5f / (offset + 1)); + } + + for (int i = 0; i < linesAfter.Count; i++) + { + int offset = i / beatDivisor.Value; + linesAfter[i].Colour = OsuColour.Gray(0.5f / (offset + 1)); + } + } + }); + + private class Grid : ScrollingHitObjectContainer + { + [Resolved] + private IManiaHitObjectComposer composer { get; set; } + + private readonly Stage stage; + + public Grid(Stage stage) + { + this.stage = stage; + + RelativeSizeAxes = Axes.None; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Clock = composer.Playfield.Clock; + } + + protected override void Update() + { + base.Update(); + + var parentQuad = Parent.ToLocalSpace(stage.HitObjectContainer.ScreenSpaceDrawQuad); + Position = parentQuad.TopLeft; + Size = parentQuad.Size; + } + } + + private class DrawableGridLine : DrawableHitObject + { + [Resolved] + private IScrollingInfo scrollingInfo { get; set; } + + private readonly IBindable direction = new Bindable(); + + public DrawableGridLine(double startTime, Color4 colour) + : base(new HitObject { StartTime = startTime }) + { + RelativeSizeAxes = Axes.X; + Height = 2; + + AddInternal(new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colour + }); + } + + [BackgroundDependencyLoader] + private void load() + { + direction.BindTo(scrollingInfo.Direction); + direction.BindValueChanged(onDirectionChanged, true); + } + + private void onDirectionChanged(ValueChangedEvent direction) + { + Origin = Anchor = direction.NewValue == ScrollingDirection.Up + ? Anchor.TopLeft + : Anchor.BottomLeft; + } + + protected override void UpdateStateTransforms(ArmedState state) + { + using (BeginAbsoluteSequence(HitObject.StartTime + 1000)) + this.FadeOut(); + } + } + } +} diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs index 1af7d06998..271e432e8d 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs @@ -18,6 +18,8 @@ namespace osu.Game.Rulesets.Mania.UI [Cached] public class ManiaPlayfield : ScrollingPlayfield { + public IReadOnlyList Stages => stages; + private readonly List stages = new List(); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => stages.Any(s => s.ReceivePositionalInputAt(screenSpacePos)); From 91d1b15d5ad141444259933678490df5db29794c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 14 May 2020 19:55:07 +0900 Subject: [PATCH 1575/2376] Integrate grid with the mania composer --- .../Edit/ManiaHitObjectComposer.cs | 58 +++++++++++++++++-- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index d7c0889c0d..11523dd384 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -6,9 +6,12 @@ using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Mania.Objects; using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Input; using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Screens.Edit.Compose.Components; @@ -20,12 +23,27 @@ namespace osu.Game.Rulesets.Mania.Edit public class ManiaHitObjectComposer : HitObjectComposer, IManiaHitObjectComposer { private DrawableManiaEditRuleset drawableRuleset; + private ManiaBeatSnapGrid beatSnapGrid; + private InputManager inputManager; public ManiaHitObjectComposer(Ruleset ruleset) : base(ruleset) { } + [BackgroundDependencyLoader] + private void load() + { + AddInternal(beatSnapGrid = new ManiaBeatSnapGrid()); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + inputManager = GetContainingInputManager(); + } + /// /// Retrieves the column that intersects a screen-space position. /// @@ -42,11 +60,43 @@ namespace osu.Game.Rulesets.Mania.Edit public IScrollingInfo ScrollingInfo => drawableRuleset.ScrollingInfo; + protected override void Update() + { + base.Update(); + + if (BlueprintContainer.CurrentTool is SelectTool) + { + if (EditorBeatmap.SelectedHitObjects.Any()) + { + beatSnapGrid.SetRange(EditorBeatmap.SelectedHitObjects.Min(h => h.StartTime), EditorBeatmap.SelectedHitObjects.Max(h => h.GetEndTime())); + beatSnapGrid.Show(); + } + else + beatSnapGrid.Hide(); + } + else + { + var placementTime = GetSnappedPosition(ToLocalSpace(inputManager.CurrentState.Mouse.Position), 0).time; + beatSnapGrid.SetRange(placementTime, placementTime); + + beatSnapGrid.Show(); + } + } + public override (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time) { - var hoc = Playfield.GetColumn(0).HitObjectContainer; + var beatSnapped = beatSnapGrid.GetSnappedPosition(position); - float targetPosition = hoc.ToLocalSpace(ToScreenSpace(position)).Y; + if (beatSnapped != null) + return beatSnapped.Value; + + return base.GetSnappedPosition(position, getTimeFromPosition(ToScreenSpace(position))); + } + + private double getTimeFromPosition(Vector2 screenSpacePosition) + { + var hoc = Playfield.Stages[0].HitObjectContainer; + float targetPosition = hoc.ToLocalSpace(screenSpacePosition).Y; if (drawableRuleset.ScrollingInfo.Direction.Value == ScrollingDirection.Down) { @@ -56,12 +106,10 @@ namespace osu.Game.Rulesets.Mania.Edit targetPosition = hoc.DrawHeight - targetPosition; } - double targetTime = drawableRuleset.ScrollingInfo.Algorithm.TimeAt(targetPosition, + return drawableRuleset.ScrollingInfo.Algorithm.TimeAt(targetPosition, EditorClock.CurrentTime, drawableRuleset.ScrollingInfo.TimeRange.Value, hoc.DrawHeight); - - return base.GetSnappedPosition(position, targetTime); } protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) From 42c3d892cd93a3671de896af87136244b36856c2 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 14 May 2020 19:55:14 +0900 Subject: [PATCH 1576/2376] Only update alive lines --- osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs index 5a3fe29770..320912ed5b 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs @@ -131,7 +131,7 @@ namespace osu.Game.Rulesets.Mania.Edit linesDuring.Clear(); linesAfter.Clear(); - foreach (var line in grid.Objects.OfType()) + foreach (var line in grid.AliveObjects.OfType()) { if (line.HitObject.StartTime < minTime) linesBefore.Add(line); From 0e334940745c5958a30a877083907ca391282953 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 14 May 2020 19:58:39 +0900 Subject: [PATCH 1577/2376] Fix flashing when changing beat divisor --- osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs | 12 ++++++++++-- .../Edit/ManiaHitObjectComposer.cs | 4 ++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs index 320912ed5b..9cb9256a7e 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs @@ -119,7 +119,15 @@ namespace osu.Game.Rulesets.Mania.Edit return (new Vector2(position.X, minDistLinePosition.Y + noteOffset), minDistLine.HitObject.StartTime); } - public void SetRange(double minTime, double maxTime) => Schedule(() => + public void SetRange(double minTime, double maxTime) + { + if (LoadState >= LoadState.Ready) + setRange(minTime, maxTime); + else + Schedule(() => setRange(minTime, maxTime)); + } + + private void setRange(double minTime, double maxTime) { var linesBefore = new List(); var linesDuring = new List(); @@ -156,7 +164,7 @@ namespace osu.Game.Rulesets.Mania.Edit linesAfter[i].Colour = OsuColour.Gray(0.5f / (offset + 1)); } } - }); + } private class Grid : ScrollingHitObjectContainer { diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index 11523dd384..1266761d12 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -60,9 +60,9 @@ namespace osu.Game.Rulesets.Mania.Edit public IScrollingInfo ScrollingInfo => drawableRuleset.ScrollingInfo; - protected override void Update() + protected override void UpdateAfterChildren() { - base.Update(); + base.UpdateAfterChildren(); if (BlueprintContainer.CurrentTool is SelectTool) { From 3441ab457d7d05dffa990117aa7ab3b84ceaa709 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 14 May 2020 20:06:34 +0900 Subject: [PATCH 1578/2376] Fix hitobjects placed at non-beatsnapped times --- .../Edit/Blueprints/ManiaPlacementBlueprint.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs index 3fb03d642f..5fe53557b3 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs @@ -24,10 +24,15 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints protected Column Column; /// - /// The current mouse position, snapped to the closest column. + /// The current beat-snapped mouse position, snapped to the closest column. /// protected Vector2 SnappedMousePosition { get; private set; } + /// + /// The gameplay time at the current beat-snapped mouse position (). + /// + protected double SnappedTime { get; private set; } + /// /// The width of the closest column to the current mouse position. /// @@ -39,6 +44,9 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints [Resolved] private IScrollingInfo scrollingInfo { get; set; } + [Resolved] + private IDistanceSnapProvider snapProvider { get; set; } + protected ManiaPlacementBlueprint(T hitObject) : base(hitObject) { @@ -54,7 +62,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints return base.OnMouseDown(e); HitObject.Column = Column.Index; - BeginPlacement(TimeAt(e.ScreenSpaceMousePosition), true); + BeginPlacement(SnappedTime, true); return true; } @@ -70,6 +78,10 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints // Snap to the column var parentPos = Parent.ToLocalSpace(Column.ToScreenSpace(new Vector2(Column.DrawWidth / 2, 0))); SnappedMousePosition = new Vector2(parentPos.X, Parent.ToLocalSpace(screenSpacePosition).Y); + + SnappedTime = TimeAt(screenSpacePosition); + if (snapProvider != null) + (SnappedMousePosition, SnappedTime) = snapProvider.GetSnappedPosition(SnappedMousePosition, SnappedTime); } protected double TimeAt(Vector2 screenSpacePosition) From f3b1c32a85050ace4ad7f410913ea3665312e3de Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 May 2020 20:18:57 +0900 Subject: [PATCH 1579/2376] Update test logic for new exception type --- osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs index ef2b20de64..7a20bd364b 100644 --- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs @@ -211,7 +211,7 @@ namespace osu.Game.Tests.NonVisual var osu = loadOsu(host); Assert.DoesNotThrow(() => osu.Migrate(customPath)); - Assert.Throws(() => osu.Migrate(customPath)); + Assert.Throws(() => osu.Migrate(customPath)); } finally { From 42f446faa9a8d19a0a49d970b8139e7bf28cf4c1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 May 2020 21:10:04 +0900 Subject: [PATCH 1580/2376] Fix remaining test failure --- osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs index b5d07ee7b1..95a1868392 100644 --- a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs @@ -14,7 +14,7 @@ namespace osu.Game.Overlays.Settings.Sections.General { protected override string Header => "Updates"; - [BackgroundDependencyLoader] + [BackgroundDependencyLoader(true)] private void load(Storage storage, OsuConfigManager config, OsuGame game) { Add(new SettingsEnumDropdown @@ -34,7 +34,7 @@ namespace osu.Game.Overlays.Settings.Sections.General Add(new SettingsButton { Text = "Change folder location...", - Action = () => game.PerformFromScreen(menu => menu.Push(new MigrationSelectScreen())) + Action = () => game?.PerformFromScreen(menu => menu.Push(new MigrationSelectScreen())) }); } } From e390d70b707c97909404b8f596dc2908a66605b4 Mon Sep 17 00:00:00 2001 From: Fukashi13 <48766178+Fukashi13@users.noreply.github.com> Date: Thu, 14 May 2020 14:33:12 +0200 Subject: [PATCH 1581/2376] bestMatch changes on entering section with screen top border --- osu.Game/Graphics/Containers/SectionsContainer.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Graphics/Containers/SectionsContainer.cs b/osu.Game/Graphics/Containers/SectionsContainer.cs index a3125614aa..8b866c8d21 100644 --- a/osu.Game/Graphics/Containers/SectionsContainer.cs +++ b/osu.Game/Graphics/Containers/SectionsContainer.cs @@ -189,15 +189,15 @@ namespace osu.Game.Graphics.Containers headerBackgroundContainer.Height = (ExpandableHeader?.LayoutSize.Y ?? 0) + (FixedHeader?.LayoutSize.Y ?? 0); headerBackgroundContainer.Y = ExpandableHeader?.Y ?? 0; - T bestMatch = null; - float minDiff = float.MaxValue; + T bestMatch = Children.FirstOrDefault(); + float minDiff = float.MinValue; float scrollOffset = FixedHeader?.LayoutSize.Y ?? 0; foreach (var section in Children) { - float diff = Math.Abs(scrollContainer.GetChildPosInContent(section) - currentScroll - scrollOffset); + float diff = scrollContainer.GetChildPosInContent(section) - currentScroll - scrollOffset; - if (diff < minDiff) + if ((minDiff < diff) & (diff < 0)) { minDiff = diff; bestMatch = section; From 155e918ca3063b4c03146144cc3b8f9a035d5566 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 May 2020 21:40:26 +0900 Subject: [PATCH 1582/2376] Remove unused parameter --- .../Overlays/Settings/Sections/Maintenance/GeneralSettings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs index 1dd079a8ab..832673703b 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs @@ -26,7 +26,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance private TriangleButton undeleteButton; [BackgroundDependencyLoader] - private void load(BeatmapManager beatmaps, ScoreManager scores, SkinManager skins, DialogOverlay dialogOverlay, OsuGame game) + private void load(BeatmapManager beatmaps, ScoreManager scores, SkinManager skins, DialogOverlay dialogOverlay) { if (beatmaps.SupportsImportFromStable) { From ef8375b442d78dea8737ef91b6bb5b3ecbf6e7ee Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 May 2020 22:42:42 +0900 Subject: [PATCH 1583/2376] Add protection against migrating to a nested folder --- .../NonVisual/CustomDataDirectoryTest.cs | 24 +++++++++++++++++++ osu.Game/IO/OsuStorage.cs | 6 +++++ 2 files changed, 30 insertions(+) diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs index ef2b20de64..433067ffdd 100644 --- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs @@ -220,6 +220,30 @@ namespace osu.Game.Tests.NonVisual } } + [Test] + public void TestMigrationToNestedTargetFails() + { + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestMigrationToSameTargetFails))) + { + try + { + var osu = loadOsu(host); + + Assert.DoesNotThrow(() => osu.Migrate(customPath)); + + string subFolder = Path.Combine(customPath, "sub"); + + Directory.CreateDirectory(subFolder); + + Assert.Throws(() => osu.Migrate(subFolder)); + } + finally + { + host.Exit(); + } + } + } + private OsuGameBase loadOsu(GameHost host) { var osu = new OsuGameBase(); diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index 71b01ce479..7c0af16a63 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.cs @@ -48,6 +48,12 @@ namespace osu.Game.IO var source = new DirectoryInfo(GetFullPath(".")); var destination = new DirectoryInfo(newLocation); + if (source.FullName == destination.FullName) + throw new ArgumentException("Destination provided is already the current location", nameof(newLocation)); + + if (destination.FullName.Contains(source.FullName)) + throw new ArgumentException("Destination provided is inside the source", nameof(newLocation)); + // ensure the new location has no files present, else hard abort if (destination.Exists) { From 827d75b152f17af984b9823dc4322a185fdaaa55 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 May 2020 22:44:27 +0900 Subject: [PATCH 1584/2376] Revert "Add protection against migrating to a nested folder" This reverts commit ef8375b442d78dea8737ef91b6bb5b3ecbf6e7ee. --- .../NonVisual/CustomDataDirectoryTest.cs | 24 ------------------- osu.Game/IO/OsuStorage.cs | 6 ----- 2 files changed, 30 deletions(-) diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs index 433067ffdd..ef2b20de64 100644 --- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs @@ -220,30 +220,6 @@ namespace osu.Game.Tests.NonVisual } } - [Test] - public void TestMigrationToNestedTargetFails() - { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestMigrationToSameTargetFails))) - { - try - { - var osu = loadOsu(host); - - Assert.DoesNotThrow(() => osu.Migrate(customPath)); - - string subFolder = Path.Combine(customPath, "sub"); - - Directory.CreateDirectory(subFolder); - - Assert.Throws(() => osu.Migrate(subFolder)); - } - finally - { - host.Exit(); - } - } - } - private OsuGameBase loadOsu(GameHost host) { var osu = new OsuGameBase(); diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index 7c0af16a63..71b01ce479 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.cs @@ -48,12 +48,6 @@ namespace osu.Game.IO var source = new DirectoryInfo(GetFullPath(".")); var destination = new DirectoryInfo(newLocation); - if (source.FullName == destination.FullName) - throw new ArgumentException("Destination provided is already the current location", nameof(newLocation)); - - if (destination.FullName.Contains(source.FullName)) - throw new ArgumentException("Destination provided is inside the source", nameof(newLocation)); - // ensure the new location has no files present, else hard abort if (destination.Exists) { From 364aa5aa129af4dbf996d4942b2f4884b6d53dbd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 May 2020 22:42:42 +0900 Subject: [PATCH 1585/2376] Add protection against migrating to a nested folder --- .../NonVisual/CustomDataDirectoryTest.cs | 24 +++++++++++++++++++ osu.Game/IO/OsuStorage.cs | 3 +++ 2 files changed, 27 insertions(+) diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs index 7a20bd364b..e8f052cdeb 100644 --- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs @@ -220,6 +220,30 @@ namespace osu.Game.Tests.NonVisual } } + [Test] + public void TestMigrationToNestedTargetFails() + { + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestMigrationToSameTargetFails))) + { + try + { + var osu = loadOsu(host); + + Assert.DoesNotThrow(() => osu.Migrate(customPath)); + + string subFolder = Path.Combine(customPath, "sub"); + + Directory.CreateDirectory(subFolder); + + Assert.Throws(() => osu.Migrate(subFolder)); + } + finally + { + host.Exit(); + } + } + } + private OsuGameBase loadOsu(GameHost host) { var osu = new OsuGameBase(); diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index 443f4fdb69..e3888c0c28 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.cs @@ -51,6 +51,9 @@ namespace osu.Game.IO if (source.FullName == destination.FullName) throw new ArgumentException("Destination provided is already the current location", nameof(newLocation)); + if (destination.FullName.Contains(source.FullName)) + throw new ArgumentException("Destination provided is inside the source", nameof(newLocation)); + // ensure the new location has no files present, else hard abort if (destination.Exists) { From 6ec55eb400a480dbf475565fdf48589b7e0ea50d Mon Sep 17 00:00:00 2001 From: Shivam Date: Thu, 14 May 2020 21:51:39 +0200 Subject: [PATCH 1586/2376] Give mappool scene its own video --- osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs index b4c6d589d7..2c4fed8d86 100644 --- a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs +++ b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs @@ -42,7 +42,7 @@ namespace osu.Game.Tournament.Screens.MapPool { InternalChildren = new Drawable[] { - new TourneyVideo("gameplay") + new TourneyVideo("mappool") { Loop = true, RelativeSizeAxes = Axes.Both, From 1768beb690a9733f3ebf966c6576b8006c011984 Mon Sep 17 00:00:00 2001 From: Shivam Date: Thu, 14 May 2020 21:52:10 +0200 Subject: [PATCH 1587/2376] Rename class SeeingEditorScreen to SeedingEditorScreen --- .../Screens/Editors/SeedingEditorScreen.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs index 46bb7b83e3..52f761e50a 100644 --- a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs @@ -19,7 +19,7 @@ using osuTK; namespace osu.Game.Tournament.Screens.Editors { - public class SeedingEditorScreen : TournamentEditorScreen + public class SeedingEditorScreen : TournamentEditorScreen { private readonly TournamentTeam team; @@ -30,14 +30,14 @@ namespace osu.Game.Tournament.Screens.Editors this.team = team; } - public class SeeingResultRow : CompositeDrawable, IModelBacked + public class SeedingResultRow : CompositeDrawable, IModelBacked { public SeedingResult Model { get; } [Resolved] private LadderInfo ladderInfo { get; set; } - public SeeingResultRow(TournamentTeam team, SeedingResult round) + public SeedingResultRow(TournamentTeam team, SeedingResult round) { Model = round; @@ -281,6 +281,6 @@ namespace osu.Game.Tournament.Screens.Editors } } - protected override SeeingResultRow CreateDrawable(SeedingResult model) => new SeeingResultRow(team, model); + protected override SeedingResultRow CreateDrawable(SeedingResult model) => new SeedingResultRow(team, model); } } From 097fcfd9ad35832a190a9bee0f7bfd14f33aa146 Mon Sep 17 00:00:00 2001 From: Fukashi13 <48766178+Fukashi13@users.noreply.github.com> Date: Fri, 15 May 2020 00:06:58 +0200 Subject: [PATCH 1588/2376] Update osu.Game/Graphics/Containers/SectionsContainer.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Graphics/Containers/SectionsContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Containers/SectionsContainer.cs b/osu.Game/Graphics/Containers/SectionsContainer.cs index 8b866c8d21..5192a7ddea 100644 --- a/osu.Game/Graphics/Containers/SectionsContainer.cs +++ b/osu.Game/Graphics/Containers/SectionsContainer.cs @@ -197,7 +197,7 @@ namespace osu.Game.Graphics.Containers { float diff = scrollContainer.GetChildPosInContent(section) - currentScroll - scrollOffset; - if ((minDiff < diff) & (diff < 0)) + if (minDiff < diff && diff < 0) { minDiff = diff; bestMatch = section; From 25bbb02999481de9adecaa38f51e3e6293018bcd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 May 2020 22:57:41 +0900 Subject: [PATCH 1589/2376] Throw better exceptions from OsuStorage --- osu.Game/IO/OsuStorage.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index 71b01ce479..8109631ef9 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.cs @@ -48,11 +48,14 @@ namespace osu.Game.IO var source = new DirectoryInfo(GetFullPath(".")); var destination = new DirectoryInfo(newLocation); + if (source.FullName == destination.FullName) + throw new ArgumentException("Destination provided is already the current location", nameof(newLocation)); + // ensure the new location has no files present, else hard abort if (destination.Exists) { if (destination.GetFiles().Length > 0 || destination.GetDirectories().Length > 0) - throw new InvalidOperationException("Migration destination already has files present"); + throw new ArgumentException("Destination provided already has files or directories present", nameof(newLocation)); deleteRecursive(destination); } From 19f117ae53fe3058ed251bd687a7cb8266e75cf8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 May 2020 20:18:57 +0900 Subject: [PATCH 1590/2376] Update test logic for new exception type --- osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs index ef2b20de64..7a20bd364b 100644 --- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs @@ -211,7 +211,7 @@ namespace osu.Game.Tests.NonVisual var osu = loadOsu(host); Assert.DoesNotThrow(() => osu.Migrate(customPath)); - Assert.Throws(() => osu.Migrate(customPath)); + Assert.Throws(() => osu.Migrate(customPath)); } finally { From 0690d81bbb1bc7ee969b7e07050a565138b5b5c0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 May 2020 22:42:42 +0900 Subject: [PATCH 1591/2376] Add protection against migrating to a nested folder --- .../NonVisual/CustomDataDirectoryTest.cs | 48 +++++++++++++++++++ osu.Game/IO/OsuStorage.cs | 9 +++- 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs index 7a20bd364b..2a98a6dbc6 100644 --- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs @@ -220,6 +220,54 @@ namespace osu.Game.Tests.NonVisual } } + [Test] + public void TestMigrationToNestedTargetFails() + { + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestMigrationToSameTargetFails))) + { + try + { + var osu = loadOsu(host); + + Assert.DoesNotThrow(() => osu.Migrate(customPath)); + + string subFolder = Path.Combine(customPath, "sub"); + + Directory.CreateDirectory(subFolder); + + Assert.Throws(() => osu.Migrate(subFolder)); + } + finally + { + host.Exit(); + } + } + } + + [Test] + public void TestMigrationToSeeminglyNestedTarget() + { + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestMigrationToSeeminglyNestedTarget))) + { + try + { + var osu = loadOsu(host); + + Assert.DoesNotThrow(() => osu.Migrate(customPath)); + + string subFolder = customPath + "sub"; + + Directory.CreateDirectory(subFolder); + + osu.Migrate(subFolder); + } + finally + { + host.Exit(); + } + } + } + private OsuGameBase loadOsu(GameHost host) { var osu = new OsuGameBase(); diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index 8109631ef9..ac28a05375 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.cs @@ -48,9 +48,16 @@ namespace osu.Game.IO var source = new DirectoryInfo(GetFullPath(".")); var destination = new DirectoryInfo(newLocation); - if (source.FullName == destination.FullName) + // using Uri is the easiest way to check equality and contains (https://stackoverflow.com/a/7710620) + var sourceUri = new Uri(source.FullName + Path.DirectorySeparatorChar); + var destinationUri = new Uri(destination.FullName + Path.DirectorySeparatorChar); + + if (sourceUri == destinationUri) throw new ArgumentException("Destination provided is already the current location", nameof(newLocation)); + if (sourceUri.IsBaseOf(destinationUri)) + throw new ArgumentException("Destination provided is inside the source", nameof(newLocation)); + // ensure the new location has no files present, else hard abort if (destination.Exists) { From 7641507c90f770bab9e096dad63132001cc532d5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 15 May 2020 10:45:57 +0900 Subject: [PATCH 1592/2376] Ensure test directories are deleted before subsequent run --- osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs index 2a98a6dbc6..d69bf94ee2 100644 --- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs @@ -233,6 +233,9 @@ namespace osu.Game.Tests.NonVisual string subFolder = Path.Combine(customPath, "sub"); + if (Directory.Exists(subFolder)) + Directory.Delete(subFolder, true); + Directory.CreateDirectory(subFolder); Assert.Throws(() => osu.Migrate(subFolder)); @@ -255,11 +258,14 @@ namespace osu.Game.Tests.NonVisual Assert.DoesNotThrow(() => osu.Migrate(customPath)); - string subFolder = customPath + "sub"; + string seeminglySubFolder = customPath + "sub"; - Directory.CreateDirectory(subFolder); + if (Directory.Exists(seeminglySubFolder)) + Directory.Delete(seeminglySubFolder, true); - osu.Migrate(subFolder); + Directory.CreateDirectory(seeminglySubFolder); + + osu.Migrate(seeminglySubFolder); } finally { From 94cf99bf978305b7f45eb0bfb1323c34e3525bb5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 15 May 2020 12:21:02 +0900 Subject: [PATCH 1593/2376] Fix mute button falling off the screen when UI scaling is used --- osu.Game/Overlays/VolumeOverlay.cs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/osu.Game/Overlays/VolumeOverlay.cs b/osu.Game/Overlays/VolumeOverlay.cs index 676d2c941a..eb639431ae 100644 --- a/osu.Game/Overlays/VolumeOverlay.cs +++ b/osu.Game/Overlays/VolumeOverlay.cs @@ -46,6 +46,13 @@ namespace osu.Game.Overlays Width = 300, Colour = ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.75f), Color4.Black.Opacity(0)) }, + muteButton = new MuteButton + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Margin = new MarginPadding(10), + Current = { BindTarget = IsMuted } + }, new FillFlowContainer { Direction = FillDirection.Vertical, @@ -56,19 +63,11 @@ namespace osu.Game.Overlays Margin = new MarginPadding { Left = offset }, Children = new Drawable[] { - volumeMeterEffect = new VolumeMeter("EFFECTS", 125, colours.BlueDarker) - { - Margin = new MarginPadding { Top = 100 + MuteButton.HEIGHT } // to counter the mute button and re-center the volume meters - }, + volumeMeterEffect = new VolumeMeter("EFFECTS", 125, colours.BlueDarker), volumeMeterMaster = new VolumeMeter("MASTER", 150, colours.PinkDarker), volumeMeterMusic = new VolumeMeter("MUSIC", 125, colours.BlueDarker), - muteButton = new MuteButton - { - Margin = new MarginPadding { Top = 100 }, - Current = { BindTarget = IsMuted } - } } - }, + } }); volumeMeterMaster.Bindable.BindTo(audio.Volume); From aea192080a51f014d79da5caad8ce70507d65a74 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 15 May 2020 13:02:46 +0900 Subject: [PATCH 1594/2376] Fix incorrect storage name --- osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs index d69bf94ee2..743c924bbd 100644 --- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs @@ -223,7 +223,7 @@ namespace osu.Game.Tests.NonVisual [Test] public void TestMigrationToNestedTargetFails() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestMigrationToSameTargetFails))) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestMigrationToNestedTargetFails))) { try { From 4cbd51feb965fe78ba3429728205715788a83558 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 15 May 2020 13:08:15 +0900 Subject: [PATCH 1595/2376] Fix test errors --- .../Edit/Blueprints/ManiaPlacementBlueprint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs index 5fe53557b3..184356b89c 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs @@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints [Resolved] private IScrollingInfo scrollingInfo { get; set; } - [Resolved] + [Resolved(CanBeNull = true)] private IDistanceSnapProvider snapProvider { get; set; } protected ManiaPlacementBlueprint(T hitObject) From 6ca102bc3fb44d751986c3daa141825d8ed094ec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 15 May 2020 13:19:03 +0900 Subject: [PATCH 1596/2376] Attempt delete operations more than once --- osu.Game/IO/OsuStorage.cs | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index 5393cbf7ae..499bcb4063 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.cs @@ -84,7 +84,7 @@ namespace osu.Game.IO if (topLevelExcludes && IGNORE_FILES.Contains(fi.Name)) continue; - fi.Delete(); + attemptOperation(() => fi.Delete()); } foreach (DirectoryInfo dir in target.GetDirectories()) @@ -92,11 +92,11 @@ namespace osu.Game.IO if (topLevelExcludes && IGNORE_DIRECTORIES.Contains(dir.Name)) continue; - dir.Delete(true); + attemptOperation(() => dir.Delete(true)); } if (target.GetFiles().Length == 0 && target.GetDirectories().Length == 0) - target.Delete(); + attemptOperation(target.Delete); } private static void copyRecursive(DirectoryInfo source, DirectoryInfo destination, bool topLevelExcludes = true) @@ -109,7 +109,7 @@ namespace osu.Game.IO if (topLevelExcludes && IGNORE_FILES.Contains(fi.Name)) continue; - attemptCopy(fi, Path.Combine(destination.FullName, fi.Name)); + attemptOperation(() => fi.CopyTo(Path.Combine(destination.FullName, fi.Name), true)); } foreach (DirectoryInfo dir in source.GetDirectories()) @@ -121,24 +121,27 @@ namespace osu.Game.IO } } - private static void attemptCopy(System.IO.FileInfo fileInfo, string destination) + /// + /// Attempt an IO operation multiple times and only throw if none of the attempts succeed. + /// + /// The action to perform. + /// The number of attempts (250ms wait between each). + private static void attemptOperation(Action action, int attempts = 10) { - int tries = 5; - while (true) { try { - fileInfo.CopyTo(destination, true); + action(); return; } catch (Exception) { - if (tries-- == 0) + if (attempts-- == 0) throw; } - Thread.Sleep(50); + Thread.Sleep(250); } } } From 392d44e1fbe8fcf6f334fe31134821a01e7adbf5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 15 May 2020 15:49:50 +0900 Subject: [PATCH 1597/2376] Always fully display one beat --- .../Edit/ManiaBeatSnapGrid.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs index 9cb9256a7e..63e887714b 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; @@ -149,6 +150,29 @@ namespace osu.Game.Rulesets.Mania.Edit linesAfter.Add(line); } + // Snapping will always happen on one of the two lines around minTime (the "target" line). + // One of those lines may exist in linesBefore and the other may exist in linesAfter, depending on whether such a line exists, and the target changes when the mid-point is crossed. + // For display purposes, one complete beat is shown at the maximum brightness such that the target line should always be bright. + bool targetLineIsLastLineBefore = false; + + if (linesBefore.Count > 0 && linesAfter.Count > 0) + targetLineIsLastLineBefore = Math.Abs(linesBefore[^1].HitObject.StartTime - minTime) <= Math.Abs(linesAfter[0].HitObject.StartTime - minTime); + else if (linesBefore.Count > 0) + targetLineIsLastLineBefore = true; + + if (targetLineIsLastLineBefore) + { + // Move the last line before to linesDuring + linesDuring.Insert(0, linesBefore[^1]); + linesBefore.RemoveAt(linesBefore.Count - 1); + } + else if (linesAfter.Count > 0) // = false does not guarantee that a line after exists (maybe at the bottom of the screen) + { + // Move the first line after to linesDuring + linesDuring.Insert(0, linesAfter[0]); + linesAfter.RemoveAt(0); + } + foreach (var l in linesDuring) l.Colour = OsuColour.Gray(0.5f); From 1c6c128d1100ca3aa8ddd151c2f46a08701c30ed Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 15 May 2020 15:51:54 +0900 Subject: [PATCH 1598/2376] Add const --- osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs index 63e887714b..77d42a0927 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs @@ -24,6 +24,11 @@ namespace osu.Game.Rulesets.Mania.Edit { public class ManiaBeatSnapGrid : CompositeDrawable { + /// + /// The brightness of bar lines one beat around the time range from . + /// + private const float first_beat_brightness = 0.5f; + [Resolved] private IManiaHitObjectComposer composer { get; set; } @@ -174,18 +179,18 @@ namespace osu.Game.Rulesets.Mania.Edit } foreach (var l in linesDuring) - l.Colour = OsuColour.Gray(0.5f); + l.Colour = OsuColour.Gray(first_beat_brightness); for (int i = 0; i < linesBefore.Count; i++) { int offset = (linesBefore.Count - i - 1) / beatDivisor.Value; - linesBefore[i].Colour = OsuColour.Gray(0.5f / (offset + 1)); + linesBefore[i].Colour = OsuColour.Gray(first_beat_brightness / (offset + 1)); } for (int i = 0; i < linesAfter.Count; i++) { int offset = i / beatDivisor.Value; - linesAfter[i].Colour = OsuColour.Gray(0.5f / (offset + 1)); + linesAfter[i].Colour = OsuColour.Gray(first_beat_brightness / (offset + 1)); } } } From 238d87f97611be6b137e8bcc9c30073e318518a8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 15 May 2020 15:56:32 +0900 Subject: [PATCH 1599/2376] Add comment about gray usage --- osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs index 77d42a0927..31ebb7bc1c 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs @@ -178,6 +178,8 @@ namespace osu.Game.Rulesets.Mania.Edit linesAfter.RemoveAt(0); } + // Grays are used rather than transparency since the lines appear on a coloured mania playfield. + foreach (var l in linesDuring) l.Colour = OsuColour.Gray(first_beat_brightness); From aec2520ef41155ea14e7bdf0c68fce4a45c7e3e1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 15 May 2020 17:31:06 +0900 Subject: [PATCH 1600/2376] Avoid disabling a host-level bindable from osu! code --- .../Settings/Sections/Input/MouseSettings.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs index e7f2f21465..65fc07d1d9 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs @@ -17,12 +17,20 @@ namespace osu.Game.Overlays.Settings.Sections.Input protected override string Header => "Mouse"; private readonly BindableBool rawInputToggle = new BindableBool(); + private Bindable sensitivityBindable = new BindableDouble(); private Bindable ignoredInputHandler; private SensitivitySetting sensitivity; [BackgroundDependencyLoader] private void load(OsuConfigManager osuConfig, FrameworkConfigManager config) { + var configSensitivity = config.GetBindable(FrameworkSetting.CursorSensitivity); + + // use local bindable to avoid changing enabled state of game host's bindable. + sensitivityBindable = configSensitivity.GetUnboundCopy(); + configSensitivity.BindValueChanged(val => sensitivityBindable.Value = val.NewValue); + sensitivityBindable.BindValueChanged(val => configSensitivity.Value = val.NewValue); + Children = new Drawable[] { new SettingsCheckbox @@ -33,7 +41,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input sensitivity = new SensitivitySetting { LabelText = "Cursor sensitivity", - Bindable = config.GetBindable(FrameworkSetting.CursorSensitivity) + Bindable = sensitivityBindable }, new SettingsCheckbox { @@ -60,7 +68,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows) { rawInputToggle.Disabled = true; - sensitivity.Bindable.Disabled = true; + sensitivityBindable.Disabled = true; } else { From 98125102a768b5a711b5cab4367969f68e38b95f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 15 May 2020 18:07:41 +0900 Subject: [PATCH 1601/2376] Add cancellation token support to CreateNestedHitObjects() --- osu.Game.Rulesets.Catch/Objects/BananaShower.cs | 5 +++-- osu.Game.Rulesets.Catch/Objects/JuiceStream.cs | 5 +++-- osu.Game.Rulesets.Mania/Objects/HoldNote.cs | 5 +++-- osu.Game.Rulesets.Osu/Objects/Slider.cs | 5 +++-- osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs | 5 +++-- osu.Game.Rulesets.Taiko/Objects/Swell.cs | 5 +++-- .../Objects/TaikoHitObject.cs | 5 +++-- osu.Game/Rulesets/Edit/PlacementBlueprint.cs | 3 ++- osu.Game/Rulesets/Objects/HitObject.cs | 17 ++++++++++++++--- 9 files changed, 37 insertions(+), 18 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/BananaShower.cs b/osu.Game.Rulesets.Catch/Objects/BananaShower.cs index c3488aec11..96ab66048a 100644 --- a/osu.Game.Rulesets.Catch/Objects/BananaShower.cs +++ b/osu.Game.Rulesets.Catch/Objects/BananaShower.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Threading; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Types; @@ -14,9 +15,9 @@ namespace osu.Game.Rulesets.Catch.Objects public override Judgement CreateJudgement() => new IgnoreJudgement(); - protected override void CreateNestedHitObjects() + protected override void CreateNestedHitObjects(CancellationToken cancellationToken) { - base.CreateNestedHitObjects(); + base.CreateNestedHitObjects(cancellationToken); createBananas(); } diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index 01011645bd..4f9a289739 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using System.Threading; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; @@ -45,9 +46,9 @@ namespace osu.Game.Rulesets.Catch.Objects TickDistance = scoringDistance / difficulty.SliderTickRate; } - protected override void CreateNestedHitObjects() + protected override void CreateNestedHitObjects(CancellationToken cancellationToken) { - base.CreateNestedHitObjects(); + base.CreateNestedHitObjects(cancellationToken); var dropletSamples = Samples.Select(s => new HitSampleInfo { diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs index eea2c31260..d8bdaa071b 100644 --- a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Threading; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; @@ -91,9 +92,9 @@ namespace osu.Game.Rulesets.Mania.Objects tickSpacing = timingPoint.BeatLength / difficulty.SliderTickRate; } - protected override void CreateNestedHitObjects() + protected override void CreateNestedHitObjects(CancellationToken cancellationToken) { - base.CreateNestedHitObjects(); + base.CreateNestedHitObjects(cancellationToken); createTicks(); diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index e5d6c20738..bc6fca2338 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -6,6 +6,7 @@ using osu.Game.Rulesets.Objects.Types; using System.Collections.Generic; using osu.Game.Rulesets.Objects; using System.Linq; +using System.Threading; using osu.Framework.Caching; using osu.Game.Audio; using osu.Game.Beatmaps; @@ -133,9 +134,9 @@ namespace osu.Game.Rulesets.Osu.Objects TickDistance = scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier; } - protected override void CreateNestedHitObjects() + protected override void CreateNestedHitObjects(CancellationToken cancellationToken) { - base.CreateNestedHitObjects(); + base.CreateNestedHitObjects(cancellationToken); foreach (var e in SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset)) diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs index dc2f277e58..8bbad220ac 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs @@ -4,6 +4,7 @@ using osu.Game.Rulesets.Objects.Types; using System; using System.Collections.Generic; +using System.Threading; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; @@ -73,14 +74,14 @@ namespace osu.Game.Rulesets.Taiko.Objects overallDifficulty = difficulty.OverallDifficulty; } - protected override void CreateNestedHitObjects() + protected override void CreateNestedHitObjects(CancellationToken cancellationToken) { createTicks(); RequiredGoodHits = NestedHitObjects.Count * Math.Min(0.15, 0.05 + 0.10 / 6 * overallDifficulty); RequiredGreatHits = NestedHitObjects.Count * Math.Min(0.30, 0.10 + 0.20 / 6 * overallDifficulty); - base.CreateNestedHitObjects(); + base.CreateNestedHitObjects(cancellationToken); } private void createTicks() diff --git a/osu.Game.Rulesets.Taiko/Objects/Swell.cs b/osu.Game.Rulesets.Taiko/Objects/Swell.cs index 2f06066a16..3a4023b3e5 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Swell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Swell.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Threading; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; @@ -29,9 +30,9 @@ namespace osu.Game.Rulesets.Taiko.Objects set => throw new NotSupportedException($"{nameof(Swell)} cannot be a strong hitobject."); } - protected override void CreateNestedHitObjects() + protected override void CreateNestedHitObjects(CancellationToken cancellationToken) { - base.CreateNestedHitObjects(); + base.CreateNestedHitObjects(cancellationToken); for (int i = 0; i < RequiredHits; i++) AddNested(new SwellTick()); diff --git a/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs index c41727557b..206bfcfdb2 100644 --- a/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Threading; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; @@ -32,9 +33,9 @@ namespace osu.Game.Rulesets.Taiko.Objects ///
  • public virtual bool IsStrong { get; set; } - protected override void CreateNestedHitObjects() + protected override void CreateNestedHitObjects(CancellationToken cancellationToken) { - base.CreateNestedHitObjects(); + base.CreateNestedHitObjects(cancellationToken); if (IsStrong) AddNested(new StrongHitObject { StartTime = this.GetEndTime() }); diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index fb1eb7adbf..49b57c3f30 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -90,7 +91,7 @@ namespace osu.Game.Rulesets.Edit public abstract void UpdatePosition(Vector2 screenSpacePosition); /// - /// Invokes , + /// Invokes , /// refreshing and parameters for the . /// protected void ApplyDefaultsToHitObject() => HitObject.ApplyDefaults(beatmap.Value.Beatmap.ControlPointInfo, beatmap.Value.Beatmap.BeatmapInfo.BaseDifficulty); diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs index cffbdbae08..8ff2bdefb3 100644 --- a/osu.Game/Rulesets/Objects/HitObject.cs +++ b/osu.Game/Rulesets/Objects/HitObject.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using JetBrains.Annotations; using Newtonsoft.Json; using osu.Framework.Bindables; @@ -99,7 +100,8 @@ namespace osu.Game.Rulesets.Objects ///
    /// The control points. /// The difficulty settings to use. - public void ApplyDefaults(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + /// The cancellation token. + public void ApplyDefaults(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty, CancellationToken cancellationToken = default) { ApplyDefaultsToSelf(controlPointInfo, difficulty); @@ -108,7 +110,7 @@ namespace osu.Game.Rulesets.Objects nestedHitObjects.Clear(); - CreateNestedHitObjects(); + CreateNestedHitObjects(cancellationToken); if (this is IHasComboInformation hasCombo) { @@ -122,7 +124,7 @@ namespace osu.Game.Rulesets.Objects nestedHitObjects.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime)); foreach (var h in nestedHitObjects) - h.ApplyDefaults(controlPointInfo, difficulty); + h.ApplyDefaults(controlPointInfo, difficulty, cancellationToken); DefaultsApplied?.Invoke(this); } @@ -136,6 +138,15 @@ namespace osu.Game.Rulesets.Objects HitWindows?.SetDifficulty(difficulty.OverallDifficulty); } + protected virtual void CreateNestedHitObjects(CancellationToken cancellationToken) + { + // ReSharper disable once MethodSupportsCancellation (https://youtrack.jetbrains.com/issue/RIDER-44520) +#pragma warning disable 618 + CreateNestedHitObjects(); +#pragma warning restore 618 + } + + [Obsolete("Use the overload with cancellation support instead.")] // can be removed 20201115 protected virtual void CreateNestedHitObjects() { } From 4079642d58572f7b342071e572b0b21c08e15b8b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 15 May 2020 18:13:47 +0900 Subject: [PATCH 1602/2376] Actually pass in the cancellation token --- osu.Game/Beatmaps/WorkingBeatmap.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index bf2b9944a4..9ea023a030 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -134,7 +134,7 @@ namespace osu.Game.Beatmaps if (cancellationSource.IsCancellationRequested) throw new BeatmapLoadTimeoutException(BeatmapInfo); - obj.ApplyDefaults(converted.ControlPointInfo, converted.BeatmapInfo.BaseDifficulty); + obj.ApplyDefaults(converted.ControlPointInfo, converted.BeatmapInfo.BaseDifficulty, cancellationSource.Token); } foreach (var mod in mods.OfType()) From 4719fcc2913eaa002f0ac62f7f47b0a670c78f42 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 15 May 2020 18:17:39 +0900 Subject: [PATCH 1603/2376] Actually use the cancellation token --- osu.Game.Rulesets.Catch/Objects/JuiceStream.cs | 4 +++- osu.Game.Rulesets.Mania/Objects/HoldNote.cs | 6 ++++-- osu.Game.Rulesets.Osu/Objects/Slider.cs | 2 +- osu.Game/Beatmaps/WorkingBeatmap.cs | 15 +++++++++++---- osu.Game/Rulesets/Objects/SliderEventGenerator.cs | 10 +++++++--- 5 files changed, 26 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index 4f9a289739..d32595c2e1 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Catch.Objects SliderEventDescriptor? lastEvent = null; - foreach (var e in SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset)) + foreach (var e in SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken)) { // generate tiny droplets since the last point if (lastEvent != null) @@ -74,6 +74,8 @@ namespace osu.Game.Rulesets.Catch.Objects for (double t = timeBetweenTiny; t < sinceLastTick; t += timeBetweenTiny) { + cancellationToken.ThrowIfCancellationRequested(); + AddNested(new TinyDroplet { StartTime = t + lastEvent.Value.Time, diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs index d8bdaa071b..e6f722a8a9 100644 --- a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs @@ -96,7 +96,7 @@ namespace osu.Game.Rulesets.Mania.Objects { base.CreateNestedHitObjects(cancellationToken); - createTicks(); + createTicks(cancellationToken); AddNested(Head = new Note { @@ -113,13 +113,15 @@ namespace osu.Game.Rulesets.Mania.Objects }); } - private void createTicks() + private void createTicks(CancellationToken cancellationToken) { if (tickSpacing == 0) return; for (double t = StartTime + tickSpacing; t <= EndTime - tickSpacing; t += tickSpacing) { + cancellationToken.ThrowIfCancellationRequested(); + AddNested(new HoldNoteTick { StartTime = t, diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index bc6fca2338..6ba0e1c6aa 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -139,7 +139,7 @@ namespace osu.Game.Rulesets.Osu.Objects base.CreateNestedHitObjects(cancellationToken); foreach (var e in - SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset)) + SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken)) { switch (e.Type) { diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 9ea023a030..8126311cbd 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -129,12 +129,19 @@ namespace osu.Game.Beatmaps processor?.PreProcess(); // Compute default values for hitobjects, including creating nested hitobjects in-case they're needed - foreach (var obj in converted.HitObjects) + try { - if (cancellationSource.IsCancellationRequested) - throw new BeatmapLoadTimeoutException(BeatmapInfo); + foreach (var obj in converted.HitObjects) + { + if (cancellationSource.IsCancellationRequested) + throw new BeatmapLoadTimeoutException(BeatmapInfo); - obj.ApplyDefaults(converted.ControlPointInfo, converted.BeatmapInfo.BaseDifficulty, cancellationSource.Token); + obj.ApplyDefaults(converted.ControlPointInfo, converted.BeatmapInfo.BaseDifficulty, cancellationSource.Token); + } + } + catch (OperationCanceledException) + { + throw new BeatmapLoadTimeoutException(BeatmapInfo); } foreach (var mod in mods.OfType()) diff --git a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs index e9ee3833b7..5f1c1cf6a0 100644 --- a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs +++ b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs @@ -4,13 +4,14 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; namespace osu.Game.Rulesets.Objects { public static class SliderEventGenerator { public static IEnumerable Generate(double startTime, double spanDuration, double velocity, double tickDistance, double totalDistance, int spanCount, - double? legacyLastTickOffset) + double? legacyLastTickOffset, CancellationToken cancellationToken = default) { // A very lenient maximum length of a slider for ticks to be generated. // This exists for edge cases such as /b/1573664 where the beatmap has been edited by the user, and should never be reached in normal usage. @@ -37,7 +38,7 @@ namespace osu.Game.Rulesets.Objects var spanStartTime = startTime + span * spanDuration; var reversed = span % 2 == 1; - var ticks = generateTicks(span, spanStartTime, spanDuration, reversed, length, tickDistance, minDistanceFromEnd); + var ticks = generateTicks(span, spanStartTime, spanDuration, reversed, length, tickDistance, minDistanceFromEnd, cancellationToken); if (reversed) { @@ -108,12 +109,15 @@ namespace osu.Game.Rulesets.Objects /// The length of the path. /// The distance between each tick. /// The distance from the end of the path at which ticks are not allowed to be added. + /// The cancellation token. /// A for each tick. If is true, the ticks will be returned in reverse-StartTime order. private static IEnumerable generateTicks(int spanIndex, double spanStartTime, double spanDuration, bool reversed, double length, double tickDistance, - double minDistanceFromEnd) + double minDistanceFromEnd, CancellationToken cancellationToken = default) { for (var d = tickDistance; d <= length; d += tickDistance) { + cancellationToken.ThrowIfCancellationRequested(); + if (d >= length - minDistanceFromEnd) break; From 9b6525bb03a09323b38ec1e2296d63ce1a73a5d8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 15 May 2020 18:44:47 +0900 Subject: [PATCH 1604/2376] Fix applied platform/user offsets being incorrect when rate adjust mods are active --- osu.Game/Screens/Play/GameplayClockContainer.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 591e969ad8..2f85d6ad1e 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -78,10 +78,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 FramedOffsetClock(adjustableClock) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 15 : 0 }; + platformOffsetClock = new HardwareCorrectionOffsetClock(adjustableClock) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 15 : 0 }; // the final usable gameplay clock with user-set offsets applied. - userOffsetClock = new FramedOffsetClock(platformOffsetClock); + userOffsetClock = new HardwareCorrectionOffsetClock(platformOffsetClock); // the clock to be exposed via DI to children. GameplayClock = new GameplayClock(userOffsetClock); @@ -248,5 +248,16 @@ namespace osu.Game.Screens.Play speedAdjustmentsApplied = false; } } + + private class HardwareCorrectionOffsetClock : FramedOffsetClock + { + // we always want to apply the same real-time offset, so it should be adjusted by the playback rate to achieve this. + public override double CurrentTime => SourceTime + Offset * Rate; + + public HardwareCorrectionOffsetClock(IClock source, bool processSource = true) + : base(source, processSource) + { + } + } } } From 6cd1753459451bfe6672e149c1099ee5e514dd55 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 15 May 2020 18:51:44 +0900 Subject: [PATCH 1605/2376] Add overload to prevent crashes (bosu) --- osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs | 10 +++++----- osu.Game/Rulesets/Objects/SliderEventGenerator.cs | 8 ++++++++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs b/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs index 9fba0f1668..6c8133660f 100644 --- a/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs +++ b/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs @@ -16,7 +16,7 @@ namespace osu.Game.Tests.Beatmaps [Test] public void TestSingleSpan() { - var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, null).ToArray(); + var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, null, default).ToArray(); Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head)); Assert.That(events[0].Time, Is.EqualTo(start_time)); @@ -31,7 +31,7 @@ namespace osu.Game.Tests.Beatmaps [Test] public void TestRepeat() { - var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 2, null).ToArray(); + var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 2, null, default).ToArray(); Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head)); Assert.That(events[0].Time, Is.EqualTo(start_time)); @@ -52,7 +52,7 @@ namespace osu.Game.Tests.Beatmaps [Test] public void TestNonEvenTicks() { - var events = SliderEventGenerator.Generate(start_time, span_duration, 1, 300, span_duration, 2, null).ToArray(); + var events = SliderEventGenerator.Generate(start_time, span_duration, 1, 300, span_duration, 2, null, default).ToArray(); Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head)); Assert.That(events[0].Time, Is.EqualTo(start_time)); @@ -85,7 +85,7 @@ namespace osu.Game.Tests.Beatmaps [Test] public void TestLegacyLastTickOffset() { - var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, 100).ToArray(); + var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, 100, default).ToArray(); Assert.That(events[2].Type, Is.EqualTo(SliderEventType.LegacyLastTick)); Assert.That(events[2].Time, Is.EqualTo(900)); @@ -97,7 +97,7 @@ namespace osu.Game.Tests.Beatmaps const double velocity = 5; const double min_distance = velocity * 10; - var events = SliderEventGenerator.Generate(start_time, span_duration, velocity, velocity, span_duration, 2, 0).ToArray(); + var events = SliderEventGenerator.Generate(start_time, span_duration, velocity, velocity, span_duration, 2, 0, default).ToArray(); Assert.Multiple(() => { diff --git a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs index 5f1c1cf6a0..6df0041e7a 100644 --- a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs +++ b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs @@ -10,6 +10,14 @@ namespace osu.Game.Rulesets.Objects { public static class SliderEventGenerator { + [Obsolete("Use the overload with cancellation support instead.")] // can be removed 20201115 + public static IEnumerable Generate(double startTime, double spanDuration, double velocity, double tickDistance, double totalDistance, int spanCount, + double? legacyLastTickOffset) + { + return Generate(startTime, spanDuration, velocity, tickDistance, totalDistance, spanCount, legacyLastTickOffset, default); + } + + // ReSharper disable once MethodOverloadWithOptionalParameter public static IEnumerable Generate(double startTime, double spanDuration, double velocity, double tickDistance, double totalDistance, int spanCount, double? legacyLastTickOffset, CancellationToken cancellationToken = default) { From 65345109991272e138f5e6f9658ee1da9802b127 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 15 May 2020 19:25:14 +0900 Subject: [PATCH 1606/2376] Use cancellation token in taiko swell/drumroll --- osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs | 6 ++++-- osu.Game.Rulesets.Taiko/Objects/Swell.cs | 3 +++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs index 8bbad220ac..7b11bce520 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs @@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Taiko.Objects protected override void CreateNestedHitObjects(CancellationToken cancellationToken) { - createTicks(); + createTicks(cancellationToken); RequiredGoodHits = NestedHitObjects.Count * Math.Min(0.15, 0.05 + 0.10 / 6 * overallDifficulty); RequiredGreatHits = NestedHitObjects.Count * Math.Min(0.30, 0.10 + 0.20 / 6 * overallDifficulty); @@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Taiko.Objects base.CreateNestedHitObjects(cancellationToken); } - private void createTicks() + private void createTicks(CancellationToken cancellationToken) { if (tickSpacing == 0) return; @@ -93,6 +93,8 @@ namespace osu.Game.Rulesets.Taiko.Objects for (double t = StartTime; t < EndTime + tickSpacing / 2; t += tickSpacing) { + cancellationToken.ThrowIfCancellationRequested(); + AddNested(new DrumRollTick { FirstTick = first, diff --git a/osu.Game.Rulesets.Taiko/Objects/Swell.cs b/osu.Game.Rulesets.Taiko/Objects/Swell.cs index 3a4023b3e5..390f8d1f3b 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Swell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Swell.cs @@ -35,7 +35,10 @@ namespace osu.Game.Rulesets.Taiko.Objects base.CreateNestedHitObjects(cancellationToken); for (int i = 0; i < RequiredHits; i++) + { + cancellationToken.ThrowIfCancellationRequested(); AddNested(new SwellTick()); + } } public override Judgement CreateJudgement() => new TaikoSwellJudgement(); From c55eb8335135359ee20ee7a3900392b35981b6da Mon Sep 17 00:00:00 2001 From: Fukashi13 <48766178+Fukashi13@users.noreply.github.com> Date: Fri, 15 May 2020 14:03:45 +0200 Subject: [PATCH 1607/2376] last section gets selected when scrolling to bottom of list --- .../Graphics/Containers/SectionsContainer.cs | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/osu.Game/Graphics/Containers/SectionsContainer.cs b/osu.Game/Graphics/Containers/SectionsContainer.cs index 5192a7ddea..b9ed9b90fc 100644 --- a/osu.Game/Graphics/Containers/SectionsContainer.cs +++ b/osu.Game/Graphics/Containers/SectionsContainer.cs @@ -189,23 +189,18 @@ namespace osu.Game.Graphics.Containers headerBackgroundContainer.Height = (ExpandableHeader?.LayoutSize.Y ?? 0) + (FixedHeader?.LayoutSize.Y ?? 0); headerBackgroundContainer.Y = ExpandableHeader?.Y ?? 0; - T bestMatch = Children.FirstOrDefault(); - float minDiff = float.MinValue; float scrollOffset = FixedHeader?.LayoutSize.Y ?? 0; + Func diff = section => scrollContainer.GetChildPosInContent(section) - currentScroll - scrollOffset; - foreach (var section in Children) + if (scrollContainer.IsScrolledToEnd()) { - float diff = scrollContainer.GetChildPosInContent(section) - currentScroll - scrollOffset; - - if (minDiff < diff && diff < 0) - { - minDiff = diff; - bestMatch = section; - } + SelectedSection.Value = Children.LastOrDefault(); + } + else + { + SelectedSection.Value = Children.TakeWhile(section => diff(section) <= 0).LastOrDefault() + ?? Children.FirstOrDefault(); } - - if (bestMatch != null) - SelectedSection.Value = bestMatch; } } From 6416ace70d07f8a5d67bfd469ee93e73f1af5266 Mon Sep 17 00:00:00 2001 From: Fukashi13 <48766178+Fukashi13@users.noreply.github.com> Date: Fri, 15 May 2020 14:31:05 +0200 Subject: [PATCH 1608/2376] fixed indent --- osu.Game/Graphics/Containers/SectionsContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Containers/SectionsContainer.cs b/osu.Game/Graphics/Containers/SectionsContainer.cs index b9ed9b90fc..d739f56828 100644 --- a/osu.Game/Graphics/Containers/SectionsContainer.cs +++ b/osu.Game/Graphics/Containers/SectionsContainer.cs @@ -199,7 +199,7 @@ namespace osu.Game.Graphics.Containers else { SelectedSection.Value = Children.TakeWhile(section => diff(section) <= 0).LastOrDefault() - ?? Children.FirstOrDefault(); + ?? Children.FirstOrDefault(); } } } From 4096463d02d8ac096f6e4c7d1d23d89b9575e0c9 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Fri, 15 May 2020 19:43:01 +0200 Subject: [PATCH 1609/2376] Move SkipOverlay internal alpha manipulation to a nested container and adjust visual tests. --- .../Visual/Gameplay/TestSceneSkipOverlay.cs | 30 ++++++++++++------- osu.Game/Screens/Play/Player.cs | 1 - osu.Game/Screens/Play/SkipOverlay.cs | 27 +++++++++-------- 3 files changed, 34 insertions(+), 24 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs index 6a0f86fe53..7c4ae4fc52 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs @@ -2,9 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Screens.Play; @@ -16,7 +16,7 @@ namespace osu.Game.Tests.Visual.Gameplay [TestFixture] public class TestSceneSkipOverlay : OsuManualInputManagerTestScene { - private SkipOverlay skip; + private TestSkipOverlay skip; private int requestCount; private double increment; @@ -37,7 +37,7 @@ namespace osu.Game.Tests.Visual.Gameplay RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - skip = new SkipOverlay(skip_time) + skip = new TestSkipOverlay(skip_time) { RequestSkip = () => { @@ -56,19 +56,19 @@ namespace osu.Game.Tests.Visual.Gameplay public void TestFadeOnIdle() { AddStep("move mouse", () => InputManager.MoveMouseTo(Vector2.Zero)); - AddUntilStep("fully visible", () => skip.Children.First().Alpha == 1); - AddUntilStep("wait for fade", () => skip.Children.First().Alpha < 1); + AddUntilStep("fully visible", () => skip.OverlayContents.Alpha == 1); + AddUntilStep("wait for fade", () => skip.OverlayContents.Alpha < 1); AddStep("move mouse", () => InputManager.MoveMouseTo(skip.ScreenSpaceDrawQuad.Centre)); - AddUntilStep("fully visible", () => skip.Children.First().Alpha == 1); - AddUntilStep("wait for fade", () => skip.Children.First().Alpha < 1); + AddUntilStep("fully visible", () => skip.OverlayContents.Alpha == 1); + AddUntilStep("wait for fade", () => skip.OverlayContents.Alpha < 1); } [Test] public void TestClickableAfterFade() { AddStep("move mouse", () => InputManager.MoveMouseTo(skip.ScreenSpaceDrawQuad.Centre)); - AddUntilStep("wait for fade", () => skip.Children.First().Alpha == 0); + AddUntilStep("wait for fade", () => skip.OverlayContents.Alpha == 0); AddStep("click", () => InputManager.Click(MouseButton.Left)); checkRequestCount(1); } @@ -105,13 +105,23 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("move mouse", () => InputManager.MoveMouseTo(skip.ScreenSpaceDrawQuad.Centre)); AddStep("button down", () => InputManager.PressButton(MouseButton.Left)); - AddUntilStep("wait for overlay disappear", () => !skip.IsPresent); - AddAssert("ensure button didn't disappear", () => skip.Children.First().Alpha > 0); + AddUntilStep("wait for overlay disappear", () => !skip.Child.IsPresent); + AddAssert("ensure button didn't disappear", () => skip.OverlayContents.Alpha > 0); AddStep("button up", () => InputManager.ReleaseButton(MouseButton.Left)); checkRequestCount(0); } private void checkRequestCount(int expected) => AddAssert($"request count is {expected}", () => requestCount == expected); + + private class TestSkipOverlay : SkipOverlay + { + public TestSkipOverlay(double startTime) + : base(startTime) + { + } + + public Drawable OverlayContents => (Child as Container).Child; + } } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index b24cef8eae..02c7b671a3 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -191,7 +191,6 @@ namespace osu.Game.Screens.Play HUDOverlay.ShowHud.Value = false; HUDOverlay.ShowHud.Disabled = true; BreakOverlay.Hide(); - skipOverlay.Disabled = true; skipOverlay.Hide(); } diff --git a/osu.Game/Screens/Play/SkipOverlay.cs b/osu.Game/Screens/Play/SkipOverlay.cs index 757dcd21ed..fec35df4e3 100644 --- a/osu.Game/Screens/Play/SkipOverlay.cs +++ b/osu.Game/Screens/Play/SkipOverlay.cs @@ -24,13 +24,14 @@ using osu.Game.Input.Bindings; namespace osu.Game.Screens.Play { - public class SkipOverlay : VisibilityContainer, IKeyBindingHandler + public class SkipOverlay : Container, IKeyBindingHandler { private readonly double startTime; public Action RequestSkip; private Button button; + private ButtonContainer buttonContainer; private Box remainingTimeBox; private FadeContainer fadeContainer; @@ -39,8 +40,6 @@ namespace osu.Game.Screens.Play [Resolved] private GameplayClock gameplayClock { get; set; } - public bool Disabled { get; set; } - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; /// @@ -63,9 +62,10 @@ namespace osu.Game.Screens.Play [BackgroundDependencyLoader(true)] private void load(OsuColour colours) { - Children = new Drawable[] + Child = buttonContainer = new ButtonContainer { - fadeContainer = new FadeContainer + RelativeSizeAxes = Axes.Both, + Child = fadeContainer = new FadeContainer { RelativeSizeAxes = Axes.Both, Children = new Drawable[] @@ -106,15 +106,8 @@ namespace osu.Game.Screens.Play button.Action = () => RequestSkip?.Invoke(); displayTime = gameplayClock.CurrentTime; - - if (!Disabled) - Show(); } - protected override void PopIn() => this.FadeIn(fade_time); - - protected override void PopOut() => this.FadeOut(fade_time); - protected override void Update() { base.Update(); @@ -124,13 +117,14 @@ namespace osu.Game.Screens.Play remainingTimeBox.Width = (float)Interpolation.Lerp(remainingTimeBox.Width, progress, Math.Clamp(Time.Elapsed / 40, 0, 1)); button.Enabled.Value = progress > 0; - State.Value = progress > 0 && !Disabled ? Visibility.Visible : Visibility.Hidden; + buttonContainer.State.Value = progress > 0 ? Visibility.Visible : Visibility.Hidden; } protected override bool OnMouseMove(MouseMoveEvent e) { if (!e.HasAnyButtonPressed) fadeContainer.Show(); + return base.OnMouseMove(e); } @@ -217,6 +211,13 @@ namespace osu.Game.Screens.Play public override void Show() => State = Visibility.Visible; } + private class ButtonContainer : VisibilityContainer + { + protected override void PopIn() => this.FadeIn(fade_time); + + protected override void PopOut() => this.FadeOut(fade_time); + } + private class Button : OsuClickableContainer { private Color4 colourNormal; From ed9d6f28297c2801e1e6ad87bb81205e8d66c5e0 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Fri, 15 May 2020 22:58:15 +0200 Subject: [PATCH 1610/2376] Fix CI inspection. --- osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs index 7c4ae4fc52..e093542d1e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs @@ -121,7 +121,7 @@ namespace osu.Game.Tests.Visual.Gameplay { } - public Drawable OverlayContents => (Child as Container).Child; + public Drawable OverlayContents => (Child as Container)?.Child; } } } From c47f02c3b72cd1f499817cd133a34dfbf9777c55 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 16 May 2020 08:24:02 +0900 Subject: [PATCH 1611/2376] Update second instance of disabling bindable --- osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs index 65fc07d1d9..f139a704bc 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs @@ -86,7 +86,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input { bool raw = !handler.NewValue.Contains("Raw"); rawInputToggle.Value = raw; - sensitivity.Bindable.Disabled = !raw; + sensitivityBindable.Disabled = !raw; }; ignoredInputHandler.TriggerChange(); From 08bb5cbcbff6e1b42fa1715224aa989cd308b073 Mon Sep 17 00:00:00 2001 From: Shivam Date: Sat, 16 May 2020 02:57:58 +0200 Subject: [PATCH 1612/2376] Introduce model to store path of stable osu! --- osu.Game.Tournament/Models/StableInfo.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 osu.Game.Tournament/Models/StableInfo.cs diff --git a/osu.Game.Tournament/Models/StableInfo.cs b/osu.Game.Tournament/Models/StableInfo.cs new file mode 100644 index 0000000000..b89160536d --- /dev/null +++ b/osu.Game.Tournament/Models/StableInfo.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 osu.Framework.Bindables; + +namespace osu.Game.Tournament.Models +{ + /// + /// Holds the complete data required to operate the tournament system. + /// + [Serializable] + public class StableInfo + { + public Bindable StablePath = new Bindable(string.Empty); + } +} From c40b3b905313a78ee229bdd49589d26c32f27bfd Mon Sep 17 00:00:00 2001 From: Shivam Date: Sat, 16 May 2020 02:59:48 +0200 Subject: [PATCH 1613/2376] Refactored stable path finding and added json config detection. This also migrates the values found in the other methods to the configuration file. --- osu.Game.Tournament/IPC/FileBasedIPC.cs | 173 ++++++++++++++++++------ 1 file changed, 135 insertions(+), 38 deletions(-) diff --git a/osu.Game.Tournament/IPC/FileBasedIPC.cs b/osu.Game.Tournament/IPC/FileBasedIPC.cs index 53ba597a7e..321a4ad0aa 100644 --- a/osu.Game.Tournament/IPC/FileBasedIPC.cs +++ b/osu.Game.Tournament/IPC/FileBasedIPC.cs @@ -4,6 +4,8 @@ using System; using System.IO; using System.Linq; +using System.Collections.Generic; +using Newtonsoft.Json; using Microsoft.Win32; using osu.Framework.Allocation; using osu.Framework.Logging; @@ -35,7 +37,15 @@ namespace osu.Game.Tournament.IPC private int lastBeatmapId; private ScheduledDelegate scheduled; - public Storage Storage { get; private set; } + [Resolved] + private StableInfo stableInfo { get; set; } + + private const string stable_config = "tournament/stable.json"; + + public Storage IPCStorage { get; private set; } + + [Resolved] + private Storage tournamentStorage { get; set; } [BackgroundDependencyLoader] private void load() @@ -47,7 +57,7 @@ namespace osu.Game.Tournament.IPC { scheduled?.Cancel(); - Storage = null; + IPCStorage = null; try { @@ -56,20 +66,20 @@ namespace osu.Game.Tournament.IPC if (string.IsNullOrEmpty(path)) return null; - Storage = new DesktopStorage(path, host as DesktopGameHost); + IPCStorage = new DesktopStorage(path, host as DesktopGameHost); const string file_ipc_filename = "ipc.txt"; const string file_ipc_state_filename = "ipc-state.txt"; const string file_ipc_scores_filename = "ipc-scores.txt"; const string file_ipc_channel_filename = "ipc-channel.txt"; - if (Storage.Exists(file_ipc_filename)) + if (IPCStorage.Exists(file_ipc_filename)) { scheduled = Scheduler.AddDelayed(delegate { try { - using (var stream = Storage.GetStream(file_ipc_filename)) + using (var stream = IPCStorage.GetStream(file_ipc_filename)) using (var sr = new StreamReader(stream)) { var beatmapId = int.Parse(sr.ReadLine()); @@ -101,7 +111,7 @@ namespace osu.Game.Tournament.IPC try { - using (var stream = Storage.GetStream(file_ipc_channel_filename)) + using (var stream = IPCStorage.GetStream(file_ipc_channel_filename)) using (var sr = new StreamReader(stream)) { ChatChannel.Value = sr.ReadLine(); @@ -114,7 +124,7 @@ namespace osu.Game.Tournament.IPC try { - using (var stream = Storage.GetStream(file_ipc_state_filename)) + using (var stream = IPCStorage.GetStream(file_ipc_state_filename)) using (var sr = new StreamReader(stream)) { State.Value = (TourneyState)Enum.Parse(typeof(TourneyState), sr.ReadLine()); @@ -127,7 +137,7 @@ namespace osu.Game.Tournament.IPC try { - using (var stream = Storage.GetStream(file_ipc_scores_filename)) + using (var stream = IPCStorage.GetStream(file_ipc_scores_filename)) using (var sr = new StreamReader(stream)) { Score1.Value = int.Parse(sr.ReadLine()); @@ -146,54 +156,141 @@ namespace osu.Game.Tournament.IPC Logger.Error(e, "Stable installation could not be found; disabling file based IPC"); } - return Storage; + return IPCStorage; } + private static bool checkExists(string p) => File.Exists(Path.Combine(p, "ipc.txt")); + private string findStablePath() { - static bool checkExists(string p) => File.Exists(Path.Combine(p, "ipc.txt")); - string stableInstallPath = string.Empty; try { - try + List> stableFindMethods = new List> { - stableInstallPath = Environment.GetEnvironmentVariable("OSU_STABLE_PATH"); + findFromJsonConfig, + findFromEnvVar, + findFromRegistry, + findFromLocalAppData, + findFromDotFolder + }; - if (checkExists(stableInstallPath)) + foreach (var r in stableFindMethods) + { + stableInstallPath = r.Invoke(); + + if (stableInstallPath != null) + { return stableInstallPath; - } - catch - { + } } - try - { - using (RegistryKey key = Registry.ClassesRoot.OpenSubKey("osu")) - stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty).ToString().Split('"')[1].Replace("osu!.exe", ""); - - if (checkExists(stableInstallPath)) - return stableInstallPath; - } - catch - { - } - - stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"osu!"); - if (checkExists(stableInstallPath)) - return stableInstallPath; - - stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".osu"); - if (checkExists(stableInstallPath)) - return stableInstallPath; - - return null; + return stableInstallPath; } finally { Logger.Log($"Stable path for tourney usage: {stableInstallPath}"); } } + + private void saveStablePath() + { + using (var stream = tournamentStorage.GetStream(stable_config, FileAccess.Write, FileMode.Create)) + using (var sw = new StreamWriter(stream)) + { + sw.Write(JsonConvert.SerializeObject(stableInfo, + new JsonSerializerSettings + { + Formatting = Formatting.Indented, + NullValueHandling = NullValueHandling.Ignore, + DefaultValueHandling = DefaultValueHandling.Ignore, + })); + } + } + + private string findFromEnvVar() + { + try + { + Logger.Log("Trying to find stable with environment variables"); + string stableInstallPath = Environment.GetEnvironmentVariable("OSU_STABLE_PATH"); + + if (checkExists(stableInstallPath)) + { + stableInfo.StablePath.Value = stableInstallPath; + saveStablePath(); + return stableInstallPath; + } + } + catch + { + } + + return null; + } + + private string findFromJsonConfig() + { + try + { + Logger.Log("Trying to find stable through the json config"); + return stableInfo.StablePath.Value; + } + catch + { + } + + return null; + } + + private string findFromLocalAppData() + { + Logger.Log("Trying to find stable in %LOCALAPPDATA%"); + string stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"osu!"); + + if (checkExists(stableInstallPath)) + { + stableInfo.StablePath.Value = stableInstallPath; + saveStablePath(); + return stableInstallPath; + } + + return null; + } + + private string findFromDotFolder() + { + Logger.Log("Trying to find stable in dotfolders"); + string stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".osu"); + + if (checkExists(stableInstallPath)) + { + stableInfo.StablePath.Value = stableInstallPath; + saveStablePath(); + return stableInstallPath; + } + + return null; + } + + private string findFromRegistry() + { + Logger.Log("Trying to find stable in registry"); + + string stableInstallPath; + + using (RegistryKey key = Registry.ClassesRoot.OpenSubKey("osu")) + stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty).ToString().Split('"')[1].Replace("osu!.exe", ""); + + if (checkExists(stableInstallPath)) + { + stableInfo.StablePath.Value = stableInstallPath; + saveStablePath(); + return stableInstallPath; + } + + return null; + } } } From 9944a514da95181dd6c4be222b2bd5d3e069dee4 Mon Sep 17 00:00:00 2001 From: Shivam Date: Sat, 16 May 2020 03:00:37 +0200 Subject: [PATCH 1614/2376] Dependency cache the ipc location file --- osu.Game.Tournament/TournamentGameBase.cs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 85db9e61fb..31c56c7fc4 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -33,6 +33,8 @@ namespace osu.Game.Tournament { private const string bracket_filename = "bracket.json"; + private const string stable_config = "tournament/stable.json"; + private LadderInfo ladder; private Storage storage; @@ -43,6 +45,7 @@ namespace osu.Game.Tournament private Bindable windowSize; private FileBasedIPC ipc; + private StableInfo stableInfo; private Drawable heightWarning; @@ -71,6 +74,7 @@ namespace osu.Game.Tournament }), true); readBracket(); + readStableConfig(); ladder.CurrentMatch.Value = ladder.Matches.FirstOrDefault(p => p.Current.Value); @@ -141,6 +145,23 @@ namespace osu.Game.Tournament }); } + private void readStableConfig() + { + if (storage.Exists(stable_config)) + { + using (Stream stream = storage.GetStream(stable_config, FileAccess.Read, FileMode.Open)) + using (var sr = new StreamReader(stream)) + { + stableInfo = JsonConvert.DeserializeObject(sr.ReadToEnd()); + } + } + + if (stableInfo == null) + stableInfo = new StableInfo(); + + dependencies.Cache(stableInfo); + } + private void readBracket() { if (storage.Exists(bracket_filename)) From 3fc888ef95f62154cf1e32aa178036926da550cb Mon Sep 17 00:00:00 2001 From: Shivam Date: Sat, 16 May 2020 03:03:10 +0200 Subject: [PATCH 1615/2376] User interface setup for custom IPC location Right now makes use of another ActionableInfo field. Probably a better idea to add an extra button to the Current IPC Storage actionable field. --- .../Components/IPCNotFoundDialog.cs | 27 ++++ osu.Game.Tournament/Screens/SetupScreen.cs | 34 +++- .../Screens/StablePathSelectScreen.cs | 149 ++++++++++++++++++ 3 files changed, 207 insertions(+), 3 deletions(-) create mode 100644 osu.Game.Tournament/Components/IPCNotFoundDialog.cs create mode 100644 osu.Game.Tournament/Screens/StablePathSelectScreen.cs diff --git a/osu.Game.Tournament/Components/IPCNotFoundDialog.cs b/osu.Game.Tournament/Components/IPCNotFoundDialog.cs new file mode 100644 index 0000000000..d4f9edc182 --- /dev/null +++ b/osu.Game.Tournament/Components/IPCNotFoundDialog.cs @@ -0,0 +1,27 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics.Sprites; +using osu.Game.Overlays.Dialog; + +namespace osu.Game.Tournament.Components +{ + public class IPCNotFoundDialog : PopupDialog + { + public IPCNotFoundDialog() + { + BodyText = "Select a directory that contains an osu! Cutting Edge installation"; + + Icon = FontAwesome.Regular.Angry; + HeaderText = @"This is an invalid IPC Directory!"; + Buttons = new PopupDialogButton[] + { + new PopupDialogOkButton + { + Text = @"Alright.", + Action = () => { Expire(); } + } + }; + } + } +} diff --git a/osu.Game.Tournament/Screens/SetupScreen.cs b/osu.Game.Tournament/Screens/SetupScreen.cs index c91379b2d6..93edd73ff8 100644 --- a/osu.Game.Tournament/Screens/SetupScreen.cs +++ b/osu.Game.Tournament/Screens/SetupScreen.cs @@ -15,6 +15,8 @@ using osu.Game.Online.API; using osu.Game.Overlays; using osu.Game.Rulesets; using osu.Game.Tournament.IPC; +using osu.Framework.Platform; +using osu.Game.Tournament.Models; using osuTK; using osuTK.Graphics; @@ -26,6 +28,7 @@ namespace osu.Game.Tournament.Screens private LoginOverlay loginOverlay; private ActionableInfo resolution; + private const string stable_config = "tournament/stable.json"; [Resolved] private MatchIPCInfo ipc { get; set; } @@ -36,8 +39,17 @@ namespace osu.Game.Tournament.Screens [Resolved] private RulesetStore rulesets { get; set; } + [Resolved(canBeNull: true)] + private TournamentSceneManager sceneManager { get; set; } + private Bindable windowSize; + [Resolved] + private Storage storage { get; set; } + + [Resolved] + private StableInfo stableInfo { get; set; } + [BackgroundDependencyLoader] private void load(FrameworkConfigManager frameworkConfig) { @@ -62,7 +74,6 @@ namespace osu.Game.Tournament.Screens private void reload() { var fileBasedIpc = ipc as FileBasedIPC; - fillFlow.Children = new Drawable[] { new ActionableInfo @@ -74,11 +85,28 @@ namespace osu.Game.Tournament.Screens fileBasedIpc?.LocateStableStorage(); reload(); }, - Value = fileBasedIpc?.Storage?.GetFullPath(string.Empty) ?? "Not found", - Failing = fileBasedIpc?.Storage == null, + Value = fileBasedIpc?.IPCStorage?.GetFullPath(string.Empty) ?? "Not found", + Failing = fileBasedIpc?.IPCStorage == null, Description = "The osu!stable installation which is currently being used as a data source. If a source is not found, make sure you have created an empty ipc.txt in your stable cutting-edge installation, and that it is registered as the default osu! install." }, new ActionableInfo + { + Label = "Custom IPC source", + ButtonText = "Change path", + Action = () => + { + stableInfo.StablePath.BindValueChanged(_ => + { + fileBasedIpc?.LocateStableStorage(); + Schedule(reload); + }); + sceneManager.SetScreen(new StablePathSelectScreen()); + }, + Value = fileBasedIpc?.IPCStorage?.GetFullPath(string.Empty) ?? "Not found", + Failing = fileBasedIpc?.IPCStorage == null, + Description = "The osu!stable installation which is currently being used as a data source. If a source is not found, you can manually select the desired osu! installation that you want to use." + }, + new ActionableInfo { Label = "Current User", ButtonText = "Change Login", diff --git a/osu.Game.Tournament/Screens/StablePathSelectScreen.cs b/osu.Game.Tournament/Screens/StablePathSelectScreen.cs new file mode 100644 index 0000000000..1faacc727f --- /dev/null +++ b/osu.Game.Tournament/Screens/StablePathSelectScreen.cs @@ -0,0 +1,149 @@ +// 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 Newtonsoft.Json; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Game.Tournament.Models; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Overlays; +using osu.Game.Tournament.IPC; +using osu.Game.Tournament.Components; +using osuTK; + +namespace osu.Game.Tournament.Screens +{ + public class StablePathSelectScreen : TournamentScreen + { + private DirectorySelector directorySelector; + + private const string stable_config = "tournament/stable.json"; + + [Resolved] + private StableInfo stableInfo { get; set; } + + [Resolved] + private MatchIPCInfo ipc { get; set; } + + private DialogOverlay overlay; + + [Resolved(canBeNull: true)] + private TournamentSceneManager sceneManager { get; set; } + + [BackgroundDependencyLoader(true)] + private void load(Storage storage, OsuColour colours) + { + // begin selection in the parent directory of the current storage location + var initialPath = new DirectoryInfo(stableInfo.StablePath.Value).FullName; + + AddInternal(new Container + { + Masking = true, + CornerRadius = 10, + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(0.5f, 0.8f), + Children = new Drawable[] + { + new Box + { + Colour = colours.GreySeafoamDark, + RelativeSizeAxes = Axes.Both, + }, + new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.Relative, 0.8f), + new Dimension(), + }, + Content = new[] + { + new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Please select a new location", + Font = OsuFont.Default.With(size: 40) + }, + }, + new Drawable[] + { + directorySelector = new DirectorySelector(initialPath) + { + RelativeSizeAxes = Axes.Both, + } + }, + new Drawable[] + { + new TriangleButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 300, + Text = "Select stable path", + Action = () => { start(storage); } + }, + } + } + } + } + }); + } + + private static bool checkExists(string p) => File.Exists(Path.Combine(p, "ipc.txt")); + + private void start(Storage storage) + { + var target = directorySelector.CurrentDirectory.Value.FullName; + + if (checkExists(target)) + { + stableInfo.StablePath.Value = target; + + try + { + using (var stream = storage.GetStream(stable_config, FileAccess.Write, FileMode.Create)) + using (var sw = new StreamWriter(stream)) + { + sw.Write(JsonConvert.SerializeObject(stableInfo, + new JsonSerializerSettings + { + Formatting = Formatting.Indented, + NullValueHandling = NullValueHandling.Ignore, + DefaultValueHandling = DefaultValueHandling.Ignore, + })); + } + + sceneManager?.SetScreen(typeof(SetupScreen)); + } + catch (Exception e) + { + Logger.Log($"Error during migration: {e.Message}", level: LogLevel.Error); + } + } + else + { + overlay = new DialogOverlay(); + overlay.Push(new IPCNotFoundDialog()); + AddInternal(overlay); + Logger.Log("Folder is not an osu! stable CE directory"); + // Return an error in the picker that the directory does not contain ipc.txt + } + } + } +} From c931bae70ec13b10f45c89ad67d223c635046186 Mon Sep 17 00:00:00 2001 From: Shivam Date: Sat, 16 May 2020 03:07:27 +0200 Subject: [PATCH 1616/2376] Add back button to TournamentScreen and the inputhandler for it --- .../Screens/Editors/TournamentEditorScreen.cs | 45 ++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs index 8e5df72cc8..11cb072c6b 100644 --- a/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs @@ -9,6 +9,9 @@ using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics.UserInterface; +using osu.Game.Input.Bindings; +using osu.Framework.Input.Bindings; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Overlays.Settings; @@ -17,7 +20,7 @@ using osuTK; namespace osu.Game.Tournament.Screens.Editors { - public abstract class TournamentEditorScreen : TournamentScreen, IProvideVideo + public abstract class TournamentEditorScreen : TournamentScreen, IProvideVideo, IKeyBindingHandler where TDrawable : Drawable, IModelBacked where TModel : class, new() { @@ -25,8 +28,19 @@ namespace osu.Game.Tournament.Screens.Editors private FillFlowContainer flow; + [Resolved(canBeNull: true)] + private TournamentSceneManager sceneManager { get; set; } + protected ControlPanel ControlPanel; + protected virtual bool IsSubScreen => false; + + protected virtual System.Type ParentScreen { get; set; } + + private BackButton backButton; + + private System.Action backAction => () => sceneManager?.SetScreen(ParentScreen); + [BackgroundDependencyLoader] private void load() { @@ -70,6 +84,19 @@ namespace osu.Game.Tournament.Screens.Editors } }); + if (IsSubScreen) + { + BackButton.Receptor receptor = new BackButton.Receptor(); + backButton = new BackButton(receptor) + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Action = () => { backAction.Invoke(); } + }; + AddInternal(backButton); + backButton.Show(); + } + Storage.CollectionChanged += (_, args) => { switch (args.Action) @@ -88,6 +115,22 @@ namespace osu.Game.Tournament.Screens.Editors flow.Add(CreateDrawable(model)); } + public bool OnPressed(GlobalAction action) + { + switch (action) + { + case GlobalAction.Back: + backAction.Invoke(); + return true; + } + + return false; + } + + public void OnReleased(GlobalAction action) + { + } + protected abstract TDrawable CreateDrawable(TModel model); } } From bf6ce390ff39d6db4c82897daf561af332491502 Mon Sep 17 00:00:00 2001 From: Shivam Date: Sat, 16 May 2020 03:07:51 +0200 Subject: [PATCH 1617/2376] Add sub screen implementation to SeedingEditorScreen --- osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs index 52f761e50a..0f980ec9a3 100644 --- a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs @@ -25,6 +25,13 @@ namespace osu.Game.Tournament.Screens.Editors protected override BindableList Storage => team.SeedingResults; + [Resolved(canBeNull: true)] + private TournamentSceneManager sceneManager { get; set; } + + protected override bool IsSubScreen => true; + + protected override System.Type ParentScreen => typeof(TeamEditorScreen); + public SeedingEditorScreen(TournamentTeam team) { this.team = team; From 9d3df14179b740231e25bbb45f1d7b51e94b5840 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 16 May 2020 11:03:27 +0900 Subject: [PATCH 1618/2376] Remove unused variable --- osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs index f139a704bc..d27ab63fb7 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs @@ -19,7 +19,6 @@ namespace osu.Game.Overlays.Settings.Sections.Input private readonly BindableBool rawInputToggle = new BindableBool(); private Bindable sensitivityBindable = new BindableDouble(); private Bindable ignoredInputHandler; - private SensitivitySetting sensitivity; [BackgroundDependencyLoader] private void load(OsuConfigManager osuConfig, FrameworkConfigManager config) @@ -38,7 +37,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input LabelText = "Raw input", Bindable = rawInputToggle }, - sensitivity = new SensitivitySetting + new SensitivitySetting { LabelText = "Cursor sensitivity", Bindable = sensitivityBindable From b1243d6a8787a71701898de661a91fd457a827b7 Mon Sep 17 00:00:00 2001 From: Shivam Date: Sat, 16 May 2020 04:05:01 +0200 Subject: [PATCH 1619/2376] Add padding to so the back button is not in the way --- .../Screens/Editors/TournamentEditorScreen.cs | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs index 11cb072c6b..bca0814d3a 100644 --- a/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs @@ -44,6 +44,18 @@ namespace osu.Game.Tournament.Screens.Editors [BackgroundDependencyLoader] private void load() { + BackButton.Receptor receptor = new BackButton.Receptor(); + backButton = new BackButton(receptor) + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Action = () => + { + if (IsSubScreen) + backAction.Invoke(); + } + }; + AddRangeInternal(new Drawable[] { new Box @@ -56,6 +68,7 @@ namespace osu.Game.Tournament.Screens.Editors RelativeSizeAxes = Axes.Both, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, + Padding = new MarginPadding { Bottom = backButton.Height }, Child = flow = new FillFlowContainer { Direction = FillDirection.Vertical, @@ -64,6 +77,7 @@ namespace osu.Game.Tournament.Screens.Editors Spacing = new Vector2(20) }, }, + backButton, ControlPanel = new ControlPanel { Children = new Drawable[] @@ -85,17 +99,7 @@ namespace osu.Game.Tournament.Screens.Editors }); if (IsSubScreen) - { - BackButton.Receptor receptor = new BackButton.Receptor(); - backButton = new BackButton(receptor) - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Action = () => { backAction.Invoke(); } - }; - AddInternal(backButton); backButton.Show(); - } Storage.CollectionChanged += (_, args) => { From 648999a2de9e9cfc0bc25e804337c8d6f2d6cf30 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 16 May 2020 18:17:12 +0900 Subject: [PATCH 1620/2376] Remove all RequiredTypes usages --- .../CatchSkinnableTestScene.cs | 9 ------- .../TestSceneBananaShower.cs | 15 ----------- .../TestSceneCatcher.cs | 9 ------- .../TestSceneDrawableHitObjects.cs | 10 ------- .../TestSceneDrawableHitObjectsHidden.cs | 5 ---- .../TestSceneFruitObjects.cs | 19 -------------- .../TestSceneHyperDash.cs | 6 ----- .../Skinning/ManiaSkinnableTestScene.cs | 10 ------- .../Skinning/TestSceneDrawableJudgement.cs | 7 ----- .../Skinning/TestSceneHitExplosion.cs | 9 ------- .../Skinning/TestSceneKeyArea.cs | 9 ------- .../Skinning/TestSceneStageBackground.cs | 10 ------- .../Skinning/TestSceneStageForeground.cs | 9 ------- .../TestSceneColumn.cs | 10 ------- .../TestSceneManiaHitObjectComposer.cs | 7 ----- .../TestSceneNotes.cs | 8 ------ .../OsuSkinnableTestScene.cs | 9 ------- .../TestSceneDrawableJudgement.cs | 7 ----- .../TestSceneGameplayCursor.cs | 14 ---------- .../TestSceneHitCircle.cs | 7 ----- .../TestSceneHitCircleHidden.cs | 5 ---- .../TestSceneOsuDistanceSnapGrid.cs | 7 ----- .../TestScenePathControlPointVisualiser.cs | 10 ------- .../TestSceneResumeOverlay.cs | 7 ----- .../TestSceneSlider.cs | 18 ------------- .../TestSceneSliderHidden.cs | 5 ---- .../TestSceneSliderInput.cs | 14 ---------- .../TestSceneSliderSelectionBlueprint.cs | 12 --------- .../TestSceneSpinner.cs | 10 ------- .../TestSceneSpinnerHidden.cs | 5 ---- .../TestSceneSpinnerSelectionBlueprint.cs | 9 ------- .../TestSceneSpinnerSpunOut.cs | 11 -------- .../Skinning/TaikoSkinnableTestScene.cs | 9 ------- .../Skinning/TestSceneDrawableBarLine.cs | 11 -------- .../Skinning/TestSceneDrawableDrumRoll.cs | 11 -------- .../Skinning/TestSceneDrawableHit.cs | 11 -------- .../Skinning/TestSceneDrawableTaikoMascot.cs | 7 ----- .../Skinning/TestSceneHitExplosion.cs | 11 -------- .../Skinning/TestSceneInputDrum.cs | 10 ------- .../Skinning/TestSceneTaikoPlayfield.cs | 11 -------- .../Background/TestSceneUserDimBackgrounds.cs | 11 -------- .../Editing/TestSceneBeatDivisorControl.cs | 2 -- .../TestSceneEditorComposeRadioButtons.cs | 4 --- .../Visual/Editing/TestSceneEditorMenuBar.cs | 4 --- .../Editing/TestSceneEditorSummaryTimeline.cs | 4 --- .../Editing/TestSceneHitObjectComposer.cs | 18 ------------- .../TestSceneTimelineBlueprintContainer.cs | 7 ----- .../Visual/Editing/TestSceneTimingScreen.cs | 14 ---------- .../Visual/Editing/TimelineTestScene.cs | 10 ------- .../Visual/Gameplay/TestSceneBreakTracker.cs | 6 ----- .../Visual/Gameplay/TestSceneFailAnimation.cs | 8 ------ .../Gameplay/TestSceneGameplayMenuOverlay.cs | 3 --- .../Visual/Gameplay/TestSceneHitErrorMeter.cs | 9 ------- .../Visual/Gameplay/TestSceneKeyCounter.cs | 9 ------- .../Visual/Gameplay/TestSceneMedalOverlay.cs | 9 ------- .../TestSceneNightcoreBeatContainer.cs | 7 ----- .../Gameplay/TestSceneReplayDownloadButton.cs | 7 ----- .../Gameplay/TestSceneScrollingHitObjects.cs | 3 --- .../Visual/Gameplay/TestSceneSongProgress.cs | 6 ----- osu.Game.Tests/Visual/Menus/IntroTestScene.cs | 9 ------- .../Visual/Menus/TestSceneToolbar.cs | 10 ------- .../TestSceneDrawableRoomPlaylist.cs | 7 ----- .../Multiplayer/TestSceneLoungeRoomInfo.cs | 6 ----- .../TestSceneLoungeRoomsContainer.cs | 7 ----- .../Multiplayer/TestSceneMatchHeader.cs | 7 ----- .../TestSceneMatchLeaderboardChatDisplay.cs | 7 ----- .../TestSceneMatchSettingsOverlay.cs | 6 ----- .../Multiplayer/TestSceneMatchSongSelect.cs | 6 ----- .../Multiplayer/TestSceneMatchSubScreen.cs | 10 ------- .../Multiplayer/TestSceneMultiScreen.cs | 11 -------- .../TestSceneOverlinedParticipants.cs | 9 ------- .../Visual/Multiplayer/TestSceneRoomStatus.cs | 9 ------- .../Online/TestSceneAccountCreationOverlay.cs | 13 ---------- .../Online/TestSceneBeatmapListingOverlay.cs | 9 ------- .../Online/TestSceneBeatmapRulesetSelector.cs | 7 ----- .../Online/TestSceneBeatmapSetOverlay.cs | 26 ------------------- .../TestSceneBeatmapSetOverlayDetails.cs | 6 ----- .../TestSceneBeatmapSetOverlaySuccessRate.cs | 7 ----- .../Online/TestSceneChangelogOverlay.cs | 12 --------- .../Online/TestSceneChannelTabControl.cs | 6 ----- .../Online/TestSceneChatLineTruncation.cs | 10 ------- .../Visual/Online/TestSceneChatLink.cs | 12 --------- .../Visual/Online/TestSceneChatOverlay.cs | 13 ---------- .../Online/TestSceneCommentsContainer.cs | 15 ----------- .../Visual/Online/TestSceneCommentsHeader.cs | 9 ------- .../Visual/Online/TestSceneCommentsPage.cs | 6 ----- .../Online/TestSceneDashboardOverlay.cs | 11 -------- .../Online/TestSceneDirectDownloadButton.cs | 7 ----- .../Visual/Online/TestSceneDirectPanel.cs | 8 ------ .../Online/TestSceneExternalLinkButton.cs | 4 --- .../Visual/Online/TestSceneFriendDisplay.cs | 7 ----- .../Online/TestSceneHistoricalSection.cs | 10 ------- .../Visual/Online/TestSceneKudosuHistory.cs | 5 ---- .../Online/TestSceneLeaderboardModSelector.cs | 7 ----- .../TestSceneLeaderboardScopeSelector.cs | 7 ----- .../Online/TestSceneProfileCounterPill.cs | 7 ----- .../Online/TestSceneProfileRulesetSelector.cs | 8 ------ .../Visual/Online/TestSceneRankGraph.cs | 9 ------- .../Online/TestSceneRankingsCountryFilter.cs | 8 ------ .../Visual/Online/TestSceneRankingsHeader.cs | 9 ------- .../Visual/Online/TestSceneRankingsOverlay.cs | 15 ----------- .../TestSceneRankingsSpotlightSelector.cs | 5 ---- .../Visual/Online/TestSceneRankingsTables.cs | 12 --------- .../Visual/Online/TestSceneScoresContainer.cs | 10 ------- .../Visual/Online/TestSceneShowMoreButton.cs | 7 ----- .../Visual/Online/TestSceneSocialOverlay.cs | 11 -------- .../Online/TestSceneSpotlightsLayout.cs | 8 ------ .../Online/TestSceneTotalCommentsCounter.cs | 7 ----- .../Visual/Online/TestSceneUserPanel.cs | 8 ------ .../Online/TestSceneUserProfileHeader.cs | 16 ------------ .../Online/TestSceneUserProfileOverlay.cs | 13 ---------- .../TestSceneUserProfilePreviousUsernames.cs | 6 ----- .../TestSceneUserProfileRecentSection.cs | 10 ------- .../Online/TestSceneUserProfileScores.cs | 9 ------- .../Visual/Online/TestSceneUserRanks.cs | 10 ------- .../Visual/Online/TestSceneVotePill.cs | 7 ----- .../Visual/Ranking/TestSceneAccuracyCircle.cs | 10 ------- .../TestSceneExpandedPanelMiddleContent.cs | 15 ----------- .../Visual/Ranking/TestSceneResultsScreen.cs | 8 ------ .../Visual/Ranking/TestSceneScorePanel.cs | 8 ------ .../Settings/TestSceneKeyBindingPanel.cs | 13 ---------- .../Visual/Settings/TestSceneSettingsPanel.cs | 9 ------- .../SongSelect/TestSceneBeatmapCarousel.cs | 15 ----------- .../SongSelect/TestSceneBeatmapLeaderboard.cs | 11 -------- .../SongSelect/TestScenePlaySongSelect.cs | 18 ------------- osu.Game.Tests/Visual/TestSceneOsuGame.cs | 5 ---- .../UserInterface/TestSceneBackButton.cs | 7 ----- .../TestSceneBeatSyncedContainer.cs | 5 ---- .../TestSceneBeatmapListingSearchControl.cs | 7 ----- .../TestSceneBeatmapListingSortTabControl.cs | 7 ----- .../TestSceneBeatmapSearchFilter.cs | 8 ------ .../UserInterface/TestSceneButtonSystem.cs | 8 ------ .../UserInterface/TestSceneCommentEditor.cs | 8 ------ .../TestSceneDeleteLocalScore.cs | 12 --------- .../TestSceneFooterButtonMods.cs | 6 ----- .../TestSceneFriendsOnlineStatusControl.cs | 10 ------- .../TestSceneHoldToConfirmOverlay.cs | 9 ------- .../TestSceneLabelledSwitchButton.cs | 8 ------ .../UserInterface/TestSceneLabelledTextBox.cs | 7 ----- .../UserInterface/TestSceneLoadingLayer.cs | 4 --- .../TestSceneLogoTrackingContainer.cs | 14 ---------- .../TestSceneModSelectOverlay.cs | 16 ------------ .../TestSceneNotificationOverlay.cs | 11 -------- .../UserInterface/TestSceneNumberBox.cs | 7 ----- .../Visual/UserInterface/TestSceneOsuMenu.cs | 8 ------ .../UserInterface/TestSceneOverlayHeader.cs | 14 ---------- .../TestSceneOverlayHeaderBackground.cs | 7 ----- .../TestSceneOverlayRulesetSelector.cs | 8 ------ .../TestSceneOverlayScrollContainer.cs | 7 ----- .../UserInterface/TestScenePlaylistOverlay.cs | 7 ----- .../UserInterface/TestScenePopupDialog.cs | 11 -------- .../TestSceneStatefulMenuItem.cs | 9 ------- .../UserInterface/TestSceneToggleMenuItem.cs | 9 ------- .../TestSceneToolbarRulesetSelector.cs | 8 ------ .../UserInterface/TestSceneUserListToolbar.cs | 10 ------- .../UserInterface/TestSceneVolumePieces.cs | 4 --- .../TestSceneDrawableTournamentMatch.cs | 9 ------- .../TestSceneDrawableTournamentTeam.cs | 13 ---------- .../Components/TestSceneMatchHeader.cs | 9 ------- .../Components/TestSceneRoundDisplay.cs | 8 ------ .../Screens/TestSceneGameplayScreen.cs | 15 ----------- osu.Game/Tests/Visual/EditorTestScene.cs | 4 --- osu.Game/Tests/Visual/ModTestScene.cs | 5 ---- 163 files changed, 1471 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/CatchSkinnableTestScene.cs b/osu.Game.Rulesets.Catch.Tests/CatchSkinnableTestScene.cs index 0c46b078b5..378772fea3 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchSkinnableTestScene.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchSkinnableTestScene.cs @@ -1,21 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; -using osu.Game.Rulesets.Catch.Skinning; using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Catch.Tests { public abstract class CatchSkinnableTestScene : SkinnableTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(CatchRuleset), - typeof(CatchLegacySkinTransformer), - }; - protected override Ruleset CreateRulesetForSkinProvider() => new CatchRuleset(); } } diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.cs index 024c4cefb0..27a2d5bd0a 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneBananaShower.cs @@ -1,13 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Objects; -using osu.Game.Rulesets.Catch.Objects.Drawables; -using osu.Game.Rulesets.Catch.UI; using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Catch.Tests @@ -15,17 +11,6 @@ namespace osu.Game.Rulesets.Catch.Tests [TestFixture] public class TestSceneBananaShower : PlayerTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(BananaShower), - typeof(Banana), - typeof(DrawableBananaShower), - typeof(DrawableBanana), - - typeof(CatchRuleset), - typeof(DrawableCatchRuleset), - }; - public TestSceneBananaShower() : base(new CatchRuleset()) { diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs index 3a3e664690..6eeda2c731 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs @@ -4,9 +4,6 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Game.Rulesets.Catch.UI; -using System; -using System.Collections.Generic; -using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -15,12 +12,6 @@ namespace osu.Game.Rulesets.Catch.Tests [TestFixture] public class TestSceneCatcher : CatchSkinnableTestScene { - public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] - { - typeof(CatcherArea), - typeof(CatcherSprite) - }).ToList(); - [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs index df5494aab0..a7094c00be 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; @@ -22,15 +21,6 @@ namespace osu.Game.Rulesets.Catch.Tests { public class TestSceneDrawableHitObjects : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(Catcher), - typeof(DrawableCatchRuleset), - typeof(DrawableFruit), - typeof(DrawableJuiceStream), - typeof(DrawableBanana) - }; - private DrawableCatchRuleset drawableRuleset; private double playfieldTime => drawableRuleset.Playfield.Time.Current; diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjectsHidden.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjectsHidden.cs index 8c3dfef39c..62fe5dca2c 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjectsHidden.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjectsHidden.cs @@ -1,9 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; -using System.Linq; using NUnit.Framework; using osu.Game.Rulesets.Catch.Mods; @@ -11,8 +8,6 @@ namespace osu.Game.Rulesets.Catch.Tests { public class TestSceneDrawableHitObjectsHidden : TestSceneDrawableHitObjects { - public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] { typeof(CatchModHidden) }).ToList(); - [SetUp] public void SetUp() => Schedule(() => { diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs index cd674bb754..c07e4fdad3 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs @@ -2,13 +2,10 @@ // 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.Graphics; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawables; -using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces; using osuTK; namespace osu.Game.Rulesets.Catch.Tests @@ -16,22 +13,6 @@ namespace osu.Game.Rulesets.Catch.Tests [TestFixture] public class TestSceneFruitObjects : CatchSkinnableTestScene { - public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] - { - typeof(CatchHitObject), - typeof(Fruit), - typeof(FruitPiece), - typeof(Droplet), - typeof(Banana), - typeof(BananaShower), - typeof(DrawableCatchHitObject), - typeof(DrawableFruit), - typeof(DrawableDroplet), - typeof(DrawableBanana), - typeof(DrawableBananaShower), - typeof(Pulp), - }).ToList(); - protected override void LoadComplete() { base.LoadComplete(); diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs index 49ff9df4d7..0a142a52f8 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Testing; @@ -18,11 +17,6 @@ namespace osu.Game.Rulesets.Catch.Tests [TestFixture] public class TestSceneHyperDash : PlayerTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(CatcherArea), - }; - public TestSceneHyperDash() : base(new CatchRuleset()) { diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs index a3c1d518c5..1d84a2dfcb 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs @@ -1,15 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; -using osu.Game.Rulesets.Mania.Skinning; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling.Algorithms; using osu.Game.Tests.Visual; @@ -27,13 +24,6 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning [Cached(Type = typeof(IScrollingInfo))] private readonly TestScrollingInfo scrollingInfo = new TestScrollingInfo(); - public override IReadOnlyList RequiredTypes => new[] - { - typeof(ManiaRuleset), - typeof(ManiaLegacySkinTransformer), - typeof(ManiaSettingsSubsection) - }; - protected override Ruleset CreateRulesetForSkinProvider() => new ManiaRuleset(); protected ManiaSkinnableTestScene() diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs index 6ab8a68176..497b80950a 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Linq; using osu.Framework.Extensions; using osu.Framework.Graphics; @@ -15,12 +14,6 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning { public class TestSceneDrawableJudgement : ManiaSkinnableTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(DrawableJudgement), - typeof(DrawableManiaJudgement) - }; - public TestSceneDrawableJudgement() { foreach (HitResult result in Enum.GetValues(typeof(HitResult)).OfType().Skip(1)) diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs index 5f046574ba..a692c0b697 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs @@ -1,15 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; using osu.Game.Rulesets.Mania.UI; using osu.Game.Skinning; @@ -21,12 +18,6 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning [TestFixture] public class TestSceneHitExplosion : ManiaSkinnableTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(DrawableNote), - typeof(DrawableManiaHitObject), - }; - public TestSceneHitExplosion() { int runcount = 0; diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneKeyArea.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneKeyArea.cs index c8f901285a..7e80419944 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneKeyArea.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneKeyArea.cs @@ -1,12 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Rulesets.Mania.Skinning; using osu.Game.Rulesets.Mania.UI.Components; using osu.Game.Skinning; using osuTK; @@ -15,12 +12,6 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning { public class TestSceneKeyArea : ManiaSkinnableTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(DefaultKeyArea), - typeof(LegacyKeyArea) - }; - [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs index a8fc68188a..87c84cf89c 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs @@ -1,12 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Game.Rulesets.Mania.Skinning; using osu.Game.Rulesets.Mania.UI.Components; using osu.Game.Skinning; @@ -14,12 +10,6 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning { public class TestSceneStageBackground : ManiaSkinnableTestScene { - public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] - { - typeof(DefaultStageBackground), - typeof(LegacyStageBackground), - }).ToList(); - [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs index d436445b59..4e99068ed5 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs @@ -1,23 +1,14 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Game.Rulesets.Mania.Skinning; using osu.Game.Skinning; namespace osu.Game.Rulesets.Mania.Tests.Skinning { public class TestSceneStageForeground : ManiaSkinnableTestScene { - public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] - { - typeof(LegacyStageForeground), - }).ToList(); - [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneColumn.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneColumn.cs index 5e06002f41..d9b1ad22fa 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneColumn.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneColumn.cs @@ -12,7 +12,6 @@ using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.UI; -using osu.Game.Rulesets.Mania.UI.Components; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Tests.Visual; @@ -24,15 +23,6 @@ namespace osu.Game.Rulesets.Mania.Tests [TestFixture] public class TestSceneColumn : ManiaInputTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(Column), - typeof(ColumnBackground), - typeof(ColumnHitObjectArea), - typeof(DefaultKeyArea), - typeof(DefaultHitTarget) - }; - [Cached(typeof(IReadOnlyList))] private IReadOnlyList mods { get; set; } = Array.Empty(); diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs index 48159c817d..6274bb1005 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -29,11 +27,6 @@ namespace osu.Game.Rulesets.Mania.Tests { public class TestSceneManiaHitObjectComposer : EditorClockTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(ManiaBlueprintContainer) - }; - private TestComposer composer; [SetUp] diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs index 8dae5e6d84..ea6a1e2e6a 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -30,12 +28,6 @@ namespace osu.Game.Rulesets.Mania.Tests [TestFixture] public class TestSceneNotes : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(DrawableNote), - typeof(DrawableHoldNote) - }; - [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game.Rulesets.Osu.Tests/OsuSkinnableTestScene.cs b/osu.Game.Rulesets.Osu.Tests/OsuSkinnableTestScene.cs index 90ebbd9f04..a0a38fc47b 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuSkinnableTestScene.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuSkinnableTestScene.cs @@ -1,21 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; -using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Osu.Tests { public abstract class OsuSkinnableTestScene : SkinnableTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(OsuRuleset), - typeof(OsuLegacySkinTransformer), - }; - protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset(); } } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs index f867630df6..c81edf4e07 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Linq; using osu.Framework.Extensions; using osu.Framework.Graphics; @@ -15,12 +14,6 @@ namespace osu.Game.Rulesets.Osu.Tests { public class TestSceneDrawableJudgement : OsuSkinnableTestScene { - public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] - { - typeof(DrawableJudgement), - typeof(DrawableOsuJudgement) - }).ToList(); - public TestSceneDrawableJudgement() { foreach (HitResult result in Enum.GetValues(typeof(HitResult)).OfType().Skip(1)) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs index 22dacc6f5e..38c2bb9b95 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs @@ -2,16 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; -using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Testing.Input; using osu.Game.Configuration; -using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Rulesets.Osu.UI.Cursor; -using osu.Game.Rulesets.UI; using osu.Game.Screens.Play; using osuTK; @@ -20,16 +16,6 @@ namespace osu.Game.Rulesets.Osu.Tests [TestFixture] public class TestSceneGameplayCursor : OsuSkinnableTestScene { - public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] - { - typeof(GameplayCursorContainer), - typeof(OsuCursorContainer), - typeof(OsuCursor), - typeof(LegacyCursor), - typeof(LegacyCursorTrail), - typeof(CursorTrail) - }).ToList(); - [Cached] private GameplayBeatmap gameplayBeatmap; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs index e117729f01..37df0d6e37 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs @@ -8,8 +8,6 @@ using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osuTK; -using System.Collections.Generic; -using System; using osu.Game.Rulesets.Mods; using System.Linq; using NUnit.Framework; @@ -20,11 +18,6 @@ namespace osu.Game.Rulesets.Osu.Tests [TestFixture] public class TestSceneHitCircle : OsuSkinnableTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(DrawableHitCircle) - }; - private int depthIndex; public TestSceneHitCircle() diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleHidden.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleHidden.cs index 21ebce8c23..45125204b6 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleHidden.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleHidden.cs @@ -1,9 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; -using System.Linq; using NUnit.Framework; using osu.Game.Rulesets.Osu.Mods; @@ -12,8 +9,6 @@ namespace osu.Game.Rulesets.Osu.Tests [TestFixture] public class TestSceneHitCircleHidden : TestSceneHitCircle { - public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] { typeof(OsuModHidden) }).ToList(); - [SetUp] public void SetUp() => Schedule(() => { diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs index 0ae49790cd..c182aa5d63 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -16,7 +15,6 @@ using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Edit; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit; -using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Tests.Visual; using osuTK; using osuTK.Graphics; @@ -28,11 +26,6 @@ namespace osu.Game.Rulesets.Osu.Tests private const double beat_length = 100; private static readonly Vector2 grid_position = new Vector2(512, 384); - public override IReadOnlyList RequiredTypes => new[] - { - typeof(CircularDistanceSnapGrid) - }; - [Cached(typeof(EditorBeatmap))] private readonly EditorBeatmap editorBeatmap; diff --git a/osu.Game.Rulesets.Osu.Tests/TestScenePathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu.Tests/TestScenePathControlPointVisualiser.cs index cbe14ff4d2..21fa283b6d 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestScenePathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestScenePathControlPointVisualiser.cs @@ -1,10 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using System.Linq; -using Humanizer; using NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Beatmaps; @@ -19,13 +16,6 @@ namespace osu.Game.Rulesets.Osu.Tests { public class TestScenePathControlPointVisualiser : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(StringHumanizeExtensions), - typeof(PathControlPointPiece), - typeof(PathControlPointConnectionPiece) - }; - private Slider slider; private PathControlPointVisualiser visualiser; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneResumeOverlay.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneResumeOverlay.cs index f4809b0c9b..a7967c407a 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneResumeOverlay.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneResumeOverlay.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; @@ -14,11 +12,6 @@ namespace osu.Game.Rulesets.Osu.Tests { public class TestSceneResumeOverlay : OsuManualInputManagerTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(OsuResumeOverlay), - }; - public TestSceneResumeOverlay() { ManualOsuInputManager osuInputManager; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs index eb6130c8a6..a9404f665a 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -21,29 +20,12 @@ using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; -using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; namespace osu.Game.Rulesets.Osu.Tests { [TestFixture] public class TestSceneSlider : OsuSkinnableTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(Slider), - typeof(SliderTick), - typeof(SliderTailCircle), - typeof(SliderBall), - typeof(SliderBody), - typeof(SnakingSliderBody), - typeof(DrawableSlider), - typeof(DrawableSliderTick), - typeof(DrawableSliderTail), - typeof(DrawableSliderHead), - typeof(DrawableSliderRepeat), - typeof(DrawableOsuHitObject) - }; - private Container content; protected override Container Content diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderHidden.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderHidden.cs index d0ee1bddb5..b2bd727c6a 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderHidden.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderHidden.cs @@ -1,9 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; -using System.Linq; using NUnit.Framework; using osu.Game.Rulesets.Osu.Mods; @@ -12,8 +9,6 @@ namespace osu.Game.Rulesets.Osu.Tests [TestFixture] public class TestSceneSliderHidden : TestSceneSlider { - public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] { typeof(OsuModHidden) }).ToList(); - [SetUp] public void SetUp() => Schedule(() => { diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs index b0c2e56c3e..b543b6fa94 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; @@ -13,8 +12,6 @@ using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.Objects.Drawables; -using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Scoring; @@ -27,17 +24,6 @@ namespace osu.Game.Rulesets.Osu.Tests { public class TestSceneSliderInput : RateAdjustedBeatmapTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(SliderBall), - typeof(DrawableSlider), - typeof(DrawableSliderTick), - typeof(DrawableSliderRepeat), - typeof(DrawableOsuHitObject), - typeof(DrawableSliderHead), - typeof(DrawableSliderTail), - }; - private const double time_before_slider = 250; private const double time_slider_start = 1500; private const double time_during_slide_1 = 2500; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSelectionBlueprint.cs index 5dd2bd18a8..d5be538d94 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSelectionBlueprint.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Utils; using osu.Game.Beatmaps; @@ -22,16 +20,6 @@ namespace osu.Game.Rulesets.Osu.Tests { public class TestSceneSliderSelectionBlueprint : SelectionBlueprintTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(SliderSelectionBlueprint), - typeof(SliderCircleSelectionBlueprint), - typeof(SliderBodyPiece), - typeof(SliderCircle), - typeof(PathControlPointVisualiser), - typeof(PathControlPointPiece) - }; - private Slider slider; private DrawableSlider drawableObject; private TestSliderBlueprint blueprint; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs index f53b64c729..65bed071cd 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; @@ -12,7 +10,6 @@ using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; -using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Osu.Tests @@ -20,13 +17,6 @@ namespace osu.Game.Rulesets.Osu.Tests [TestFixture] public class TestSceneSpinner : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(SpinnerDisc), - typeof(DrawableSpinner), - typeof(DrawableOsuHitObject) - }; - private readonly Container content; protected override Container Content => content; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerHidden.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerHidden.cs index dd863deed2..91b6a05fe3 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerHidden.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerHidden.cs @@ -1,9 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; -using System.Linq; using NUnit.Framework; using osu.Game.Rulesets.Osu.Mods; @@ -12,8 +9,6 @@ namespace osu.Game.Rulesets.Osu.Tests [TestFixture] public class TestSceneSpinnerHidden : TestSceneSpinner { - public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] { typeof(OsuModHidden) }).ToList(); - [SetUp] public void SetUp() => Schedule(() => { diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerSelectionBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerSelectionBlueprint.cs index d777ca3610..011463ab14 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerSelectionBlueprint.cs @@ -1,14 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners; -using osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners.Components; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Tests.Visual; @@ -18,12 +15,6 @@ namespace osu.Game.Rulesets.Osu.Tests { public class TestSceneSpinnerSelectionBlueprint : SelectionBlueprintTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(SpinnerSelectionBlueprint), - typeof(SpinnerPiece) - }; - public TestSceneSpinnerSelectionBlueprint() { var spinner = new Spinner diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerSpunOut.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerSpunOut.cs index e406f9ddff..d1210db6b1 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerSpunOut.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerSpunOut.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; @@ -12,7 +10,6 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; -using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Osu.Tests @@ -20,14 +17,6 @@ namespace osu.Game.Rulesets.Osu.Tests [TestFixture] public class TestSceneSpinnerSpunOut : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(SpinnerDisc), - typeof(DrawableSpinner), - typeof(DrawableOsuHitObject), - typeof(OsuModSpunOut) - }; - [SetUp] public void SetUp() => Schedule(() => { diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TaikoSkinnableTestScene.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TaikoSkinnableTestScene.cs index 161154b1a7..69250a14e1 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TaikoSkinnableTestScene.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TaikoSkinnableTestScene.cs @@ -1,21 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; -using osu.Game.Rulesets.Taiko.Skinning; using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Taiko.Tests.Skinning { public abstract class TaikoSkinnableTestScene : SkinnableTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(TaikoRuleset), - typeof(TaikoLegacySkinTransformer), - }; - protected override Ruleset CreateRulesetForSkinProvider() => new TaikoRuleset(); } } diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs index 70493aa69a..f6aec20d53 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs @@ -1,9 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; -using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -12,7 +9,6 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects.Drawables; -using osu.Game.Rulesets.Taiko.Skinning; using osu.Game.Rulesets.Taiko.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Tests.Visual; @@ -22,13 +18,6 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning [TestFixture] public class TestSceneDrawableBarLine : TaikoSkinnableTestScene { - public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] - { - typeof(DrawableBarLine), - typeof(LegacyBarLine), - typeof(BarLine), - }).ToList(); - [Cached(typeof(IScrollingInfo))] private ScrollingTestContainer.TestScrollingInfo info = new ScrollingTestContainer.TestScrollingInfo { diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRoll.cs index 554894bf68..44646e5fc9 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRoll.cs @@ -1,9 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; -using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -11,7 +8,6 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects.Drawables; -using osu.Game.Rulesets.Taiko.Skinning; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Tests.Visual; @@ -20,13 +16,6 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning [TestFixture] public class TestSceneDrawableDrumRoll : TaikoSkinnableTestScene { - public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] - { - typeof(DrawableDrumRoll), - typeof(DrawableDrumRollTick), - typeof(LegacyDrumRoll), - }).ToList(); - [Cached(typeof(IScrollingInfo))] private ScrollingTestContainer.TestScrollingInfo info = new ScrollingTestContainer.TestScrollingInfo { diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs index 6a3c98a514..9930d97d31 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs @@ -1,9 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; -using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -11,20 +8,12 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects.Drawables; -using osu.Game.Rulesets.Taiko.Skinning; namespace osu.Game.Rulesets.Taiko.Tests.Skinning { [TestFixture] public class TestSceneDrawableHit : TaikoSkinnableTestScene { - public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] - { - typeof(DrawableHit), - typeof(LegacyHit), - typeof(LegacyCirclePiece), - }).ToList(); - [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs index bd3b360577..d200c44a02 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using System.Linq; using Humanizer; @@ -27,12 +26,6 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning [TestFixture] public class TestSceneDrawableTaikoMascot : TaikoSkinnableTestScene { - public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] - { - typeof(DrawableTaikoMascot), - typeof(TaikoMascotAnimation) - }).ToList(); - [Cached(typeof(IScrollingInfo))] private ScrollingTestContainer.TestScrollingInfo info = new ScrollingTestContainer.TestScrollingInfo { diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs index 791c438c94..2b5efec7f9 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs @@ -1,9 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; -using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -11,7 +8,6 @@ using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects.Drawables; -using osu.Game.Rulesets.Taiko.Skinning; using osu.Game.Rulesets.Taiko.UI; namespace osu.Game.Rulesets.Taiko.Tests.Skinning @@ -19,13 +15,6 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning [TestFixture] public class TestSceneHitExplosion : TaikoSkinnableTestScene { - public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] - { - typeof(HitExplosion), - typeof(LegacyHitExplosion), - typeof(DefaultHitExplosion), - }).ToList(); - [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneInputDrum.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneInputDrum.cs index 412027ca61..fa6c9da174 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneInputDrum.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneInputDrum.cs @@ -1,15 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; -using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Rulesets.Taiko.Skinning; using osu.Game.Rulesets.Taiko.UI; using osuTK; @@ -18,12 +14,6 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning [TestFixture] public class TestSceneInputDrum : TaikoSkinnableTestScene { - public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] - { - typeof(InputDrum), - typeof(LegacyInputDrum), - }).ToList(); - [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs index e02ad53ed8..7b7e2c43d1 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs @@ -2,15 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Taiko.Beatmaps; -using osu.Game.Rulesets.Taiko.Skinning; using osu.Game.Rulesets.Taiko.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Tests.Visual; @@ -19,14 +16,6 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning { public class TestSceneTaikoPlayfield : TaikoSkinnableTestScene { - public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] - { - typeof(TaikoHitTarget), - typeof(TaikoLegacyHitTarget), - typeof(PlayfieldBackgroundRight), - typeof(LegacyTaikoScroller), - }).ToList(); - [Cached(typeof(IScrollingInfo))] private ScrollingTestContainer.TestScrollingInfo info = new ScrollingTestContainer.TestScrollingInfo { diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs index f97aa48f11..76d0c7a50f 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using System.Linq; using System.Threading; using NUnit.Framework; @@ -39,15 +37,6 @@ namespace osu.Game.Tests.Visual.Background [TestFixture] public class TestSceneUserDimBackgrounds : OsuManualInputManagerTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(ScreenWithBeatmapBackground), - typeof(PlayerLoader), - typeof(Player), - typeof(UserDimContainer), - typeof(OsuScreen) - }; - private DummySongSelect songSelect; private TestPlayerLoader playerLoader; private LoadBlockingTestPlayer player; diff --git a/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs b/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs index f6e69fd8bf..6cf5e6a987 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; @@ -18,7 +17,6 @@ namespace osu.Game.Tests.Visual.Editing { public class TestSceneBeatDivisorControl : OsuManualInputManagerTestScene { - public override IReadOnlyList RequiredTypes => new[] { typeof(BindableBeatDivisor) }; private BeatDivisorControl beatDivisorControl; private BindableBeatDivisor bindableBeatDivisor; diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorComposeRadioButtons.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorComposeRadioButtons.cs index 2deeaef1f6..e4d7e025a8 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorComposeRadioButtons.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorComposeRadioButtons.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Screens.Edit.Components.RadioButtons; @@ -12,8 +10,6 @@ namespace osu.Game.Tests.Visual.Editing [TestFixture] public class TestSceneEditorComposeRadioButtons : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] { typeof(DrawableRadioButton) }; - public TestSceneEditorComposeRadioButtons() { RadioButtonCollection collection; diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorMenuBar.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorMenuBar.cs index 2cbdacb61c..3cb44d9ae8 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorMenuBar.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorMenuBar.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -15,8 +13,6 @@ namespace osu.Game.Tests.Visual.Editing [TestFixture] public class TestSceneEditorMenuBar : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] { typeof(EditorMenuBar), typeof(ScreenSelectionTabControl) }; - public TestSceneEditorMenuBar() { Add(new Container diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs index c92423545d..3adc1bd425 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -15,8 +13,6 @@ namespace osu.Game.Tests.Visual.Editing [TestFixture] public class TestSceneEditorSummaryTimeline : EditorClockTestScene { - public override IReadOnlyList RequiredTypes => new[] { typeof(SummaryTimeline) }; - [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs index ddaca26220..7ca24346aa 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs @@ -1,9 +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 JetBrains.Annotations; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Timing; @@ -13,11 +11,8 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Edit; -using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles; -using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit; -using osu.Game.Screens.Edit.Compose.Components; using osuTK; namespace osu.Game.Tests.Visual.Editing @@ -25,19 +20,6 @@ namespace osu.Game.Tests.Visual.Editing [TestFixture] public class TestSceneHitObjectComposer : EditorClockTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(SelectionHandler), - typeof(DragBox), - typeof(HitObjectComposer), - typeof(OsuHitObjectComposer), - typeof(BlueprintContainer), - typeof(NotNullAttribute), - typeof(HitCirclePiece), - typeof(HitCircleSelectionBlueprint), - typeof(HitCirclePlacementBlueprint), - }; - [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineBlueprintContainer.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineBlueprintContainer.cs index 5ab2f49b4a..e931be044c 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineBlueprintContainer.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineBlueprintContainer.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Screens.Edit.Compose.Components.Timeline; @@ -12,11 +10,6 @@ namespace osu.Game.Tests.Visual.Editing [TestFixture] public class TestSceneTimelineBlueprintContainer : TimelineTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(TimelineHitObjectBlueprint), - }; - public override Drawable CreateTestComponent() => new TimelineBlueprintContainer(); protected override void LoadComplete() diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs index a6dbe9571e..2a7f9389d1 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Allocation; using osu.Game.Rulesets.Osu.Beatmaps; @@ -14,18 +12,6 @@ namespace osu.Game.Tests.Visual.Editing [TestFixture] public class TestSceneTimingScreen : EditorClockTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(ControlPointTable), - typeof(ControlPointSettings), - typeof(Section<>), - typeof(TimingSection), - typeof(EffectSection), - typeof(SampleSection), - typeof(DifficultySection), - typeof(RowAttribute) - }; - [Cached(typeof(EditorBeatmap))] private readonly EditorBeatmap editorBeatmap; diff --git a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs index 56b2860e96..01ef7e6170 100644 --- a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs +++ b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; @@ -22,14 +20,6 @@ namespace osu.Game.Tests.Visual.Editing { public abstract class TimelineTestScene : EditorClockTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(TimelineArea), - typeof(Timeline), - typeof(TimelineButton), - typeof(CentreMarker) - }; - protected TimelineArea TimelineArea { get; private set; } [BackgroundDependencyLoader] diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs index a6f996c30d..be17721b88 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; @@ -15,11 +14,6 @@ namespace osu.Game.Tests.Visual.Gameplay [TestFixture] public class TestSceneBreakTracker : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(BreakOverlay), - }; - private readonly BreakOverlay breakOverlay; private readonly TestBreakTracker breakTracker; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs index de257c9e53..85aaf20a19 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -18,13 +17,6 @@ namespace osu.Game.Tests.Visual.Gameplay return new FailPlayer(); } - public override IReadOnlyList RequiredTypes => new[] - { - typeof(TestSceneAllRulesetPlayers), - typeof(TestPlayer), - typeof(Player), - }; - protected override void AddCheckSteps() { AddUntilStep("wait for fail", () => Player.HasFailed); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayMenuOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayMenuOverlay.cs index ea3e0c2293..e8b8c7c8e9 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayMenuOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayMenuOverlay.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -20,8 +19,6 @@ namespace osu.Game.Tests.Visual.Gameplay [Description("player pause/fail screens")] public class TestSceneGameplayMenuOverlay : OsuManualInputManagerTestScene { - public override IReadOnlyList RequiredTypes => new[] { typeof(FailOverlay), typeof(PauseOverlay) }; - private FailOverlay failOverlay; private PauseOverlay pauseOverlay; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs index 1527cba6fc..253b8d9c55 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs @@ -2,8 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; -using System; -using System.Collections.Generic; using osu.Game.Rulesets.Judgements; using osu.Framework.Utils; using osu.Framework.Graphics; @@ -22,13 +20,6 @@ namespace osu.Game.Tests.Visual.Gameplay { public class TestSceneHitErrorMeter : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(HitErrorMeter), - typeof(BarHitErrorMeter), - typeof(ColourHitErrorMeter) - }; - private BarHitErrorMeter barMeter; private BarHitErrorMeter barMeter2; private ColourHitErrorMeter colourMeter; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs index 593dcd245c..d7a3f80256 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; @@ -15,13 +13,6 @@ namespace osu.Game.Tests.Visual.Gameplay [TestFixture] public class TestSceneKeyCounter : OsuManualInputManagerTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(KeyCounterKeyboard), - typeof(KeyCounterMouse), - typeof(KeyCounterDisplay) - }; - public TestSceneKeyCounter() { KeyCounterKeyboard testCounter; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneMedalOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneMedalOverlay.cs index 41722b430e..0ada3cf05f 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneMedalOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneMedalOverlay.cs @@ -1,11 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using NUnit.Framework; using osu.Game.Overlays; -using osu.Game.Overlays.MedalSplash; using osu.Game.Users; namespace osu.Game.Tests.Visual.Gameplay @@ -13,12 +10,6 @@ namespace osu.Game.Tests.Visual.Gameplay [TestFixture] public class TestSceneMedalOverlay : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(MedalOverlay), - typeof(DrawableMedal), - }; - public TestSceneMedalOverlay() { AddStep(@"display", () => diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneNightcoreBeatContainer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneNightcoreBeatContainer.cs index 3473b03eaf..951ee1489d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneNightcoreBeatContainer.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneNightcoreBeatContainer.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using System.Linq; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Game.Beatmaps.Timing; @@ -15,11 +13,6 @@ namespace osu.Game.Tests.Visual.Gameplay { public class TestSceneNightcoreBeatContainer : TestSceneBeatSyncedContainer { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(ModNightcore<>) - }; - protected override void LoadComplete() { base.LoadComplete(); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs index c9561a70fa..a35437a286 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs @@ -7,8 +7,6 @@ using osu.Game.Online; using osu.Game.Online.API.Requests.Responses; using osu.Game.Scoring; using osu.Game.Users; -using System; -using System.Collections.Generic; using osu.Framework.Allocation; using osu.Game.Rulesets; using osu.Game.Screens.Ranking; @@ -21,11 +19,6 @@ namespace osu.Game.Tests.Visual.Gameplay [Resolved] private RulesetStore rulesets { get; set; } - public override IReadOnlyList RequiredTypes => new[] - { - typeof(ReplayDownloadButton) - }; - private TestReplayDownloadButton downloadButton; public TestSceneReplayDownloadButton() diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs index d03716db2e..0d15e495e3 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs @@ -17,7 +17,6 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Timing; -using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osuTK; using osuTK.Graphics; @@ -27,8 +26,6 @@ namespace osu.Game.Tests.Visual.Gameplay [TestFixture] public class TestSceneScrollingHitObjects : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] { typeof(Playfield) }; - [Cached(typeof(IReadOnlyList))] private IReadOnlyList mods { get; set; } = Array.Empty(); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs index b9b13d7bd8..733e8f4290 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Allocation; @@ -20,11 +19,6 @@ namespace osu.Game.Tests.Visual.Gameplay [TestFixture] public class TestSceneSongProgress : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(SongProgressBar), - }; - private SongProgress progress; private TestSongProgressGraph graph; private readonly Container progressContainer; diff --git a/osu.Game.Tests/Visual/Menus/IntroTestScene.cs b/osu.Game.Tests/Visual/Menus/IntroTestScene.cs index 33811f9529..2d2f1a1618 100644 --- a/osu.Game.Tests/Visual/Menus/IntroTestScene.cs +++ b/osu.Game.Tests/Visual/Menus/IntroTestScene.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -18,13 +16,6 @@ namespace osu.Game.Tests.Visual.Menus [TestFixture] public abstract class IntroTestScene : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(StartupScreen), - typeof(IntroScreen), - typeof(IntroTestScene), - }; - [Cached] private OsuLogo logo; diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs index 8fbbc8ebd8..b4985cad9f 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -17,14 +15,6 @@ namespace osu.Game.Tests.Visual.Menus [TestFixture] public class TestSceneToolbar : OsuManualInputManagerTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(ToolbarButton), - typeof(ToolbarRulesetSelector), - typeof(ToolbarRulesetTabButton), - typeof(ToolbarNotificationButton), - }; - private Toolbar toolbar; [Resolved] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index 713ba13439..5ef4dd6773 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; @@ -22,12 +21,6 @@ namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneDrawableRoomPlaylist : OsuManualInputManagerTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(DrawableRoomPlaylist), - typeof(DrawableRoomPlaylistItem) - }; - private TestPlaylist playlist; [Test] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs index 1e1bc9725c..8b74eb5f27 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Online.Multiplayer; @@ -14,11 +13,6 @@ namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneLoungeRoomInfo : MultiplayerTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(RoomInfo) - }; - [SetUp] public void Setup() => Schedule(() => { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs index b5d946d049..77b41c89b0 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -23,12 +22,6 @@ namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneLoungeRoomsContainer : MultiplayerTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(RoomsContainer), - typeof(DrawableRoom) - }; - [Cached(Type = typeof(IRoomManager))] private TestRoomManager roomManager = new TestRoomManager(); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs index cf40995fc0..38eb3181bf 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using osu.Game.Beatmaps; using osu.Game.Online.Multiplayer; using osu.Game.Rulesets.Osu; @@ -14,11 +12,6 @@ namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneMatchHeader : MultiplayerTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(Header) - }; - public TestSceneMatchHeader() { Room.Playlist.Add(new PlaylistItem diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboardChatDisplay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboardChatDisplay.cs index e46386b263..72bbc11cd0 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboardChatDisplay.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboardChatDisplay.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Screens.Multi.Match.Components; @@ -12,11 +10,6 @@ namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneMatchLeaderboardChatDisplay : MultiplayerTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(LeaderboardChatDisplay) - }; - protected override bool UseOnlineAPI => true; public TestSceneMatchLeaderboardChatDisplay() diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs index 047e9d860d..d2e8c22c39 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -18,11 +17,6 @@ namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneMatchSettingsOverlay : MultiplayerTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(MatchSettingsOverlay) - }; - [Cached(Type = typeof(IRoomManager))] private TestRoomManager roomManager = new TestRoomManager(); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs index 2c6f34d8a6..5cff2d7d05 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs @@ -23,12 +23,6 @@ namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneMatchSongSelect : MultiplayerTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(MatchSongSelect), - typeof(MatchBeatmapDetailArea), - }; - [Resolved] private BeatmapManager beatmapManager { get; set; } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs index 7f79e306ad..d678d5a814 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -20,7 +19,6 @@ using osu.Game.Screens.Multi.Match.Components; using osu.Game.Tests.Beatmaps; using osu.Game.Users; using osuTK.Input; -using Header = osu.Game.Screens.Multi.Match.Components.Header; namespace osu.Game.Tests.Visual.Multiplayer { @@ -28,14 +26,6 @@ namespace osu.Game.Tests.Visual.Multiplayer { protected override bool UseOnlineAPI => true; - public override IReadOnlyList RequiredTypes => new[] - { - typeof(Screens.Multi.Multiplayer), - typeof(MatchSubScreen), - typeof(Header), - typeof(Footer) - }; - [Cached(typeof(IRoomManager))] private readonly TestRoomManager roomManager = new TestRoomManager(); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiScreen.cs index dfe61a4dda..61859c9da3 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiScreen.cs @@ -1,11 +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 NUnit.Framework; -using osu.Game.Screens.Multi.Lounge; -using osu.Game.Screens.Multi.Lounge.Components; namespace osu.Game.Tests.Visual.Multiplayer { @@ -14,13 +10,6 @@ namespace osu.Game.Tests.Visual.Multiplayer { protected override bool UseOnlineAPI => true; - public override IReadOnlyList RequiredTypes => new[] - { - typeof(Screens.Multi.Multiplayer), - typeof(LoungeSubScreen), - typeof(FilterControl) - }; - public TestSceneMultiScreen() { Screens.Multi.Multiplayer multi = new Screens.Multi.Multiplayer(); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedParticipants.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedParticipants.cs index 1fc258a225..7ea3bba23f 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedParticipants.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedParticipants.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Screens.Multi.Components; @@ -12,13 +10,6 @@ namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneOverlinedParticipants : MultiplayerTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(OverlinedParticipants), - typeof(OverlinedDisplay), - typeof(ParticipantsList) - }; - protected override bool UseOnlineAPI => true; public TestSceneOverlinedParticipants() diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomStatus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomStatus.cs index 74d1645f6d..1925e0ef4f 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomStatus.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomStatus.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Online.Multiplayer; @@ -13,13 +11,6 @@ namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneRoomStatus : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(RoomStatusEnded), - typeof(RoomStatusOpen), - typeof(RoomStatusPlaying) - }; - public TestSceneRoomStatus() { Child = new FillFlowContainer diff --git a/osu.Game.Tests/Visual/Online/TestSceneAccountCreationOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneAccountCreationOverlay.cs index a53a818065..6c8ec917ba 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneAccountCreationOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneAccountCreationOverlay.cs @@ -1,30 +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; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Overlays; -using osu.Game.Overlays.AccountCreation; using osu.Game.Users; namespace osu.Game.Tests.Visual.Online { public class TestSceneAccountCreationOverlay : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(ErrorTextFlowContainer), - typeof(AccountCreationBackground), - typeof(ScreenEntry), - typeof(ScreenWarning), - typeof(ScreenWelcome), - typeof(AccountCreationScreen), - }; - private readonly Container userPanelArea; private Bindable localUser; diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs index 64d1a9ddcd..6cb1687d1f 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs @@ -1,22 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using osu.Game.Overlays; using NUnit.Framework; -using osu.Game.Overlays.BeatmapListing; namespace osu.Game.Tests.Visual.Online { public class TestSceneBeatmapListingOverlay : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(BeatmapListingOverlay), - typeof(BeatmapListingFilterControl) - }; - protected override bool UseOnlineAPI => true; private readonly BeatmapListingOverlay overlay; diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapRulesetSelector.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapRulesetSelector.cs index 8b077c8de3..eb34187cd6 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapRulesetSelector.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapRulesetSelector.cs @@ -8,7 +8,6 @@ using osu.Game.Beatmaps; using osu.Game.Overlays; using osu.Game.Overlays.BeatmapSet; using osu.Game.Rulesets; -using System; using System.Collections.Generic; using System.Linq; @@ -16,12 +15,6 @@ namespace osu.Game.Tests.Visual.Online { public class TestSceneBeatmapRulesetSelector : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(BeatmapRulesetSelector), - typeof(BeatmapRulesetTabItem), - }; - [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs index 5ca2c9868f..c5d1fd6887 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs @@ -6,8 +6,6 @@ using osu.Framework.Allocation; using osu.Game.Beatmaps; using osu.Game.Overlays; using osu.Game.Overlays.BeatmapSet; -using osu.Game.Overlays.BeatmapSet.Buttons; -using osu.Game.Overlays.BeatmapSet.Scores; using osu.Game.Rulesets; using osu.Game.Users; using System; @@ -21,30 +19,6 @@ namespace osu.Game.Tests.Visual.Online { private readonly TestBeatmapSetOverlay overlay; - public override IReadOnlyList RequiredTypes => new[] - { - typeof(Header), - typeof(ScoreTable), - typeof(ScoreTableRowBackground), - typeof(DrawableTopScore), - typeof(ScoresContainer), - typeof(AuthorInfo), - typeof(BasicStats), - typeof(BeatmapPicker), - typeof(Details), - typeof(HeaderDownloadButton), - typeof(FavouriteButton), - typeof(Header), - typeof(HeaderButton), - typeof(Info), - typeof(PreviewButton), - typeof(SuccessRate), - typeof(BeatmapAvailability), - typeof(BeatmapRulesetSelector), - typeof(BeatmapRulesetTabItem), - typeof(NotSupporterPlaceholder) - }; - protected override bool UseOnlineAPI => true; public TestSceneBeatmapSetOverlay() diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlayDetails.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlayDetails.cs index dea1e710b5..f7099b0615 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlayDetails.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlayDetails.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; @@ -17,11 +16,6 @@ namespace osu.Game.Tests.Visual.Online { public class TestSceneBeatmapSetOverlayDetails : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(Details) - }; - private RatingsExposingDetails details; [Cached] diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlaySuccessRate.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlaySuccessRate.cs index 03003daf81..4cb22bf1fe 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlaySuccessRate.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlaySuccessRate.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -21,11 +19,6 @@ namespace osu.Game.Tests.Visual.Online { public class TestSceneBeatmapSetOverlaySuccessRate : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(Details) - }; - private GraphExposingSuccessRate successRate; [Cached] diff --git a/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs index 22d20f7098..02f6de2269 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using NUnit.Framework; using osu.Game.Online.API.Requests.Responses; @@ -15,17 +14,6 @@ namespace osu.Game.Tests.Visual.Online { private TestChangelogOverlay changelog; - public override IReadOnlyList RequiredTypes => new[] - { - typeof(ChangelogUpdateStreamControl), - typeof(ChangelogUpdateStreamItem), - typeof(ChangelogHeader), - typeof(ChangelogContent), - typeof(ChangelogListing), - typeof(ChangelogSingleBuild), - typeof(ChangelogBuild), - }; - protected override bool UseOnlineAPI => true; [SetUp] diff --git a/osu.Game.Tests/Visual/Online/TestSceneChannelTabControl.cs b/osu.Game.Tests/Visual/Online/TestSceneChannelTabControl.cs index 1fb3f4ba45..73e1fc9b35 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChannelTabControl.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChannelTabControl.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Extensions.Color4Extensions; @@ -21,11 +20,6 @@ namespace osu.Game.Tests.Visual.Online { public class TestSceneChannelTabControl : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(ChannelTabControl), - }; - private readonly TestTabControl channelTabControl; public TestSceneChannelTabControl() diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatLineTruncation.cs b/osu.Game.Tests/Visual/Online/TestSceneChatLineTruncation.cs index 4773e84a5e..8408b7dd60 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatLineTruncation.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatLineTruncation.cs @@ -2,12 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Graphics.Containers; using osu.Game.Online.Chat; using osu.Game.Overlays.Chat; using osu.Game.Users; @@ -19,14 +17,6 @@ namespace osu.Game.Tests.Visual.Online { private readonly TestChatLineContainer textContainer; - public override IReadOnlyList RequiredTypes => new[] - { - typeof(ChatLine), - typeof(Message), - typeof(LinkFlowContainer), - typeof(MessageFormatter) - }; - public TestSceneChatLineTruncation() { Add(textContainer = new TestChatLineContainer diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs index 7a257a1603..9e69530a77 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -11,7 +10,6 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; -using osu.Game.Graphics.Containers; using osu.Game.Online.Chat; using osu.Game.Overlays; using osu.Game.Overlays.Chat; @@ -27,16 +25,6 @@ namespace osu.Game.Tests.Visual.Online private readonly DialogOverlay dialogOverlay; private Color4 linkColour; - public override IReadOnlyList RequiredTypes => new[] - { - typeof(ChatLine), - typeof(Message), - typeof(LinkFlowContainer), - typeof(DummyEchoMessage), - typeof(LocalEchoMessage), - typeof(MessageFormatter) - }; - public TestSceneChatLink() { Add(dialogOverlay = new DialogOverlay { Depth = float.MinValue }); diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index 14924dda21..05b33e4386 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; @@ -14,7 +13,6 @@ using osu.Framework.Testing; using osu.Game.Graphics.UserInterface; 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; @@ -24,17 +22,6 @@ namespace osu.Game.Tests.Visual.Online { public class TestSceneChatOverlay : OsuManualInputManagerTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(ChatLine), - typeof(DrawableChannel), - typeof(ChannelSelectorTabItem), - typeof(ChannelTabControl), - typeof(ChannelTabItem), - typeof(PrivateChannelTabItem), - typeof(TabCloseButton) - }; - private TestChatOverlay chatOverlay; private ChannelManager channelManager; diff --git a/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs index ece280659c..42e6b9087c 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using NUnit.Framework; using osu.Game.Online.API.Requests; using osu.Framework.Graphics.Containers; @@ -18,19 +16,6 @@ namespace osu.Game.Tests.Visual.Online [TestFixture] public class TestSceneCommentsContainer : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(CommentsContainer), - typeof(CommentsHeader), - typeof(DrawableComment), - typeof(HeaderButton), - typeof(OverlaySortTabControl<>), - typeof(ShowChildrenButton), - typeof(DeletedCommentsCounter), - typeof(VotePill), - typeof(CommentsPage), - }; - protected override bool UseOnlineAPI => true; [Cached] diff --git a/osu.Game.Tests/Visual/Online/TestSceneCommentsHeader.cs b/osu.Game.Tests/Visual/Online/TestSceneCommentsHeader.cs index c688d600a3..03eac5d85b 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneCommentsHeader.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneCommentsHeader.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -14,13 +12,6 @@ namespace osu.Game.Tests.Visual.Online [TestFixture] public class TestSceneCommentsHeader : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(CommentsHeader), - typeof(HeaderButton), - typeof(OverlaySortTabControl<>), - }; - [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); diff --git a/osu.Game.Tests/Visual/Online/TestSceneCommentsPage.cs b/osu.Game.Tests/Visual/Online/TestSceneCommentsPage.cs index a28a0107a1..7fdf0708e0 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneCommentsPage.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneCommentsPage.cs @@ -20,12 +20,6 @@ namespace osu.Game.Tests.Visual.Online { public class TestSceneCommentsPage : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(DrawableComment), - typeof(CommentsPage), - }; - [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); diff --git a/osu.Game.Tests/Visual/Online/TestSceneDashboardOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneDashboardOverlay.cs index df95f24686..960d3fa248 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneDashboardOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneDashboardOverlay.cs @@ -1,24 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using NUnit.Framework; using osu.Game.Overlays; -using osu.Game.Overlays.Dashboard; -using osu.Game.Overlays.Dashboard.Friends; namespace osu.Game.Tests.Visual.Online { public class TestSceneDashboardOverlay : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(DashboardOverlay), - typeof(DashboardOverlayHeader), - typeof(FriendDisplay) - }; - protected override bool UseOnlineAPI => true; private readonly DashboardOverlay overlay; diff --git a/osu.Game.Tests/Visual/Online/TestSceneDirectDownloadButton.cs b/osu.Game.Tests/Visual/Online/TestSceneDirectDownloadButton.cs index 9fe873cb6a..684ce10820 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneDirectDownloadButton.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneDirectDownloadButton.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -18,11 +16,6 @@ namespace osu.Game.Tests.Visual.Online { public class TestSceneDirectDownloadButton : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(BeatmapPanelDownloadButton) - }; - private TestDownloadButton downloadButton; [Resolved] diff --git a/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs index d6ed654bac..74ece5da05 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -18,13 +17,6 @@ namespace osu.Game.Tests.Visual.Online [Cached(typeof(IPreviewTrackOwner))] public class TestSceneDirectPanel : OsuTestScene, IPreviewTrackOwner { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(GridBeatmapPanel), - typeof(ListBeatmapPanel), - typeof(IconPill) - }; - private BeatmapSetInfo getUndownloadableBeatmapSet() => new BeatmapSetInfo { OnlineBeatmapSetID = 123, diff --git a/osu.Game.Tests/Visual/Online/TestSceneExternalLinkButton.cs b/osu.Game.Tests/Visual/Online/TestSceneExternalLinkButton.cs index 637b577021..31bb276cd4 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneExternalLinkButton.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneExternalLinkButton.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using osu.Game.Graphics.UserInterface; using osuTK; @@ -10,8 +8,6 @@ namespace osu.Game.Tests.Visual.Online { public class TestSceneExternalLinkButton : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] { typeof(ExternalLinkButton) }; - public TestSceneExternalLinkButton() { Child = new ExternalLinkButton("https://osu.ppy.sh/home") diff --git a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs index 0b5ff1c960..72033fc121 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs @@ -16,13 +16,6 @@ namespace osu.Game.Tests.Visual.Online { public class TestSceneFriendDisplay : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(FriendDisplay), - typeof(FriendOnlineStreamControl), - typeof(UserListToolbar) - }; - protected override bool UseOnlineAPI => true; [Cached] diff --git a/osu.Game.Tests/Visual/Online/TestSceneHistoricalSection.cs b/osu.Game.Tests/Visual/Online/TestSceneHistoricalSection.cs index d098ea8b16..3ecca85ef1 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneHistoricalSection.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneHistoricalSection.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -11,7 +9,6 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Overlays; using osu.Game.Overlays.Profile.Sections; -using osu.Game.Overlays.Profile.Sections.Historical; using osu.Game.Users; namespace osu.Game.Tests.Visual.Online @@ -21,13 +18,6 @@ namespace osu.Game.Tests.Visual.Online { protected override bool UseOnlineAPI => true; - public override IReadOnlyList RequiredTypes => new[] - { - typeof(HistoricalSection), - typeof(PaginatedMostPlayedBeatmapContainer), - typeof(DrawableMostPlayedBeatmap), - }; - [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink); diff --git a/osu.Game.Tests/Visual/Online/TestSceneKudosuHistory.cs b/osu.Game.Tests/Visual/Online/TestSceneKudosuHistory.cs index 325d657f0e..2231139856 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneKudosuHistory.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneKudosuHistory.cs @@ -16,11 +16,6 @@ namespace osu.Game.Tests.Visual.Online { public class TestSceneKudosuHistory : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(DrawableKudosuHistoryItem), - }; - private readonly Box background; public TestSceneKudosuHistory() diff --git a/osu.Game.Tests/Visual/Online/TestSceneLeaderboardModSelector.cs b/osu.Game.Tests/Visual/Online/TestSceneLeaderboardModSelector.cs index 7327e80d06..54e655d4ec 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneLeaderboardModSelector.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneLeaderboardModSelector.cs @@ -2,8 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Game.Overlays.BeatmapSet; -using System; -using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using osu.Framework.Graphics; @@ -23,11 +21,6 @@ namespace osu.Game.Tests.Visual.Online { public class TestSceneLeaderboardModSelector : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(LeaderboardModSelector), - }; - public TestSceneLeaderboardModSelector() { LeaderboardModSelector modSelector; diff --git a/osu.Game.Tests/Visual/Online/TestSceneLeaderboardScopeSelector.cs b/osu.Game.Tests/Visual/Online/TestSceneLeaderboardScopeSelector.cs index f9a7bc99c3..afa559280c 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneLeaderboardScopeSelector.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneLeaderboardScopeSelector.cs @@ -2,8 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Game.Overlays.BeatmapSet; -using System; -using System.Collections.Generic; using osu.Framework.Graphics; using osu.Framework.Bindables; using osu.Game.Screens.Select.Leaderboards; @@ -17,11 +15,6 @@ namespace osu.Game.Tests.Visual.Online [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); - public override IReadOnlyList RequiredTypes => new[] - { - typeof(LeaderboardScopeSelector), - }; - public TestSceneLeaderboardScopeSelector() { Bindable scope = new Bindable(); diff --git a/osu.Game.Tests/Visual/Online/TestSceneProfileCounterPill.cs b/osu.Game.Tests/Visual/Online/TestSceneProfileCounterPill.cs index 5e2b125521..eaa989f0de 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneProfileCounterPill.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneProfileCounterPill.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -14,11 +12,6 @@ namespace osu.Game.Tests.Visual.Online { public class TestSceneProfileCounterPill : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(CounterPill) - }; - [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Red); diff --git a/osu.Game.Tests/Visual/Online/TestSceneProfileRulesetSelector.cs b/osu.Game.Tests/Visual/Online/TestSceneProfileRulesetSelector.cs index 826624f686..6a847e4269 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneProfileRulesetSelector.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneProfileRulesetSelector.cs @@ -3,8 +3,6 @@ using osu.Framework.Graphics; using osu.Game.Overlays.Profile.Header.Components; -using System; -using System.Collections.Generic; using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Osu; @@ -18,12 +16,6 @@ namespace osu.Game.Tests.Visual.Online { public class TestSceneProfileRulesetSelector : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(ProfileRulesetSelector), - typeof(ProfileRulesetTabItem), - }; - [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green); diff --git a/osu.Game.Tests/Visual/Online/TestSceneRankGraph.cs b/osu.Game.Tests/Visual/Online/TestSceneRankGraph.cs index 8f7e7498a9..3b31192259 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneRankGraph.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneRankGraph.cs @@ -1,15 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; -using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osu.Game.Overlays.Profile.Header.Components; using osu.Game.Users; @@ -20,12 +17,6 @@ namespace osu.Game.Tests.Visual.Online [TestFixture] public class TestSceneRankGraph : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(RankGraph), - typeof(LineGraph) - }; - [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink); diff --git a/osu.Game.Tests/Visual/Online/TestSceneRankingsCountryFilter.cs b/osu.Game.Tests/Visual/Online/TestSceneRankingsCountryFilter.cs index 79862deb16..458ba80712 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneRankingsCountryFilter.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneRankingsCountryFilter.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; using osu.Game.Overlays.Rankings; @@ -18,12 +16,6 @@ namespace osu.Game.Tests.Visual.Online { public class TestSceneRankingsCountryFilter : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(CountryFilter), - typeof(CountryPill) - }; - [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green); diff --git a/osu.Game.Tests/Visual/Online/TestSceneRankingsHeader.cs b/osu.Game.Tests/Visual/Online/TestSceneRankingsHeader.cs index 1e711b3cd7..677952681c 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneRankingsHeader.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneRankingsHeader.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Overlays; @@ -14,13 +12,6 @@ namespace osu.Game.Tests.Visual.Online { public class TestSceneRankingsHeader : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(RankingsOverlayHeader), - typeof(CountryFilter), - typeof(CountryPill) - }; - [Cached] private readonly OverlayColourProvider overlayColour = new OverlayColourProvider(OverlayColourScheme.Green); diff --git a/osu.Game.Tests/Visual/Online/TestSceneRankingsOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneRankingsOverlay.cs index 83e5cd0fe7..626f545b91 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneRankingsOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneRankingsOverlay.cs @@ -1,9 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; -using osu.Game.Overlays.Rankings.Tables; using osu.Framework.Allocation; using osu.Game.Overlays; using NUnit.Framework; @@ -17,18 +14,6 @@ namespace osu.Game.Tests.Visual.Online { protected override bool UseOnlineAPI => true; - public override IReadOnlyList RequiredTypes => new[] - { - typeof(PerformanceTable), - typeof(ScoresTable), - typeof(CountriesTable), - typeof(TableRowBackground), - typeof(UserBasedTable), - typeof(RankingsTable<>), - typeof(RankingsOverlay), - typeof(RankingsOverlayHeader) - }; - [Cached(typeof(RankingsOverlay))] private readonly RankingsOverlay rankingsOverlay; diff --git a/osu.Game.Tests/Visual/Online/TestSceneRankingsSpotlightSelector.cs b/osu.Game.Tests/Visual/Online/TestSceneRankingsSpotlightSelector.cs index f27ab1e775..997db827f3 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneRankingsSpotlightSelector.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneRankingsSpotlightSelector.cs @@ -15,11 +15,6 @@ namespace osu.Game.Tests.Visual.Online { public class TestSceneRankingsSpotlightSelector : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(SpotlightSelector), - }; - protected override bool UseOnlineAPI => true; [Cached] diff --git a/osu.Game.Tests/Visual/Online/TestSceneRankingsTables.cs b/osu.Game.Tests/Visual/Online/TestSceneRankingsTables.cs index 8542a5e46e..a3b102dc76 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneRankingsTables.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneRankingsTables.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using osu.Framework.Graphics.Containers; using osu.Game.Overlays.Rankings.Tables; using osu.Framework.Graphics; @@ -24,16 +22,6 @@ namespace osu.Game.Tests.Visual.Online { protected override bool UseOnlineAPI => true; - public override IReadOnlyList RequiredTypes => new[] - { - typeof(PerformanceTable), - typeof(ScoresTable), - typeof(CountriesTable), - typeof(TableRowBackground), - typeof(UserBasedTable), - typeof(RankingsTable<>) - }; - [Resolved] private IAPIProvider api { get; set; } diff --git a/osu.Game.Tests/Visual/Online/TestSceneScoresContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneScoresContainer.cs index 51f4089058..0cb8cc22ec 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneScoresContainer.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneScoresContainer.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -20,15 +19,6 @@ namespace osu.Game.Tests.Visual.Online { public class TestSceneScoresContainer : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(DrawableTopScore), - typeof(TopScoreUserSection), - typeof(TopScoreStatisticsSection), - typeof(ScoreTable), - typeof(ScoreTableRowBackground), - }; - [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); diff --git a/osu.Game.Tests/Visual/Online/TestSceneShowMoreButton.cs b/osu.Game.Tests/Visual/Online/TestSceneShowMoreButton.cs index b9fbbfef6b..273f593c32 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneShowMoreButton.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneShowMoreButton.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using osu.Framework.Graphics; using osu.Game.Graphics.UserInterface; using osu.Framework.Allocation; @@ -12,11 +10,6 @@ namespace osu.Game.Tests.Visual.Online { public class TestSceneShowMoreButton : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(ShowMoreButton), - }; - public TestSceneShowMoreButton() { TestButton button = null; diff --git a/osu.Game.Tests/Visual/Online/TestSceneSocialOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneSocialOverlay.cs index 24341cbd05..77e77d90c1 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneSocialOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneSocialOverlay.cs @@ -1,11 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using NUnit.Framework; using osu.Game.Overlays; -using osu.Game.Overlays.Social; using osu.Game.Users; namespace osu.Game.Tests.Visual.Online @@ -15,14 +12,6 @@ namespace osu.Game.Tests.Visual.Online { protected override bool UseOnlineAPI => true; - public override IReadOnlyList RequiredTypes => new[] - { - typeof(UserPanel), - typeof(FilterControl), - typeof(UserGridPanel), - typeof(UserListPanel) - }; - public TestSceneSocialOverlay() { SocialOverlay s = new SocialOverlay diff --git a/osu.Game.Tests/Visual/Online/TestSceneSpotlightsLayout.cs b/osu.Game.Tests/Visual/Online/TestSceneSpotlightsLayout.cs index d025a8d7c2..266dcb013b 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneSpotlightsLayout.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneSpotlightsLayout.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -19,12 +17,6 @@ namespace osu.Game.Tests.Visual.Online { public class TestSceneSpotlightsLayout : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(SpotlightsLayout), - typeof(SpotlightSelector), - }; - protected override bool UseOnlineAPI => true; [Cached] diff --git a/osu.Game.Tests/Visual/Online/TestSceneTotalCommentsCounter.cs b/osu.Game.Tests/Visual/Online/TestSceneTotalCommentsCounter.cs index 8ecbf0891b..f168ae5035 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneTotalCommentsCounter.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneTotalCommentsCounter.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using osu.Framework.Graphics; using osu.Framework.Bindables; using osu.Game.Overlays.Comments; @@ -14,11 +12,6 @@ namespace osu.Game.Tests.Visual.Online { public class TestSceneTotalCommentsCounter : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(TotalCommentsCounter), - }; - [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs index a38f045e7f..f763e50067 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -17,13 +16,6 @@ namespace osu.Game.Tests.Visual.Online [TestFixture] public class TestSceneUserPanel : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(UserPanel), - typeof(UserListPanel), - typeof(UserGridPanel), - }; - private readonly Bindable activity = new Bindable(); private readonly Bindable status = new Bindable(); diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs index 523de4e38f..04b741b2bb 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs @@ -2,15 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using osu.Framework.Allocation; -using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Overlays; using osu.Game.Overlays.Profile; -using osu.Game.Overlays.Profile.Header; -using osu.Game.Overlays.Profile.Header.Components; using osu.Game.Users; namespace osu.Game.Tests.Visual.Online @@ -19,18 +15,6 @@ namespace osu.Game.Tests.Visual.Online { protected override bool UseOnlineAPI => true; - public override IReadOnlyList RequiredTypes => new[] - { - typeof(ProfileHeader), - typeof(RankGraph), - typeof(LineGraph), - typeof(TabControlOverlayHeader<>.OverlayHeaderTabControl), - typeof(CentreHeaderContainer), - typeof(BottomHeaderContainer), - typeof(DetailHeaderContainer), - typeof(ProfileHeaderButton) - }; - [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green); diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs index 15f9c9a013..7ade24f4de 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs @@ -2,16 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; -using osu.Game.Graphics.Containers; -using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Overlays; using osu.Game.Overlays.Profile; -using osu.Game.Overlays.Profile.Header.Components; using osu.Game.Users; namespace osu.Game.Tests.Visual.Online @@ -26,15 +22,6 @@ namespace osu.Game.Tests.Visual.Online [Resolved] private IAPIProvider api { get; set; } - public override IReadOnlyList RequiredTypes => new[] - { - typeof(ProfileHeader), - typeof(RankGraph), - typeof(LineGraph), - typeof(SectionsContainer<>), - typeof(SupporterIcon) - }; - public static readonly User TEST_USER = new User { Username = @"Somebody", diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfilePreviousUsernames.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfilePreviousUsernames.cs index 048a1950fd..1e9d62f379 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfilePreviousUsernames.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfilePreviousUsernames.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -17,11 +16,6 @@ namespace osu.Game.Tests.Visual.Online [TestFixture] public class TestSceneUserProfilePreviousUsernames : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(PreviousUsernames) - }; - [Resolved] private IAPIProvider api { get; set; } diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileRecentSection.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileRecentSection.cs index 06091f3c81..0973076c40 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileRecentSection.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileRecentSection.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; @@ -14,7 +13,6 @@ using osu.Game.Graphics.Containers; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; -using osu.Game.Overlays.Profile.Sections; using osu.Game.Overlays.Profile.Sections.Recent; namespace osu.Game.Tests.Visual.Online @@ -22,14 +20,6 @@ namespace osu.Game.Tests.Visual.Online [TestFixture] public class TestSceneUserProfileRecentSection : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(RecentSection), - typeof(DrawableRecentActivity), - typeof(PaginatedRecentActivityContainer), - typeof(MedalIcon) - }; - [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green); diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileScores.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileScores.cs index f1e745bd14..5dca218531 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileScores.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileScores.cs @@ -2,8 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; -using osu.Game.Overlays.Profile.Sections; using osu.Game.Overlays.Profile.Sections.Ranks; using osu.Framework.Graphics; using osu.Game.Scoring; @@ -19,13 +17,6 @@ namespace osu.Game.Tests.Visual.Online { public class TestSceneUserProfileScores : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(DrawableProfileScore), - typeof(DrawableProfileWeightedScore), - typeof(ProfileItemContainer), - }; - public TestSceneUserProfileScores() { var firstScore = new ScoreInfo diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserRanks.cs b/osu.Game.Tests/Visual/Online/TestSceneUserRanks.cs index c8e94b2915..c22cff4af6 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserRanks.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserRanks.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -12,7 +10,6 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Overlays; using osu.Game.Overlays.Profile.Sections; -using osu.Game.Overlays.Profile.Sections.Ranks; using osu.Game.Users; namespace osu.Game.Tests.Visual.Online @@ -22,13 +19,6 @@ namespace osu.Game.Tests.Visual.Online { protected override bool UseOnlineAPI => true; - public override IReadOnlyList RequiredTypes => new[] - { - typeof(DrawableProfileScore), - typeof(DrawableProfileWeightedScore), - typeof(RanksSection) - }; - [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green); diff --git a/osu.Game.Tests/Visual/Online/TestSceneVotePill.cs b/osu.Game.Tests/Visual/Online/TestSceneVotePill.cs index 770cef8f1b..9bb29541ec 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneVotePill.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneVotePill.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Overlays.Comments; @@ -15,11 +13,6 @@ namespace osu.Game.Tests.Visual.Online [TestFixture] public class TestSceneVotePill : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(VotePill) - }; - [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs index 0781cba924..1e87893f39 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -23,15 +22,6 @@ namespace osu.Game.Tests.Visual.Ranking { public class TestSceneAccuracyCircle : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(AccuracyCircle), - typeof(RankBadge), - typeof(RankNotch), - typeof(RankText), - typeof(SmoothCircularProgress) - }; - [Test] public void TestLowDRank() { diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs index 328a0e0c27..106b4187ee 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -22,8 +21,6 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking.Expanded; -using osu.Game.Screens.Ranking.Expanded.Accuracy; -using osu.Game.Screens.Ranking.Expanded.Statistics; using osu.Game.Tests.Beatmaps; using osu.Game.Users; using osuTK; @@ -35,18 +32,6 @@ namespace osu.Game.Tests.Visual.Ranking [Resolved] private RulesetStore rulesetStore { get; set; } - public override IReadOnlyList RequiredTypes => new[] - { - typeof(ExpandedPanelMiddleContent), - typeof(AccuracyCircle), - typeof(AccuracyStatistic), - typeof(ComboStatistic), - typeof(CounterStatistic), - typeof(StarRatingDisplay), - typeof(StatisticDisplay), - typeof(TotalScoreCounter) - }; - [Test] public void TestMapWithKnownMapper() { diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index bd5b039bc1..aa0ce89d93 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -26,14 +26,6 @@ namespace osu.Game.Tests.Visual.Ranking { private BeatmapManager beatmaps; - public override IReadOnlyList RequiredTypes => new[] - { - typeof(ResultsScreen), - typeof(RetryButton), - typeof(ReplayDownloadButton), - typeof(TestPlayer) - }; - [BackgroundDependencyLoader] private void load(BeatmapManager beatmaps) { diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs index 1e55885385..78511b1be1 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Rulesets.Mods; @@ -19,13 +18,6 @@ namespace osu.Game.Tests.Visual.Ranking { public class TestSceneScorePanel : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(ScorePanel), - typeof(PanelState), - typeof(ExpandedPanelMiddleContent), - typeof(ExpandedPanelTopContent), - }; [Test] public void TestDRank() diff --git a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs index 426ff988c4..745820696a 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs @@ -1,11 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using NUnit.Framework; using osu.Game.Overlays; -using osu.Game.Overlays.KeyBinding; namespace osu.Game.Tests.Visual.Settings { @@ -14,16 +11,6 @@ namespace osu.Game.Tests.Visual.Settings { private readonly KeyBindingPanel panel; - public override IReadOnlyList RequiredTypes => new[] - { - typeof(KeyBindingRow), - typeof(GlobalKeyBindingsSection), - typeof(KeyBindingRow), - typeof(KeyBindingsSubsection), - typeof(RulesetBindingsSection), - typeof(VariantBindingsSubsection), - }; - public TestSceneKeyBindingPanel() { Child = panel = new KeyBindingPanel(); diff --git a/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs index 668fdf2c20..115d2fec7d 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs @@ -1,13 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics.Containers; using osu.Game.Overlays; -using osu.Game.Overlays.Settings; namespace osu.Game.Tests.Visual.Settings { @@ -17,12 +14,6 @@ namespace osu.Game.Tests.Visual.Settings private readonly SettingsPanel settings; private readonly DialogOverlay dialogOverlay; - public override IReadOnlyList RequiredTypes => new[] - { - typeof(SettingsFooter), - typeof(SettingsOverlay), - }; - public TestSceneSettingsPanel() { settings = new SettingsOverlay diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index f68ed4154b..2f12194ede 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -26,21 +26,6 @@ namespace osu.Game.Tests.Visual.SongSelect private TestBeatmapCarousel carousel; private RulesetStore rulesets; - public override IReadOnlyList RequiredTypes => new[] - { - typeof(CarouselItem), - typeof(CarouselGroup), - typeof(CarouselGroupEagerSelect), - typeof(CarouselBeatmap), - typeof(CarouselBeatmapSet), - - typeof(DrawableCarouselItem), - typeof(CarouselItemState), - - typeof(DrawableCarouselBeatmap), - typeof(DrawableCarouselBeatmapSet), - }; - private readonly Stack selectedSets = new Stack(); private readonly HashSet eagerSelectedIDs = new HashSet(); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index 1198488bda..48b718c04d 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -2,14 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Leaderboards; using osu.Game.Overlays; -using osu.Game.Online.Placeholders; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Scoring; using osu.Game.Screens.Select.Leaderboards; @@ -20,15 +18,6 @@ namespace osu.Game.Tests.Visual.SongSelect { public class TestSceneBeatmapLeaderboard : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(Placeholder), - typeof(MessagePlaceholder), - typeof(RetrievalFailurePlaceholder), - typeof(UserTopScoreContainer), - typeof(Leaderboard), - }; - private readonly FailableLeaderboard leaderboard; [Cached] diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 81fd1b66e5..a7e2dbeccb 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -45,24 +45,6 @@ namespace osu.Game.Tests.Visual.SongSelect private WorkingBeatmap defaultBeatmap; - public override IReadOnlyList RequiredTypes => new[] - { - typeof(Screens.Select.SongSelect), - typeof(BeatmapCarousel), - - typeof(CarouselItem), - typeof(CarouselGroup), - typeof(CarouselGroupEagerSelect), - typeof(CarouselBeatmap), - typeof(CarouselBeatmapSet), - - typeof(DrawableCarouselItem), - typeof(CarouselItemState), - - typeof(DrawableCarouselBeatmap), - typeof(DrawableCarouselBeatmapSet), - }; - private TestSongSelect songSelect; [BackgroundDependencyLoader] diff --git a/osu.Game.Tests/Visual/TestSceneOsuGame.cs b/osu.Game.Tests/Visual/TestSceneOsuGame.cs index 2eaac2a45f..22ae5257e7 100644 --- a/osu.Game.Tests/Visual/TestSceneOsuGame.cs +++ b/osu.Game.Tests/Visual/TestSceneOsuGame.cs @@ -34,11 +34,6 @@ namespace osu.Game.Tests.Visual [TestFixture] public class TestSceneOsuGame : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(OsuLogo), - }; - private IReadOnlyList requiredGameDependencies => new[] { typeof(OsuGame), diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBackButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBackButton.cs index b7d7053dcd..2440911c11 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBackButton.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBackButton.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -14,11 +12,6 @@ namespace osu.Game.Tests.Visual.UserInterface { public class TestSceneBackButton : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(TwoLayerButton) - }; - public TestSceneBackButton() { BackButton button; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs index b0b673d6a4..4c32e995e8 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs @@ -26,11 +26,6 @@ namespace osu.Game.Tests.Visual.UserInterface { private readonly NowPlayingOverlay np; - public override IReadOnlyList RequiredTypes => new[] - { - typeof(BeatSyncedContainer) - }; - [Cached] private MusicController musicController = new MusicController(); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs index d6ede950df..a4698a9a32 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -17,11 +15,6 @@ namespace osu.Game.Tests.Visual.UserInterface { public class TestSceneBeatmapListingSearchControl : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(BeatmapListingSearchControl), - }; - [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSortTabControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSortTabControl.cs index f643d4e3fe..5364f0bef5 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSortTabControl.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSortTabControl.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -15,11 +13,6 @@ namespace osu.Game.Tests.Visual.UserInterface { public class TestSceneBeatmapListingSortTabControl : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(OverlaySortTabControl<>), - }; - [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapSearchFilter.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapSearchFilter.cs index 283fe03af3..37b7b64615 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapSearchFilter.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapSearchFilter.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -16,12 +14,6 @@ namespace osu.Game.Tests.Visual.UserInterface { public class TestSceneBeatmapSearchFilter : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(BeatmapSearchFilterRow<>), - typeof(BeatmapSearchRulesetFilterRow) - }; - [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneButtonSystem.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneButtonSystem.cs index f0e1c38525..1bb5cadc6a 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneButtonSystem.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneButtonSystem.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; @@ -17,13 +16,6 @@ namespace osu.Game.Tests.Visual.UserInterface [TestFixture] public class TestSceneButtonSystem : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(ButtonSystem), - typeof(ButtonArea), - typeof(Button) - }; - private OsuLogo logo; private ButtonSystem buttons; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs index cef04a4c18..d0a2ca83e3 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -17,12 +15,6 @@ namespace osu.Game.Tests.Visual.UserInterface { public class TestSceneCommentEditor : OsuManualInputManagerTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(CommentEditor), - typeof(CancellableCommentEditor), - }; - [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index a812b4dc79..eb4750a597 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; @@ -15,7 +14,6 @@ using osu.Game.Beatmaps; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Leaderboards; -using osu.Game.Online.Placeholders; using osu.Game.Overlays; using osu.Game.Rulesets; using osu.Game.Scoring; @@ -29,16 +27,6 @@ namespace osu.Game.Tests.Visual.UserInterface { public class TestSceneDeleteLocalScore : OsuManualInputManagerTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(Placeholder), - typeof(MessagePlaceholder), - typeof(RetrievalFailurePlaceholder), - typeof(UserTopScoreContainer), - typeof(Leaderboard), - typeof(LeaderboardScore), - }; - private readonly ContextMenuContainer contextMenuContainer; private readonly BeatmapLeaderboard leaderboard; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs index 63197ed26a..1e3b1c2ffd 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs @@ -14,12 +14,6 @@ namespace osu.Game.Tests.Visual.UserInterface { public class TestSceneFooterButtonMods : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(FooterButtonMods), - typeof(FooterButton) - }; - private readonly TestFooterButtonMods footerButtonMods; public TestSceneFooterButtonMods() diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs index f6dcf78d55..9fa5c83dba 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFriendsOnlineStatusControl.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; @@ -15,15 +14,6 @@ namespace osu.Game.Tests.Visual.UserInterface { public class TestSceneFriendsOnlineStatusControl : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(FriendOnlineStreamControl), - typeof(FriendsOnlineStatusItem), - typeof(OverlayStreamControl<>), - typeof(OverlayStreamItem<>), - typeof(FriendStream) - }; - [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneHoldToConfirmOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneHoldToConfirmOverlay.cs index feef1dae6b..cea91d422e 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneHoldToConfirmOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneHoldToConfirmOverlay.cs @@ -1,11 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using osu.Framework.Graphics; using osu.Game.Graphics; -using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Screens.Menu; @@ -15,12 +12,6 @@ namespace osu.Game.Tests.Visual.UserInterface { protected override double TimePerAction => 100; // required for the early exit test, since hold-to-confirm delay is 200ms - public override IReadOnlyList RequiredTypes => new[] - { - typeof(ExitConfirmOverlay), - typeof(HoldToConfirmContainer), - }; - public TestSceneHoldToConfirmOverlay() { bool fired = false; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSwitchButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSwitchButton.cs index 6ca4d9fa4c..903f1242b4 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSwitchButton.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSwitchButton.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -12,12 +10,6 @@ namespace osu.Game.Tests.Visual.UserInterface { public class TestSceneLabelledSwitchButton : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(LabelledSwitchButton), - typeof(SwitchButton) - }; - [TestCase(false)] [TestCase(true)] public void TestSwitchButton(bool hasDescription) => createSwitchButton(hasDescription); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledTextBox.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledTextBox.cs index 8208b55952..c11ba0aa59 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledTextBox.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledTextBox.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -14,11 +12,6 @@ namespace osu.Game.Tests.Visual.UserInterface [TestFixture] public class TestSceneLabelledTextBox : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(LabelledTextBox), - }; - [TestCase(false)] [TestCase(true)] public void TestTextBox(bool hasDescription) => createTextBox(hasDescription); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLoadingLayer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLoadingLayer.cs index 7e9654715b..1be191fc29 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneLoadingLayer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLoadingLayer.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -19,8 +17,6 @@ namespace osu.Game.Tests.Visual.UserInterface private Drawable dimContent; private LoadingLayer overlay; - public override IReadOnlyList RequiredTypes => new[] { typeof(LoadingSpinner) }; - private Container content; [SetUp] diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLogoTrackingContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLogoTrackingContainer.cs index 4e394b5ed8..010e4330d7 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneLogoTrackingContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLogoTrackingContainer.cs @@ -2,17 +2,14 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.UserInterface; using osu.Framework.Utils; using osu.Framework.Testing; using osu.Game.Graphics.Containers; using osu.Game.Screens.Menu; -using osu.Game.Screens.Play; using osuTK; using osuTK.Graphics; @@ -20,17 +17,6 @@ namespace osu.Game.Tests.Visual.UserInterface { public class TestSceneLogoTrackingContainer : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(PlayerLoader), - typeof(Player), - typeof(LogoTrackingContainer), - typeof(ButtonSystem), - typeof(ButtonSystemState), - typeof(Menu), - typeof(MainMenu) - }; - private OsuLogo logo; private TestLogoTrackingContainer trackingContainer; private Container transferContainer; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index ec6ee6bc83..ce691bff70 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -12,12 +12,10 @@ using osu.Framework.Testing; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Mods; -using osu.Game.Overlays.Mods.Sections; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; -using osu.Game.Rulesets.UI; using osu.Game.Screens.Play.HUD; using osuTK; using osuTK.Graphics; @@ -27,20 +25,6 @@ namespace osu.Game.Tests.Visual.UserInterface [Description("mod select and icon display")] public class TestSceneModSelectOverlay : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(ModDisplay), - typeof(ModSection), - typeof(ModIcon), - typeof(ModButton), - typeof(ModButtonEmpty), - typeof(DifficultyReductionSection), - typeof(DifficultyIncreaseSection), - typeof(AutomationSection), - typeof(ConversionSection), - typeof(FunSection), - }; - private RulesetStore rulesets; private ModDisplay modDisplay; private TestModSelectOverlay modSelect; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs index f8ace73168..43ba23e6c6 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; @@ -18,16 +17,6 @@ namespace osu.Game.Tests.Visual.UserInterface [TestFixture] public class TestSceneNotificationOverlay : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(NotificationSection), - typeof(SimpleNotification), - typeof(ProgressNotification), - typeof(ProgressCompletionNotification), - typeof(IHasCompletionTarget), - typeof(Notification) - }; - private NotificationOverlay notificationOverlay; private readonly List progressingNotifications = new List(); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNumberBox.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNumberBox.cs index f73450db60..97a3f62b2d 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneNumberBox.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNumberBox.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -14,11 +12,6 @@ namespace osu.Game.Tests.Visual.UserInterface [TestFixture] public class TestSceneNumberBox : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(OsuNumberBox), - }; - private OsuNumberBox numberBox; [BackgroundDependencyLoader] diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuMenu.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuMenu.cs index 9ea76c2c7b..387deea76c 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuMenu.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuMenu.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; @@ -14,12 +12,6 @@ namespace osu.Game.Tests.Visual.UserInterface { public class TestSceneOsuMenu : OsuManualInputManagerTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(OsuMenu), - typeof(DrawableOsuMenuItem) - }; - private OsuMenu menu; private bool actionPerformed; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs index c81ec9f663..60af5b37ef 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs @@ -3,8 +3,6 @@ using osu.Framework.Graphics.Containers; using osu.Game.Overlays; -using System; -using System.Collections.Generic; using osu.Framework.Graphics; using osu.Game.Graphics.Sprites; using osu.Framework.Allocation; @@ -15,18 +13,6 @@ namespace osu.Game.Tests.Visual.UserInterface { public class TestSceneOverlayHeader : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(OverlayHeader), - typeof(TabControlOverlayHeader<>), - typeof(BreadcrumbControlOverlayHeader), - typeof(TestNoControlHeader), - typeof(TestStringTabControlHeader), - typeof(TestEnumTabControlHeader), - typeof(TestBreadcrumbControlHeader), - typeof(OverlayHeaderBackground) - }; - private readonly FillFlowContainer flow; public TestSceneOverlayHeader() diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeaderBackground.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeaderBackground.cs index 5a0b28e24a..db414d23a0 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeaderBackground.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeaderBackground.cs @@ -3,8 +3,6 @@ using osu.Framework.Graphics.Containers; using osu.Game.Overlays; -using System; -using System.Collections.Generic; using osu.Framework.Graphics; using osuTK; @@ -12,11 +10,6 @@ namespace osu.Game.Tests.Visual.UserInterface { public class TestSceneOverlayHeaderBackground : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(OverlayHeaderBackground) - }; - public TestSceneOverlayHeaderBackground() { Add(new BasicScrollContainer diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayRulesetSelector.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayRulesetSelector.cs index 8a98127793..f4fa41a3b7 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayRulesetSelector.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayRulesetSelector.cs @@ -2,8 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; -using System; -using System.Collections.Generic; using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Osu; @@ -20,12 +18,6 @@ namespace osu.Game.Tests.Visual.UserInterface { public class TestSceneOverlayRulesetSelector : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(OverlayRulesetSelector), - typeof(OverlayRulesetTabItem), - }; - private readonly OverlayRulesetSelector selector; private readonly Bindable ruleset = new Bindable(); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs index e9e63613c0..7fa730e02b 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayScrollContainer.cs @@ -3,8 +3,6 @@ using osu.Framework.Graphics.Containers; using osu.Game.Overlays; -using System; -using System.Collections.Generic; using osu.Framework.Graphics; using osu.Framework.Allocation; using osu.Framework.Graphics.Shapes; @@ -17,11 +15,6 @@ namespace osu.Game.Tests.Visual.UserInterface { public class TestSceneOverlayScrollContainer : OsuManualInputManagerTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(OverlayScrollContainer) - }; - [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); diff --git a/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs index 7476b52b49..a470244f53 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -16,12 +15,6 @@ namespace osu.Game.Tests.Visual.UserInterface { public class TestScenePlaylistOverlay : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(PlaylistOverlay), - typeof(Playlist) - }; - private readonly BindableList beatmapSets = new BindableList(); [SetUp] diff --git a/osu.Game.Tests/Visual/UserInterface/TestScenePopupDialog.cs b/osu.Game.Tests/Visual/UserInterface/TestScenePopupDialog.cs index 7207506ccd..8e53c7c402 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestScenePopupDialog.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestScenePopupDialog.cs @@ -1,12 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Dialog; namespace osu.Game.Tests.Visual.UserInterface @@ -14,14 +11,6 @@ namespace osu.Game.Tests.Visual.UserInterface [TestFixture] public class TestScenePopupDialog : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(PopupDialogOkButton), - typeof(PopupDialogCancelButton), - typeof(PopupDialogButton), - typeof(DialogButton), - }; - public TestScenePopupDialog() { AddStep("new popup", () => diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneStatefulMenuItem.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneStatefulMenuItem.cs index 85fea73bf5..29aeb6a4b2 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneStatefulMenuItem.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneStatefulMenuItem.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -14,14 +13,6 @@ namespace osu.Game.Tests.Visual.UserInterface { public class TestSceneStatefulMenuItem : OsuManualInputManagerTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(OsuMenu), - typeof(StatefulMenuItem), - typeof(TernaryStateMenuItem), - typeof(DrawableStatefulMenuItem), - }; - [Test] public void TestTernaryMenuItem() { diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneToggleMenuItem.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneToggleMenuItem.cs index 2abda56a28..9fb8e747f3 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneToggleMenuItem.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneToggleMenuItem.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using osu.Framework.Graphics; using osu.Game.Graphics.UserInterface; @@ -10,13 +8,6 @@ namespace osu.Game.Tests.Visual.UserInterface { public class TestSceneToggleMenuItem : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(OsuMenu), - typeof(ToggleMenuItem), - typeof(DrawableStatefulMenuItem) - }; - public TestSceneToggleMenuItem() { Add(new OsuMenu(Direction.Vertical, true) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneToolbarRulesetSelector.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneToolbarRulesetSelector.cs index 9738f73548..cdfbb14cba 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneToolbarRulesetSelector.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneToolbarRulesetSelector.cs @@ -3,8 +3,6 @@ using osu.Framework.Graphics.Containers; using osu.Game.Overlays.Toolbar; -using System; -using System.Collections.Generic; using osu.Framework.Graphics; using System.Linq; using NUnit.Framework; @@ -16,12 +14,6 @@ namespace osu.Game.Tests.Visual.UserInterface { public class TestSceneToolbarRulesetSelector : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(ToolbarRulesetSelector), - typeof(ToolbarRulesetTabButton), - }; - [Resolved] private RulesetStore rulesets { get; set; } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneUserListToolbar.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneUserListToolbar.cs index 1546972580..8f7140ed7c 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneUserListToolbar.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneUserListToolbar.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -15,14 +13,6 @@ namespace osu.Game.Tests.Visual.UserInterface { public class TestSceneUserListToolbar : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(UserSortTabControl), - typeof(OverlaySortTabControl<>), - typeof(OverlayPanelDisplayStyleControl), - typeof(UserListToolbar), - }; - [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneVolumePieces.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneVolumePieces.cs index 2fe6240b22..c8478c8eca 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneVolumePieces.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneVolumePieces.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using osu.Framework.Graphics; using osu.Game.Overlays.Volume; using osuTK; @@ -12,8 +10,6 @@ namespace osu.Game.Tests.Visual.UserInterface { public class TestSceneVolumePieces : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] { typeof(VolumeMeter), typeof(MuteButton) }; - protected override void LoadComplete() { VolumeMeter meter; diff --git a/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentMatch.cs b/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentMatch.cs index e65b708fea..f98f55dfbc 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentMatch.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentMatch.cs @@ -1,11 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Tournament.Components; using osu.Game.Tournament.Models; using osu.Game.Tournament.Screens.Ladder.Components; @@ -13,12 +10,6 @@ namespace osu.Game.Tournament.Tests.Components { public class TestSceneDrawableTournamentMatch : TournamentTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(TournamentMatch), - typeof(DrawableTournamentTeam), - }; - public TestSceneDrawableTournamentMatch() { Container level1; diff --git a/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs b/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs index 01edcb66e4..376c59ec2d 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Tests.Visual; @@ -17,17 +15,6 @@ namespace osu.Game.Tournament.Tests.Components { public class TestSceneDrawableTournamentTeam : OsuGridTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(DrawableTeamFlag), - typeof(DrawableTeamTitle), - typeof(DrawableTeamTitleWithHeader), - typeof(DrawableMatchTeam), - typeof(DrawableTeamWithPlayers), - typeof(GroupTeam), - typeof(TeamDisplay), - }; - public TestSceneDrawableTournamentTeam() : base(4, 3) { diff --git a/osu.Game.Tournament.Tests/Components/TestSceneMatchHeader.cs b/osu.Game.Tournament.Tests/Components/TestSceneMatchHeader.cs index 9f885ed827..b29e4964b6 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneMatchHeader.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneMatchHeader.cs @@ -1,12 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; -using osu.Game.Tournament.Components; using osu.Game.Tournament.Screens.Gameplay.Components; using osuTK; @@ -14,12 +11,6 @@ namespace osu.Game.Tournament.Tests.Components { public class TestSceneMatchHeader : TournamentTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(DrawableTournamentHeaderText), - typeof(DrawableTournamentHeaderLogo), - }; - public TestSceneMatchHeader() { Child = new FillFlowContainer diff --git a/osu.Game.Tournament.Tests/Components/TestSceneRoundDisplay.cs b/osu.Game.Tournament.Tests/Components/TestSceneRoundDisplay.cs index 6f71627ce4..13bca7bea1 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneRoundDisplay.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneRoundDisplay.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using osu.Framework.Graphics; using osu.Game.Tournament.Components; using osu.Game.Tournament.Models; @@ -11,12 +9,6 @@ namespace osu.Game.Tournament.Tests.Components { public class TestSceneRoundDisplay : TournamentTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(DrawableTournamentHeaderText), - typeof(DrawableTournamentHeaderLogo), - }; - public TestSceneRoundDisplay() { Children = new Drawable[] diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs index 34fa7a4997..c1159dc000 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneGameplayScreen.cs @@ -1,13 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using osu.Framework.Allocation; using osu.Game.Tournament.Components; -using osu.Game.Tournament.Screens; using osu.Game.Tournament.Screens.Gameplay; -using osu.Game.Tournament.Screens.Gameplay.Components; namespace osu.Game.Tournament.Tests.Screens { @@ -16,17 +12,6 @@ namespace osu.Game.Tournament.Tests.Screens [Cached] private TournamentMatchChatDisplay chat = new TournamentMatchChatDisplay { Width = 0.5f }; - public override IReadOnlyList RequiredTypes => new[] - { - typeof(TeamScore), - typeof(TeamScoreDisplay), - typeof(TeamDisplay), - typeof(MatchHeader), - typeof(MatchScoreDisplay), - typeof(BeatmapInfoScreen), - typeof(SongBar), - }; - [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game/Tests/Visual/EditorTestScene.cs b/osu.Game/Tests/Visual/EditorTestScene.cs index caf2bc0ff1..2f6e6fb599 100644 --- a/osu.Game/Tests/Visual/EditorTestScene.cs +++ b/osu.Game/Tests/Visual/EditorTestScene.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Testing; @@ -15,8 +13,6 @@ namespace osu.Game.Tests.Visual { public abstract class EditorTestScene : ScreenTestScene { - public override IReadOnlyList RequiredTypes => new[] { typeof(Editor), typeof(EditorScreen) }; - protected Editor Editor { get; private set; } private readonly Ruleset ruleset; diff --git a/osu.Game/Tests/Visual/ModTestScene.cs b/osu.Game/Tests/Visual/ModTestScene.cs index 8b41fb5075..1fa638b3d8 100644 --- a/osu.Game/Tests/Visual/ModTestScene.cs +++ b/osu.Game/Tests/Visual/ModTestScene.cs @@ -14,11 +14,6 @@ namespace osu.Game.Tests.Visual { protected sealed override bool HasCustomSteps => true; - public override IReadOnlyList RequiredTypes => new[] - { - typeof(ModTestScene) - }; - protected ModTestScene(Ruleset ruleset) : base(ruleset) { From 2bde4fc3eed85600b421a2cb30e83e310a5f68de Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 16 May 2020 18:17:32 +0900 Subject: [PATCH 1621/2376] Initial implementation of contracted score panel --- .../Scoring/OsuScoreProcessor.cs | 2 +- .../TestSceneContractedPanelMiddleContent.cs | 118 ++++++++ .../ContractedPanelMiddleContent.cs | 255 ++++++++++++++++++ osu.Game/Screens/Ranking/ScorePanel.cs | 11 +- 4 files changed, 381 insertions(+), 5 deletions(-) create mode 100644 osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs create mode 100644 osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs index 1de7d488f3..79a6ea7e92 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs @@ -8,7 +8,7 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Scoring { - internal class OsuScoreProcessor : ScoreProcessor + public class OsuScoreProcessor : ScoreProcessor { protected override JudgementResult CreateResult(HitObject hitObject, Judgement judgement) => new OsuJudgementResult(hitObject, judgement); diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs new file mode 100644 index 0000000000..af3d13777c --- /dev/null +++ b/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs @@ -0,0 +1,118 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Screens.Ranking; +using osu.Game.Screens.Ranking.Contracted; +using osu.Game.Tests.Beatmaps; +using osu.Game.Users; +using osuTK; + +namespace osu.Game.Tests.Visual.Ranking +{ + public class TestSceneContractedPanelMiddleContent : OsuTestScene + { + [Resolved] + private RulesetStore rulesetStore { get; set; } + + [Test] + public void TestMapWithKnownMapper() + { + var author = new User { Username = "mapper_name" }; + + AddStep("show example score", () => showPanel(createTestBeatmap(author), createTestScore())); + + AddAssert("mapper name present", () => this.ChildrenOfType().Any(spriteText => spriteText.Text == "mapper_name")); + } + + [Test] + public void TestMapWithUnknownMapper() + { + AddStep("show example score", () => showPanel(createTestBeatmap(null), createTestScore())); + + AddAssert("mapped by text not present", () => + this.ChildrenOfType().All(spriteText => !containsAny(spriteText.Text, "mapped", "by"))); + } + + private void showPanel(WorkingBeatmap workingBeatmap, ScoreInfo score) + { + Child = new ContractedPanelMiddleContentContainer(workingBeatmap, score); + } + + private WorkingBeatmap createTestBeatmap(User author) + { + var beatmap = new TestBeatmap(rulesetStore.GetRuleset(0)); + beatmap.Metadata.Author = author; + beatmap.Metadata.Title = "Verrrrrrrrrrrrrrrrrrry looooooooooooooooooooooooong beatmap title"; + beatmap.Metadata.Artist = "Verrrrrrrrrrrrrrrrrrry looooooooooooooooooooooooong beatmap artist"; + + return new TestWorkingBeatmap(beatmap); + } + + private ScoreInfo createTestScore() => new ScoreInfo + { + User = new User + { + Id = 2, + Username = "peppy", + }, + Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, + Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }, + TotalScore = 999999, + Accuracy = 0.95, + MaxCombo = 999, + Rank = ScoreRank.S, + Date = DateTimeOffset.Now, + Statistics = + { + { HitResult.Miss, 1 }, + { HitResult.Meh, 50 }, + { HitResult.Good, 100 }, + { HitResult.Great, 300 }, + } + }; + + private bool containsAny(string text, params string[] stringsToMatch) => stringsToMatch.Any(text.Contains); + + private class ContractedPanelMiddleContentContainer : Container + { + [Cached] + private Bindable workingBeatmap { get; set; } + + public ContractedPanelMiddleContentContainer(WorkingBeatmap beatmap, ScoreInfo score) + { + workingBeatmap = new Bindable(beatmap); + + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + Size = new Vector2(ScorePanel.CONTRACTED_WIDTH, 700); + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#353535"), + }, + new ContractedPanelMiddleContent(score), + }; + } + } + } +} diff --git a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs new file mode 100644 index 0000000000..5ecb3fbd0b --- /dev/null +++ b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs @@ -0,0 +1,255 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Extensions; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.Leaderboards; +using osu.Game.Scoring; +using osu.Game.Screens.Play.HUD; +using osu.Game.Users; +using osu.Game.Users.Drawables; +using osu.Game.Utils; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.Ranking.Contracted +{ + /// + /// The content that appears in the middle of a contracted . + /// + public class ContractedPanelMiddleContent : CompositeDrawable + { + private readonly ScoreInfo score; + + /// + /// Creates a new . + /// + /// The to display. + public ContractedPanelMiddleContent(ScoreInfo score) + { + this.score = score; + RelativeSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Container + { + Name = "Background", + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerExponent = 2.5f, + CornerRadius = 20, + EdgeEffect = new EdgeEffectParameters + { + Colour = Color4.Black.Opacity(0.25f), + Type = EdgeEffectType.Shadow, + Radius = 1, + Offset = new Vector2(0, 4) + }, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("444") + }, + new UserCoverBackground + { + RelativeSizeAxes = Axes.Both, + User = score.User, + }, + } + }, + new Container + { + Name = "Background overlay", + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Bottom = -1 }, + Child = new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerExponent = 2.5f, + CornerRadius = 20, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientVertical(Color4.Black.Opacity(0.5f), Color4Extensions.FromHex("#444")) + } + } + }, + new Container + { + Name = "Foreground", + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(10), + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 10), + Children = new Drawable[] + { + new UpdateableAvatar(score.User) + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Size = new Vector2(140), + Masking = true, + CornerExponent = 2.5f, + CornerRadius = 20, + EdgeEffect = new EdgeEffectParameters + { + Colour = Color4.Black.Opacity(0.25f), + Type = EdgeEffectType.Shadow, + Radius = 8, + Offset = new Vector2(0, 4), + } + }, + new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = score.UserString, + Font = OsuFont.GetFont(size: 16, weight: FontWeight.SemiBold) + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 5), + ChildrenEnumerable = score.SortedStatistics.Select(s => createStatistic(s.Key.GetDescription(), s.Value.ToString())) + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Top = 10 }, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 5), + Children = new[] + { + createStatistic("Max Combo", $"x{score.MaxCombo}"), + createStatistic("Accuracy", $"{score.Accuracy.FormatAccuracy()}"), + } + }, + new ModDisplay + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + AutoSizeAxes = Axes.Both, + ExpansionMode = ExpansionMode.AlwaysExpanded, + DisplayUnrankedText = false, + Current = { Value = score.Mods }, + Scale = new Vector2(0.5f), + } + } + } + } + } + } + }, + }, + new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Vertical = 5 }, + Child = new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = score.TotalScore.ToString(), + Font = OsuFont.GetFont(size: 20, weight: FontWeight.Medium, fixedWidth: true), + Spacing = new Vector2(-1, 0) + }, + }, + new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Top = 2 }, + Child = new DrawableRank(score.Rank) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + } + }, + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + } + } + } + }, + }, + RowDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.Absolute, 45), + } + }; + } + + private Drawable createStatistic(string key, string value) => new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Text = key, + Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold) + }, + new OsuSpriteText + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Text = value, + Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold), + Colour = Color4Extensions.FromHex("#FFDD55") + } + } + }; + } +} diff --git a/osu.Game/Screens/Ranking/ScorePanel.cs b/osu.Game/Screens/Ranking/ScorePanel.cs index c055df7ccc..bf57cb4dd9 100644 --- a/osu.Game/Screens/Ranking/ScorePanel.cs +++ b/osu.Game/Screens/Ranking/ScorePanel.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Scoring; +using osu.Game.Screens.Ranking.Contracted; using osu.Game.Screens.Ranking.Expanded; using osuTK; using osuTK.Graphics; @@ -21,12 +22,12 @@ namespace osu.Game.Screens.Ranking /// /// Width of the panel when contracted. /// - private const float contracted_width = 160; + public const float CONTRACTED_WIDTH = 160; /// /// Height of the panel when contracted. /// - private const float contracted_height = 320; + private const float contracted_height = 385; /// /// Width of the panel when expanded. @@ -71,7 +72,7 @@ namespace osu.Game.Screens.Ranking private static readonly ColourInfo expanded_top_layer_colour = ColourInfo.GradientVertical(Color4Extensions.FromHex("#444"), Color4Extensions.FromHex("#333")); private static readonly ColourInfo expanded_middle_layer_colour = ColourInfo.GradientVertical(Color4Extensions.FromHex("#555"), Color4Extensions.FromHex("#333")); private static readonly Color4 contracted_top_layer_colour = Color4Extensions.FromHex("#353535"); - private static readonly Color4 contracted_middle_layer_colour = Color4Extensions.FromHex("#444"); + private static readonly Color4 contracted_middle_layer_colour = Color4Extensions.FromHex("#353535"); public event Action StateChanged; @@ -193,10 +194,12 @@ namespace osu.Game.Screens.Ranking break; case PanelState.Contracted: - this.ResizeTo(new Vector2(contracted_width, contracted_height), resize_duration, Easing.OutQuint); + this.ResizeTo(new Vector2(CONTRACTED_WIDTH, contracted_height), resize_duration, Easing.OutQuint); topLayerBackground.FadeColour(contracted_top_layer_colour, resize_duration, Easing.OutQuint); middleLayerBackground.FadeColour(contracted_middle_layer_colour, resize_duration, Easing.OutQuint); + + middleLayerContentContainer.Add(topLayerContent = new ContractedPanelMiddleContent(score).With(d => d.Alpha = 0)); break; } From 3df92925ee704dce60d127c04557d3aaac15c9c3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 16 May 2020 18:22:07 +0900 Subject: [PATCH 1622/2376] Add score panel test --- .../Visual/Ranking/TestSceneScorePanel.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs index 1e55885385..7431002c02 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs @@ -107,13 +107,23 @@ namespace osu.Game.Tests.Visual.Ranking addPanelStep(score); } - private void addPanelStep(ScoreInfo score) => AddStep("add panel", () => + [Test] + public void TestContractedPanel() + { + var score = createScore(); + score.Accuracy = 0.925; + score.Rank = ScoreRank.A; + + addPanelStep(score, PanelState.Contracted); + } + + private void addPanelStep(ScoreInfo score, PanelState state = PanelState.Expanded) => AddStep("add panel", () => { Child = new ScorePanel(score) { Anchor = Anchor.Centre, Origin = Anchor.Centre, - State = PanelState.Expanded + State = state }; }); From 9b7b1ef605aa3fc7dbe22682dae4bc496974d28c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 16 May 2020 18:23:18 +0900 Subject: [PATCH 1623/2376] Add cover urls --- .../Visual/Ranking/TestSceneContractedPanelMiddleContent.cs | 1 + osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs index af3d13777c..f7694c10ec 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs @@ -72,6 +72,7 @@ namespace osu.Game.Tests.Visual.Ranking { Id = 2, Username = "peppy", + CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", }, Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }, diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs index 7431002c02..27905f95fd 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs @@ -133,6 +133,7 @@ namespace osu.Game.Tests.Visual.Ranking { Id = 2, Username = "peppy", + CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", }, Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }, From 0279bcf3c8cc2ee3bc53f5b7aaa9925870dae71c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 16 May 2020 18:28:22 +0900 Subject: [PATCH 1624/2376] Fix missed issues --- osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs index 78511b1be1..880e331b92 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs @@ -10,7 +10,6 @@ using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Ranking; -using osu.Game.Screens.Ranking.Expanded; using osu.Game.Tests.Beatmaps; using osu.Game.Users; @@ -18,7 +17,6 @@ namespace osu.Game.Tests.Visual.Ranking { public class TestSceneScorePanel : OsuTestScene { - [Test] public void TestDRank() { From 8c5ccf574b3e6fbf6e26a85491713f3acf71ab7a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 16 May 2020 18:28:15 +0900 Subject: [PATCH 1625/2376] Add better fix for 1px bleed --- .../ContractedPanelMiddleContent.cs | 153 ++++++++---------- 1 file changed, 66 insertions(+), 87 deletions(-) diff --git a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs index 5ecb3fbd0b..1d7d5c4130 100644 --- a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs @@ -53,22 +53,22 @@ namespace osu.Game.Screens.Ranking.Contracted new Container { RelativeSizeAxes = Axes.Both, + Masking = true, + CornerExponent = 2.5f, + CornerRadius = 20, + EdgeEffect = new EdgeEffectParameters + { + Colour = Color4.Black.Opacity(0.25f), + Type = EdgeEffectType.Shadow, + Radius = 1, + Offset = new Vector2(0, 4) + }, Children = new Drawable[] { - new Container + // Buffered container is used to prevent 1px bleed outside the masking region + new BufferedContainer { - Name = "Background", RelativeSizeAxes = Axes.Both, - Masking = true, - CornerExponent = 2.5f, - CornerRadius = 20, - EdgeEffect = new EdgeEffectParameters - { - Colour = Color4.Black.Opacity(0.25f), - Type = EdgeEffectType.Shadow, - Radius = 1, - Offset = new Vector2(0, 4) - }, Children = new Drawable[] { new Box @@ -81,95 +81,74 @@ namespace osu.Game.Screens.Ranking.Contracted RelativeSizeAxes = Axes.Both, User = score.User, }, - } - }, - new Container - { - Name = "Background overlay", - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Bottom = -1 }, - Child = new Container - { - RelativeSizeAxes = Axes.Both, - Masking = true, - CornerExponent = 2.5f, - CornerRadius = 20, - Child = new Box + new Box { RelativeSizeAxes = Axes.Both, Colour = ColourInfo.GradientVertical(Color4.Black.Opacity(0.5f), Color4Extensions.FromHex("#444")) - } + }, } }, - new Container + new FillFlowContainer { - Name = "Foreground", RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(10), + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 10), Children = new Drawable[] { + new UpdateableAvatar(score.User) + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Size = new Vector2(140), + Masking = true, + CornerExponent = 2.5f, + CornerRadius = 20, + EdgeEffect = new EdgeEffectParameters + { + Colour = Color4.Black.Opacity(0.25f), + Type = EdgeEffectType.Shadow, + Radius = 8, + Offset = new Vector2(0, 4), + } + }, + new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = score.UserString, + Font = OsuFont.GetFont(size: 16, weight: FontWeight.SemiBold) + }, new FillFlowContainer { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding(10), + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 10), - Children = new Drawable[] + Spacing = new Vector2(0, 5), + ChildrenEnumerable = score.SortedStatistics.Select(s => createStatistic(s.Key.GetDescription(), s.Value.ToString())) + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Top = 10 }, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 5), + Children = new[] { - new UpdateableAvatar(score.User) - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Size = new Vector2(140), - Masking = true, - CornerExponent = 2.5f, - CornerRadius = 20, - EdgeEffect = new EdgeEffectParameters - { - Colour = Color4.Black.Opacity(0.25f), - Type = EdgeEffectType.Shadow, - Radius = 8, - Offset = new Vector2(0, 4), - } - }, - new OsuSpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Text = score.UserString, - Font = OsuFont.GetFont(size: 16, weight: FontWeight.SemiBold) - }, - new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 5), - ChildrenEnumerable = score.SortedStatistics.Select(s => createStatistic(s.Key.GetDescription(), s.Value.ToString())) - }, - new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Margin = new MarginPadding { Top = 10 }, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 5), - Children = new[] - { - createStatistic("Max Combo", $"x{score.MaxCombo}"), - createStatistic("Accuracy", $"{score.Accuracy.FormatAccuracy()}"), - } - }, - new ModDisplay - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - AutoSizeAxes = Axes.Both, - ExpansionMode = ExpansionMode.AlwaysExpanded, - DisplayUnrankedText = false, - Current = { Value = score.Mods }, - Scale = new Vector2(0.5f), - } + createStatistic("Max Combo", $"x{score.MaxCombo}"), + createStatistic("Accuracy", $"{score.Accuracy.FormatAccuracy()}"), } + }, + new ModDisplay + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + AutoSizeAxes = Axes.Both, + ExpansionMode = ExpansionMode.AlwaysExpanded, + DisplayUnrankedText = false, + Current = { Value = score.Mods }, + Scale = new Vector2(0.5f), } } } From cfa5a81e7844eb136f7e1fed6a04b3374788709e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 16 May 2020 18:28:25 +0900 Subject: [PATCH 1626/2376] Cleanup testscene --- osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs index 3b8ce7d837..0dbafb18bc 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs @@ -10,7 +10,6 @@ using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Ranking; -using osu.Game.Screens.Ranking.Expanded; using osu.Game.Tests.Beatmaps; using osu.Game.Users; @@ -18,7 +17,6 @@ namespace osu.Game.Tests.Visual.Ranking { public class TestSceneScorePanel : OsuTestScene { - [Test] public void TestDRank() { From e3c1112b5ab10e981ce59c1c2a851ecdcf7a0dbb Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 16 May 2020 19:00:20 +0900 Subject: [PATCH 1627/2376] Initial integration into results screen --- osu.Game/Screens/Ranking/ResultsScreen.cs | 57 +++++++++++++++++++++-- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index cfba1e6e3e..f2458d9f1f 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -10,6 +11,9 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Screens; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Rulesets; using osu.Game.Scoring; using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Play; @@ -31,11 +35,18 @@ namespace osu.Game.Screens.Ranking [Resolved(CanBeNull = true)] private Player player { get; set; } + [Resolved] + private IAPIProvider api { get; set; } + + [Resolved] + private RulesetStore rulesets { get; set; } + public readonly ScoreInfo Score; private readonly bool allowRetry; private Drawable bottomPanel; + private Container contractedPanels; public ResultsScreen(ScoreInfo score, bool allowRetry = true) { @@ -52,12 +63,29 @@ namespace osu.Game.Screens.Ranking { new ResultsScrollContainer { - Child = new ScorePanel(Score) + Children = new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - State = PanelState.Expanded - }, + new ScorePanel(Score) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + State = PanelState.Expanded + }, + new OsuScrollContainer(Direction.Horizontal) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + ScrollbarVisible = false, + Child = contractedPanels = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both + } + } + } }, bottomPanel = new Container { @@ -105,6 +133,25 @@ namespace osu.Game.Screens.Ranking } } + protected override void LoadComplete() + { + base.LoadComplete(); + + var req = new GetScoresRequest(Score.Beatmap, Score.Ruleset); + + req.Success += r => + { + contractedPanels.ChildrenEnumerable = r.Scores.Select(s => s.CreateScoreInfo(rulesets)).Select(s => new ScorePanel(s) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + State = PanelState.Contracted + }); + }; + + api.Queue(req); + } + public override void OnEntering(IScreen last) { base.OnEntering(last); From 358345cee72cf0e0c6ae57936ee8df27de4d72ac Mon Sep 17 00:00:00 2001 From: Shivam Date: Sat, 16 May 2020 12:50:56 +0200 Subject: [PATCH 1628/2376] Change logic for parentscreen/subscreen relation --- .../Screens/TestSceneSeedingEditorScreen.cs | 2 +- .../Screens/Editors/SeedingEditorScreen.cs | 7 ++----- .../Screens/Editors/TeamEditorScreen.cs | 6 +++--- .../Screens/Editors/TournamentEditorScreen.cs | 16 +++++++++------- 4 files changed, 15 insertions(+), 16 deletions(-) diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneSeedingEditorScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneSeedingEditorScreen.cs index 17cccd34b6..8d12d5393d 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneSeedingEditorScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneSeedingEditorScreen.cs @@ -16,7 +16,7 @@ namespace osu.Game.Tournament.Tests.Screens { var match = CreateSampleMatch(); - Add(new SeedingEditorScreen(match.Team1.Value) + Add(new SeedingEditorScreen(match.Team1.Value, new TeamEditorScreen()) { Width = 0.85f // create room for control panel }); diff --git a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs index 0f980ec9a3..0973a7dc75 100644 --- a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs @@ -28,11 +28,8 @@ namespace osu.Game.Tournament.Screens.Editors [Resolved(canBeNull: true)] private TournamentSceneManager sceneManager { get; set; } - protected override bool IsSubScreen => true; - - protected override System.Type ParentScreen => typeof(TeamEditorScreen); - - public SeedingEditorScreen(TournamentTeam team) + public SeedingEditorScreen(TournamentTeam team, TournamentScreen parentScreen) + : base(parentScreen) { this.team = team; } diff --git a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs index 81487f1bcf..dbfcfe4225 100644 --- a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs @@ -38,7 +38,7 @@ namespace osu.Game.Tournament.Screens.Editors }); } - protected override TeamRow CreateDrawable(TournamentTeam model) => new TeamRow(model); + protected override TeamRow CreateDrawable(TournamentTeam model) => new TeamRow(model, this); private void addAllCountries() { @@ -63,7 +63,7 @@ namespace osu.Game.Tournament.Screens.Editors [Resolved] private LadderInfo ladderInfo { get; set; } - public TeamRow(TournamentTeam team) + public TeamRow(TournamentTeam team, TournamentScreen parent) { Model = team; @@ -154,7 +154,7 @@ namespace osu.Game.Tournament.Screens.Editors Text = "Edit seeding results", Action = () => { - sceneManager?.SetScreen(new SeedingEditorScreen(team)); + sceneManager?.SetScreen(new SeedingEditorScreen(team, parent)); } }, } diff --git a/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs index bca0814d3a..c0b56f1e68 100644 --- a/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs @@ -33,13 +33,15 @@ namespace osu.Game.Tournament.Screens.Editors protected ControlPanel ControlPanel; - protected virtual bool IsSubScreen => false; - - protected virtual System.Type ParentScreen { get; set; } - + private readonly TournamentScreen parentScreen; private BackButton backButton; - private System.Action backAction => () => sceneManager?.SetScreen(ParentScreen); + private System.Action backAction => () => sceneManager?.SetScreen(parentScreen.GetType()); + + protected TournamentEditorScreen(TournamentScreen parentScreen = null) + { + this.parentScreen = parentScreen; + } [BackgroundDependencyLoader] private void load() @@ -51,7 +53,7 @@ namespace osu.Game.Tournament.Screens.Editors Origin = Anchor.BottomLeft, Action = () => { - if (IsSubScreen) + if (parentScreen != null) backAction.Invoke(); } }; @@ -98,7 +100,7 @@ namespace osu.Game.Tournament.Screens.Editors } }); - if (IsSubScreen) + if (parentScreen != null) backButton.Show(); Storage.CollectionChanged += (_, args) => From 2c0ac8cc36dd55c859c05b9deb14efdb90479ffb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 17 May 2020 17:25:26 +0900 Subject: [PATCH 1629/2376] Move padding to fill, not scroll container --- osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs index c0b56f1e68..7043328aa7 100644 --- a/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs @@ -70,13 +70,13 @@ namespace osu.Game.Tournament.Screens.Editors RelativeSizeAxes = Axes.Both, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Padding = new MarginPadding { Bottom = backButton.Height }, Child = flow = new FillFlowContainer { Direction = FillDirection.Vertical, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Spacing = new Vector2(20) + Spacing = new Vector2(20), + Padding = new MarginPadding { Bottom = backButton.Height * 2 }, }, }, backButton, From 864c1a73ae0ac1a1217ea85d8977004d171ff8bf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 17 May 2020 17:27:52 +0900 Subject: [PATCH 1630/2376] Only add back button if required --- .../Screens/Editors/TournamentEditorScreen.cs | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs index 7043328aa7..b92818b84a 100644 --- a/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs @@ -46,18 +46,6 @@ namespace osu.Game.Tournament.Screens.Editors [BackgroundDependencyLoader] private void load() { - BackButton.Receptor receptor = new BackButton.Receptor(); - backButton = new BackButton(receptor) - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Action = () => - { - if (parentScreen != null) - backAction.Invoke(); - } - }; - AddRangeInternal(new Drawable[] { new Box @@ -76,10 +64,8 @@ namespace osu.Game.Tournament.Screens.Editors RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Spacing = new Vector2(20), - Padding = new MarginPadding { Bottom = backButton.Height * 2 }, }, }, - backButton, ControlPanel = new ControlPanel { Children = new Drawable[] @@ -101,7 +87,21 @@ namespace osu.Game.Tournament.Screens.Editors }); if (parentScreen != null) - backButton.Show(); + { + AddInternal(backButton = new BackButton(new BackButton.Receptor()) + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + State = { Value = Visibility.Visible }, + Action = () => + { + if (parentScreen != null) + backAction.Invoke(); + } + }); + + flow.Padding = new MarginPadding { Bottom = backButton.Height * 2 }; + } Storage.CollectionChanged += (_, args) => { @@ -126,7 +126,7 @@ namespace osu.Game.Tournament.Screens.Editors switch (action) { case GlobalAction.Back: - backAction.Invoke(); + backAction?.Invoke(); return true; } From 13d4997c9165401c5123fd8a995cb9c8d9cb52e8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 17 May 2020 17:35:10 +0900 Subject: [PATCH 1631/2376] Remove custom back action logic (use receptor as intended) --- .../Screens/Editors/TournamentEditorScreen.cs | 30 ++----------------- osu.Game/Graphics/UserInterface/BackButton.cs | 8 +++-- 2 files changed, 8 insertions(+), 30 deletions(-) diff --git a/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs index b92818b84a..a5a2c5c15f 100644 --- a/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs @@ -10,8 +10,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics.UserInterface; -using osu.Game.Input.Bindings; -using osu.Framework.Input.Bindings; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Overlays.Settings; @@ -20,7 +18,7 @@ using osuTK; namespace osu.Game.Tournament.Screens.Editors { - public abstract class TournamentEditorScreen : TournamentScreen, IProvideVideo, IKeyBindingHandler + public abstract class TournamentEditorScreen : TournamentScreen, IProvideVideo where TDrawable : Drawable, IModelBacked where TModel : class, new() { @@ -36,8 +34,6 @@ namespace osu.Game.Tournament.Screens.Editors private readonly TournamentScreen parentScreen; private BackButton backButton; - private System.Action backAction => () => sceneManager?.SetScreen(parentScreen.GetType()); - protected TournamentEditorScreen(TournamentScreen parentScreen = null) { this.parentScreen = parentScreen; @@ -88,16 +84,12 @@ namespace osu.Game.Tournament.Screens.Editors if (parentScreen != null) { - AddInternal(backButton = new BackButton(new BackButton.Receptor()) + AddInternal(backButton = new BackButton { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, State = { Value = Visibility.Visible }, - Action = () => - { - if (parentScreen != null) - backAction.Invoke(); - } + Action = () => sceneManager?.SetScreen(parentScreen.GetType()) }); flow.Padding = new MarginPadding { Bottom = backButton.Height * 2 }; @@ -121,22 +113,6 @@ namespace osu.Game.Tournament.Screens.Editors flow.Add(CreateDrawable(model)); } - public bool OnPressed(GlobalAction action) - { - switch (action) - { - case GlobalAction.Back: - backAction?.Invoke(); - return true; - } - - return false; - } - - public void OnReleased(GlobalAction action) - { - } - protected abstract TDrawable CreateDrawable(TModel model); } } diff --git a/osu.Game/Graphics/UserInterface/BackButton.cs b/osu.Game/Graphics/UserInterface/BackButton.cs index 88ba7ede6e..37a8f7b1b4 100644 --- a/osu.Game/Graphics/UserInterface/BackButton.cs +++ b/osu.Game/Graphics/UserInterface/BackButton.cs @@ -16,10 +16,8 @@ namespace osu.Game.Graphics.UserInterface private readonly TwoLayerButton button; - public BackButton(Receptor receptor) + public BackButton(Receptor receptor = null) { - receptor.OnBackPressed = () => button.Click(); - Size = TwoLayerButton.SIZE_EXTENDED; Child = button = new TwoLayerButton @@ -30,6 +28,10 @@ namespace osu.Game.Graphics.UserInterface Icon = OsuIcon.LeftCircle, Action = () => Action?.Invoke() }; + + Add(receptor ??= new Receptor()); + + receptor.OnBackPressed = () => button.Click(); } [BackgroundDependencyLoader] From 76c5be7bc1039611957ac5ba2b781255ae36ee23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 17 May 2020 17:16:22 +0200 Subject: [PATCH 1632/2376] Disallow catch-specific judgements in mania --- .../Scoring/ManiaHitWindows.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaHitWindows.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaHitWindows.cs index 549f0f9214..289f8a00ef 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaHitWindows.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaHitWindows.cs @@ -7,5 +7,20 @@ namespace osu.Game.Rulesets.Mania.Scoring { public class ManiaHitWindows : HitWindows { + public override bool IsHitResultAllowed(HitResult result) + { + switch (result) + { + case HitResult.Perfect: + case HitResult.Great: + case HitResult.Good: + case HitResult.Ok: + case HitResult.Meh: + case HitResult.Miss: + return true; + } + + return false; + } } } From bc6b64b1d7a2ffc5310a69a69a08a8b1a01f0be7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 17 May 2020 21:55:01 +0200 Subject: [PATCH 1633/2376] Add failing test --- .../NonVisual/BarLineGeneratorTest.cs | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 osu.Game.Tests/NonVisual/BarLineGeneratorTest.cs diff --git a/osu.Game.Tests/NonVisual/BarLineGeneratorTest.cs b/osu.Game.Tests/NonVisual/BarLineGeneratorTest.cs new file mode 100644 index 0000000000..e663e1128e --- /dev/null +++ b/osu.Game.Tests/NonVisual/BarLineGeneratorTest.cs @@ -0,0 +1,73 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using NUnit.Framework; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Beatmaps.Timing; +using osu.Game.Rulesets.Objects; + +namespace osu.Game.Tests.NonVisual +{ + public class BarLineGeneratorTest + { + [Test] + public void TestRoundingErrorCompensation() + { + // The aim of this test is to make sure bar line generation compensates for floating-point errors. + // The premise of the test is that we have a single timing point that should result in bar lines + // that start at a time point that is a whole number every seventh beat. + + // The fact it's every seventh beat is important - it's a number indivisible by 2, which makes + // it susceptible to rounding inaccuracies. In fact this was originally spotted in cases of maps + // that met exactly this criteria. + + const int beat_length_numerator = 2000; + const int beat_length_denominator = 7; + const TimeSignatures signature = TimeSignatures.SimpleQuadruple; + + var beatmap = new Beatmap + { + HitObjects = new List + { + new HitObject { StartTime = 0 }, + new HitObject { StartTime = 120_000 } + }, + ControlPointInfo = new ControlPointInfo() + }; + + beatmap.ControlPointInfo.Add(0, new TimingControlPoint + { + BeatLength = (double)beat_length_numerator / beat_length_denominator, + TimeSignature = signature + }); + + var barLines = new BarLineGenerator(beatmap).BarLines; + + for (int i = 0; i * beat_length_denominator < barLines.Count; i++) + { + var barLine = barLines[i * beat_length_denominator]; + var expectedTime = beat_length_numerator * (int)signature * i; + + // every seventh bar's start time should be at least greater than the whole number we expect. + // It cannot be less, as that can affect overlapping scroll algorithms + // (the previous timing point might be chosen incorrectly if this is not the case) + Assert.GreaterOrEqual(barLine.StartTime, expectedTime); + + // on the other side, make sure we don't stray too far from the expected time either. + Assert.IsTrue(Precision.AlmostEquals(barLine.StartTime, expectedTime)); + + // check major/minor lines for good measure too + Assert.AreEqual(i % (int)signature == 0, barLine.Major); + } + } + + private class BarLine : IBarLine + { + public double StartTime { get; set; } + public bool Major { get; set; } + } + } +} From 17ae392a759771801a8a3d325c4e630f4c60f770 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 17 May 2020 22:08:49 +0200 Subject: [PATCH 1634/2376] Apply rounding to bar line start times --- osu.Game/Rulesets/Objects/BarLineGenerator.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/osu.Game/Rulesets/Objects/BarLineGenerator.cs b/osu.Game/Rulesets/Objects/BarLineGenerator.cs index 5588e9c0b7..9556b52735 100644 --- a/osu.Game/Rulesets/Objects/BarLineGenerator.cs +++ b/osu.Game/Rulesets/Objects/BarLineGenerator.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Utils; @@ -46,6 +47,16 @@ namespace osu.Game.Rulesets.Objects for (double t = currentTimingPoint.Time; Precision.DefinitelyBigger(endTime, t); t += barLength, currentBeat++) { + var roundedTime = Math.Round(t, MidpointRounding.AwayFromZero); + + // in the case of some bar lengths, rounding errors can cause t to be slightly less than + // the expected whole number value due to floating point inaccuracies. + // if this is the case, apply rounding. + if (Precision.AlmostEquals(t, roundedTime)) + { + t = roundedTime; + } + BarLines.Add(new TBarLine { StartTime = t, From 80d188ec91caa05af9c71854a25b8d543aae7f05 Mon Sep 17 00:00:00 2001 From: Shivam Date: Sun, 17 May 2020 22:26:42 +0200 Subject: [PATCH 1635/2376] Update xmldoc with accurate information about the model --- osu.Game.Tournament/Models/StableInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Models/StableInfo.cs b/osu.Game.Tournament/Models/StableInfo.cs index b89160536d..4818842151 100644 --- a/osu.Game.Tournament/Models/StableInfo.cs +++ b/osu.Game.Tournament/Models/StableInfo.cs @@ -7,7 +7,7 @@ using osu.Framework.Bindables; namespace osu.Game.Tournament.Models { /// - /// Holds the complete data required to operate the tournament system. + /// Holds the path to locate the osu! stable cutting-edge installation. /// [Serializable] public class StableInfo From 4bc858a2159bc2c73033800a48381831f7a42276 Mon Sep 17 00:00:00 2001 From: Shivam Date: Sun, 17 May 2020 22:27:44 +0200 Subject: [PATCH 1636/2376] Force a read of the location file during detection --- osu.Game.Tournament/IPC/FileBasedIPC.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tournament/IPC/FileBasedIPC.cs b/osu.Game.Tournament/IPC/FileBasedIPC.cs index 321a4ad0aa..0454ef4e41 100644 --- a/osu.Game.Tournament/IPC/FileBasedIPC.cs +++ b/osu.Game.Tournament/IPC/FileBasedIPC.cs @@ -232,15 +232,17 @@ namespace osu.Game.Tournament.IPC private string findFromJsonConfig() { - try + Logger.Log("Trying to find stable through the json config"); + if (tournamentStorage.Exists(stable_config)) { - Logger.Log("Trying to find stable through the json config"); - return stableInfo.StablePath.Value; + using (Stream stream = tournamentStorage.GetStream(stable_config, FileAccess.Read, FileMode.Open)) + using (var sr = new StreamReader(stream)) + { + stableInfo = JsonConvert.DeserializeObject(sr.ReadToEnd()); + return stableInfo.StablePath.Value; + } } - catch - { - } - + return null; } From fbbf51851ecad9f379d410cb60594d1ee81310e8 Mon Sep 17 00:00:00 2001 From: Shivam Date: Sun, 17 May 2020 22:28:24 +0200 Subject: [PATCH 1637/2376] Moved refresh button to directoryselector --- .../Screens/StablePathSelectScreen.cs | 145 ++++++++++++------ 1 file changed, 97 insertions(+), 48 deletions(-) diff --git a/osu.Game.Tournament/Screens/StablePathSelectScreen.cs b/osu.Game.Tournament/Screens/StablePathSelectScreen.cs index 1faacc727f..8b75bd9290 100644 --- a/osu.Game.Tournament/Screens/StablePathSelectScreen.cs +++ b/osu.Game.Tournament/Screens/StablePathSelectScreen.cs @@ -43,75 +43,107 @@ namespace osu.Game.Tournament.Screens private void load(Storage storage, OsuColour colours) { // begin selection in the parent directory of the current storage location - var initialPath = new DirectoryInfo(stableInfo.StablePath.Value).FullName; + var initialPath = new DirectoryInfo(storage.GetFullPath(string.Empty)).Parent?.FullName; - AddInternal(new Container + if (!string.IsNullOrEmpty(stableInfo.StablePath.Value)) { - Masking = true, - CornerRadius = 10, - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(0.5f, 0.8f), - Children = new Drawable[] + // If the original path info for osu! stable is not empty, set it to the parent directory of that location + initialPath = new DirectoryInfo(stableInfo.StablePath.Value).Parent?.FullName; + } + + AddRangeInternal(new Drawable[] + { + new Container { - new Box + Masking = true, + CornerRadius = 10, + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(0.5f, 0.8f), + Children = new Drawable[] { - Colour = colours.GreySeafoamDark, - RelativeSizeAxes = Axes.Both, - }, - new GridContainer - { - RelativeSizeAxes = Axes.Both, - RowDimensions = new[] + new Box { - new Dimension(), - new Dimension(GridSizeMode.Relative, 0.8f), - new Dimension(), + Colour = colours.GreySeafoamDark, + RelativeSizeAxes = Axes.Both, }, - Content = new[] + new GridContainer { - new Drawable[] + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] { - new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Text = "Please select a new location", - Font = OsuFont.Default.With(size: 40) - }, + new Dimension(), + new Dimension(GridSizeMode.Relative, 0.8f), + new Dimension(), }, - new Drawable[] + Content = new[] { - directorySelector = new DirectorySelector(initialPath) + new Drawable[] { - RelativeSizeAxes = Axes.Both, + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Please select a new location", + Font = OsuFont.Default.With(size: 40) + }, + }, + new Drawable[] + { + directorySelector = new DirectorySelector(initialPath) + { + RelativeSizeAxes = Axes.Both, + } + }, + new Drawable[] + { + new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(20), + Children = new Drawable[] + { + new TriangleButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 300, + Text = "Select stable path", + Action = () => changePath(storage) + }, + new TriangleButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 300, + Text = "Auto detect", + Action = autoDetect + }, + } + } } - }, - new Drawable[] - { - new TriangleButton - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Width = 300, - Text = "Select stable path", - Action = () => { start(storage); } - }, } } - } + }, + }, + new BackButton + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + State = { Value = Visibility.Visible }, + Action = () => sceneManager?.SetScreen(typeof(SetupScreen)) } }); } - private static bool checkExists(string p) => File.Exists(Path.Combine(p, "ipc.txt")); - - private void start(Storage storage) + private void changePath(Storage storage) { var target = directorySelector.CurrentDirectory.Value.FullName; - if (checkExists(target)) + if (File.Exists(Path.Combine(target, "ipc.txt"))) { stableInfo.StablePath.Value = target; @@ -145,5 +177,22 @@ namespace osu.Game.Tournament.Screens // Return an error in the picker that the directory does not contain ipc.txt } } + + private void autoDetect() + { + var fileBasedIpc = ipc as FileBasedIPC; + fileBasedIpc?.LocateStableStorage(); + if (fileBasedIpc?.IPCStorage == null) + { + // Could not auto detect + overlay = new DialogOverlay(); + overlay.Push(new IPCNotFoundDialog()); + AddInternal(overlay); + } + else + { + sceneManager?.SetScreen(typeof(SetupScreen)); + } + } } } From a97100216ca3da33c3aba2b91971d6baf3eb24df Mon Sep 17 00:00:00 2001 From: Shivam Date: Sun, 17 May 2020 22:28:54 +0200 Subject: [PATCH 1638/2376] Changed behaviour of refresh button in SetupScreen --- osu.Game.Tournament/Screens/SetupScreen.cs | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/osu.Game.Tournament/Screens/SetupScreen.cs b/osu.Game.Tournament/Screens/SetupScreen.cs index 93edd73ff8..dcaadc8247 100644 --- a/osu.Game.Tournament/Screens/SetupScreen.cs +++ b/osu.Game.Tournament/Screens/SetupScreen.cs @@ -82,20 +82,7 @@ namespace osu.Game.Tournament.Screens ButtonText = "Refresh", Action = () => { - fileBasedIpc?.LocateStableStorage(); - reload(); - }, - Value = fileBasedIpc?.IPCStorage?.GetFullPath(string.Empty) ?? "Not found", - Failing = fileBasedIpc?.IPCStorage == null, - Description = "The osu!stable installation which is currently being used as a data source. If a source is not found, make sure you have created an empty ipc.txt in your stable cutting-edge installation, and that it is registered as the default osu! install." - }, - new ActionableInfo - { - Label = "Custom IPC source", - ButtonText = "Change path", - Action = () => - { - stableInfo.StablePath.BindValueChanged(_ => + stableInfo.StablePath.BindValueChanged(_ => { fileBasedIpc?.LocateStableStorage(); Schedule(reload); @@ -104,7 +91,7 @@ namespace osu.Game.Tournament.Screens }, Value = fileBasedIpc?.IPCStorage?.GetFullPath(string.Empty) ?? "Not found", Failing = fileBasedIpc?.IPCStorage == null, - Description = "The osu!stable installation which is currently being used as a data source. If a source is not found, you can manually select the desired osu! installation that you want to use." + Description = "The osu!stable installation which is currently being used as a data source. If a source is not found, make sure you have created an empty ipc.txt in your stable cutting-edge installation." }, new ActionableInfo { From 59b006f9ac8688b75fa6556fb286f27f37cdbbca Mon Sep 17 00:00:00 2001 From: Shivam Date: Sun, 17 May 2020 22:46:43 +0200 Subject: [PATCH 1639/2376] Make IPC error dialog reusable and inspectcode fixes --- .../{IPCNotFoundDialog.cs => IPCErrorDialog.cs} | 11 +++++------ osu.Game.Tournament/IPC/FileBasedIPC.cs | 3 ++- osu.Game.Tournament/Screens/SetupScreen.cs | 2 +- osu.Game.Tournament/Screens/StablePathSelectScreen.cs | 5 +++-- 4 files changed, 11 insertions(+), 10 deletions(-) rename osu.Game.Tournament/Components/{IPCNotFoundDialog.cs => IPCErrorDialog.cs} (65%) diff --git a/osu.Game.Tournament/Components/IPCNotFoundDialog.cs b/osu.Game.Tournament/Components/IPCErrorDialog.cs similarity index 65% rename from osu.Game.Tournament/Components/IPCNotFoundDialog.cs rename to osu.Game.Tournament/Components/IPCErrorDialog.cs index d4f9edc182..07fd0ac973 100644 --- a/osu.Game.Tournament/Components/IPCNotFoundDialog.cs +++ b/osu.Game.Tournament/Components/IPCErrorDialog.cs @@ -6,14 +6,13 @@ using osu.Game.Overlays.Dialog; namespace osu.Game.Tournament.Components { - public class IPCNotFoundDialog : PopupDialog + public class IPCErrorDialog : PopupDialog { - public IPCNotFoundDialog() + public IPCErrorDialog(string headerText, string bodyText) { - BodyText = "Select a directory that contains an osu! Cutting Edge installation"; - - Icon = FontAwesome.Regular.Angry; - HeaderText = @"This is an invalid IPC Directory!"; + Icon = FontAwesome.Regular.SadTear; + HeaderText = headerText; + BodyText = bodyText; Buttons = new PopupDialogButton[] { new PopupDialogOkButton diff --git a/osu.Game.Tournament/IPC/FileBasedIPC.cs b/osu.Game.Tournament/IPC/FileBasedIPC.cs index 0454ef4e41..730779a46b 100644 --- a/osu.Game.Tournament/IPC/FileBasedIPC.cs +++ b/osu.Game.Tournament/IPC/FileBasedIPC.cs @@ -233,6 +233,7 @@ namespace osu.Game.Tournament.IPC private string findFromJsonConfig() { Logger.Log("Trying to find stable through the json config"); + if (tournamentStorage.Exists(stable_config)) { using (Stream stream = tournamentStorage.GetStream(stable_config, FileAccess.Read, FileMode.Open)) @@ -242,7 +243,7 @@ namespace osu.Game.Tournament.IPC return stableInfo.StablePath.Value; } } - + return null; } diff --git a/osu.Game.Tournament/Screens/SetupScreen.cs b/osu.Game.Tournament/Screens/SetupScreen.cs index dcaadc8247..4f6d063b10 100644 --- a/osu.Game.Tournament/Screens/SetupScreen.cs +++ b/osu.Game.Tournament/Screens/SetupScreen.cs @@ -82,7 +82,7 @@ namespace osu.Game.Tournament.Screens ButtonText = "Refresh", Action = () => { - stableInfo.StablePath.BindValueChanged(_ => + stableInfo.StablePath.BindValueChanged(_ => { fileBasedIpc?.LocateStableStorage(); Schedule(reload); diff --git a/osu.Game.Tournament/Screens/StablePathSelectScreen.cs b/osu.Game.Tournament/Screens/StablePathSelectScreen.cs index 8b75bd9290..35c2272918 100644 --- a/osu.Game.Tournament/Screens/StablePathSelectScreen.cs +++ b/osu.Game.Tournament/Screens/StablePathSelectScreen.cs @@ -171,7 +171,7 @@ namespace osu.Game.Tournament.Screens else { overlay = new DialogOverlay(); - overlay.Push(new IPCNotFoundDialog()); + overlay.Push(new IPCErrorDialog("This is an invalid IPC Directory", "Select a directory that contains an osu! stable cutting edge installation and make sure it has an empty ipc.txt file in it.")); AddInternal(overlay); Logger.Log("Folder is not an osu! stable CE directory"); // Return an error in the picker that the directory does not contain ipc.txt @@ -182,11 +182,12 @@ namespace osu.Game.Tournament.Screens { var fileBasedIpc = ipc as FileBasedIPC; fileBasedIpc?.LocateStableStorage(); + if (fileBasedIpc?.IPCStorage == null) { // Could not auto detect overlay = new DialogOverlay(); - overlay.Push(new IPCNotFoundDialog()); + overlay.Push(new IPCErrorDialog("Failed to auto detect", "An osu! stable cutting-edge installation could not be auto detected.\nPlease try and manually point to the directory.")); AddInternal(overlay); } else From 9bfdfbea43e14bbad24554a2d8758327882561a7 Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 18 May 2020 00:47:31 +0200 Subject: [PATCH 1640/2376] Move stablestorage check to path selection screen Also forced stablepath to be empty during auto detection so it checks other sources to load ipc from --- osu.Game.Tournament/IPC/FileBasedIPC.cs | 15 +++++++-------- osu.Game.Tournament/Screens/SetupScreen.cs | 1 - .../Screens/StablePathSelectScreen.cs | 4 ++++ 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tournament/IPC/FileBasedIPC.cs b/osu.Game.Tournament/IPC/FileBasedIPC.cs index 730779a46b..6d1cd7cc3c 100644 --- a/osu.Game.Tournament/IPC/FileBasedIPC.cs +++ b/osu.Game.Tournament/IPC/FileBasedIPC.cs @@ -232,16 +232,15 @@ namespace osu.Game.Tournament.IPC private string findFromJsonConfig() { - Logger.Log("Trying to find stable through the json config"); - - if (tournamentStorage.Exists(stable_config)) + try { - using (Stream stream = tournamentStorage.GetStream(stable_config, FileAccess.Read, FileMode.Open)) - using (var sr = new StreamReader(stream)) - { - stableInfo = JsonConvert.DeserializeObject(sr.ReadToEnd()); + Logger.Log("Trying to find stable through the json config"); + + if (!string.IsNullOrEmpty(stableInfo.StablePath.Value)) return stableInfo.StablePath.Value; - } + } + catch + { } return null; diff --git a/osu.Game.Tournament/Screens/SetupScreen.cs b/osu.Game.Tournament/Screens/SetupScreen.cs index 4f6d063b10..e0fc98e031 100644 --- a/osu.Game.Tournament/Screens/SetupScreen.cs +++ b/osu.Game.Tournament/Screens/SetupScreen.cs @@ -84,7 +84,6 @@ namespace osu.Game.Tournament.Screens { stableInfo.StablePath.BindValueChanged(_ => { - fileBasedIpc?.LocateStableStorage(); Schedule(reload); }); sceneManager.SetScreen(new StablePathSelectScreen()); diff --git a/osu.Game.Tournament/Screens/StablePathSelectScreen.cs b/osu.Game.Tournament/Screens/StablePathSelectScreen.cs index 35c2272918..5c488ae352 100644 --- a/osu.Game.Tournament/Screens/StablePathSelectScreen.cs +++ b/osu.Game.Tournament/Screens/StablePathSelectScreen.cs @@ -142,6 +142,7 @@ namespace osu.Game.Tournament.Screens private void changePath(Storage storage) { var target = directorySelector.CurrentDirectory.Value.FullName; + Logger.Log($"Changing Stable CE location to {target}"); if (File.Exists(Path.Combine(target, "ipc.txt"))) { @@ -161,6 +162,8 @@ namespace osu.Game.Tournament.Screens })); } + var fileBasedIpc = ipc as FileBasedIPC; + fileBasedIpc?.LocateStableStorage(); sceneManager?.SetScreen(typeof(SetupScreen)); } catch (Exception e) @@ -180,6 +183,7 @@ namespace osu.Game.Tournament.Screens private void autoDetect() { + stableInfo.StablePath.Value = string.Empty; // This forces findStablePath() to look elsewhere. var fileBasedIpc = ipc as FileBasedIPC; fileBasedIpc?.LocateStableStorage(); From 7a839c1486cf96d322187474f34d3fd4f63c4b81 Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 18 May 2020 00:50:08 +0200 Subject: [PATCH 1641/2376] Renamed Refresh button to Change source --- osu.Game.Tournament/Screens/SetupScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/SetupScreen.cs b/osu.Game.Tournament/Screens/SetupScreen.cs index e0fc98e031..478240f8b4 100644 --- a/osu.Game.Tournament/Screens/SetupScreen.cs +++ b/osu.Game.Tournament/Screens/SetupScreen.cs @@ -79,7 +79,7 @@ namespace osu.Game.Tournament.Screens new ActionableInfo { Label = "Current IPC source", - ButtonText = "Refresh", + ButtonText = "Change source", Action = () => { stableInfo.StablePath.BindValueChanged(_ => From a0a54efd4ec7f8ab4b1aaebf965cdd2e693fee4e Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 18 May 2020 01:05:34 +0200 Subject: [PATCH 1642/2376] Fix test crashing because of sceneManager not being nullable --- osu.Game.Tournament/Screens/SetupScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/SetupScreen.cs b/osu.Game.Tournament/Screens/SetupScreen.cs index 478240f8b4..1c479bdec4 100644 --- a/osu.Game.Tournament/Screens/SetupScreen.cs +++ b/osu.Game.Tournament/Screens/SetupScreen.cs @@ -86,7 +86,7 @@ namespace osu.Game.Tournament.Screens { Schedule(reload); }); - sceneManager.SetScreen(new StablePathSelectScreen()); + sceneManager?.SetScreen(new StablePathSelectScreen()); }, Value = fileBasedIpc?.IPCStorage?.GetFullPath(string.Empty) ?? "Not found", Failing = fileBasedIpc?.IPCStorage == null, From 59ef6002bd9e3b32adbd00761456687c36ff312c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 18 May 2020 13:12:30 +0900 Subject: [PATCH 1643/2376] 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 650ebde54d..eaad4daf35 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index ee6206e166..9112dfe46e 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -24,7 +24,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index cbf8600c62..3f0630af5f 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From 1865cd0762f6e99bfabba22a76ca2165e0b2c08d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 18 May 2020 15:10:59 +0900 Subject: [PATCH 1644/2376] Fix possible exceptions in performance calculators --- .../Difficulty/CatchPerformanceCalculator.cs | 12 ++++++------ .../Difficulty/ManiaPerformanceCalculator.cs | 13 +++++++------ .../Difficulty/OsuPerformanceCalculator.cs | 9 +++++---- .../Difficulty/TaikoPerformanceCalculator.cs | 9 +++++---- 4 files changed, 23 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs index e7ce680365..2dc28fad35 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs @@ -4,12 +4,12 @@ using System; using System.Collections.Generic; using System.Linq; +using osu.Framework.Extensions; using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; -using osu.Game.Scoring.Legacy; namespace osu.Game.Rulesets.Catch.Difficulty { @@ -34,11 +34,11 @@ namespace osu.Game.Rulesets.Catch.Difficulty { mods = Score.Mods; - fruitsHit = Score?.GetCount300() ?? Score.Statistics[HitResult.Perfect]; - ticksHit = Score?.GetCount100() ?? 0; - tinyTicksHit = Score?.GetCount50() ?? 0; - tinyTicksMissed = Score?.GetCountKatu() ?? 0; - misses = Score.Statistics[HitResult.Miss]; + fruitsHit = Score.Statistics.GetOrDefault(HitResult.Perfect); + ticksHit = Score.Statistics.GetOrDefault(HitResult.LargeTickHit); + tinyTicksHit = Score.Statistics.GetOrDefault(HitResult.SmallTickHit); + tinyTicksMissed = Score.Statistics.GetOrDefault(HitResult.SmallTickMiss); + misses = Score.Statistics.GetOrDefault(HitResult.Miss); // Don't count scores made with supposedly unranked mods if (mods.Any(m => !m.Ranked)) diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs index 3f7a2baedd..91383c5548 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using osu.Framework.Extensions; using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; @@ -37,12 +38,12 @@ namespace osu.Game.Rulesets.Mania.Difficulty { mods = Score.Mods; scaledScore = Score.TotalScore; - countPerfect = Score.Statistics[HitResult.Perfect]; - countGreat = Score.Statistics[HitResult.Great]; - countGood = Score.Statistics[HitResult.Good]; - countOk = Score.Statistics[HitResult.Ok]; - countMeh = Score.Statistics[HitResult.Meh]; - countMiss = Score.Statistics[HitResult.Miss]; + countPerfect = Score.Statistics.GetOrDefault(HitResult.Perfect); + countGreat = Score.Statistics.GetOrDefault(HitResult.Great); + countGood = Score.Statistics.GetOrDefault(HitResult.Good); + countOk = Score.Statistics.GetOrDefault(HitResult.Ok); + countMeh = Score.Statistics.GetOrDefault(HitResult.Meh); + countMiss = Score.Statistics.GetOrDefault(HitResult.Miss); if (mods.Any(m => !m.Ranked)) return 0; diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index ce8ecf02ac..4022b554f4 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using osu.Framework.Extensions; using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; @@ -45,10 +46,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty mods = Score.Mods; accuracy = Score.Accuracy; scoreMaxCombo = Score.MaxCombo; - countGreat = Score.Statistics[HitResult.Great]; - countGood = Score.Statistics[HitResult.Good]; - countMeh = Score.Statistics[HitResult.Meh]; - countMiss = Score.Statistics[HitResult.Miss]; + countGreat = Score.Statistics.GetOrDefault(HitResult.Great); + countGood = Score.Statistics.GetOrDefault(HitResult.Good); + countMeh = Score.Statistics.GetOrDefault(HitResult.Meh); + countMiss = Score.Statistics.GetOrDefault(HitResult.Miss); // Don't count scores made with supposedly unranked mods if (mods.Any(m => !m.Ranked)) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index 3a0fb64622..bc147b53ac 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using osu.Framework.Extensions; using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; @@ -31,10 +32,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty public override double Calculate(Dictionary categoryDifficulty = null) { mods = Score.Mods; - countGreat = Score.Statistics[HitResult.Great]; - countGood = Score.Statistics[HitResult.Good]; - countMeh = Score.Statistics[HitResult.Meh]; - countMiss = Score.Statistics[HitResult.Miss]; + countGreat = Score.Statistics.GetOrDefault(HitResult.Great); + countGood = Score.Statistics.GetOrDefault(HitResult.Good); + countMeh = Score.Statistics.GetOrDefault(HitResult.Meh); + countMiss = Score.Statistics.GetOrDefault(HitResult.Miss); // Don't count scores made with supposedly unranked mods if (mods.Any(m => !m.Ranked)) From d9bb90078b5f0b9d4c32635417f818fe1c562073 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 18 May 2020 17:47:47 +0900 Subject: [PATCH 1645/2376] Move grids to inside columns --- .../Edit/ManiaBeatSnapGrid.cs | 48 +++++++++---------- osu.Game.Rulesets.Mania/UI/Column.cs | 2 + .../UI/Components/ColumnHitObjectArea.cs | 8 ++++ 3 files changed, 32 insertions(+), 26 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs index 31ebb7bc1c..9a998366e9 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs @@ -7,12 +7,10 @@ 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.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; -using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI.Scrolling; @@ -22,7 +20,7 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Mania.Edit { - public class ManiaBeatSnapGrid : CompositeDrawable + public class ManiaBeatSnapGrid : Component { /// /// The brightness of bar lines one beat around the time range from . @@ -54,15 +52,32 @@ namespace osu.Game.Rulesets.Mania.Edit { foreach (var stage in composer.Playfield.Stages) { - var grid = new Grid(stage); - grids.Add(grid); + foreach (var column in stage.Columns) + { + var grid = new Grid(); - AddInternal(grid); + grids.Add(grid); + column.UnderlayElements.Add(grid); + } } beatDivisor.BindValueChanged(_ => createLines(), true); } + public override void Hide() + { + base.Hide(); + foreach (var grid in grids) + grid.Hide(); + } + + public override void Show() + { + base.Show(); + foreach (var grid in grids) + grid.Show(); + } + private void createLines() { foreach (var grid in grids) @@ -145,7 +160,7 @@ namespace osu.Game.Rulesets.Mania.Edit linesDuring.Clear(); linesAfter.Clear(); - foreach (var line in grid.AliveObjects.OfType()) + foreach (var line in grid.Objects.OfType()) { if (line.HitObject.StartTime < minTime) linesBefore.Add(line); @@ -202,30 +217,11 @@ namespace osu.Game.Rulesets.Mania.Edit [Resolved] private IManiaHitObjectComposer composer { get; set; } - private readonly Stage stage; - - public Grid(Stage stage) - { - this.stage = stage; - - RelativeSizeAxes = Axes.None; - } - protected override void LoadComplete() { base.LoadComplete(); - Clock = composer.Playfield.Clock; } - - protected override void Update() - { - base.Update(); - - var parentQuad = Parent.ToLocalSpace(stage.HitObjectContainer.ScreenSpaceDrawQuad); - Position = parentQuad.TopLeft; - Size = parentQuad.Size; - } } private class DrawableGridLine : DrawableHitObject diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 506a07f26b..511d6c8623 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -37,6 +37,8 @@ namespace osu.Game.Rulesets.Mania.UI internal readonly Container TopLevelContainer; + public Container UnderlayElements => hitObjectArea.UnderlayElements; + public Column(int index) { Index = index; diff --git a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs index cb79bf7f43..b365ae45a9 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs @@ -12,6 +12,9 @@ namespace osu.Game.Rulesets.Mania.UI.Components public class ColumnHitObjectArea : HitObjectArea { public readonly Container Explosions; + + public readonly Container UnderlayElements; + private readonly Drawable hitTarget; public ColumnHitObjectArea(int columnIndex, HitObjectContainer hitObjectContainer) @@ -19,6 +22,11 @@ namespace osu.Game.Rulesets.Mania.UI.Components { AddRangeInternal(new[] { + UnderlayElements = new Container + { + RelativeSizeAxes = Axes.Both, + Depth = 2, + }, hitTarget = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitTarget, columnIndex), _ => new DefaultHitTarget()) { RelativeSizeAxes = Axes.X, From 16e85ae0b1c0399b2a3dce77df4758b3c51f6079 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 18 May 2020 17:52:04 +0900 Subject: [PATCH 1646/2376] Remove Grid class --- .../Edit/ManiaBeatSnapGrid.cs | 20 ++++--------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs index 9a998366e9..5b13b1421c 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs @@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Mania.Edit [Resolved] private BindableBeatDivisor beatDivisor { get; set; } - private readonly List grids = new List(); + private readonly List grids = new List(); [BackgroundDependencyLoader] private void load() @@ -54,10 +54,10 @@ namespace osu.Game.Rulesets.Mania.Edit { foreach (var column in stage.Columns) { - var grid = new Grid(); + var lineContainer = new ScrollingHitObjectContainer(); - grids.Add(grid); - column.UnderlayElements.Add(grid); + grids.Add(lineContainer); + column.UnderlayElements.Add(lineContainer); } } @@ -212,18 +212,6 @@ namespace osu.Game.Rulesets.Mania.Edit } } - private class Grid : ScrollingHitObjectContainer - { - [Resolved] - private IManiaHitObjectComposer composer { get; set; } - - protected override void LoadComplete() - { - base.LoadComplete(); - Clock = composer.Playfield.Clock; - } - } - private class DrawableGridLine : DrawableHitObject { [Resolved] From b43e9781566e45723dfe33ebc24fc8083edd0f96 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Mon, 18 May 2020 17:44:56 +0800 Subject: [PATCH 1647/2376] Unify to use double in CatchPerformanceCalculator. --- .../Difficulty/CatchPerformanceCalculator.cs | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs index 2dc28fad35..2ee7cea645 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs @@ -52,8 +52,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty // Longer maps are worth more double lengthBonus = - 0.95f + 0.3f * Math.Min(1.0f, numTotalHits / 2500.0f) + - (numTotalHits > 2500 ? (float)Math.Log10(numTotalHits / 2500.0f) * 0.475f : 0.0f); + 0.95 + 0.3 * Math.Min(1.0, numTotalHits / 2500.0) + + (numTotalHits > 2500 ? Math.Log10(numTotalHits / 2500.0) * 0.475 : 0.0); // Longer maps are worth more value *= lengthBonus; @@ -65,14 +65,14 @@ namespace osu.Game.Rulesets.Catch.Difficulty if (Attributes.MaxCombo > 0) value *= Math.Min(Math.Pow(Score.MaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0); - float approachRate = (float)Attributes.ApproachRate; - float approachRateFactor = 1.0f; - if (approachRate > 9.0f) - approachRateFactor += 0.1f * (approachRate - 9.0f); // 10% for each AR above 9 - if (approachRate > 10.0f) - approachRateFactor += 0.1f * (approachRate - 10.0f); // Additional 10% at AR 11, 30% total - else if (approachRate < 8.0f) - approachRateFactor += 0.025f * (8.0f - approachRate); // 2.5% for each AR below 8 + double approachRate = Attributes.ApproachRate; + double approachRateFactor = 1.0; + if (approachRate > 9.0) + approachRateFactor += 0.1 * (approachRate - 9.0); // 10% for each AR above 9 + if (approachRate > 10.0) + approachRateFactor += 0.1 * (approachRate - 10.0); // Additional 10% at AR 11, 30% total + else if (approachRate < 8.0) + approachRateFactor += 0.025 * (8.0 - approachRate); // 2.5% for each AR below 8 value *= approachRateFactor; @@ -80,10 +80,10 @@ namespace osu.Game.Rulesets.Catch.Difficulty { value *= 1.05 + 0.075 * (10.0 - Math.Min(10.0, Attributes.ApproachRate)); // 7.5% for each AR below 10 // Hiddens gives almost nothing on max approach rate, and more the lower it is - if (approachRate <= 10.0f) - value *= 1.05f + 0.075f * (10.0f - approachRate); // 7.5% for each AR below 10 - else if (approachRate > 10.0f) - value *= 1.01f + 0.04f * (11.0f - Math.Min(11.0f, approachRate)); // 5% at AR 10, 1% at AR 11 + if (approachRate <= 10.0) + value *= 1.05 + 0.075 * (10.0 - approachRate); // 7.5% for each AR below 10 + else if (approachRate > 10.0) + value *= 1.01 + 0.04 * (11.0 - Math.Min(11.0, approachRate)); // 5% at AR 10, 1% at AR 11 } if (mods.Any(m => m is ModFlashlight)) @@ -100,7 +100,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty return value; } - private float accuracy() => totalHits() == 0 ? 0 : Math.Clamp((float)totalSuccessfulHits() / totalHits(), 0, 1); + private double accuracy() => totalHits() == 0 ? 0 : Math.Clamp((double)totalSuccessfulHits() / totalHits(), 0, 1); private int totalHits() => tinyTicksHit + ticksHit + fruitsHit + misses + tinyTicksMissed; private int totalSuccessfulHits() => tinyTicksHit + ticksHit + fruitsHit; private int totalComboHits() => misses + ticksHit + fruitsHit; From 373aae06109b845c078dcaaacab8deb5406094b3 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Mon, 18 May 2020 17:45:32 +0800 Subject: [PATCH 1648/2376] Use int for total hits in OsuPerformanceCalculator. --- 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 4022b554f4..c8c6db06d7 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -204,7 +204,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty return accuracyValue; } - private double totalHits => countGreat + countGood + countMeh + countMiss; - private double totalSuccessfulHits => countGreat + countGood + countMeh; + private int totalHits => countGreat + countGood + countMeh + countMiss; + private int totalSuccessfulHits => countGreat + countGood + countMeh; } } From c20902f249883ab6a16ca95d3096463767cc015a Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Mon, 18 May 2020 18:22:03 +0800 Subject: [PATCH 1649/2376] Fix double in accuracy calculation in OsuPerformanceCalculator. --- 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 c8c6db06d7..6f4c0f9cfa 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -181,7 +181,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty int amountHitObjectsWithAccuracy = countHitCircles; if (amountHitObjectsWithAccuracy > 0) - betterAccuracyPercentage = ((countGreat - (totalHits - amountHitObjectsWithAccuracy)) * 6 + countGood * 2 + countMeh) / (amountHitObjectsWithAccuracy * 6); + betterAccuracyPercentage = ((countGreat - (totalHits - amountHitObjectsWithAccuracy)) * 6 + countGood * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6); else betterAccuracyPercentage = 0; From 49ee05c3c4dbd8b8d9d6f673fb887665b384b592 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 18 May 2020 19:37:49 +0900 Subject: [PATCH 1650/2376] Make into CompositeDrawable --- osu.Game/Screens/Play/SkipOverlay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/SkipOverlay.cs b/osu.Game/Screens/Play/SkipOverlay.cs index fec35df4e3..b123757ded 100644 --- a/osu.Game/Screens/Play/SkipOverlay.cs +++ b/osu.Game/Screens/Play/SkipOverlay.cs @@ -24,7 +24,7 @@ using osu.Game.Input.Bindings; namespace osu.Game.Screens.Play { - public class SkipOverlay : Container, IKeyBindingHandler + public class SkipOverlay : CompositeDrawable, IKeyBindingHandler { private readonly double startTime; @@ -62,7 +62,7 @@ namespace osu.Game.Screens.Play [BackgroundDependencyLoader(true)] private void load(OsuColour colours) { - Child = buttonContainer = new ButtonContainer + InternalChild = buttonContainer = new ButtonContainer { RelativeSizeAxes = Axes.Both, Child = fadeContainer = new FadeContainer From b35b150f3883609e62e31e8bb6216e82b888cc6c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 18 May 2020 19:32:14 +0900 Subject: [PATCH 1651/2376] Simplify colouring logic --- .../Edit/ManiaBeatSnapGrid.cs | 66 +++---------------- 1 file changed, 9 insertions(+), 57 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs index 5b13b1421c..e771a9753f 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs @@ -22,10 +22,7 @@ namespace osu.Game.Rulesets.Mania.Edit { public class ManiaBeatSnapGrid : Component { - /// - /// The brightness of bar lines one beat around the time range from . - /// - private const float first_beat_brightness = 0.5f; + private const double visible_range = 1500; [Resolved] private IManiaHitObjectComposer composer { get; set; } @@ -150,64 +147,19 @@ namespace osu.Game.Rulesets.Mania.Edit private void setRange(double minTime, double maxTime) { - var linesBefore = new List(); - var linesDuring = new List(); - var linesAfter = new List(); - foreach (var grid in grids) { - linesBefore.Clear(); - linesDuring.Clear(); - linesAfter.Clear(); - foreach (var line in grid.Objects.OfType()) { - if (line.HitObject.StartTime < minTime) - linesBefore.Add(line); - else if (line.HitObject.StartTime <= maxTime) - linesDuring.Add(line); + double lineTime = line.HitObject.StartTime; + + if (lineTime >= minTime && lineTime <= maxTime) + line.Colour = Color4.White; else - linesAfter.Add(line); - } - - // Snapping will always happen on one of the two lines around minTime (the "target" line). - // One of those lines may exist in linesBefore and the other may exist in linesAfter, depending on whether such a line exists, and the target changes when the mid-point is crossed. - // For display purposes, one complete beat is shown at the maximum brightness such that the target line should always be bright. - bool targetLineIsLastLineBefore = false; - - if (linesBefore.Count > 0 && linesAfter.Count > 0) - targetLineIsLastLineBefore = Math.Abs(linesBefore[^1].HitObject.StartTime - minTime) <= Math.Abs(linesAfter[0].HitObject.StartTime - minTime); - else if (linesBefore.Count > 0) - targetLineIsLastLineBefore = true; - - if (targetLineIsLastLineBefore) - { - // Move the last line before to linesDuring - linesDuring.Insert(0, linesBefore[^1]); - linesBefore.RemoveAt(linesBefore.Count - 1); - } - else if (linesAfter.Count > 0) // = false does not guarantee that a line after exists (maybe at the bottom of the screen) - { - // Move the first line after to linesDuring - linesDuring.Insert(0, linesAfter[0]); - linesAfter.RemoveAt(0); - } - - // Grays are used rather than transparency since the lines appear on a coloured mania playfield. - - foreach (var l in linesDuring) - l.Colour = OsuColour.Gray(first_beat_brightness); - - for (int i = 0; i < linesBefore.Count; i++) - { - int offset = (linesBefore.Count - i - 1) / beatDivisor.Value; - linesBefore[i].Colour = OsuColour.Gray(first_beat_brightness / (offset + 1)); - } - - for (int i = 0; i < linesAfter.Count; i++) - { - int offset = i / beatDivisor.Value; - linesAfter[i].Colour = OsuColour.Gray(first_beat_brightness / (offset + 1)); + { + double timeSeparation = lineTime < minTime ? minTime - lineTime : lineTime - maxTime; + line.Colour = OsuColour.Gray((float)Math.Max(0, 1 - timeSeparation / visible_range)); + } } } } From 2fd25f5ee6776fd9c17f3ae25a1e00a6d435ba42 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 18 May 2020 19:54:26 +0900 Subject: [PATCH 1652/2376] Fix tests --- osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs index e093542d1e..12ada088a1 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs @@ -105,7 +105,7 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("move mouse", () => InputManager.MoveMouseTo(skip.ScreenSpaceDrawQuad.Centre)); AddStep("button down", () => InputManager.PressButton(MouseButton.Left)); - AddUntilStep("wait for overlay disappear", () => !skip.Child.IsPresent); + AddUntilStep("wait for overlay disappear", () => !skip.OverlayContents.IsPresent); AddAssert("ensure button didn't disappear", () => skip.OverlayContents.Alpha > 0); AddStep("button up", () => InputManager.ReleaseButton(MouseButton.Left)); checkRequestCount(0); @@ -121,7 +121,7 @@ namespace osu.Game.Tests.Visual.Gameplay { } - public Drawable OverlayContents => (Child as Container)?.Child; + public Drawable OverlayContents => (InternalChild as Container)?.Child; } } } From f98ee2718552e1663318e7466d70cc51e983264d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 18 May 2020 20:01:00 +0900 Subject: [PATCH 1653/2376] Fix referencing wrong child --- .../Visual/Gameplay/TestSceneSkipOverlay.cs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs index 12ada088a1..7ed7a116b4 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs @@ -56,19 +56,19 @@ namespace osu.Game.Tests.Visual.Gameplay public void TestFadeOnIdle() { AddStep("move mouse", () => InputManager.MoveMouseTo(Vector2.Zero)); - AddUntilStep("fully visible", () => skip.OverlayContents.Alpha == 1); - AddUntilStep("wait for fade", () => skip.OverlayContents.Alpha < 1); + AddUntilStep("fully visible", () => skip.FadingContent.Alpha == 1); + AddUntilStep("wait for fade", () => skip.FadingContent.Alpha < 1); AddStep("move mouse", () => InputManager.MoveMouseTo(skip.ScreenSpaceDrawQuad.Centre)); - AddUntilStep("fully visible", () => skip.OverlayContents.Alpha == 1); - AddUntilStep("wait for fade", () => skip.OverlayContents.Alpha < 1); + AddUntilStep("fully visible", () => skip.FadingContent.Alpha == 1); + AddUntilStep("wait for fade", () => skip.FadingContent.Alpha < 1); } [Test] public void TestClickableAfterFade() { AddStep("move mouse", () => InputManager.MoveMouseTo(skip.ScreenSpaceDrawQuad.Centre)); - AddUntilStep("wait for fade", () => skip.OverlayContents.Alpha == 0); + AddUntilStep("wait for fade", () => skip.FadingContent.Alpha == 0); AddStep("click", () => InputManager.Click(MouseButton.Left)); checkRequestCount(1); } @@ -105,8 +105,8 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("move mouse", () => InputManager.MoveMouseTo(skip.ScreenSpaceDrawQuad.Centre)); AddStep("button down", () => InputManager.PressButton(MouseButton.Left)); - AddUntilStep("wait for overlay disappear", () => !skip.OverlayContents.IsPresent); - AddAssert("ensure button didn't disappear", () => skip.OverlayContents.Alpha > 0); + AddUntilStep("wait for overlay disappear", () => !skip.OverlayContent.IsPresent); + AddAssert("ensure button didn't disappear", () => skip.FadingContent.Alpha > 0); AddStep("button up", () => InputManager.ReleaseButton(MouseButton.Left)); checkRequestCount(0); } @@ -121,7 +121,9 @@ namespace osu.Game.Tests.Visual.Gameplay { } - public Drawable OverlayContents => (InternalChild as Container)?.Child; + public Drawable OverlayContent => InternalChild; + + public Drawable FadingContent => (OverlayContent as Container)?.Child; } } } From 406f39e8bfc281fc91c3e96ce12077423974006f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 18 May 2020 21:27:26 +0900 Subject: [PATCH 1654/2376] Construct online visible lines --- .../TestSceneManiaBeatSnapGrid.cs | 2 +- .../Edit/ManiaBeatSnapGrid.cs | 135 ++++++++++-------- .../Edit/ManiaHitObjectComposer.cs | 9 +- 3 files changed, 81 insertions(+), 65 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaBeatSnapGrid.cs index 84419313e6..941cf4e7c8 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaBeatSnapGrid.cs @@ -61,7 +61,7 @@ namespace osu.Game.Rulesets.Mania.Tests float relativePosition = Playfield.Stages[0].HitObjectContainer.ToLocalSpace(e.ScreenSpaceMousePosition).Y / Playfield.Stages[0].HitObjectContainer.DrawHeight; double timeValue = scrollingInfo.TimeRange.Value * relativePosition; - beatSnapGrid.SetRange(timeValue, timeValue); + beatSnapGrid.SelectionTimeRange = (timeValue, timeValue); return true; } diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs index e771a9753f..a16fb52f01 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Caching; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps; @@ -22,7 +23,7 @@ namespace osu.Game.Rulesets.Mania.Edit { public class ManiaBeatSnapGrid : Component { - private const double visible_range = 1500; + private const double visible_range = 750; [Resolved] private IManiaHitObjectComposer composer { get; set; } @@ -44,6 +45,34 @@ namespace osu.Game.Rulesets.Mania.Edit private readonly List grids = new List(); + private readonly Cached lineCache = new Cached(); + + private (double start, double end)? selectionTimeRange; + + public (double start, double end)? SelectionTimeRange + { + get => selectionTimeRange; + set + { + if (value == selectionTimeRange) + return; + + selectionTimeRange = value; + lineCache.Invalidate(); + } + } + + protected override void Update() + { + base.Update(); + + if (!lineCache.IsValid) + { + lineCache.Validate(); + createLines(); + } + } + [BackgroundDependencyLoader] private void load() { @@ -61,49 +90,65 @@ namespace osu.Game.Rulesets.Mania.Edit beatDivisor.BindValueChanged(_ => createLines(), true); } - public override void Hide() - { - base.Hide(); - foreach (var grid in grids) - grid.Hide(); - } - - public override void Show() - { - base.Show(); - foreach (var grid in grids) - grid.Show(); - } - private void createLines() { foreach (var grid in grids) grid.Clear(); - for (int i = 0; i < beatmap.ControlPointInfo.TimingPoints.Count; i++) + if (selectionTimeRange == null) + return; + + var range = selectionTimeRange.Value; + + var timingPoint = beatmap.ControlPointInfo.TimingPointAt(range.start - visible_range); + + double time = timingPoint.Time; + int beat = 0; + + // progress time until in the visible range. + while (time < range.start - visible_range) { - var point = beatmap.ControlPointInfo.TimingPoints[i]; - var until = i + 1 < beatmap.ControlPointInfo.TimingPoints.Count ? beatmap.ControlPointInfo.TimingPoints[i + 1].Time : working.Value.Track.Length; + time += timingPoint.BeatLength / beatDivisor.Value; + beat++; + } - int beat = 0; + while (time < range.end + visible_range) + { + var nextTimingPoint = beatmap.ControlPointInfo.TimingPointAt(time); - for (double t = point.Time; t < until; t += point.BeatLength / beatDivisor.Value) + // switch to the next timing point if we have reached it. + if (nextTimingPoint != timingPoint) { - var indexInBeat = beat % beatDivisor.Value; - Color4 colour; + beat = 0; + timingPoint = nextTimingPoint; + } - if (indexInBeat == 0) - colour = BindableBeatDivisor.GetColourFor(1, colours); + Color4 colour = BindableBeatDivisor.GetColourFor( + BindableBeatDivisor.GetDivisorForBeatIndex(Math.Max(1, beat), beatDivisor.Value), colours); + + foreach (var grid in grids) + grid.Add(new DrawableGridLine(time, colour)); + + beat++; + time += timingPoint.BeatLength / beatDivisor.Value; + } + + foreach (var grid in grids) + { + // required to update ScrollingHitObjectContainer's cache. + grid.UpdateSubTree(); + + foreach (var line in grid.Objects.OfType()) + { + time = line.HitObject.StartTime; + + if (time >= range.start && time <= range.end) + line.Alpha = 1; else { - var divisor = BindableBeatDivisor.GetDivisorForBeatIndex(beat, beatDivisor.Value); - colour = BindableBeatDivisor.GetColourFor(divisor, colours); + double timeSeparation = time < range.start ? range.start - time : time - range.end; + line.Alpha = (float)Math.Max(0, 1 - timeSeparation / visible_range); } - - foreach (var grid in grids) - grid.Add(new DrawableGridLine(t, colour)); - - beat++; } } } @@ -112,6 +157,7 @@ namespace osu.Game.Rulesets.Mania.Edit { float minDist = float.PositiveInfinity; DrawableGridLine minDistLine = null; + Vector2 minDistLinePosition = Vector2.Zero; foreach (var grid in grids) @@ -137,33 +183,6 @@ namespace osu.Game.Rulesets.Mania.Edit return (new Vector2(position.X, minDistLinePosition.Y + noteOffset), minDistLine.HitObject.StartTime); } - public void SetRange(double minTime, double maxTime) - { - if (LoadState >= LoadState.Ready) - setRange(minTime, maxTime); - else - Schedule(() => setRange(minTime, maxTime)); - } - - private void setRange(double minTime, double maxTime) - { - foreach (var grid in grids) - { - foreach (var line in grid.Objects.OfType()) - { - double lineTime = line.HitObject.StartTime; - - if (lineTime >= minTime && lineTime <= maxTime) - line.Colour = Color4.White; - else - { - double timeSeparation = lineTime < minTime ? minTime - lineTime : lineTime - maxTime; - line.Colour = OsuColour.Gray((float)Math.Max(0, 1 - timeSeparation / visible_range)); - } - } - } - } - private class DrawableGridLine : DrawableHitObject { [Resolved] diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index 1266761d12..475320ece3 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -68,18 +68,15 @@ namespace osu.Game.Rulesets.Mania.Edit { if (EditorBeatmap.SelectedHitObjects.Any()) { - beatSnapGrid.SetRange(EditorBeatmap.SelectedHitObjects.Min(h => h.StartTime), EditorBeatmap.SelectedHitObjects.Max(h => h.GetEndTime())); - beatSnapGrid.Show(); + beatSnapGrid.SelectionTimeRange = (EditorBeatmap.SelectedHitObjects.Min(h => h.StartTime), EditorBeatmap.SelectedHitObjects.Max(h => h.GetEndTime())); } else - beatSnapGrid.Hide(); + beatSnapGrid.SelectionTimeRange = null; } else { var placementTime = GetSnappedPosition(ToLocalSpace(inputManager.CurrentState.Mouse.Position), 0).time; - beatSnapGrid.SetRange(placementTime, placementTime); - - beatSnapGrid.Show(); + beatSnapGrid.SelectionTimeRange = (placementTime, placementTime); } } From 013683c23ba1d02bf348ae6288d56b253adc9385 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 19 May 2020 00:17:13 +0900 Subject: [PATCH 1655/2376] Fix taiko rim markers incorrectly playing as whistle samples --- osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index d332f90cd4..1e1f9ae09b 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -52,7 +52,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected override IEnumerable GetSamples() { // normal and claps are always handled by the drum (see DrumSampleMapping). - var samples = HitObject.Samples.Where(s => s.Name != HitSampleInfo.HIT_NORMAL && s.Name != HitSampleInfo.HIT_CLAP); + // in addition, whistles are excluded as they are an alternative rim marker. + + var samples = HitObject.Samples.Where(s => + s.Name != HitSampleInfo.HIT_NORMAL + && s.Name != HitSampleInfo.HIT_CLAP + && s.Name != HitSampleInfo.HIT_WHISTLE); if (HitObject.Type == HitType.Rim && HitObject.IsStrong) { From 9415e45aea5afb899edc21bd866accb0923c82eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 18 May 2020 20:11:19 +0200 Subject: [PATCH 1656/2376] Add overlay layer to enumeration type --- osu.Game/Beatmaps/Legacy/LegacyStoryLayer.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Legacy/LegacyStoryLayer.cs b/osu.Game/Beatmaps/Legacy/LegacyStoryLayer.cs index 48e8bdbb76..ea23c49c4a 100644 --- a/osu.Game/Beatmaps/Legacy/LegacyStoryLayer.cs +++ b/osu.Game/Beatmaps/Legacy/LegacyStoryLayer.cs @@ -9,6 +9,7 @@ namespace osu.Game.Beatmaps.Legacy Fail = 1, Pass = 2, Foreground = 3, - Video = 4 + Overlay = 4, + Video = 5 } } From e9710b6f836578850646db5e78e06f8985066a35 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 19 May 2020 09:43:05 +0900 Subject: [PATCH 1657/2376] Add taiko type conversion test coverage --- .../TaikoBeatmapConversionTest.cs | 3 +- ...-type-conversions-expected-conversion.json | 116 ++++++++++++++++++ .../Beatmaps/sample-to-type-conversions.osu | 62 ++++++++++ 3 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/sample-to-type-conversions-expected-conversion.json create mode 100644 osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/sample-to-type-conversions.osu diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs index 8c26ca70ac..f7729138ff 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs @@ -19,6 +19,7 @@ namespace osu.Game.Rulesets.Taiko.Tests [NonParallelizable] [TestCase("basic")] [TestCase("slider-generating-drumroll")] + [TestCase("sample-to-type-conversions")] public void Test(string name) => base.Test(name); protected override IEnumerable CreateConvertValue(HitObject hitObject) @@ -41,7 +42,7 @@ namespace osu.Game.Rulesets.Taiko.Tests public struct ConvertValue : IEquatable { /// - /// A sane value to account for osu!stable using ints everwhere. + /// A sane value to account for osu!stable using ints everywhere. /// private const float conversion_lenience = 2; diff --git a/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/sample-to-type-conversions-expected-conversion.json b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/sample-to-type-conversions-expected-conversion.json new file mode 100644 index 0000000000..47ca6aef68 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/sample-to-type-conversions-expected-conversion.json @@ -0,0 +1,116 @@ +{ + "Mappings": [ + { + "StartTime": 110.0, + "Objects": [ + { + "StartTime": 110.0, + "EndTime": 110.0, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + } + ] + }, + { + "StartTime": 538.0, + "Objects": [ + { + "StartTime": 538.0, + "EndTime": 538.0, + "IsRim": true, + "IsCentre": false, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + } + ] + }, + { + "StartTime": 967.0, + "Objects": [ + { + "StartTime": 967.0, + "EndTime": 967.0, + "IsRim": true, + "IsCentre": false, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + } + ] + }, + { + "StartTime": 1395.0, + "Objects": [ + { + "StartTime": 1395.0, + "EndTime": 1395.0, + "IsRim": true, + "IsCentre": false, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + } + ] + }, + { + "StartTime": 1824.0, + "Objects": [ + { + "StartTime": 1824.0, + "EndTime": 1824.0, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": true + } + ] + }, + { + "StartTime": 2252.0, + "Objects": [ + { + "StartTime": 2252.0, + "EndTime": 2252.0, + "IsRim": true, + "IsCentre": false, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": true + } + ] + }, + { + "StartTime": 2681.0, + "Objects": [ + { + "StartTime": 2681.0, + "EndTime": 2681.0, + "IsRim": true, + "IsCentre": false, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": true + } + ] + }, + { + "StartTime": 3110.0, + "Objects": [ + { + "StartTime": 3110.0, + "EndTime": 3110.0, + "IsRim": true, + "IsCentre": false, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": true + } + ] + } + ] +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/sample-to-type-conversions.osu b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/sample-to-type-conversions.osu new file mode 100644 index 0000000000..a3537e7149 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/sample-to-type-conversions.osu @@ -0,0 +1,62 @@ +osu file format v14 + +[General] +AudioFilename: audio.mp3 +AudioLeadIn: 0 +PreviewTime: -1 +Countdown: 0 +SampleSet: Normal +StackLeniency: 0.5 +Mode: 1 +LetterboxInBreaks: 0 +WidescreenStoryboard: 1 + +[Editor] +Bookmarks: 110,13824,54967,82395,109824 +DistanceSpacing: 0.1 +BeatDivisor: 4 +GridSize: 32 +TimelineZoom: 3.099999 + +[Metadata] +Title:test +TitleUnicode:test +Artist:sample conversion +ArtistUnicode:sample conversion +Creator:banchobot +Version:sample test +Source: +Tags: +BeatmapID:0 +BeatmapSetID:-1 + +[Difficulty] +HPDrainRate:6 +CircleSize:2 +OverallDifficulty:6 +ApproachRate:7 +SliderMultiplier:1.4 +SliderTickRate:4 + +[Events] +//Background and Video events +//Break Periods +//Storyboard Layer 0 (Background) +//Storyboard Layer 1 (Fail) +//Storyboard Layer 2 (Pass) +//Storyboard Layer 3 (Foreground) +//Storyboard Layer 4 (Overlay) +//Storyboard Sound Samples + +[TimingPoints] +110,428.571428571429,4,1,0,100,1,0 + +[HitObjects] +256,192,110,5,0,0:0:0:0: +256,192,538,1,8,0:0:0:0: +256,192,967,1,2,0:0:0:0: +256,192,1395,1,10,0:0:0:0: +256,192,1824,1,4,0:0:0:0: +256,192,2252,1,12,0:0:0:0: +256,192,2681,1,6,0:0:0:0: +256,192,3110,1,14,0:0:0:0: From 3ee698cfa02bf3cb7d24f37761a75b211810b702 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 19 May 2020 12:39:09 +0900 Subject: [PATCH 1658/2376] Fix being able to press enter to create matches --- .../Screens/Multi/Match/Components/MatchSettingsOverlay.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs b/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs index 5d68de9ce6..54c4f8f7c7 100644 --- a/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs +++ b/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs @@ -133,7 +133,6 @@ namespace osu.Game.Screens.Multi.Match.Components { RelativeSizeAxes = Axes.X, TabbableContentContainer = this, - OnCommit = (sender, text) => apply(), }, }, new Section("Duration") @@ -196,7 +195,6 @@ namespace osu.Game.Screens.Multi.Match.Components RelativeSizeAxes = Axes.X, TabbableContentContainer = this, ReadOnly = true, - OnCommit = (sender, text) => apply() }, }, new Section("Password (optional)") @@ -207,7 +205,6 @@ namespace osu.Game.Screens.Multi.Match.Components RelativeSizeAxes = Axes.X, TabbableContentContainer = this, ReadOnly = true, - OnCommit = (sender, text) => apply() }, }, }, @@ -331,6 +328,9 @@ namespace osu.Game.Screens.Multi.Match.Components private void apply() { + if (!ApplyButton.Enabled.Value) + return; + hideError(); RoomName.Value = NameField.Text; From 6d3ca4ec43c2963f9fee14be4508fafe10287c73 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 19 May 2020 13:16:46 +0900 Subject: [PATCH 1659/2376] Fix failing tests --- .../Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs index d2e8c22c39..34c6940552 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs @@ -69,6 +69,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { settings.NameField.Current.Value = expected_name; settings.DurationField.Current.Value = expectedDuration; + Room.Playlist.Add(new PlaylistItem { Beatmap = { Value = CreateBeatmap(Ruleset.Value).BeatmapInfo } }); roomManager.CreateRequested = r => { @@ -89,6 +90,9 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("setup", () => { + Room.Name.Value = "Test Room"; + Room.Playlist.Add(new PlaylistItem { Beatmap = { Value = CreateBeatmap(Ruleset.Value).BeatmapInfo } }); + fail = true; roomManager.CreateRequested = _ => !fail; }); From 052ad79fc6e012b3901214bf4005a263a2fa16e3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 19 May 2020 16:44:22 +0900 Subject: [PATCH 1660/2376] Convert dangerous events to IBindables --- .../Beatmaps/IO/ImportBeatmapTest.cs | 4 +- osu.Game/Beatmaps/BeatmapManager.cs | 13 ++-- osu.Game/Database/ArchiveModelManager.cs | 13 ++-- .../DownloadableArchiveModelManager.cs | 15 +++-- osu.Game/Database/IModelDownloader.cs | 7 +- osu.Game/Database/IModelManager.cs | 7 +- osu.Game/Online/DownloadTrackingComposite.cs | 65 ++++++++++++------- osu.Game/OsuGameBase.cs | 13 +++- osu.Game/Overlays/MusicController.cs | 46 +++++++------ .../Overlays/Settings/Sections/SkinSection.cs | 30 +++++---- .../Multi/Match/Components/ReadyButton.cs | 50 +++++++------- .../Screens/Multi/Match/MatchSubScreen.cs | 26 ++++---- osu.Game/Screens/Select/BeatmapCarousel.cs | 49 +++++++++----- .../Screens/Select/Carousel/TopLocalRank.cs | 32 ++++----- .../Select/Leaderboards/BeatmapLeaderboard.cs | 15 ++--- osu.Game/Skinning/SkinManager.cs | 13 ++-- 16 files changed, 234 insertions(+), 164 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index ba6f5fc85c..43fab186aa 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -156,8 +156,8 @@ namespace osu.Game.Tests.Beatmaps.IO var manager = osu.Dependencies.Get(); // ReSharper disable once AccessToModifiedClosure - manager.ItemAdded += _ => Interlocked.Increment(ref itemAddRemoveFireCount); - manager.ItemRemoved += _ => Interlocked.Increment(ref itemAddRemoveFireCount); + manager.ItemAdded.BindValueChanged(_ => Interlocked.Increment(ref itemAddRemoveFireCount)); + manager.ItemRemoved.BindValueChanged(_ => Interlocked.Increment(ref itemAddRemoveFireCount)); var imported = await LoadOszIntoOsu(osu); diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 34ad1df6bc..7aaf0ca08d 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -12,6 +12,7 @@ using System.Threading.Tasks; 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.Lists; @@ -38,12 +39,16 @@ namespace osu.Game.Beatmaps /// /// Fired when a single difficulty has been hidden. /// - public event Action BeatmapHidden; + public IBindable> BeatmapHidden => beatmapHidden; + + private readonly Bindable> beatmapHidden = new Bindable>(); /// /// Fired when a single difficulty has been restored. /// - public event Action BeatmapRestored; + public IBindable> BeatmapRestored => beatmapRestored; + + private readonly Bindable> beatmapRestored = new Bindable>(); /// /// A default representation of a WorkingBeatmap to use when no beatmap is available. @@ -74,8 +79,8 @@ namespace osu.Game.Beatmaps DefaultBeatmap = defaultBeatmap; beatmaps = (BeatmapStore)ModelStore; - beatmaps.BeatmapHidden += b => BeatmapHidden?.Invoke(b); - beatmaps.BeatmapRestored += b => BeatmapRestored?.Invoke(b); + beatmaps.BeatmapHidden += b => beatmapHidden.Value = new WeakReference(b); + beatmaps.BeatmapRestored += b => beatmapRestored.Value = new WeakReference(b); onlineLookupQueue = new BeatmapOnlineLookupQueue(api, storage); exportStorage = storage.GetStorageForDirectory("exports"); diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 839f9075e5..33b16cbaaf 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -11,6 +11,7 @@ using Humanizer; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; using osu.Framework; +using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Logging; @@ -56,13 +57,17 @@ namespace osu.Game.Database /// Fired when a new becomes available in the database. /// This is not guaranteed to run on the update thread. /// - public event Action ItemAdded; + public IBindable> ItemAdded => itemAdded; + + private readonly Bindable> itemAdded = new Bindable>(); /// /// Fired when a is removed from the database. /// This is not guaranteed to run on the update thread. /// - public event Action ItemRemoved; + public IBindable> ItemRemoved => itemRemoved; + + private readonly Bindable> itemRemoved = new Bindable>(); public virtual string[] HandledExtensions => new[] { ".zip" }; @@ -82,8 +87,8 @@ namespace osu.Game.Database ContextFactory = contextFactory; ModelStore = modelStore; - ModelStore.ItemAdded += item => handleEvent(() => ItemAdded?.Invoke(item)); - ModelStore.ItemRemoved += s => handleEvent(() => ItemRemoved?.Invoke(s)); + ModelStore.ItemAdded += item => handleEvent(() => itemAdded.Value = new WeakReference(item)); + ModelStore.ItemRemoved += item => handleEvent(() => itemRemoved.Value = new WeakReference(item)); Files = new FileStore(contextFactory, storage); diff --git a/osu.Game/Database/DownloadableArchiveModelManager.cs b/osu.Game/Database/DownloadableArchiveModelManager.cs index 1b90898c8d..8f469ca590 100644 --- a/osu.Game/Database/DownloadableArchiveModelManager.cs +++ b/osu.Game/Database/DownloadableArchiveModelManager.cs @@ -10,6 +10,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using osu.Framework.Bindables; namespace osu.Game.Database { @@ -23,9 +24,13 @@ namespace osu.Game.Database where TModel : class, IHasFiles, IHasPrimaryKey, ISoftDelete, IEquatable where TFileModel : class, INamedFileInfo, new() { - public event Action> DownloadBegan; + public IBindable>> DownloadBegan => downloadBegan; - public event Action> DownloadFailed; + private readonly Bindable>> downloadBegan = new Bindable>>(); + + public IBindable>> DownloadFailed => downloadFailed; + + private readonly Bindable>> downloadFailed = new Bindable>>(); private readonly IAPIProvider api; @@ -81,7 +86,7 @@ namespace osu.Game.Database // for now a failed import will be marked as a failed download for simplicity. if (!imported.Any()) - DownloadFailed?.Invoke(request); + downloadFailed.Value = new WeakReference>(request); currentDownloads.Remove(request); }, TaskCreationOptions.LongRunning); @@ -100,14 +105,14 @@ namespace osu.Game.Database api.PerformAsync(request); - DownloadBegan?.Invoke(request); + downloadBegan.Value = new WeakReference>(request); return true; void triggerFailure(Exception error) { currentDownloads.Remove(request); - DownloadFailed?.Invoke(request); + downloadFailed.Value = new WeakReference>(request); notification.State = ProgressNotificationState.Cancelled; diff --git a/osu.Game/Database/IModelDownloader.cs b/osu.Game/Database/IModelDownloader.cs index 99aeb4eacf..0cb633280e 100644 --- a/osu.Game/Database/IModelDownloader.cs +++ b/osu.Game/Database/IModelDownloader.cs @@ -1,8 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Game.Online.API; using System; +using osu.Game.Online.API; +using osu.Framework.Bindables; namespace osu.Game.Database { @@ -17,13 +18,13 @@ namespace osu.Game.Database /// Fired when a download begins. /// This is NOT run on the update thread and should be scheduled. /// - event Action> DownloadBegan; + IBindable>> DownloadBegan { get; } /// /// Fired when a download is interrupted, either due to user cancellation or failure. /// This is NOT run on the update thread and should be scheduled. /// - event Action> DownloadFailed; + IBindable>> DownloadFailed { get; } /// /// Checks whether a given is already available in the local store. diff --git a/osu.Game/Database/IModelManager.cs b/osu.Game/Database/IModelManager.cs index 1bdbbb48e6..852b385798 100644 --- a/osu.Game/Database/IModelManager.cs +++ b/osu.Game/Database/IModelManager.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Bindables; namespace osu.Game.Database { @@ -9,11 +10,11 @@ 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 where TModel : class { - event Action ItemAdded; + IBindable> ItemAdded { get; } - event Action ItemRemoved; + IBindable> ItemRemoved { get; } } } diff --git a/osu.Game/Online/DownloadTrackingComposite.cs b/osu.Game/Online/DownloadTrackingComposite.cs index 0769be2998..47de7d75ed 100644 --- a/osu.Game/Online/DownloadTrackingComposite.cs +++ b/osu.Game/Online/DownloadTrackingComposite.cs @@ -34,6 +34,11 @@ namespace osu.Game.Online Model.Value = model; } + private IBindable> managerAdded; + private IBindable> managerRemoved; + private IBindable>> managerDownloadBegan; + private IBindable>> managerDownloadFailed; + [BackgroundDependencyLoader(true)] private void load() { @@ -47,23 +52,39 @@ namespace osu.Game.Online attachDownload(manager.GetExistingDownload(modelInfo.NewValue)); }, true); - manager.DownloadBegan += downloadBegan; - manager.DownloadFailed += downloadFailed; - manager.ItemAdded += itemAdded; - manager.ItemRemoved += itemRemoved; + managerDownloadBegan = manager.DownloadBegan.GetBoundCopy(); + managerDownloadBegan.BindValueChanged(downloadBegan); + managerDownloadFailed = manager.DownloadFailed.GetBoundCopy(); + managerDownloadFailed.BindValueChanged(downloadFailed); + managerAdded = manager.ItemAdded.GetBoundCopy(); + managerAdded.BindValueChanged(itemAdded); + managerRemoved = manager.ItemRemoved.GetBoundCopy(); + managerRemoved.BindValueChanged(itemRemoved); } - private void downloadBegan(ArchiveDownloadRequest request) => Schedule(() => + private void downloadBegan(ValueChangedEvent>> weakRequest) { - if (request.Model.Equals(Model.Value)) - attachDownload(request); - }); + if (weakRequest.NewValue.TryGetTarget(out var request)) + { + Schedule(() => + { + if (request.Model.Equals(Model.Value)) + attachDownload(request); + }); + } + } - private void downloadFailed(ArchiveDownloadRequest request) => Schedule(() => + private void downloadFailed(ValueChangedEvent>> weakRequest) { - if (request.Model.Equals(Model.Value)) - attachDownload(null); - }); + if (weakRequest.NewValue.TryGetTarget(out var request)) + { + Schedule(() => + { + if (request.Model.Equals(Model.Value)) + attachDownload(null); + }); + } + } private ArchiveDownloadRequest attachedRequest; @@ -107,9 +128,17 @@ namespace osu.Game.Online private void onRequestFailure(Exception e) => Schedule(() => attachDownload(null)); - private void itemAdded(TModel s) => setDownloadStateFromManager(s, DownloadState.LocallyAvailable); + private void itemAdded(ValueChangedEvent> weakItem) + { + if (weakItem.NewValue.TryGetTarget(out var item)) + setDownloadStateFromManager(item, DownloadState.LocallyAvailable); + } - private void itemRemoved(TModel s) => setDownloadStateFromManager(s, DownloadState.NotDownloaded); + private void itemRemoved(ValueChangedEvent> weakItem) + { + if (weakItem.NewValue.TryGetTarget(out var item)) + setDownloadStateFromManager(item, DownloadState.NotDownloaded); + } private void setDownloadStateFromManager(TModel s, DownloadState state) => Schedule(() => { @@ -125,14 +154,6 @@ namespace osu.Game.Online { base.Dispose(isDisposing); - if (manager != null) - { - manager.DownloadBegan -= downloadBegan; - manager.DownloadFailed -= downloadFailed; - manager.ItemAdded -= itemAdded; - manager.ItemRemoved -= itemRemoved; - } - State.UnbindAll(); attachDownload(null); diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 11a3834c71..c367c3b636 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -186,8 +186,17 @@ namespace osu.Game return ScoreManager.QueryScores(s => beatmapIds.Contains(s.Beatmap.ID)).ToList(); } - BeatmapManager.ItemRemoved += i => ScoreManager.Delete(getBeatmapScores(i), true); - BeatmapManager.ItemAdded += i => ScoreManager.Undelete(getBeatmapScores(i), true); + BeatmapManager.ItemRemoved.BindValueChanged(i => + { + if (i.NewValue.TryGetTarget(out var item)) + ScoreManager.Delete(getBeatmapScores(item), true); + }); + + BeatmapManager.ItemAdded.BindValueChanged(i => + { + if (i.NewValue.TryGetTarget(out var item)) + ScoreManager.Undelete(getBeatmapScores(item), true); + }); dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore)); dependencies.Cache(SettingsStore = new SettingsStore(contextFactory)); diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index ded641b262..35f3cb0e25 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -60,11 +60,16 @@ namespace osu.Game.Overlays [Resolved(canBeNull: true)] private OnScreenDisplay onScreenDisplay { get; set; } + private IBindable> managerAdded; + private IBindable> managerRemoved; + [BackgroundDependencyLoader] private void load() { - beatmaps.ItemAdded += handleBeatmapAdded; - beatmaps.ItemRemoved += handleBeatmapRemoved; + managerAdded = beatmaps.ItemAdded.GetBoundCopy(); + managerAdded.BindValueChanged(beatmapAdded); + managerRemoved = beatmaps.ItemRemoved.GetBoundCopy(); + managerRemoved.BindValueChanged(beatmapRemoved); beatmapSets.AddRange(beatmaps.GetAllUsableBeatmapSets(IncludedDetails.Minimal).OrderBy(_ => RNG.Next())); } @@ -93,16 +98,28 @@ namespace osu.Game.Overlays /// public bool IsPlaying => current?.Track.IsRunning ?? false; - private void handleBeatmapAdded(BeatmapSetInfo set) => Schedule(() => + private void beatmapAdded(ValueChangedEvent> weakSet) { - if (!beatmapSets.Contains(set)) - beatmapSets.Add(set); - }); + if (weakSet.NewValue.TryGetTarget(out var set)) + { + Schedule(() => + { + if (!beatmapSets.Contains(set)) + beatmapSets.Add(set); + }); + } + } - private void handleBeatmapRemoved(BeatmapSetInfo set) => Schedule(() => + private void beatmapRemoved(ValueChangedEvent> weakSet) { - beatmapSets.RemoveAll(s => s.ID == set.ID); - }); + if (weakSet.NewValue.TryGetTarget(out var set)) + { + Schedule(() => + { + beatmapSets.RemoveAll(s => s.ID == set.ID); + }); + } + } private ScheduledDelegate seekDelegate; @@ -299,17 +316,6 @@ namespace osu.Game.Overlays } } - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - if (beatmaps != null) - { - beatmaps.ItemAdded -= handleBeatmapAdded; - beatmaps.ItemRemoved -= handleBeatmapRemoved; - } - } - public bool OnPressed(GlobalAction action) { if (beatmap.Disabled) diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 75c8db1612..94080f5592 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -30,6 +31,9 @@ namespace osu.Game.Overlays.Settings.Sections [Resolved] private SkinManager skins { get; set; } + private IBindable> managerAdded; + private IBindable> managerRemoved; + [BackgroundDependencyLoader] private void load(OsuConfigManager config) { @@ -66,8 +70,11 @@ namespace osu.Game.Overlays.Settings.Sections }, }; - skins.ItemAdded += itemAdded; - skins.ItemRemoved += itemRemoved; + managerAdded = skins.ItemAdded.GetBoundCopy(); + managerAdded.BindValueChanged(itemAdded); + + managerRemoved = skins.ItemRemoved.GetBoundCopy(); + managerRemoved.BindValueChanged(itemRemoved); config.BindWith(OsuSetting.Skin, configBindable); @@ -82,19 +89,16 @@ namespace osu.Game.Overlays.Settings.Sections dropdownBindable.BindValueChanged(skin => configBindable.Value = skin.NewValue.ID); } - private void itemRemoved(SkinInfo s) => Schedule(() => skinDropdown.Items = skinDropdown.Items.Where(i => i.ID != s.ID).ToArray()); - - private void itemAdded(SkinInfo s) => Schedule(() => skinDropdown.Items = skinDropdown.Items.Append(s).ToArray()); - - protected override void Dispose(bool isDisposing) + private void itemAdded(ValueChangedEvent> weakItem) { - base.Dispose(isDisposing); + if (weakItem.NewValue.TryGetTarget(out var item)) + Schedule(() => skinDropdown.Items = skinDropdown.Items.Append(item).ToArray()); + } - if (skins != null) - { - skins.ItemAdded -= itemAdded; - skins.ItemRemoved -= itemRemoved; - } + private void itemRemoved(ValueChangedEvent> weakItem) + { + if (weakItem.NewValue.TryGetTarget(out var item)) + Schedule(() => skinDropdown.Items = skinDropdown.Items.Where(i => i.ID != item.ID).ToArray()); } private class SizeSlider : OsuSliderBar diff --git a/osu.Game/Screens/Multi/Match/Components/ReadyButton.cs b/osu.Game/Screens/Multi/Match/Components/ReadyButton.cs index 8f484d3672..4420b2d58a 100644 --- a/osu.Game/Screens/Multi/Match/Components/ReadyButton.cs +++ b/osu.Game/Screens/Multi/Match/Components/ReadyButton.cs @@ -32,11 +32,16 @@ namespace osu.Game.Screens.Multi.Match.Components Text = "Start"; } + private IBindable> managerAdded; + private IBindable> managerRemoved; + [BackgroundDependencyLoader] private void load(OsuColour colours) { - beatmaps.ItemAdded += beatmapAdded; - beatmaps.ItemRemoved += beatmapRemoved; + managerAdded = beatmaps.ItemAdded.GetBoundCopy(); + managerAdded.BindValueChanged(beatmapAdded); + managerRemoved = beatmaps.ItemRemoved.GetBoundCopy(); + managerRemoved.BindValueChanged(beatmapRemoved); SelectedItem.BindValueChanged(item => updateSelectedItem(item.NewValue), true); @@ -56,24 +61,30 @@ namespace osu.Game.Screens.Multi.Match.Components hasBeatmap = beatmaps.QueryBeatmap(b => b.OnlineBeatmapID == beatmapId) != null; } - private void beatmapAdded(BeatmapSetInfo model) + private void beatmapAdded(ValueChangedEvent> weakSet) { - int? beatmapId = SelectedItem.Value?.Beatmap.Value?.OnlineBeatmapID; - if (beatmapId == null) - return; + if (weakSet.NewValue.TryGetTarget(out var set)) + { + int? beatmapId = SelectedItem.Value?.Beatmap.Value?.OnlineBeatmapID; + if (beatmapId == null) + return; - if (model.Beatmaps.Any(b => b.OnlineBeatmapID == beatmapId)) - Schedule(() => hasBeatmap = true); + if (set.Beatmaps.Any(b => b.OnlineBeatmapID == beatmapId)) + Schedule(() => hasBeatmap = true); + } } - private void beatmapRemoved(BeatmapSetInfo model) + private void beatmapRemoved(ValueChangedEvent> weakSet) { - int? beatmapId = SelectedItem.Value?.Beatmap.Value?.OnlineBeatmapID; - if (beatmapId == null) - return; + if (weakSet.NewValue.TryGetTarget(out var set)) + { + int? beatmapId = SelectedItem.Value?.Beatmap.Value?.OnlineBeatmapID; + if (beatmapId == null) + return; - if (model.Beatmaps.Any(b => b.OnlineBeatmapID == beatmapId)) - Schedule(() => hasBeatmap = false); + if (set.Beatmaps.Any(b => b.OnlineBeatmapID == beatmapId)) + Schedule(() => hasBeatmap = false); + } } protected override void Update() @@ -95,16 +106,5 @@ namespace osu.Game.Screens.Multi.Match.Components Enabled.Value = hasBeatmap && hasEnoughTime; } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - if (beatmaps != null) - { - beatmaps.ItemAdded -= beatmapAdded; - beatmaps.ItemRemoved -= beatmapRemoved; - } - } } } diff --git a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs index eef53126c0..caa547ac72 100644 --- a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs +++ b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs @@ -50,6 +50,8 @@ namespace osu.Game.Screens.Multi.Match private LeaderboardChatDisplay leaderboardChatDisplay; private MatchSettingsOverlay settingsOverlay; + private IBindable> managerAdded; + public MatchSubScreen(Room room) { Title = room.RoomID.Value == null ? "New room" : room.Name.Value; @@ -181,7 +183,8 @@ namespace osu.Game.Screens.Multi.Match SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(selectedItemChanged)); SelectedItem.Value = playlist.FirstOrDefault(); - beatmapManager.ItemAdded += beatmapAdded; + managerAdded = beatmapManager.ItemAdded.GetBoundCopy(); + managerAdded.BindValueChanged(beatmapAdded); } public override bool OnExiting(IScreen next) @@ -214,13 +217,16 @@ namespace osu.Game.Screens.Multi.Match Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); } - private void beatmapAdded(BeatmapSetInfo model) => Schedule(() => + private void beatmapAdded(ValueChangedEvent> weakSet) { - if (Beatmap.Value != beatmapManager.DefaultBeatmap) - return; + Schedule(() => + { + if (Beatmap.Value != beatmapManager.DefaultBeatmap) + return; - updateWorkingBeatmap(); - }); + updateWorkingBeatmap(); + }); + } private void onStart() { @@ -235,13 +241,5 @@ namespace osu.Game.Screens.Multi.Match break; } } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - if (beatmapManager != null) - beatmapManager.ItemAdded -= beatmapAdded; - } } } diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 96b779cd20..f23e1b1ef2 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -131,6 +131,11 @@ namespace osu.Game.Screens.Select private CarouselRoot root; + private IBindable> itemAdded; + private IBindable> itemRemoved; + private IBindable> itemHidden; + private IBindable> itemRestored; + public BeatmapCarousel() { root = new CarouselRoot(this); @@ -161,10 +166,14 @@ namespace osu.Game.Screens.Select RightClickScrollingEnabled.ValueChanged += enabled => scroll.RightMouseScrollbar = enabled.NewValue; RightClickScrollingEnabled.TriggerChange(); - beatmaps.ItemAdded += beatmapAdded; - beatmaps.ItemRemoved += beatmapRemoved; - beatmaps.BeatmapHidden += beatmapHidden; - beatmaps.BeatmapRestored += beatmapRestored; + itemAdded = beatmaps.ItemAdded.GetBoundCopy(); + itemAdded.BindValueChanged(beatmapAdded); + itemRemoved = beatmaps.ItemRemoved.GetBoundCopy(); + itemRemoved.BindValueChanged(beatmapRemoved); + itemHidden = beatmaps.BeatmapHidden.GetBoundCopy(); + itemHidden.BindValueChanged(beatmapHidden); + itemRestored = beatmaps.BeatmapRestored.GetBoundCopy(); + itemRestored.BindValueChanged(beatmapRestored); loadBeatmapSets(GetLoadableBeatmaps()); } @@ -562,26 +571,34 @@ namespace osu.Game.Screens.Select { base.Dispose(isDisposing); - if (beatmaps != null) - { - beatmaps.ItemAdded -= beatmapAdded; - beatmaps.ItemRemoved -= beatmapRemoved; - beatmaps.BeatmapHidden -= beatmapHidden; - beatmaps.BeatmapRestored -= beatmapRestored; - } - // aggressively dispose "off-screen" items to reduce GC pressure. foreach (var i in Items) i.Dispose(); } - private void beatmapRemoved(BeatmapSetInfo item) => RemoveBeatmapSet(item); + private void beatmapRemoved(ValueChangedEvent> weakItem) + { + if (weakItem.NewValue.TryGetTarget(out var item)) + RemoveBeatmapSet(item); + } - private void beatmapAdded(BeatmapSetInfo item) => UpdateBeatmapSet(item); + private void beatmapAdded(ValueChangedEvent> weakItem) + { + if (weakItem.NewValue.TryGetTarget(out var item)) + UpdateBeatmapSet(item); + } - private void beatmapRestored(BeatmapInfo b) => UpdateBeatmapSet(beatmaps.QueryBeatmapSet(s => s.ID == b.BeatmapSetInfoID)); + private void beatmapRestored(ValueChangedEvent> weakItem) + { + if (weakItem.NewValue.TryGetTarget(out var b)) + UpdateBeatmapSet(beatmaps.QueryBeatmapSet(s => s.ID == b.BeatmapSetInfoID)); + } - private void beatmapHidden(BeatmapInfo b) => UpdateBeatmapSet(beatmaps.QueryBeatmapSet(s => s.ID == b.BeatmapSetInfoID)); + private void beatmapHidden(ValueChangedEvent> weakItem) + { + if (weakItem.NewValue.TryGetTarget(out var b)) + UpdateBeatmapSet(beatmaps.QueryBeatmapSet(s => s.ID == b.BeatmapSetInfoID)); + } private CarouselBeatmapSet createCarouselSet(BeatmapSetInfo beatmapSet) { diff --git a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs index e981550c84..aed25787b0 100644 --- a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs +++ b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -27,6 +28,9 @@ namespace osu.Game.Screens.Select.Carousel [Resolved] private IAPIProvider api { get; set; } + private IBindable> itemAdded; + private IBindable> itemRemoved; + public TopLocalRank(BeatmapInfo beatmap) : base(null) { @@ -36,17 +40,24 @@ namespace osu.Game.Screens.Select.Carousel [BackgroundDependencyLoader] private void load() { - scores.ItemAdded += scoreChanged; - scores.ItemRemoved += scoreChanged; + itemAdded = scores.ItemAdded.GetBoundCopy(); + itemAdded.BindValueChanged(scoreChanged); + + itemRemoved = scores.ItemRemoved.GetBoundCopy(); + itemRemoved.BindValueChanged(scoreChanged); + ruleset.ValueChanged += _ => fetchAndLoadTopScore(); fetchAndLoadTopScore(); } - private void scoreChanged(ScoreInfo score) + private void scoreChanged(ValueChangedEvent> weakScore) { - if (score.BeatmapInfoID == beatmap.ID) - fetchAndLoadTopScore(); + if (weakScore.NewValue.TryGetTarget(out var score)) + { + if (score.BeatmapInfoID == beatmap.ID) + fetchAndLoadTopScore(); + } } private ScheduledDelegate scheduledRankUpdate; @@ -75,16 +86,5 @@ namespace osu.Game.Screens.Select.Carousel .OrderByDescending(s => s.TotalScore) .FirstOrDefault(); } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - if (scores != null) - { - scores.ItemAdded -= scoreChanged; - scores.ItemRemoved -= scoreChanged; - } - } } } diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index e36493c82f..8e85eb4eb2 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -60,6 +60,8 @@ namespace osu.Game.Screens.Select.Leaderboards private UserTopScoreContainer topScoreContainer; + private IBindable> itemRemoved; + /// /// Whether to apply the game's currently selected mods as a filter when retrieving scores. /// @@ -104,7 +106,8 @@ namespace osu.Game.Screens.Select.Leaderboards ScoreSelected = s => ScoreSelected?.Invoke(s) }); - scoreManager.ItemRemoved += onScoreRemoved; + itemRemoved = scoreManager.ItemRemoved.GetBoundCopy(); + itemRemoved.BindValueChanged(onScoreRemoved); } protected override void Reset() @@ -113,7 +116,7 @@ namespace osu.Game.Screens.Select.Leaderboards TopScore = null; } - private void onScoreRemoved(ScoreInfo score) => Schedule(RefreshScores); + private void onScoreRemoved(ValueChangedEvent> score) => Schedule(RefreshScores); protected override bool IsOnlineScope => Scope != BeatmapLeaderboardScope.Local; @@ -190,13 +193,5 @@ namespace osu.Game.Screens.Select.Leaderboards { Action = () => ScoreSelected?.Invoke(model) }; - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - if (scoreManager != null) - scoreManager.ItemRemoved -= onScoreRemoved; - } } } diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 3d469ab6e1..d65c74ef62 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -43,12 +43,15 @@ namespace osu.Game.Skinning this.audio = audio; this.legacyDefaultResources = legacyDefaultResources; - ItemRemoved += removedInfo => + ItemRemoved.BindValueChanged(weakRemovedInfo => { - // check the removed skin is not the current user choice. if it is, switch back to default. - if (removedInfo.ID == CurrentSkinInfo.Value.ID) - CurrentSkinInfo.Value = SkinInfo.Default; - }; + if (weakRemovedInfo.NewValue.TryGetTarget(out var removedInfo)) + { + // check the removed skin is not the current user choice. if it is, switch back to default. + if (removedInfo.ID == CurrentSkinInfo.Value.ID) + CurrentSkinInfo.Value = SkinInfo.Default; + } + }); CurrentSkinInfo.ValueChanged += skin => CurrentSkin.Value = GetSkin(skin.NewValue); CurrentSkin.ValueChanged += skin => From d56466e2b9376318edc845c84c361fbf3927161d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 19 May 2020 19:07:35 +0900 Subject: [PATCH 1661/2376] Add very basic pooling of grid lines --- .../Edit/ManiaBeatSnapGrid.cs | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs index a16fb52f01..067438af39 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs @@ -90,10 +90,17 @@ namespace osu.Game.Rulesets.Mania.Edit beatDivisor.BindValueChanged(_ => createLines(), true); } + private readonly Stack availableLines = new Stack(); + private void createLines() { foreach (var grid in grids) - grid.Clear(); + { + foreach (var line in grid.Objects.OfType()) + availableLines.Push(line); + + grid.Clear(false); + } if (selectionTimeRange == null) return; @@ -127,7 +134,15 @@ namespace osu.Game.Rulesets.Mania.Edit BindableBeatDivisor.GetDivisorForBeatIndex(Math.Max(1, beat), beatDivisor.Value), colours); foreach (var grid in grids) - grid.Add(new DrawableGridLine(time, colour)); + { + if (!availableLines.TryPop(out var line)) + line = new DrawableGridLine(); + + line.HitObject.StartTime = time; + line.Colour = colour; + + grid.Add(line); + } beat++; time += timingPoint.BeatLength / beatDivisor.Value; @@ -190,17 +205,13 @@ namespace osu.Game.Rulesets.Mania.Edit private readonly IBindable direction = new Bindable(); - public DrawableGridLine(double startTime, Color4 colour) - : base(new HitObject { StartTime = startTime }) + public DrawableGridLine() + : base(new HitObject()) { RelativeSizeAxes = Axes.X; Height = 2; - AddInternal(new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colour - }); + AddInternal(new Box { RelativeSizeAxes = Axes.Both }); } [BackgroundDependencyLoader] From a6f3dc53f72e3100da0cb6d422a872522371e38e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 19 May 2020 23:01:36 +0900 Subject: [PATCH 1662/2376] Fix time value not being updated for next timing point --- osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs index 067438af39..5803c67b80 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs @@ -127,6 +127,7 @@ namespace osu.Game.Rulesets.Mania.Edit if (nextTimingPoint != timingPoint) { beat = 0; + time = nextTimingPoint.Time; timingPoint = nextTimingPoint; } From c28a9bdb804a05ee5fac61b5728931885650cdf5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 19 May 2020 23:02:20 +0900 Subject: [PATCH 1663/2376] Move load method up --- .../Edit/ManiaBeatSnapGrid.cs | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs index 5803c67b80..0b7834addb 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs @@ -51,7 +51,6 @@ namespace osu.Game.Rulesets.Mania.Edit public (double start, double end)? SelectionTimeRange { - get => selectionTimeRange; set { if (value == selectionTimeRange) @@ -62,17 +61,6 @@ namespace osu.Game.Rulesets.Mania.Edit } } - protected override void Update() - { - base.Update(); - - if (!lineCache.IsValid) - { - lineCache.Validate(); - createLines(); - } - } - [BackgroundDependencyLoader] private void load() { @@ -90,6 +78,17 @@ namespace osu.Game.Rulesets.Mania.Edit beatDivisor.BindValueChanged(_ => createLines(), true); } + protected override void Update() + { + base.Update(); + + if (!lineCache.IsValid) + { + lineCache.Validate(); + createLines(); + } + } + private readonly Stack availableLines = new Stack(); private void createLines() From 85156c62efd7900dbce4115e27f96327f4fd0345 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 19 May 2020 23:05:08 +0900 Subject: [PATCH 1664/2376] Add xmldoc and address some code quality concerns --- .../Edit/ManiaBeatSnapGrid.cs | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs index 0b7834addb..98e15e3fa8 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs @@ -21,12 +21,27 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Mania.Edit { + /// + /// A grid which displays coloured beat divisor lines in proximity to the selection or placement cursor. + /// public class ManiaBeatSnapGrid : Component { private const double visible_range = 750; - [Resolved] - private IManiaHitObjectComposer composer { get; set; } + /// + /// The range of time values of the current selection. + /// + public (double start, double end)? SelectionTimeRange + { + set + { + if (value == selectionTimeRange) + return; + + selectionTimeRange = value; + lineCache.Invalidate(); + } + } [Resolved] private EditorBeatmap beatmap { get; set; } @@ -49,20 +64,8 @@ namespace osu.Game.Rulesets.Mania.Edit private (double start, double end)? selectionTimeRange; - public (double start, double end)? SelectionTimeRange - { - set - { - if (value == selectionTimeRange) - return; - - selectionTimeRange = value; - lineCache.Invalidate(); - } - } - [BackgroundDependencyLoader] - private void load() + private void load(IManiaHitObjectComposer composer) { foreach (var stage in composer.Playfield.Stages) { From db4e3047ddf02cb09a7603c68a0ab854e1cfe6b2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 19 May 2020 23:28:13 +0900 Subject: [PATCH 1665/2376] Add test for final sample output --- .../TestSceneSampleOutput.cs | 47 +++++++++++++++++++ .../Objects/Drawables/DrawableHit.cs | 2 +- .../Drawables/DrawableTaikoHitObject.cs | 2 +- .../Objects/Drawables/DrawableHitObject.cs | 2 +- .../Tests/Beatmaps/BeatmapConversionTest.cs | 11 +++-- 5 files changed, 58 insertions(+), 6 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko.Tests/TestSceneSampleOutput.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneSampleOutput.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneSampleOutput.cs new file mode 100644 index 0000000000..564ab91291 --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneSampleOutput.cs @@ -0,0 +1,47 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Game.Audio; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Taiko.Objects.Drawables; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Taiko.Tests +{ + /// + /// Taiko has some interesting rules for legacy mappings. + /// + public class TestSceneSampleOutput : PlayerTestScene + { + public TestSceneSampleOutput() + : base(new TaikoRuleset()) + { + } + + public override void SetUpSteps() + { + base.SetUpSteps(); + AddAssert("has correct samples", () => + { + var names = Player.DrawableRuleset.Playfield.AllHitObjects.OfType().Select(h => string.Join(',', h.GetSamples().Select(s => s.Name))); + + var expected = new[] + { + string.Empty, + string.Empty, + string.Empty, + string.Empty, + HitSampleInfo.HIT_FINISH, + HitSampleInfo.HIT_WHISTLE, + HitSampleInfo.HIT_WHISTLE, + HitSampleInfo.HIT_WHISTLE, + }; + + return names.SequenceEqual(expected); + }); + } + + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TaikoBeatmapConversionTest().GetBeatmap("sample-to-type-conversions"); + } +} diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index 1e1f9ae09b..81b969eaf3 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables ? new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.CentreHit), _ => new CentreHitCirclePiece(), confineMode: ConfineMode.ScaleToFit) : new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.RimHit), _ => new RimHitCirclePiece(), confineMode: ConfineMode.ScaleToFit); - protected override IEnumerable GetSamples() + public override IEnumerable GetSamples() { // normal and claps are always handled by the drum (see DrumSampleMapping). // in addition, whistles are excluded as they are an alternative rim marker. diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs index 90daf3950c..3ab09d4cbe 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs @@ -166,7 +166,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables } // Most osu!taiko hitsounds are managed by the drum (see DrumSampleMapping). - protected override IEnumerable GetSamples() => Enumerable.Empty(); + public override IEnumerable GetSamples() => Enumerable.Empty(); protected abstract SkinnableDrawable CreateMainPiece(); diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index ba6571fe1a..33ea02c22f 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Objects.Drawables protected SkinnableSound Samples { get; private set; } - protected virtual IEnumerable GetSamples() => HitObject.Samples; + public virtual IEnumerable GetSamples() => HitObject.Samples; private readonly Lazy> nestedHitObjects = new Lazy>(); public IReadOnlyList NestedHitObjects => nestedHitObjects.IsValueCreated ? nestedHitObjects.Value : (IReadOnlyList)Array.Empty(); diff --git a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs index b60add6e3b..06e82394ec 100644 --- a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs +++ b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs @@ -99,7 +99,7 @@ namespace osu.Game.Tests.Beatmaps private ConvertResult convert(string name, Mod[] mods) { - var beatmap = getBeatmap(name); + var beatmap = GetBeatmap(name); var rulesetInstance = CreateRuleset(); beatmap.BeatmapInfo.Ruleset = beatmap.BeatmapInfo.RulesetID == rulesetInstance.RulesetInfo.ID ? rulesetInstance.RulesetInfo : new RulesetInfo(); @@ -143,14 +143,19 @@ namespace osu.Game.Tests.Beatmaps } } - private IBeatmap getBeatmap(string name) + public IBeatmap GetBeatmap(string name) { using (var resStream = openResource($"{resource_namespace}.{name}.osu")) using (var stream = new LineBufferedReader(resStream)) { var decoder = Decoder.GetDecoder(stream); ((LegacyBeatmapDecoder)decoder).ApplyOffsets = false; - return decoder.Decode(stream); + var beatmap = decoder.Decode(stream); + + // not sure but seems to be required. + beatmap.BeatmapInfo.Ruleset = CreateRuleset().RulesetInfo; + + return beatmap; } } From da8729e6bde39ec4255d498cde1e50a7135d5601 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 19 May 2020 23:28:42 +0900 Subject: [PATCH 1666/2376] 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 650ebde54d..f0f16d3763 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index ee6206e166..010ef8578a 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -24,7 +24,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index cbf8600c62..88b0c7dd8a 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From e21178570484780c8467f47529ea407a6fd5e24c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 18 May 2020 21:01:13 +0200 Subject: [PATCH 1667/2376] Add overlay layer to storyboard definition --- osu.Game/Storyboards/Storyboard.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game/Storyboards/Storyboard.cs b/osu.Game/Storyboards/Storyboard.cs index d13c874ee2..b0fb583d62 100644 --- a/osu.Game/Storyboards/Storyboard.cs +++ b/osu.Game/Storyboards/Storyboard.cs @@ -19,19 +19,26 @@ namespace osu.Game.Storyboards public double FirstEventTime => Layers.Min(l => l.Elements.FirstOrDefault()?.StartTime ?? 0); + /// + /// Depth of the currently front-most storyboard layer, excluding the overlay layer. + /// + private int minimumLayerDepth; + public Storyboard() { layers.Add("Video", new StoryboardLayer("Video", 4, false)); layers.Add("Background", new StoryboardLayer("Background", 3)); layers.Add("Fail", new StoryboardLayer("Fail", 2) { VisibleWhenPassing = false, }); layers.Add("Pass", new StoryboardLayer("Pass", 1) { VisibleWhenFailing = false, }); - layers.Add("Foreground", new StoryboardLayer("Foreground", 0)); + layers.Add("Foreground", new StoryboardLayer("Foreground", minimumLayerDepth = 0)); + + layers.Add("Overlay", new StoryboardLayer("Overlay", int.MinValue)); } public StoryboardLayer GetLayer(string name) { if (!layers.TryGetValue(name, out var layer)) - layers[name] = layer = new StoryboardLayer(name, layers.Values.Min(l => l.Depth) - 1); + layers[name] = layer = new StoryboardLayer(name, --minimumLayerDepth); return layer; } From 6e27247cdf7bc46b4317cbb98f3dfb5b20c769a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 18 May 2020 22:10:02 +0200 Subject: [PATCH 1668/2376] Adjust storyboard decoder test in line with changes --- .../Beatmaps/Formats/LegacyStoryboardDecoderTest.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs index 2fdeadca02..9ebedb3c80 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs @@ -26,7 +26,7 @@ namespace osu.Game.Tests.Beatmaps.Formats var storyboard = decoder.Decode(stream); Assert.IsTrue(storyboard.HasDrawable); - Assert.AreEqual(5, storyboard.Layers.Count()); + Assert.AreEqual(6, storyboard.Layers.Count()); StoryboardLayer background = storyboard.Layers.FirstOrDefault(l => l.Depth == 3); Assert.IsNotNull(background); @@ -56,6 +56,13 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.IsTrue(foreground.VisibleWhenPassing); Assert.AreEqual("Foreground", foreground.Name); + StoryboardLayer overlay = storyboard.Layers.FirstOrDefault(l => l.Depth == int.MinValue); + Assert.IsNotNull(overlay); + Assert.IsEmpty(overlay.Elements); + Assert.IsTrue(overlay.VisibleWhenFailing); + Assert.IsTrue(overlay.VisibleWhenPassing); + Assert.AreEqual("Overlay", overlay.Name); + int spriteCount = background.Elements.Count(x => x.GetType() == typeof(StoryboardSprite)); int animationCount = background.Elements.Count(x => x.GetType() == typeof(StoryboardAnimation)); int sampleCount = background.Elements.Count(x => x.GetType() == typeof(StoryboardSampleInfo)); From 2398f2e537e8375b63f0de39b637751c1cbaca96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 18 May 2020 21:12:14 +0200 Subject: [PATCH 1669/2376] Expose drawable overlay layer --- osu.Game/Storyboards/Drawables/DrawableStoryboard.cs | 3 +++ osu.Game/Storyboards/StoryboardLayer.cs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs index c4d796e30b..ec461fa095 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboard.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 System.Threading; using osuTK; using osu.Framework.Allocation; @@ -72,6 +73,8 @@ namespace osu.Game.Storyboards.Drawables } } + public DrawableStoryboardLayer OverlayLayer => Children.Single(layer => layer.Name == "Overlay"); + private void updateLayerVisibility() { foreach (var layer in Children) diff --git a/osu.Game/Storyboards/StoryboardLayer.cs b/osu.Game/Storyboards/StoryboardLayer.cs index 142bc60deb..1cde7cf67a 100644 --- a/osu.Game/Storyboards/StoryboardLayer.cs +++ b/osu.Game/Storyboards/StoryboardLayer.cs @@ -33,6 +33,6 @@ namespace osu.Game.Storyboards } public DrawableStoryboardLayer CreateDrawable() - => new DrawableStoryboardLayer(this) { Depth = Depth, }; + => new DrawableStoryboardLayer(this) { Depth = Depth, Name = Name }; } } From ce4301c5b8a5a36dafb4dac54d04c4eb49920b4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 19 May 2020 19:47:01 +0200 Subject: [PATCH 1670/2376] Add overlay layer to player by proxying --- osu.Game/Screens/Play/DimmableStoryboard.cs | 15 +++++++++++++-- osu.Game/Screens/Play/Player.cs | 1 + 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/DimmableStoryboard.cs b/osu.Game/Screens/Play/DimmableStoryboard.cs index eabdee95fb..74c84f648c 100644 --- a/osu.Game/Screens/Play/DimmableStoryboard.cs +++ b/osu.Game/Screens/Play/DimmableStoryboard.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Graphics.Containers; using osu.Game.Graphics.Containers; using osu.Game.Storyboards; using osu.Game.Storyboards.Drawables; @@ -13,6 +14,8 @@ namespace osu.Game.Screens.Play /// public class DimmableStoryboard : UserDimContainer { + public Container OverlayLayerContainer; + private readonly Storyboard storyboard; private DrawableStoryboard drawableStoryboard; @@ -24,6 +27,8 @@ namespace osu.Game.Screens.Play [BackgroundDependencyLoader] private void load() { + Add(OverlayLayerContainer = new Container()); + initializeStoryboard(false); } @@ -46,9 +51,15 @@ namespace osu.Game.Screens.Play drawableStoryboard = storyboard.CreateDrawable(); if (async) - LoadComponentAsync(drawableStoryboard, Add); + LoadComponentAsync(drawableStoryboard, onStoryboardCreated); else - Add(drawableStoryboard); + onStoryboardCreated(drawableStoryboard); + } + + private void onStoryboardCreated(DrawableStoryboard storyboard) + { + Add(storyboard); + OverlayLayerContainer.Add(storyboard.OverlayLayer.CreateProxy()); } } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 1ec3a69b24..77da038ab3 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -264,6 +264,7 @@ namespace osu.Game.Screens.Play { target.AddRange(new[] { + DimmableStoryboard.OverlayLayerContainer.CreateProxy(), BreakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, ScoreProcessor) { Clock = DrawableRuleset.FrameStableClock, From 963806474148d80ddc706e0f47f8bf5d8a22e6a9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 20 May 2020 10:06:23 +0900 Subject: [PATCH 1671/2376] Tidy up ruleset assignment code --- osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs index 06e82394ec..6ada632850 100644 --- a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs +++ b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs @@ -101,9 +101,6 @@ namespace osu.Game.Tests.Beatmaps { var beatmap = GetBeatmap(name); - var rulesetInstance = CreateRuleset(); - beatmap.BeatmapInfo.Ruleset = beatmap.BeatmapInfo.RulesetID == rulesetInstance.RulesetInfo.ID ? rulesetInstance.RulesetInfo : new RulesetInfo(); - var converterResult = new Dictionary>(); var working = new ConversionWorkingBeatmap(beatmap) @@ -115,7 +112,7 @@ namespace osu.Game.Tests.Beatmaps } }; - working.GetPlayableBeatmap(rulesetInstance.RulesetInfo, mods); + working.GetPlayableBeatmap(CreateRuleset().RulesetInfo, mods); return new ConvertResult { @@ -152,8 +149,8 @@ namespace osu.Game.Tests.Beatmaps ((LegacyBeatmapDecoder)decoder).ApplyOffsets = false; var beatmap = decoder.Decode(stream); - // not sure but seems to be required. - beatmap.BeatmapInfo.Ruleset = CreateRuleset().RulesetInfo; + var rulesetInstance = CreateRuleset(); + beatmap.BeatmapInfo.Ruleset = beatmap.BeatmapInfo.RulesetID == rulesetInstance.RulesetInfo.ID ? rulesetInstance.RulesetInfo : new RulesetInfo(); return beatmap; } From 76080368e900f2b866f46235eebff0f7bda19a6d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 20 May 2020 10:14:08 +0900 Subject: [PATCH 1672/2376] Mark test as headless --- osu.Game.Rulesets.Taiko.Tests/TestSceneSampleOutput.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneSampleOutput.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneSampleOutput.cs index 564ab91291..d541aa8de8 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneSampleOutput.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneSampleOutput.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Linq; +using osu.Framework.Testing; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Rulesets.Taiko.Objects.Drawables; @@ -12,6 +13,7 @@ namespace osu.Game.Rulesets.Taiko.Tests /// /// Taiko has some interesting rules for legacy mappings. /// + [HeadlessTest] public class TestSceneSampleOutput : PlayerTestScene { public TestSceneSampleOutput() From d31a59b07466eae570e1127624c30286666339b4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 20 May 2020 14:55:36 +0900 Subject: [PATCH 1673/2376] Fix logic results in infinite loop on default timing point return --- osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs index 98e15e3fa8..05990eadd7 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs @@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Mania.Edit var nextTimingPoint = beatmap.ControlPointInfo.TimingPointAt(time); // switch to the next timing point if we have reached it. - if (nextTimingPoint != timingPoint) + if (nextTimingPoint.Time > timingPoint.Time) { beat = 0; time = nextTimingPoint.Time; From 0bc3073d49aeff5ed5647fca342f158725222510 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 20 May 2020 15:01:32 +0900 Subject: [PATCH 1674/2376] Fix test failures --- .../TestSceneManiaHitObjectComposer.cs | 14 +++++++++----- osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs index 6274bb1005..bad3d7854e 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs @@ -42,6 +42,7 @@ namespace osu.Game.Rulesets.Mania.Tests public void TestDragOffscreenSelectionVerticallyUpScroll() { DrawableHitObject lastObject = null; + double originalTime = 0; Vector2 originalPosition = Vector2.Zero; setScrollStep(ScrollingDirection.Up); @@ -49,6 +50,7 @@ namespace osu.Game.Rulesets.Mania.Tests AddStep("seek to last object", () => { lastObject = this.ChildrenOfType().Single(d => d.HitObject == composer.EditorBeatmap.HitObjects.Last()); + originalTime = lastObject.HitObject.StartTime; Clock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime); }); @@ -64,19 +66,20 @@ namespace osu.Game.Rulesets.Mania.Tests AddStep("move mouse downwards", () => { - InputManager.MoveMouseTo(lastObject, new Vector2(0, 20)); + InputManager.MoveMouseTo(lastObject, new Vector2(0, lastObject.ScreenSpaceDrawQuad.Height * 2)); InputManager.ReleaseButton(MouseButton.Left); }); AddAssert("hitobjects not moved columns", () => composer.EditorBeatmap.HitObjects.All(h => ((ManiaHitObject)h).Column == 0)); AddAssert("hitobjects moved downwards", () => lastObject.DrawPosition.Y - originalPosition.Y > 0); - AddAssert("hitobjects not moved too far", () => lastObject.DrawPosition.Y - originalPosition.Y < 50); + AddAssert("hitobject has moved time", () => lastObject.HitObject.StartTime == originalTime + 125); } [Test] public void TestDragOffscreenSelectionVerticallyDownScroll() { DrawableHitObject lastObject = null; + double originalTime = 0; Vector2 originalPosition = Vector2.Zero; setScrollStep(ScrollingDirection.Down); @@ -84,6 +87,7 @@ namespace osu.Game.Rulesets.Mania.Tests AddStep("seek to last object", () => { lastObject = this.ChildrenOfType().Single(d => d.HitObject == composer.EditorBeatmap.HitObjects.Last()); + originalTime = lastObject.HitObject.StartTime; Clock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime); }); @@ -99,13 +103,13 @@ namespace osu.Game.Rulesets.Mania.Tests AddStep("move mouse upwards", () => { - InputManager.MoveMouseTo(lastObject, new Vector2(0, -20)); + InputManager.MoveMouseTo(lastObject, new Vector2(0, -lastObject.ScreenSpaceDrawQuad.Height * 2)); InputManager.ReleaseButton(MouseButton.Left); }); AddAssert("hitobjects not moved columns", () => composer.EditorBeatmap.HitObjects.All(h => ((ManiaHitObject)h).Column == 0)); AddAssert("hitobjects moved upwards", () => originalPosition.Y - lastObject.DrawPosition.Y > 0); - AddAssert("hitobjects not moved too far", () => originalPosition.Y - lastObject.DrawPosition.Y < 50); + AddAssert("hitobject has moved time", () => lastObject.HitObject.StartTime == originalTime + 125); } [Test] @@ -207,7 +211,7 @@ namespace osu.Game.Rulesets.Mania.Tests }; for (int i = 0; i < 10; i++) - EditorBeatmap.Add(new Note { StartTime = 100 * i }); + EditorBeatmap.Add(new Note { StartTime = 125 * i }); } } } diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs index 05990eadd7..fa8f8a755a 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs @@ -180,7 +180,7 @@ namespace osu.Game.Rulesets.Mania.Edit foreach (var grid in grids) { - foreach (var line in grid.AliveObjects.OfType()) + foreach (var line in grid.Objects.OfType()) { Vector2 linePos = line.ToSpaceOfOtherDrawable(line.OriginPosition, this); float d = Vector2.Distance(position, linePos); From 85088c9b3baa96ece3ad5a1404585eeb23e5c8bb Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 20 May 2020 15:08:33 +0900 Subject: [PATCH 1675/2376] Privatise setter --- osu.Game/Screens/Play/DimmableStoryboard.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/DimmableStoryboard.cs b/osu.Game/Screens/Play/DimmableStoryboard.cs index 74c84f648c..58eb95b7c6 100644 --- a/osu.Game/Screens/Play/DimmableStoryboard.cs +++ b/osu.Game/Screens/Play/DimmableStoryboard.cs @@ -14,7 +14,7 @@ namespace osu.Game.Screens.Play ///